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( - + / - + - +