-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
02bb84a
commit 069760d
Showing
5 changed files
with
244 additions
and
1 deletion.
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
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
120 changes: 120 additions & 0 deletions
120
apps/web/src/app/[orgShortCode]/settings/org/setup/billing/_components/plans-table.tsx
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,120 @@ | ||
'use client'; | ||
import { Button } from '@/src/components/shadcn-ui/button'; | ||
import { api } from '@/src/lib/trpc'; | ||
import { useGlobalStore } from '@/src/providers/global-store-provider'; | ||
import { useCallback, useEffect, useRef, useState } from 'react'; | ||
import { AlertDialog as Dialog } from '@radix-ui/themes'; | ||
import useAwaitableModal, { | ||
type ModalComponent | ||
} from '@/src/hooks/use-awaitable-modal'; | ||
|
||
export function PlansTable() { | ||
const [period /*setPeriod*/] = useState<'monthly' | 'yearly'>('monthly'); | ||
|
||
const [StripeWatcherRoot, openPaymentModal] = useAwaitableModal( | ||
StripeWatcher, | ||
{ | ||
period | ||
} | ||
); | ||
|
||
return ( | ||
<div className="flex w-full max-w-2xl gap-4"> | ||
<div className="flex flex-1 flex-col gap-2 rounded border p-2"> | ||
<div className="font-display text-2xl">Free</div> | ||
<div className="h-[200px]">Free Plan Perks Here</div> | ||
<Button disabled>Your Current Plan</Button> | ||
</div> | ||
<div className="flex flex-1 flex-col gap-2 rounded border p-2"> | ||
<div className="font-display text-2xl">Pro</div> | ||
<div className="h-[200px]">Pro Plan Perks Here</div> | ||
{/* Monthly only for now, setup the period selector */} | ||
<Button onClick={() => openPaymentModal().catch(() => null)}> | ||
Upgrade | ||
</Button> | ||
</div> | ||
<StripeWatcherRoot /> | ||
</div> | ||
); | ||
} | ||
|
||
function StripeWatcher({ | ||
open, | ||
period, | ||
onClose, | ||
onResolve | ||
}: ModalComponent<{ period: 'monthly' | 'yearly' }>) { | ||
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); | ||
const { | ||
data: paymentLinkInfo, | ||
error: linkError, | ||
isLoading: paymentLinkLoading | ||
} = api.org.setup.billing.getOrgSubscriptionPaymentLink.useQuery( | ||
{ | ||
orgShortCode, | ||
period, | ||
plan: 'pro' | ||
}, | ||
{ enabled: open } | ||
); | ||
const paymentLinkCache = | ||
api.useUtils().org.setup.billing.getOrgSubscriptionPaymentLink; | ||
|
||
const overviewApi = api.useUtils().org.setup.billing.getOrgBillingOverview; | ||
const timeout = useRef<NodeJS.Timeout | null>(null); | ||
const checkPayment = useCallback(async () => { | ||
const overview = await overviewApi.fetch({ orgShortCode }); | ||
if (overview.currentPlan === 'pro') { | ||
await overviewApi.invalidate({ orgShortCode }); | ||
if (timeout.current) { | ||
clearTimeout(timeout.current); | ||
} | ||
onResolve(null); | ||
return; | ||
} else { | ||
timeout.current = setTimeout(() => { | ||
void checkPayment(); | ||
}, 7500); | ||
} | ||
}, [onResolve, orgShortCode, overviewApi]); | ||
|
||
useEffect(() => { | ||
if (!open || !paymentLinkInfo) return; | ||
window.open(paymentLinkInfo.subLink, '_blank'); | ||
timeout.current = setTimeout(() => { | ||
void checkPayment(); | ||
}, 10000); | ||
return () => { | ||
if (timeout.current) { | ||
clearTimeout(timeout.current); | ||
} | ||
void paymentLinkCache.reset(); | ||
}; | ||
}, [open, paymentLinkInfo, paymentLinkCache, checkPayment]); | ||
|
||
return ( | ||
<Dialog.Root open={open}> | ||
<Dialog.Content> | ||
<Dialog.Title className="p-2">Upgrade to Pro</Dialog.Title> | ||
<Dialog.Description className="space-y-2 p-2"> | ||
{paymentLinkLoading | ||
? 'Generating Payment Link' | ||
: 'Waiting For you to complete your Payment (This may take a few seconds)'} | ||
</Dialog.Description> | ||
<div className="flex flex-col gap-2 p-2"> | ||
We are waiting for you to complete your payment, If you have already | ||
done the payment, please wait for a few seconds for the payment to | ||
reflect. If this modal is not detecting your payment, please close | ||
this modal and try refreshing. If the issue persists, please contact | ||
support. | ||
</div> | ||
<div className="text-red-10 space-y-2 font-medium"> | ||
{linkError?.message} | ||
</div> | ||
<div className="flex flex-col gap-2 py-2"> | ||
<Button onClick={() => onClose()}>Close</Button> | ||
</div> | ||
</Dialog.Content> | ||
</Dialog.Root> | ||
); | ||
} |
94 changes: 94 additions & 0 deletions
94
apps/web/src/app/[orgShortCode]/settings/org/setup/billing/page.tsx
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,94 @@ | ||
'use client'; | ||
|
||
import { Button } from '@/src/components/shadcn-ui/button'; | ||
import { api } from '@/src/lib/trpc'; | ||
import { useGlobalStore } from '@/src/providers/global-store-provider'; | ||
import { useState } from 'react'; | ||
import { PlansTable } from './_components/plans-table'; | ||
import CalEmbed from '@calcom/embed-react'; | ||
import Link from 'next/link'; | ||
import { cn } from '@/src/lib/utils'; | ||
|
||
export default function Page() { | ||
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode); | ||
const { data, isLoading } = | ||
api.org.setup.billing.getOrgBillingOverview.useQuery({ | ||
orgShortCode | ||
}); | ||
|
||
const { data: portalLink } = | ||
api.org.setup.billing.getOrgStripePortalLink.useQuery( | ||
{ orgShortCode }, | ||
{ | ||
enabled: data?.currentPlan === 'pro' | ||
} | ||
); | ||
|
||
const [showPlan, setShowPlans] = useState(false); | ||
|
||
return ( | ||
<div className="flex w-full flex-col gap-2 p-4"> | ||
<h1 className="font-display text-3xl leading-5">Billing</h1> | ||
<div>Manage your organization's subscription</div> | ||
{isLoading && <div>Loading...</div>} | ||
{data && ( | ||
<> | ||
<div className="my-2 flex gap-8"> | ||
<div className="flex flex-col"> | ||
<div className="text-muted-foreground">Current Plan</div> | ||
<div className="font-display text-2xl"> | ||
{data.currentPlan === 'pro' ? 'Pro' : 'Free'} | ||
</div> | ||
</div> | ||
{data.totalUsers && ( | ||
<div className="flex flex-col"> | ||
<div className="text-muted-foreground">Users</div> | ||
<div className="font-display text-right text-2xl"> | ||
{data.totalUsers} | ||
</div> | ||
</div> | ||
)} | ||
{data.currentPlan === 'pro' && ( | ||
<div className="flex flex-col"> | ||
<div className="text-muted-foreground">Billing Period</div> | ||
<div className="font-display text-2xl"> | ||
{data.currentPeriod === 'monthly' ? 'Monthly' : 'Yearly'} | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
{data.currentPlan !== 'pro' && !showPlan && ( | ||
<Button | ||
onClick={() => setShowPlans(true)} | ||
className="w-fit"> | ||
Upgrade | ||
</Button> | ||
)} | ||
{showPlan && <PlansTable />} | ||
{data.currentPlan === 'pro' && ( | ||
<Button | ||
className={cn( | ||
'w-fit', | ||
!portalLink && 'pointer-events-none opacity-75' | ||
)} | ||
asChild> | ||
<Link | ||
href={portalLink?.portalLink ?? '#'} | ||
target="_blank"> | ||
Manage Your Subscription | ||
</Link> | ||
</Button> | ||
)} | ||
{data.currentPlan === 'pro' && ( | ||
<div className="my-4 flex w-full flex-1 flex-col gap-2"> | ||
<div className="font-display text-xl"> | ||
Jump on a Free Onboarding Call | ||
</div> | ||
<CalEmbed calLink="mc/unboarding" /> | ||
</div> | ||
)} | ||
</> | ||
)} | ||
</div> | ||
); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.