Perf improvements and billing updates

This commit is contained in:
giancarlo
2024-03-26 16:49:11 +08:00
parent 8626ea30c7
commit 4032aed827
39 changed files with 1261 additions and 1090 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { ArrowRightIcon } from '@radix-ui/react-icons';
import { ArrowUpRight } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
@@ -15,7 +15,7 @@ export function BillingPortalCard() {
return (
<Card>
<CardHeader>
<CardTitle>Manage your Subscription</CardTitle>
<CardTitle>Manage your Billing Details</CardTitle>
<CardDescription>
You can change your plan or cancel your subscription at any time.
@@ -23,15 +23,13 @@ export function BillingPortalCard() {
</CardHeader>
<CardContent className={'space-y-2'}>
<Button className={'w-full'}>
<span>Manage your Billing Settings</span>
<ArrowRightIcon className={'ml-2 h-4'} />
</Button>
<div>
<Button>
<span>Visit the billing portal</span>
<p className={'text-sm'}>
Visit the billing portal to manage your subscription (update payment
method, cancel subscription, etc.)
</p>
<ArrowUpRight className={'h-4'} />
</Button>
</div>
</CardContent>
</Card>
);

View File

@@ -0,0 +1,63 @@
import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
export function CurrentPlanAlert(
props: React.PropsWithoutRef<{
status: Database['public']['Enums']['subscription_status'];
}>,
) {
let variant: 'success' | 'warning' | 'destructive';
let text: string;
let title: string;
switch (props.status) {
case 'active':
variant = 'success';
title = 'Active';
text = 'Your subscription is active';
break;
case 'trialing':
variant = 'success';
title = 'Trial';
text = 'You are currently on a trial';
break;
case 'past_due':
variant = 'destructive';
title = 'Past Due';
text = 'Your subscription payment is past due';
break;
case 'canceled':
variant = 'destructive';
title = 'Canceled';
text = 'You have canceled your subscription';
break;
case 'unpaid':
variant = 'destructive';
title = 'Unpaid';
text = 'Your subscription payment is unpaid';
break;
case 'incomplete':
variant = 'warning';
title = 'Incomplete';
text = 'We are processing your subscription payment';
break;
case 'incomplete_expired':
variant = 'destructive';
title = 'Incomplete Expired';
text = 'Your subscription payment has expired';
break;
case 'paused':
variant = 'warning';
title = 'Paused';
text = 'Your subscription is paused';
break;
}
return (
<Alert variant={variant}>
<AlertTitle>{title}</AlertTitle>
<AlertDescription>{text}</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,48 @@
import { Database } from '@kit/supabase/database';
import { Badge } from '@kit/ui/badge';
export function CurrentPlanBadge(
props: React.PropsWithoutRef<{
status: Database['public']['Enums']['subscription_status'];
}>,
) {
let variant: 'success' | 'warning' | 'destructive';
let text: string;
switch (props.status) {
case 'active':
variant = 'success';
text = 'Active';
break;
case 'trialing':
variant = 'success';
text = 'Trialing';
break;
case 'past_due':
variant = 'destructive';
text = 'Past due';
break;
case 'canceled':
variant = 'destructive';
text = 'Canceled';
break;
case 'unpaid':
variant = 'destructive';
text = 'Unpaid';
break;
case 'incomplete':
variant = 'warning';
text = 'Incomplete';
break;
case 'incomplete_expired':
variant = 'destructive';
text = 'Incomplete expired';
break;
case 'paused':
variant = 'warning';
text = 'Paused';
break;
}
return <Badge variant={variant}>{text}</Badge>;
}

View File

@@ -1,8 +1,15 @@
import { formatDate } from 'date-fns';
import { BadgeCheck, CheckCircle2 } from 'lucide-react';
import { z } from 'zod';
import { BillingSchema, getProductPlanPairFromId } from '@kit/billing';
import { Database } from '@kit/supabase/database';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@kit/ui/accordion';
import {
Card,
CardContent,
@@ -11,6 +18,10 @@ import {
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { CurrentPlanAlert } from './current-plan-alert';
import { CurrentPlanBadge } from './current-plan-badge';
export function CurrentPlanCard({
subscription,
@@ -27,41 +38,103 @@ export function CurrentPlanCard({
return (
<Card>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
<CardTitle>Your Plan</CardTitle>
<CardDescription>{product.description}</CardDescription>
<CardDescription>
You can change your plan or cancel your subscription at any time.
</CardDescription>
</CardHeader>
<CardContent className={'space-y-4 text-sm'}>
<div>
<div className={'font-semibold'}>
Your Current Plan: <span>{plan.name}</span>
<CardContent className={'space-y-2.5 text-sm'}>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center space-x-2 text-lg font-semibold'}>
<BadgeCheck
className={
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
}
/>
<span>{product.name}</span>
<CurrentPlanBadge status={subscription.status} />
</div>
<div className={'text-muted-foreground'}>
Renews every {subscription.interval} at{' '}
<span>{product.currency}</span> <span>{plan.price}</span>
</div>
</div>
<div>
<div className={'font-semibold'}>
Your Subscription is currently <span>{subscription.status}</span>
</div>
<CurrentPlanAlert status={subscription.status} />
</div>
<If condition={subscription.cancel_at_period_end}>
<div>
<div className={'font-semibold'}>
Cancellation Date:{' '}
<span>{formatDate(subscription.period_ends_at, 'P')}</span>
</div>
</div>
</If>
<div>
<Accordion type="single" collapsible>
<AccordionItem value="features">
<AccordionTrigger>Plan details</AccordionTrigger>
<If condition={!subscription.cancel_at_period_end}>
<div>
<div className={'font-semibold'}>
Next Billing Date:{' '}
<span>{formatDate(subscription.period_ends_at, 'P')}</span>{' '}
</div>
</div>
</If>
<AccordionContent className="space-y-2.5">
<If condition={subscription.status === 'trialing'}>
<div className="flex flex-col">
<span className="font-medium">Your trial ends on</span>
<div className={'text-muted-foreground'}>
<span>
{subscription.trial_ends_at
? formatDate(subscription.trial_ends_at, 'P')
: ''}
</span>
</div>
</div>
</If>
<If condition={subscription.cancel_at_period_end}>
<div className="flex flex-col">
<span className="font-medium">
Your subscription will be cancelled at the end of the
period
</span>
<div className={'text-muted-foreground'}>
<span>
{formatDate(subscription.period_ends_at ?? '', 'P')}
</span>
</div>
</div>
</If>
<If condition={!subscription.cancel_at_period_end}>
<div className="flex flex-col space-y-1">
<span className="font-medium">Your next bill</span>
<div className={'text-muted-foreground'}>
Your next bill is for {product.currency} {plan.price} on{' '}
<span>
{formatDate(subscription.period_ends_at ?? '', 'P')}
</span>{' '}
</div>
</div>
</If>
<div className="flex flex-col space-y-1">
<span className="font-medium">Features</span>
<ul>
{product.features.map((item) => {
return (
<li key={item} className="flex items-center space-x-0.5">
<CheckCircle2 className="h-4 text-green-500" />
<span>
<Trans i18nKey={item} defaults={item} />
</span>
</li>
);
})}
</ul>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</CardContent>
</Card>
);

View File

@@ -15,10 +15,22 @@ export function EmbeddedCheckout(
provider: BillingProvider;
onClose?: () => void;
}>,
) {
return (
<LazyCheckout onClose={props.onClose} checkoutToken={props.checkoutToken} />
);
}
function LazyCheckout(
props: React.PropsWithChildren<{
checkoutToken: string;
provider: BillingProvider;
onClose?: () => void;
}>,
) {
const CheckoutComponent = useMemo(
() => memo(loadCheckoutComponent(props.provider)),
[],
[props.provider],
);
return (
@@ -69,7 +81,7 @@ function buildLazyComponent<
) {
let LoadedComponent: ReturnType<typeof lazy> | null = null;
const LazyComponent = forwardRef((props, ref) => {
const LazyComponent = forwardRef(function LazyDynamicComponent(props, ref) {
if (!LoadedComponent) {
LoadedComponent = lazy(load);
}