Skip to content

Commit

Permalink
feat: billing page
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle committed May 25, 2024
1 parent 02bb84a commit 069760d
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 1 deletion.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"check": "tsc --noEmit && next lint"
},
"dependencies": {
"@calcom/embed-react": "^1.5.0",
"@phosphor-icons/react": "^2.1.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/[orgShortCode]/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function Layout({
children
}: Readonly<{ children: React.ReactNode }>) {
return (
<Flex className="h-full">
<Flex className="h-full w-full">
<SettingsSidebar />
<Flex className="flex-1">{children}</Flex>
</Flex>
Expand Down
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>
);
}
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&apos;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>
);
}
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 069760d

Please sign in to comment.