Refactor code and improve usage of package dependencies

This commit updates the naming convention of icons from Lucide-React, moving some package dependencies to "peerDependencies" in 'team-accounts', 'admin' and 'auth'. Additionally, it includes tweaks to the development server command in apps/web package.json and adds a logger reference to the shared package. Furthermore, cleanup work has been performed within the features and UI packages, and new scripts to interact with Stripe have been added to the root package.json.
This commit is contained in:
giancarlo
2024-03-26 01:34:19 +08:00
parent 95793c42b4
commit ee507e0816
92 changed files with 1691 additions and 1270 deletions

View File

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

View File

@@ -2,7 +2,7 @@
import Link from 'next/link';
import { CheckIcon, ChevronRightIcon } from 'lucide-react';
import { Check, ChevronRight } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
@@ -55,9 +55,9 @@ function SuccessSessionStatus({
'flex flex-col items-center justify-center space-y-4 text-center'
}
>
<CheckIcon
<Check
className={
'w-16 rounded-full bg-green-500 p-1 text-white ring-8' +
'h-16 w-16 rounded-full bg-green-500 p-1 text-white ring-8' +
' ring-green-500/30 dark:ring-green-500/50'
}
/>
@@ -87,7 +87,7 @@ function SuccessSessionStatus({
<Trans i18nKey={'subscription:checkoutSuccessBackButton'} />
</span>
<ChevronRightIcon className={'h-4'} />
<ChevronRight className={'h-4'} />
</span>
</Link>
</Button>

View File

@@ -1 +1,68 @@
export function CurrentPlanCard(props: React.PropsWithChildren<{}>) {}
import { formatDate } from 'date-fns';
import { z } from 'zod';
import { BillingSchema, getProductPlanPairFromId } from '@kit/billing';
import { Database } from '@kit/supabase/database';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
export function CurrentPlanCard({
subscription,
config,
}: React.PropsWithChildren<{
subscription: Database['public']['Tables']['subscriptions']['Row'];
config: z.infer<typeof BillingSchema>;
}>) {
const { plan, product } = getProductPlanPairFromId(
config,
subscription.variant_id,
);
return (
<Card>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
<CardDescription>{product.description}</CardDescription>
</CardHeader>
<CardContent className={'space-y-4 text-sm'}>
<div>
<div className={'font-semibold'}>
Your Current Plan: <span>{plan.name}</span>
</div>
</div>
<div>
<div className={'font-semibold'}>
Your Subscription is currently <span>{subscription.status}</span>
</div>
</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>
<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>
</CardContent>
</Card>
);
}

View File

@@ -1,9 +1,14 @@
import { lazy } from 'react';
import { Suspense, forwardRef, lazy, memo, useMemo } from 'react';
import { Database } from '@kit/supabase/database';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
type BillingProvider = Database['public']['Enums']['billing_provider'];
const Fallback = (
<LoadingOverlay fullPage={false}>Loading Checkout...</LoadingOverlay>
);
export function EmbeddedCheckout(
props: React.PropsWithChildren<{
checkoutToken: string;
@@ -11,7 +16,10 @@ export function EmbeddedCheckout(
onClose?: () => void;
}>,
) {
const CheckoutComponent = loadCheckoutComponent(props.provider);
const CheckoutComponent = useMemo(
() => memo(loadCheckoutComponent(props.provider)),
[],
);
return (
<CheckoutComponent
@@ -24,10 +32,12 @@ export function EmbeddedCheckout(
function loadCheckoutComponent(provider: BillingProvider) {
switch (provider) {
case 'stripe': {
return lazy(() => {
return import('@kit/stripe/components').then((c) => ({
default: c.StripeCheckout,
}));
return buildLazyComponent(() => {
return import('@kit/stripe/components').then(({ StripeCheckout }) => {
return {
default: StripeCheckout,
};
});
});
}
@@ -43,3 +53,33 @@ function loadCheckoutComponent(provider: BillingProvider) {
throw new Error(`Unsupported provider: ${provider as string}`);
}
}
function buildLazyComponent<
Cmp extends React.ComponentType<
React.PropsWithChildren<{
onClose?: (() => unknown) | undefined;
checkoutToken: string;
}>
>,
>(
load: () => Promise<{
default: Cmp;
}>,
fallback = Fallback,
) {
let LoadedComponent: ReturnType<typeof lazy> | null = null;
const LazyComponent = forwardRef((props, ref) => {
if (!LoadedComponent) {
LoadedComponent = lazy(load);
}
return (
<Suspense fallback={fallback}>
<LoadedComponent {...props} ref={ref} />
</Suspense>
);
});
return memo(LazyComponent);
}

View File

@@ -2,3 +2,4 @@ export * from './plan-picker';
export * from './current-plan-card';
export * from './embedded-checkout';
export * from './billing-session-status';
export * from './billing-portal-card';

View File

@@ -1,7 +1,7 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { ArrowRightIcon } from 'lucide-react';
import { ArrowRight } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -212,7 +212,7 @@ export function PlanPicker(
) : (
<>
<span>Proceed to payment</span>
<ArrowRightIcon className={'ml-2 h-4 w-4'} />
<ArrowRight className={'ml-2 h-4 w-4'} />
</>
)}
</Button>

View File

@@ -6,7 +6,7 @@ import { Database } from '@kit/supabase/database';
export class BillingEventHandlerService {
constructor(
private readonly client: SupabaseClient<Database>,
private readonly clientProvider: () => SupabaseClient<Database>,
private readonly strategy: BillingWebhookHandlerService,
) {}
@@ -19,6 +19,8 @@ export class BillingEventHandlerService {
return this.strategy.handleWebhookEvent(event, {
onSubscriptionDeleted: async (subscriptionId: string) => {
const client = this.clientProvider();
// Handle the subscription deleted event
// here we delete the subscription from the database
Logger.info(
@@ -29,7 +31,7 @@ export class BillingEventHandlerService {
'Processing subscription deleted event',
);
const { error } = await this.client
const { error } = await client
.from('subscriptions')
.delete()
.match({ id: subscriptionId });
@@ -47,6 +49,8 @@ export class BillingEventHandlerService {
);
},
onSubscriptionUpdated: async (subscription) => {
const client = this.clientProvider();
const ctx = {
namespace: 'billing',
subscriptionId: subscription.id,
@@ -58,7 +62,7 @@ export class BillingEventHandlerService {
// Handle the subscription updated event
// here we update the subscription in the database
const { error } = await this.client
const { error } = await client
.from('subscriptions')
.update(subscription)
.match({ id: subscription.id });
@@ -77,9 +81,11 @@ export class BillingEventHandlerService {
Logger.info(ctx, 'Successfully updated subscription');
},
onCheckoutSessionCompleted: async (subscription) => {
onCheckoutSessionCompleted: async (subscription, customerId) => {
// Handle the checkout session completed event
// here we add the subscription to the database
const client = this.clientProvider();
const ctx = {
namespace: 'billing',
subscriptionId: subscription.id,
@@ -89,12 +95,21 @@ export class BillingEventHandlerService {
Logger.info(ctx, 'Processing checkout session completed event...');
const { error } = await this.client.rpc('add_subscription', {
subscription,
const { id, ...data } = subscription;
const { error } = await client.rpc('add_subscription', {
...data,
subscription_id: subscription.id,
customer_id: customerId,
price_amount: subscription.price_amount ?? 0,
period_starts_at: subscription.period_starts_at!,
period_ends_at: subscription.period_ends_at!,
trial_starts_at: subscription.trial_starts_at!,
trial_ends_at: subscription.trial_ends_at!,
});
if (error) {
Logger.error(ctx, 'Failed to add subscription');
Logger.error({ ...ctx, error }, 'Failed to add subscription');
throw new Error('Failed to add subscription');
}

View File

@@ -10,11 +10,11 @@ import { BillingEventHandlerFactoryService } from './billing-gateway-factory.ser
* defined in the host application.
*/
export async function getBillingEventHandlerService(
client: ReturnType<typeof getSupabaseServerActionClient>,
clientProvider: () => ReturnType<typeof getSupabaseServerActionClient>,
provider: Database['public']['Enums']['billing_provider'],
) {
const strategy =
await BillingEventHandlerFactoryService.GetProviderStrategy(provider);
return new BillingEventHandlerService(client, strategy);
return new BillingEventHandlerService(clientProvider, strategy);
}