Perf improvements and billing updates
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user