Update UI style and enhance billing services
Several changes have been made in this commit. Firstly, updates have been made to the site-header-account-section and the pricing-table components to enhance UI aesthetics. Secondly, billing services have been significantly improved. A new method for retrieving plan information by an ID has been introduced. This method is available for all strategy services, including Stripe and Lemon-Squeezy. Furthermore, the way context and logging are handled during the billing process has been streamlined for better readability and efficiency.
This commit is contained in:
@@ -61,7 +61,7 @@ function AuthButtons() {
|
|||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
|
|
||||||
<Link href={pathsConfig.auth.signIn}>
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
<Button variant={'ghost'}>
|
<Button className={'rounded-full'} variant={'ghost'}>
|
||||||
<Trans i18nKey={'auth:signIn'} />
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -51,4 +51,11 @@ export abstract class BillingStrategyProviderService {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
abstract getPlanById(planId: string): Promise<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
interval: string;
|
||||||
|
amount: number;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function PricingItem(
|
|||||||
className={cn(
|
className={cn(
|
||||||
props.className,
|
props.className,
|
||||||
`s-full flex flex-1 grow flex-col items-stretch justify-between space-y-8 self-stretch
|
`s-full flex flex-1 grow flex-col items-stretch justify-between space-y-8 self-stretch
|
||||||
rounded-lg p-6 ring-2 lg:w-4/12 xl:max-w-[22rem] xl:p-8`,
|
rounded-lg p-6 ring-2 lg:w-4/12 xl:max-w-[19rem]`,
|
||||||
{
|
{
|
||||||
['ring-primary']: highlighted,
|
['ring-primary']: highlighted,
|
||||||
['dark:shadow-primary/30 shadow-none ring-transparent dark:shadow-sm']:
|
['dark:shadow-primary/30 shadow-none ring-transparent dark:shadow-sm']:
|
||||||
|
|||||||
@@ -127,4 +127,16 @@ export class BillingGatewayService {
|
|||||||
|
|
||||||
return strategy.updateSubscription(payload);
|
return strategy.updateSubscription(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a plan by the specified plan ID.
|
||||||
|
* @param planId
|
||||||
|
*/
|
||||||
|
async getPlanById(planId: string) {
|
||||||
|
const strategy = await BillingGatewayFactoryService.GetProviderStrategy(
|
||||||
|
this.provider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return strategy.getPlanById(planId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
cancelSubscription,
|
cancelSubscription,
|
||||||
createUsageRecord,
|
createUsageRecord,
|
||||||
getCheckout,
|
getCheckout,
|
||||||
|
getVariant,
|
||||||
updateSubscriptionItem,
|
updateSubscriptionItem,
|
||||||
} from '@lemonsqueezy/lemonsqueezy.js';
|
} from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -25,18 +26,19 @@ import { createLemonSqueezyCheckout } from './create-lemon-squeezy-checkout';
|
|||||||
export class LemonSqueezyBillingStrategyService
|
export class LemonSqueezyBillingStrategyService
|
||||||
implements BillingStrategyProviderService
|
implements BillingStrategyProviderService
|
||||||
{
|
{
|
||||||
|
private readonly namespace = 'billing.lemon-squeezy';
|
||||||
|
|
||||||
async createCheckoutSession(
|
async createCheckoutSession(
|
||||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||||
) {
|
) {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
logger.info(
|
const ctx = {
|
||||||
{
|
name: this.namespace,
|
||||||
name: 'billing.lemon-squeezy',
|
...params,
|
||||||
...params,
|
};
|
||||||
},
|
|
||||||
'Creating checkout session...',
|
logger.info(ctx, 'Creating checkout session...');
|
||||||
);
|
|
||||||
|
|
||||||
const { data: response, error } = await createLemonSqueezyCheckout(params);
|
const { data: response, error } = await createLemonSqueezyCheckout(params);
|
||||||
|
|
||||||
@@ -45,9 +47,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
customerId: params.customerId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
},
|
},
|
||||||
'Failed to create checkout session',
|
'Failed to create checkout session',
|
||||||
@@ -56,14 +56,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
throw new Error('Failed to create checkout session');
|
throw new Error('Failed to create checkout session');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(ctx, 'Checkout session created successfully');
|
||||||
{
|
|
||||||
name: 'billing.lemon-squeezy',
|
|
||||||
customerId: params.customerId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
},
|
|
||||||
'Checkout session created successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
checkoutToken: response.data.attributes.url,
|
checkoutToken: response.data.attributes.url,
|
||||||
@@ -75,13 +68,12 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
) {
|
) {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
logger.info(
|
const ctx = {
|
||||||
{
|
name: this.namespace,
|
||||||
name: 'billing.lemon-squeezy',
|
...params,
|
||||||
customerId: params.customerId,
|
};
|
||||||
},
|
|
||||||
'Creating billing portal session...',
|
logger.info(ctx, 'Creating billing portal session...');
|
||||||
);
|
|
||||||
|
|
||||||
const { data, error } =
|
const { data, error } =
|
||||||
await createLemonSqueezyBillingPortalSession(params);
|
await createLemonSqueezyBillingPortalSession(params);
|
||||||
@@ -89,8 +81,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
if (error ?? !data) {
|
if (error ?? !data) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
customerId: params.customerId,
|
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
},
|
},
|
||||||
'Failed to create billing portal session',
|
'Failed to create billing portal session',
|
||||||
@@ -99,13 +90,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
throw new Error('Failed to create billing portal session');
|
throw new Error('Failed to create billing portal session');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(ctx, 'Billing portal session created successfully');
|
||||||
{
|
|
||||||
name: 'billing.lemon-squeezy',
|
|
||||||
customerId: params.customerId,
|
|
||||||
},
|
|
||||||
'Billing portal session created successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
return { url: data };
|
return { url: data };
|
||||||
}
|
}
|
||||||
@@ -115,13 +100,12 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
) {
|
) {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
logger.info(
|
const ctx = {
|
||||||
{
|
name: this.namespace,
|
||||||
name: 'billing.lemon-squeezy',
|
subscriptionId: params.subscriptionId,
|
||||||
subscriptionId: params.subscriptionId,
|
};
|
||||||
},
|
|
||||||
'Cancelling subscription...',
|
logger.info(ctx, 'Cancelling subscription...');
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await cancelSubscription(params.subscriptionId);
|
const { error } = await cancelSubscription(params.subscriptionId);
|
||||||
@@ -129,8 +113,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
if (error) {
|
if (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
subscriptionId: params.subscriptionId,
|
|
||||||
error: error.message,
|
error: error.message,
|
||||||
},
|
},
|
||||||
'Failed to cancel subscription',
|
'Failed to cancel subscription',
|
||||||
@@ -139,20 +122,13 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(ctx, 'Subscription cancelled successfully');
|
||||||
{
|
|
||||||
name: 'billing.lemon-squeezy',
|
|
||||||
subscriptionId: params.subscriptionId,
|
|
||||||
},
|
|
||||||
'Subscription cancelled successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
subscriptionId: params.subscriptionId,
|
|
||||||
error: (error as Error)?.message,
|
error: (error as Error)?.message,
|
||||||
},
|
},
|
||||||
'Failed to cancel subscription',
|
'Failed to cancel subscription',
|
||||||
@@ -167,21 +143,19 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
) {
|
) {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
logger.info(
|
const ctx = {
|
||||||
{
|
name: this.namespace,
|
||||||
name: 'billing.lemon-squeezy',
|
sessionId: params.sessionId,
|
||||||
sessionId: params.sessionId,
|
};
|
||||||
},
|
|
||||||
'Retrieving checkout session...',
|
logger.info(ctx, 'Retrieving checkout session...');
|
||||||
);
|
|
||||||
|
|
||||||
const { data: session, error } = await getCheckout(params.sessionId);
|
const { data: session, error } = await getCheckout(params.sessionId);
|
||||||
|
|
||||||
if (error ?? !session?.data) {
|
if (error ?? !session?.data) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
sessionId: params.sessionId,
|
|
||||||
error: error?.message,
|
error: error?.message,
|
||||||
},
|
},
|
||||||
'Failed to retrieve checkout session',
|
'Failed to retrieve checkout session',
|
||||||
@@ -190,13 +164,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
throw new Error('Failed to retrieve checkout session');
|
throw new Error('Failed to retrieve checkout session');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(ctx, 'Checkout session retrieved successfully');
|
||||||
{
|
|
||||||
name: 'billing.lemon-squeezy',
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
},
|
|
||||||
'Checkout session retrieved successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { id, attributes } = session.data;
|
const { id, attributes } = session.data;
|
||||||
|
|
||||||
@@ -213,13 +181,12 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
logger.info(
|
const ctx = {
|
||||||
{
|
name: this.namespace,
|
||||||
name: 'billing.lemon-squeezy',
|
subscriptionItemId: params.subscriptionItemId,
|
||||||
subscriptionItemId: params.subscriptionItemId,
|
};
|
||||||
},
|
|
||||||
'Reporting usage...',
|
logger.info(ctx, 'Reporting usage...');
|
||||||
);
|
|
||||||
|
|
||||||
const { error } = await createUsageRecord({
|
const { error } = await createUsageRecord({
|
||||||
quantity: params.usage.quantity,
|
quantity: params.usage.quantity,
|
||||||
@@ -230,8 +197,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
if (error) {
|
if (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{
|
{
|
||||||
name: 'billing.lemon-squeezy',
|
...ctx,
|
||||||
subscriptionItemId: params.subscriptionItemId,
|
|
||||||
error,
|
error,
|
||||||
},
|
},
|
||||||
'Failed to report usage',
|
'Failed to report usage',
|
||||||
@@ -240,13 +206,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
throw new Error('Failed to report usage');
|
throw new Error('Failed to report usage');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(ctx, 'Usage reported successfully');
|
||||||
{
|
|
||||||
name: 'billing.lemon-squeezy',
|
|
||||||
subscriptionItemId: params.subscriptionItemId,
|
|
||||||
},
|
|
||||||
'Usage reported successfully',
|
|
||||||
);
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
@@ -257,7 +217,7 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
name: 'billing.lemon-squeezy',
|
name: this.namespace,
|
||||||
...params,
|
...params,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,11 +236,58 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
'Failed to update subscription',
|
'Failed to update subscription',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw error;
|
throw new Error('Failed to update subscription');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(ctx, 'Subscription updated successfully');
|
logger.info(ctx, 'Subscription updated successfully');
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPlanById(planId: string) {
|
||||||
|
const logger = await getLogger();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
name: this.namespace,
|
||||||
|
planId,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(ctx, 'Retrieving plan by ID...');
|
||||||
|
|
||||||
|
const { error, data } = await getVariant(planId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
'Failed to retrieve plan by ID',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error('Failed to retrieve plan by ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
},
|
||||||
|
'Plan not found',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error('Plan not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(ctx, 'Plan retrieved successfully');
|
||||||
|
|
||||||
|
const attrs = data.data.attributes;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.data.id,
|
||||||
|
name: attrs.name,
|
||||||
|
interval: attrs.interval ?? '',
|
||||||
|
amount: attrs.price,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,36 @@ export class StripeBillingStrategyService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPlanById(planId: string) {
|
||||||
|
const logger = await getLogger();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
name: this.namespace,
|
||||||
|
planId,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(ctx, 'Retrieving plan by id...');
|
||||||
|
|
||||||
|
const stripe = await this.stripeProvider();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plan = await stripe.plans.retrieve(planId);
|
||||||
|
|
||||||
|
logger.info(ctx, 'Plan retrieved successfully');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: plan.id,
|
||||||
|
name: plan.nickname ?? '',
|
||||||
|
amount: plan.amount ?? 0,
|
||||||
|
interval: plan.interval,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ ...ctx, error }, 'Failed to retrieve plan');
|
||||||
|
|
||||||
|
throw new Error('Failed to retrieve plan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async stripeProvider(): Promise<Stripe> {
|
private async stripeProvider(): Promise<Stripe> {
|
||||||
return createStripeClient();
|
return createStripeClient();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user