Skip to content

Commit

Permalink
fix: add "change plan" page
Browse files Browse the repository at this point in the history
  • Loading branch information
travis committed Jan 31, 2024
1 parent 4faccf3 commit a2d5dac
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 8 deletions.
97 changes: 97 additions & 0 deletions src/app/plans/change/page.tsx
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>
)
}
7 changes: 4 additions & 3 deletions src/components/UsageBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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'>
Expand Down
26 changes: 21 additions & 5 deletions src/hooks.tsx
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
}

0 comments on commit a2d5dac

Please sign in to comment.