-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
model it after https://web3.storage/pricing/ needs storacha/w3ui#623 to work
- Loading branch information
Showing
3 changed files
with
122 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'use client' | ||
|
||
import { usePlan } from "@/hooks" | ||
import { useW3, DID } from "@w3ui/react" | ||
import { CheckCircleIcon } from '@heroicons/react/24/outline' | ||
import DefaultLoader from "@/components/Loader" | ||
import { useState } from "react" | ||
|
||
interface PlanSectionProps { | ||
planID: DID | ||
planName: string | ||
flatFee: number | ||
flatFeeAllotment: number | ||
perGbFee: number | ||
} | ||
|
||
const planRanks: Record<string, number> = { | ||
'did:web:starter.web3.storage': 0, | ||
'did:web:lite.web3.storage': 1, | ||
'did:web:business.web3.storage': 2 | ||
} | ||
|
||
const buttonText = (currentPlan: string, newPlan: string) => (planRanks[currentPlan] > planRanks[newPlan]) ? 'Downgrade' : 'Upgrade' | ||
|
||
|
||
function PlanSection ({ planID, planName, flatFee, flatFeeAllotment, perGbFee }: PlanSectionProps) { | ||
const [{ accounts }] = useW3() | ||
const { data: plan, setPlan, isLoading } = usePlan(accounts[0]) | ||
const currentPlanID = plan?.product | ||
const isCurrentPlan = currentPlanID === planID | ||
const [isUpdatingPlan, setIsUpdatingPlan] = useState(false) | ||
async function selectPlan (selectedPlanID: DID) { | ||
setIsUpdatingPlan(true) | ||
await setPlan(selectedPlanID) | ||
setIsUpdatingPlan(false) | ||
} | ||
return ( | ||
<div className={`flex flex-col items-center rounded border-2 border-solid border-slate-800 w-96 p-8 ${isCurrentPlan ? 'bg-blue-800/10' : 'bg-slate-800/10'}`}> | ||
<div className='text-2xl mb-6 flex flex-row justify-between w-full'> | ||
<div className='font-bold'>{planName}</div> | ||
<div> | ||
<span className='font-bold'>${flatFee}</span><span className='text-slate-600'>/mo</span> | ||
</div> | ||
</div> | ||
<div className='flex flex-row self-start space-x-2 mb-4'> | ||
<div className='pt-1'> | ||
<CheckCircleIcon className='w-6 h-6 font-bold' /> | ||
</div> | ||
<div className='flex flex-col'> | ||
<div className='text-xl font-bold'>{flatFeeAllotment}GB storage</div> | ||
<div>Additional at ${perGbFee}/GB per month</div> | ||
</div> | ||
</div> | ||
<div className='flex flex-row self-start space-x-2 mb-8'> | ||
<div className='pt-1'> | ||
<CheckCircleIcon className='w-6 h-6 font-bold' /> | ||
</div> | ||
<div className='flex flex-col'> | ||
<div className='text-xl font-bold'>{flatFeeAllotment}GB egress per month</div> | ||
<div>Additional at ${perGbFee}/GB per month</div> | ||
</div> | ||
</div> | ||
{ | ||
(isLoading || isUpdatingPlan || !currentPlanID) ? ( | ||
<DefaultLoader className='h-6 w-6' /> | ||
) : ( | ||
isCurrentPlan ? ( | ||
<div className='h-4'> | ||
{(currentPlanID === planID) && ( | ||
<h5 className='font-bold'>Current Plan</h5> | ||
)} | ||
</div> | ||
) : ( | ||
<button className={`text-white bg-slate-800 hover:bg-blue-800 rounded py-2 px-8 text-sm font-medium transition-colors ease-in`} | ||
disabled={isCurrentPlan || isLoading} onClick={() => selectPlan(planID)}>{currentPlanID && buttonText(currentPlanID, planID)}</button> | ||
) | ||
) | ||
} | ||
</div > | ||
) | ||
} | ||
|
||
export default function Plans () { | ||
|
||
|
||
return ( | ||
<div className='py-8 flex flex-col items-center'> | ||
<h1 className='text-2xl font-mono mb-8 font-bold'>Plans</h1> | ||
<p className='mb-4'>Pick the price plan that works for you.</p> | ||
<div className='flex flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-2'> | ||
<PlanSection planID='did:web:starter.web3.storage' planName='Starter' flatFee={0} flatFeeAllotment={5} perGbFee={0.15} /> | ||
<PlanSection planID='did:web:lite.web3.storage' planName='Lite' flatFee={10} flatFeeAllotment={100} perGbFee={0.05} /> | ||
<PlanSection planID='did:web:business.web3.storage' planName='Business' flatFee={100} flatFeeAllotment={2000} perGbFee={0.03} /> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
import { ReactNode } from 'react' | ||
import { useW3, SpaceDID } from '@w3ui/react' | ||
import useSWR from 'swr' | ||
import Link from 'next/link' | ||
import { GB, TB, filesize } from '@/lib' | ||
import { usePlan } from '@/hooks' | ||
|
||
|
@@ -61,11 +62,11 @@ export function UsageBar (): ReactNode { | |
{plan?.product ? ( | ||
<div className='lg:text-right text-xs tracking-wider font-mono flex flex-row justify-end space-x-2'> | ||
<div>Plan: <strong>{Plans[plan.product]?.name ?? plan.product}</strong></div> | ||
<a className='underline' | ||
href='mailto:[email protected]?subject=How%20to%20change%20my%20payment%20plan?' | ||
<Link className='underline' | ||
href='/plans/change' | ||
title='Automated support for switching plans is currently in progress. to change your plan, please email [email protected].'> | ||
change plan | ||
</a> | ||
</Link> | ||
<a className='underline' | ||
href={process.env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_LINK} | ||
target='_blank' rel='noopener noreferrer'> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,29 @@ | ||
import { Account, PlanGetSuccess } from '@w3ui/react' | ||
import useSWR from 'swr' | ||
import { Account, DID, PlanGetSuccess, PlanSetSuccess, PlanSetFailure, Result } from '@w3ui/react' | ||
import useSWR, { SWRResponse, useSWRConfig } from 'swr' | ||
|
||
export const usePlan = (account: Account) => | ||
useSWR<PlanGetSuccess | undefined>(`/plan/${account?.did() ?? ''}`, { | ||
/** | ||
* calculate the cache key for a plan's account | ||
*/ | ||
const planKey = (account: Account) => account ? `/plan/${account.did()}` : undefined | ||
|
||
type UsePlanResult = SWRResponse<PlanGetSuccess | undefined> & { | ||
setPlan: (plan: DID) => Promise<Result<PlanSetSuccess, PlanSetFailure>> | ||
} | ||
|
||
export const usePlan = (account: Account) => { | ||
const { mutate } = useSWRConfig() | ||
const result = useSWR<PlanGetSuccess | undefined>(planKey(account), { | ||
fetcher: async () => { | ||
if (!account) return | ||
const result = await account.plan.get() | ||
if (result.error) throw new Error('getting plan', { cause: result.error }) | ||
return result.ok | ||
}, | ||
onError: err => console.error(err.message, err.cause) | ||
}) | ||
}) | ||
// @ts-ignore it's important to assign this into the existing object | ||
// to avoid calling the getters in SWRResponse when copying values over - | ||
// I can't think of a cleaner way to do this but open to refactoring | ||
result.setPlan = async (plan: DID) => await mutate(planKey(account), account.plan.set(plan)) | ||
return result as UsePlanResult | ||
} |