From 898e1c3dbbb78fb56d4e5391379ea9ab0772ff2e Mon Sep 17 00:00:00 2001 From: giancarlo Date: Wed, 1 May 2024 16:26:35 +0700 Subject: [PATCH] Add support for custom billing plans This commit includes updates to the billing schema to allow for the creation of custom billing plans. These types of plans have no line items associated with them. Changes have also been made to properly handle the absence of primary line items in such plans. --- apps/web/public/locales/en/billing.json | 1 + apps/web/tsconfig.json | 2 +- .../billing/core/src/create-billing-schema.ts | 35 ++++++++++++++++--- .../gateway/src/components/pricing-table.tsx | 24 ++++++++----- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/apps/web/public/locales/en/billing.json b/apps/web/public/locales/en/billing.json index 87d3d94b5..75d50ac77 100644 --- a/apps/web/public/locales/en/billing.json +++ b/apps/web/public/locales/en/billing.json @@ -26,6 +26,7 @@ "month": "Billed monthly", "year": "Billed yearly" }, + "custom": "Custom Plan", "lifetime": "Lifetime", "trialPeriod": "{{period}} day trial", "perPeriod": "per {{period}}", diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 532c69e7a..cde8bb425 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -26,5 +26,5 @@ "lib/**/*.ts", "app" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", ".next"] } diff --git a/packages/billing/core/src/create-billing-schema.ts b/packages/billing/core/src/create-billing-schema.ts index db6322413..3fe45709c 100644 --- a/packages/billing/core/src/create-billing-schema.ts +++ b/packages/billing/core/src/create-billing-schema.ts @@ -87,12 +87,17 @@ export const PlanSchema = z }) .min(1), interval: BillingIntervalSchema.optional(), + custom: z.boolean().default(false).optional(), + label: z.string().min(1).optional(), + href: z.string().min(1).optional(), lineItems: z.array(LineItemSchema).refine( (schema) => { const types = schema.map((item) => item.type); + const perSeat = types.filter( (type) => type === LineItemType.PerSeat, ).length; + const flat = types.filter((type) => type === LineItemType.Flat).length; return perSeat <= 1 && flat <= 1; @@ -111,10 +116,32 @@ export const PlanSchema = z .optional(), paymentType: PaymentTypeSchema, }) - .refine((data) => data.lineItems.length > 0, { - message: 'Plans must have at least one line item', - path: ['lineItems'], - }) + .refine( + (data) => { + if (data.custom) { + return data.lineItems.length === 0; + } + + return data.lineItems.length > 0; + }, + { + message: 'Non-Custom Plans must have at least one line item', + path: ['lineItems'], + }, + ) + .refine( + (data) => { + if (data.custom) { + return data.lineItems.length === 0; + } + + return data.lineItems.length > 0; + }, + { + message: 'Custom Plans must have 0 line items', + path: ['lineItems'], + }, + ) .refine( (data) => data.paymentType !== 'one-time' || data.interval === undefined, { diff --git a/packages/billing/gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx index 1dc0b2826..40770711d 100644 --- a/packages/billing/gateway/src/components/pricing-table.tsx +++ b/packages/billing/gateway/src/components/pricing-table.tsx @@ -83,8 +83,8 @@ export function PricingTable({ const primaryLineItem = getPrimaryLineItem(config, plan.id); - if (!primaryLineItem) { - throw new Error(`Base line item was not found`); + if (!plan.custom && !primaryLineItem) { + throw new Error(`Primary line item not found for plan ${plan.id}`); } return ( @@ -115,7 +115,7 @@ function PricingItem( selectable: boolean; - primaryLineItem: z.infer; + primaryLineItem: z.infer | undefined; redirectToCheckout?: boolean; @@ -145,10 +145,12 @@ function PricingItem( ) { const highlighted = props.product.highlighted ?? false; + const lineItem = props.primaryLineItem; + // we want to exclude the primary plan from the list of line items // since we are displaying the primary line item separately as the main price const lineItemsToDisplay = props.plan.lineItems.filter((item) => { - return item.id !== props.primaryLineItem.id; + return item.id !== lineItem?.id; }); return ( @@ -205,7 +207,11 @@ function PricingItem(
- {formatCurrency(props.product.currency, props.primaryLineItem.cost)} + {lineItem ? ( + formatCurrency(props.product.currency, lineItem.cost) + ) : ( + + )} @@ -225,7 +231,7 @@ function PricingItem( - + / - + - +