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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user