Revert "Unify workspace dropdowns; Update layouts (#458)"
This reverts commit 4bc8448a1d.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum LineItemType {
|
||||
Flat = 'flat',
|
||||
@@ -19,13 +19,42 @@ export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
|
||||
|
||||
export const LineItemSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
cost: z.number().min(0),
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the line item. Defined by the Provider.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the line item. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description:
|
||||
'Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
|
||||
' from the line item. This is useful if you want to provide a more detailed description to the user.',
|
||||
})
|
||||
.optional(),
|
||||
cost: z
|
||||
.number({
|
||||
description: 'Cost of the line item. Displayed to the user.',
|
||||
})
|
||||
.min(0),
|
||||
type: LineItemTypeSchema,
|
||||
unit: z.string().optional(),
|
||||
setupFee: z.number().positive().optional(),
|
||||
unit: z
|
||||
.string({
|
||||
description:
|
||||
'Unit of the line item. Displayed to the user. Example "seat" or "GB"',
|
||||
})
|
||||
.optional(),
|
||||
setupFee: z
|
||||
.number({
|
||||
description: `Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`,
|
||||
})
|
||||
.positive()
|
||||
.optional(),
|
||||
tiers: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -61,8 +90,16 @@ export const LineItemSchema = z
|
||||
|
||||
export const PlanSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
id: z
|
||||
.string({
|
||||
description: 'Unique identifier for the plan. Defined by yourself.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the plan. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
interval: BillingIntervalSchema.optional(),
|
||||
custom: z.boolean().default(false).optional(),
|
||||
label: z.string().min(1).optional(),
|
||||
@@ -85,7 +122,13 @@ export const PlanSchema = z
|
||||
path: ['lineItems'],
|
||||
},
|
||||
),
|
||||
trialDays: z.number().positive().optional(),
|
||||
trialDays: z
|
||||
.number({
|
||||
description:
|
||||
'Number of days for the trial period. Leave empty for no trial.',
|
||||
})
|
||||
.positive()
|
||||
.optional(),
|
||||
paymentType: PaymentTypeSchema,
|
||||
})
|
||||
.refine(
|
||||
@@ -164,15 +207,56 @@ export const PlanSchema = z
|
||||
|
||||
const ProductSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
currency: z.string().min(3).max(3),
|
||||
badge: z.string().optional(),
|
||||
features: z.array(z.string()).nonempty(),
|
||||
enableDiscountField: z.boolean().optional(),
|
||||
highlighted: z.boolean().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the product. Defined by th Provider.',
|
||||
})
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the product. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description: 'Description of the product. Displayed to the user.',
|
||||
})
|
||||
.min(1),
|
||||
currency: z
|
||||
.string({
|
||||
description: 'Currency code for the product. Displayed to the user.',
|
||||
})
|
||||
.min(3)
|
||||
.max(3),
|
||||
badge: z
|
||||
.string({
|
||||
description:
|
||||
'Badge for the product. Displayed to the user. Example: "Popular"',
|
||||
})
|
||||
.optional(),
|
||||
features: z
|
||||
.array(
|
||||
z.string({
|
||||
description: 'Features of the product. Displayed to the user.',
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
enableDiscountField: z
|
||||
.boolean({
|
||||
description: 'Enable discount field for the product in the checkout.',
|
||||
})
|
||||
.optional(),
|
||||
highlighted: z
|
||||
.boolean({
|
||||
description: 'Highlight this product. Displayed to the user.',
|
||||
})
|
||||
.optional(),
|
||||
hidden: z
|
||||
.boolean({
|
||||
description: 'Hide this product from being displayed to users.',
|
||||
})
|
||||
.optional(),
|
||||
plans: z.array(PlanSchema),
|
||||
})
|
||||
.refine((data) => data.plans.length > 0, {
|
||||
@@ -253,14 +337,14 @@ const BillingSchema = z
|
||||
},
|
||||
);
|
||||
|
||||
export function createBillingSchema(config: z.output<typeof BillingSchema>) {
|
||||
export function createBillingSchema(config: z.infer<typeof BillingSchema>) {
|
||||
return BillingSchema.parse(config);
|
||||
}
|
||||
|
||||
export type BillingConfig = z.output<typeof BillingSchema>;
|
||||
export type ProductSchema = z.output<typeof ProductSchema>;
|
||||
export type BillingConfig = z.infer<typeof BillingSchema>;
|
||||
export type ProductSchema = z.infer<typeof ProductSchema>;
|
||||
|
||||
export function getPlanIntervals(config: z.output<typeof BillingSchema>) {
|
||||
export function getPlanIntervals(config: z.infer<typeof BillingSchema>) {
|
||||
const intervals = config.products
|
||||
.flatMap((product) => product.plans.map((plan) => plan.interval))
|
||||
.filter(Boolean);
|
||||
@@ -279,7 +363,7 @@ export function getPlanIntervals(config: z.output<typeof BillingSchema>) {
|
||||
* @param planId
|
||||
*/
|
||||
export function getPrimaryLineItem(
|
||||
config: z.output<typeof BillingSchema>,
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
@@ -307,7 +391,7 @@ export function getPrimaryLineItem(
|
||||
}
|
||||
|
||||
export function getProductPlanPair(
|
||||
config: z.output<typeof BillingSchema>,
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
@@ -322,7 +406,7 @@ export function getProductPlanPair(
|
||||
}
|
||||
|
||||
export function getProductPlanPairByVariantId(
|
||||
config: z.output<typeof BillingSchema>,
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
planId: string,
|
||||
) {
|
||||
for (const product of config.products) {
|
||||
@@ -338,7 +422,7 @@ export function getProductPlanPairByVariantId(
|
||||
throw new Error('Plan not found');
|
||||
}
|
||||
|
||||
export type PlanTypeMap = Map<string, z.output<typeof LineItemTypeSchema>>;
|
||||
export type PlanTypeMap = Map<string, z.infer<typeof LineItemTypeSchema>>;
|
||||
|
||||
/**
|
||||
* @name getPlanTypesMap
|
||||
@@ -346,7 +430,7 @@ export type PlanTypeMap = Map<string, z.output<typeof LineItemTypeSchema>>;
|
||||
* @param config
|
||||
*/
|
||||
export function getPlanTypesMap(
|
||||
config: z.output<typeof BillingSchema>,
|
||||
config: z.infer<typeof BillingSchema>,
|
||||
): PlanTypeMap {
|
||||
const planTypes: PlanTypeMap = new Map();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CancelSubscriptionParamsSchema = z.object({
|
||||
subscriptionId: z.string(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateBillingPortalSessionSchema = z.object({
|
||||
returnUrl: z.string().url(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PlanSchema } from '../create-billing-schema';
|
||||
|
||||
@@ -15,5 +15,5 @@ export const CreateBillingCheckoutSchema = z.object({
|
||||
quantity: z.number(),
|
||||
}),
|
||||
),
|
||||
metadata: z.record(z.string(), z.string()).optional(),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const TimeFilter = z.object({
|
||||
startTime: z.number(),
|
||||
endTime: z.number(),
|
||||
});
|
||||
const TimeFilter = z.object(
|
||||
{
|
||||
startTime: z.number(),
|
||||
endTime: z.number(),
|
||||
},
|
||||
{
|
||||
description: `The time range to filter the usage records. Used for Stripe`,
|
||||
},
|
||||
);
|
||||
|
||||
const PageFilter = z.object({
|
||||
page: z.number(),
|
||||
size: z.number(),
|
||||
});
|
||||
const PageFilter = z.object(
|
||||
{
|
||||
page: z.number(),
|
||||
size: z.number(),
|
||||
},
|
||||
{
|
||||
description: `The page and size to filter the usage records. Used for LS`,
|
||||
},
|
||||
);
|
||||
|
||||
export const QueryBillingUsageSchema = z.object({
|
||||
id: z.string(),
|
||||
customerId: z.string(),
|
||||
id: z.string({
|
||||
description:
|
||||
'The id of the usage record. For Stripe a meter ID, for LS a subscription item ID.',
|
||||
}),
|
||||
customerId: z.string({
|
||||
description: 'The id of the customer in the billing system',
|
||||
}),
|
||||
filter: z.union([TimeFilter, PageFilter]),
|
||||
});
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ReportBillingUsageSchema = z.object({
|
||||
id: z.string(),
|
||||
eventName: z.string().optional(),
|
||||
id: z.string({
|
||||
description:
|
||||
'The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.',
|
||||
}),
|
||||
eventName: z
|
||||
.string({
|
||||
description: 'The name of the event that triggered the usage',
|
||||
})
|
||||
.optional(),
|
||||
usage: z.object({
|
||||
quantity: z.number(),
|
||||
action: z.enum(['increment', 'set']).optional(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RetrieveCheckoutSessionSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateSubscriptionParamsSchema = z.object({
|
||||
subscriptionId: z.string().min(1),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
CancelSubscriptionParamsSchema,
|
||||
@@ -13,13 +13,13 @@ import { UpsertSubscriptionParams } from '../types';
|
||||
|
||||
export abstract class BillingStrategyProviderService {
|
||||
abstract createBillingPortalSession(
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
): Promise<{
|
||||
url: string;
|
||||
}>;
|
||||
|
||||
abstract retrieveCheckoutSession(
|
||||
params: z.output<typeof RetrieveCheckoutSessionSchema>,
|
||||
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
|
||||
): Promise<{
|
||||
checkoutToken: string | null;
|
||||
status: 'complete' | 'expired' | 'open';
|
||||
@@ -31,31 +31,31 @@ export abstract class BillingStrategyProviderService {
|
||||
}>;
|
||||
|
||||
abstract createCheckoutSession(
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
): Promise<{
|
||||
checkoutToken: string;
|
||||
}>;
|
||||
|
||||
abstract cancelSubscription(
|
||||
params: z.output<typeof CancelSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract reportUsage(
|
||||
params: z.output<typeof ReportBillingUsageSchema>,
|
||||
params: z.infer<typeof ReportBillingUsageSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
abstract queryUsage(
|
||||
params: z.output<typeof QueryBillingUsageSchema>,
|
||||
params: z.infer<typeof QueryBillingUsageSchema>,
|
||||
): Promise<{
|
||||
value: number;
|
||||
}>;
|
||||
|
||||
abstract updateSubscriptionItem(
|
||||
params: z.output<typeof UpdateSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-i18next": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -17,19 +17,19 @@ export function BillingPortalCard() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey="billing.billingPortalCardTitle" />
|
||||
<Trans i18nKey="billing:billingPortalCardTitle" />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey="billing.billingPortalCardDescription" />
|
||||
<Trans i18nKey="billing:billingPortalCardDescription" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className={'space-y-2'}>
|
||||
<div>
|
||||
<Button type="submit" data-test={'manage-billing-redirect-button'}>
|
||||
<Button data-test={'manage-billing-redirect-button'}>
|
||||
<span>
|
||||
<Trans i18nKey="billing.billingPortalCardButton" />
|
||||
<Trans i18nKey="billing:billingPortalCardButton" />
|
||||
</span>
|
||||
|
||||
<ArrowUpRight className={'h-4'} />
|
||||
|
||||
@@ -41,7 +41,7 @@ export function BillingSessionStatus({
|
||||
|
||||
<Heading level={3}>
|
||||
<span className={'mr-4 font-semibold'}>
|
||||
<Trans i18nKey={'billing.checkoutSuccessTitle'} />
|
||||
<Trans i18nKey={'billing:checkoutSuccessTitle'} />
|
||||
</span>
|
||||
🎉
|
||||
</Heading>
|
||||
@@ -49,26 +49,22 @@ export function BillingSessionStatus({
|
||||
<div className={'text-muted-foreground flex flex-col space-y-4'}>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={'billing.checkoutSuccessDescription'}
|
||||
i18nKey={'billing:checkoutSuccessDescription'}
|
||||
values={{ customerEmail }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
data-test={'checkout-success-back-link'}
|
||||
render={
|
||||
<Link href={redirectPath}>
|
||||
<span>
|
||||
<Trans i18nKey={'billing.checkoutSuccessBackButton'} />
|
||||
</span>
|
||||
<Button data-test={'checkout-success-back-link'} asChild>
|
||||
<Link href={redirectPath}>
|
||||
<span>
|
||||
<Trans i18nKey={'billing:checkoutSuccessBackButton'} />
|
||||
</span>
|
||||
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -44,11 +44,11 @@ export function CurrentLifetimeOrderCard({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey="billing.planCardTitle" />
|
||||
<Trans i18nKey="billing:planCardTitle" />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey="billing.planCardDescription" />
|
||||
<Trans i18nKey="billing:planCardDescription" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function CurrentLifetimeOrderCard({
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<span className="font-semibold">
|
||||
<Trans i18nKey="billing.detailsLabel" />
|
||||
<Trans i18nKey="billing:detailsLabel" />
|
||||
</span>
|
||||
|
||||
<LineItemDetails
|
||||
|
||||
@@ -21,7 +21,7 @@ export function CurrentPlanAlert(
|
||||
status: Enums<'subscription_status'>;
|
||||
}>,
|
||||
) {
|
||||
const prefix = 'billing.status';
|
||||
const prefix = 'billing:status';
|
||||
|
||||
const text = `${prefix}.${props.status}.description`;
|
||||
const title = `${prefix}.${props.status}.heading`;
|
||||
|
||||
@@ -23,7 +23,7 @@ export function CurrentPlanBadge(
|
||||
status: Status;
|
||||
}>,
|
||||
) {
|
||||
const text = `billing.status.${props.status}.badge`;
|
||||
const text = `billing:status.${props.status}.badge`;
|
||||
const variant = statusBadgeMap[props.status];
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,11 +48,11 @@ export function CurrentSubscriptionCard({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey="billing.planCardTitle" />
|
||||
<Trans i18nKey="billing:planCardTitle" />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey="billing.planCardDescription" />
|
||||
<Trans i18nKey="billing:planCardDescription" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function CurrentSubscriptionCard({
|
||||
|
||||
<div className="flex flex-col gap-y-1 border-y border-dashed py-4">
|
||||
<span className="font-semibold">
|
||||
<Trans i18nKey="billing.detailsLabel" />
|
||||
<Trans i18nKey="billing:detailsLabel" />
|
||||
</span>
|
||||
|
||||
<LineItemDetails
|
||||
@@ -110,12 +110,12 @@ export function CurrentSubscriptionCard({
|
||||
<InfoIcon className={'h-4 w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="billing.trialAlertTitle" />
|
||||
<Trans i18nKey="billing:trialAlertTitle" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="billing.trialAlertDescription"
|
||||
i18nKey="billing:trialAlertDescription"
|
||||
values={{
|
||||
date: formatDate(
|
||||
subscription.trial_ends_at ?? '',
|
||||
@@ -134,12 +134,12 @@ export function CurrentSubscriptionCard({
|
||||
<MessageCircleWarning className={'h-4 w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="billing.subscriptionCancelled" />
|
||||
<Trans i18nKey="billing:subscriptionCancelled" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="billing.cancelSubscriptionDate"
|
||||
i18nKey="billing:cancelSubscriptionDate"
|
||||
values={{
|
||||
date: formatDate(
|
||||
subscription.period_ends_at ?? '',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { PlusSquare } from 'lucide-react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { LineItemSchema } from '@kit/billing';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
@@ -14,14 +14,14 @@ const className = 'flex text-secondary-foreground items-center text-sm';
|
||||
|
||||
export function LineItemDetails(
|
||||
props: React.PropsWithChildren<{
|
||||
lineItems: z.output<typeof LineItemSchema>[];
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
currency: string;
|
||||
selectedInterval?: string | undefined;
|
||||
alwaysDisplayMonthlyPrice?: boolean;
|
||||
}>,
|
||||
) {
|
||||
const t = useTranslations('billing');
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const currencyCode = props?.currency.toLowerCase();
|
||||
|
||||
const shouldDisplayMonthlyPrice =
|
||||
@@ -32,16 +32,16 @@ export function LineItemDetails(
|
||||
return '';
|
||||
}
|
||||
|
||||
const i18nKey = `units.${unit}` as never;
|
||||
const i18nKey = `billing:units.${unit}`;
|
||||
|
||||
if (!t.has(i18nKey)) {
|
||||
if (!i18n.exists(i18nKey)) {
|
||||
return unit;
|
||||
}
|
||||
|
||||
return t(i18nKey, {
|
||||
count,
|
||||
defaultValue: unit,
|
||||
} as never);
|
||||
});
|
||||
};
|
||||
|
||||
const getDisplayCost = (cost: number, hasTiers: boolean) => {
|
||||
@@ -82,7 +82,7 @@ export function LineItemDetails(
|
||||
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.setupFee'}
|
||||
i18nKey={'billing:setupFee'}
|
||||
values={{
|
||||
setupFee: formatCurrency({
|
||||
currencyCode,
|
||||
@@ -111,18 +111,18 @@ export function LineItemDetails(
|
||||
<PlusSquare className={'w-3'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'billing.basePlan'} />
|
||||
<Trans i18nKey={'billing:basePlan'} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<If
|
||||
condition={props.selectedInterval}
|
||||
fallback={<Trans i18nKey={'billing.lifetime'} />}
|
||||
fallback={<Trans i18nKey={'billing:lifetime'} />}
|
||||
>
|
||||
(
|
||||
<Trans
|
||||
i18nKey={`billing.billingInterval.${props.selectedInterval}`}
|
||||
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
|
||||
/>
|
||||
)
|
||||
</If>
|
||||
@@ -149,7 +149,7 @@ export function LineItemDetails(
|
||||
<span className={'flex gap-x-2 text-sm'}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.perUnit'}
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: getUnitLabel(unit, 1),
|
||||
}}
|
||||
@@ -172,10 +172,10 @@ export function LineItemDetails(
|
||||
<span>
|
||||
<If
|
||||
condition={Boolean(unit) && !isDefaultSeatUnit}
|
||||
fallback={<Trans i18nKey={'billing.perTeamMember'} />}
|
||||
fallback={<Trans i18nKey={'billing:perTeamMember'} />}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={'billing.perUnitShort'}
|
||||
i18nKey={'billing:perUnitShort'}
|
||||
values={{
|
||||
unit: getUnitLabel(unit, 1),
|
||||
}}
|
||||
@@ -215,7 +215,7 @@ export function LineItemDetails(
|
||||
<span className={'flex space-x-1'}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.perUnit'}
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: getUnitLabel(unit, 1),
|
||||
}}
|
||||
@@ -268,11 +268,11 @@ function Tiers({
|
||||
unit,
|
||||
}: {
|
||||
currency: string;
|
||||
item: z.infer<typeof LineItemSchema>;
|
||||
unit?: string;
|
||||
item: z.output<typeof LineItemSchema>;
|
||||
}) {
|
||||
const t = useTranslations('billing');
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
|
||||
// Helper to safely convert tier values to numbers for pluralization
|
||||
// Falls back to plural form (2) for 'unlimited' values
|
||||
@@ -285,13 +285,10 @@ function Tiers({
|
||||
const getUnitLabel = (count: number) => {
|
||||
if (!unit) return '';
|
||||
|
||||
return t(
|
||||
`units.${unit}` as never,
|
||||
{
|
||||
count,
|
||||
defaultValue: unit,
|
||||
} as never,
|
||||
);
|
||||
return t(`billing:units.${unit}`, {
|
||||
count,
|
||||
defaultValue: unit,
|
||||
});
|
||||
};
|
||||
|
||||
const tiers = item.tiers?.map((tier, index) => {
|
||||
@@ -330,7 +327,7 @@ function Tiers({
|
||||
<If condition={tiersLength > 1}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.andAbove'}
|
||||
i18nKey={'billing:andAbove'}
|
||||
values={{
|
||||
unit: getUnitLabel(getSafeCount(previousTierFrom) - 1),
|
||||
previousTier: getSafeCount(previousTierFrom) - 1,
|
||||
@@ -341,7 +338,7 @@ function Tiers({
|
||||
<If condition={tiersLength === 1}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.forEveryUnit'}
|
||||
i18nKey={'billing:forEveryUnit'}
|
||||
values={{
|
||||
unit: getUnitLabel(1),
|
||||
}}
|
||||
@@ -353,7 +350,7 @@ function Tiers({
|
||||
<If condition={isIncluded}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.includedUpTo'}
|
||||
i18nKey={'billing:includedUpTo'}
|
||||
values={{
|
||||
unit: getUnitLabel(getSafeCount(upTo)),
|
||||
upTo,
|
||||
@@ -371,7 +368,7 @@ function Tiers({
|
||||
</span>{' '}
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing.fromPreviousTierUpTo'}
|
||||
i18nKey={'billing:fromPreviousTierUpTo'}
|
||||
values={{
|
||||
previousTierFrom,
|
||||
unit: getUnitLabel(1),
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { LineItemSchema } from '@kit/billing';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
type PlanCostDisplayProps = {
|
||||
primaryLineItem: z.output<typeof LineItemSchema>;
|
||||
primaryLineItem: z.infer<typeof LineItemSchema>;
|
||||
currencyCode: string;
|
||||
interval?: string;
|
||||
alwaysDisplayMonthlyPrice?: boolean;
|
||||
@@ -30,7 +30,7 @@ export function PlanCostDisplay({
|
||||
alwaysDisplayMonthlyPrice = true,
|
||||
className,
|
||||
}: PlanCostDisplayProps) {
|
||||
const locale = useLocale();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const { shouldDisplayTier, lowestTier, tierTranslationKey, displayCost } =
|
||||
useMemo(() => {
|
||||
@@ -62,8 +62,8 @@ export function PlanCostDisplay({
|
||||
isMultiTier,
|
||||
lowestTier,
|
||||
tierTranslationKey: isMultiTier
|
||||
? 'billing.startingAtPriceUnit'
|
||||
: 'billing.priceUnit',
|
||||
? 'billing:startingAtPriceUnit'
|
||||
: 'billing:priceUnit',
|
||||
displayCost: cost,
|
||||
};
|
||||
}, [primaryLineItem, interval, alwaysDisplayMonthlyPrice]);
|
||||
@@ -72,7 +72,7 @@ export function PlanCostDisplay({
|
||||
const formattedCost = formatCurrency({
|
||||
currencyCode: currencyCode.toLowerCase(),
|
||||
value: lowestTier?.cost ?? 0,
|
||||
locale: locale,
|
||||
locale: i18n.language,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -91,7 +91,7 @@ export function PlanCostDisplay({
|
||||
const formattedCost = formatCurrency({
|
||||
currencyCode: currencyCode.toLowerCase(),
|
||||
value: displayCost,
|
||||
locale: locale,
|
||||
locale: i18n.language,
|
||||
});
|
||||
|
||||
return <span className={className}>{formattedCost}</span>;
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useMemo } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
BillingConfig,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
@@ -49,7 +50,7 @@ export function PlanPicker(
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
const t = useTranslations('billing');
|
||||
const { t } = useTranslation(`billing`);
|
||||
|
||||
const intervals = useMemo(
|
||||
() => getPlanIntervals(props.config),
|
||||
@@ -136,7 +137,7 @@ export function PlanPicker(
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'flex flex-col gap-4'}>
|
||||
<FormControl>
|
||||
<FormControl id={'plan-picker-id'}>
|
||||
<RadioGroup name={field.name} value={field.value}>
|
||||
<div className={'flex space-x-1'}>
|
||||
{intervals.map((interval) => {
|
||||
@@ -146,23 +147,6 @@ export function PlanPicker(
|
||||
<label
|
||||
htmlFor={interval}
|
||||
key={interval}
|
||||
onClick={() => {
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
if (selectedProduct) {
|
||||
const plan = selectedProduct.plans.find(
|
||||
(item) => item.interval === interval,
|
||||
);
|
||||
|
||||
form.setValue('planId', plan?.id ?? '', {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'focus-within:border-primary flex items-center gap-x-2.5 rounded-md px-2.5 py-2 transition-colors',
|
||||
{
|
||||
@@ -174,6 +158,27 @@ export function PlanPicker(
|
||||
<RadioGroupItem
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
if (selectedProduct) {
|
||||
const plan = selectedProduct.plans.find(
|
||||
(item) => item.interval === interval,
|
||||
);
|
||||
|
||||
form.setValue(
|
||||
'planId',
|
||||
plan?.id ?? '',
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<span
|
||||
@@ -182,7 +187,7 @@ export function PlanPicker(
|
||||
})}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={`billing.billingInterval.${interval}`}
|
||||
i18nKey={`billing:billingInterval.${interval}`}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
@@ -239,28 +244,15 @@ export function PlanPicker(
|
||||
<RadioGroupItemLabel
|
||||
selected={selected}
|
||||
key={primaryLineItem.id}
|
||||
htmlFor={primaryLineItem.id}
|
||||
className="rounded-md !border-transparent"
|
||||
onClick={() => {
|
||||
if (selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('planId', planId, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
form.setValue('productId', product.id, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-col content-center gap-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0'
|
||||
}
|
||||
>
|
||||
<div
|
||||
<Label
|
||||
htmlFor={plan.id}
|
||||
className={
|
||||
'flex flex-col justify-center space-y-2.5'
|
||||
}
|
||||
@@ -271,11 +263,24 @@ export function PlanPicker(
|
||||
key={plan.id + selected}
|
||||
id={plan.id}
|
||||
value={plan.id}
|
||||
onClick={() => {
|
||||
if (selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('planId', planId, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
form.setValue('productId', product.id, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className="font-semibold">
|
||||
<Trans
|
||||
i18nKey={`billing.plans.${product.id}.name`}
|
||||
i18nKey={`billing:plans.${product.id}.name`}
|
||||
defaults={product.name}
|
||||
/>
|
||||
</span>
|
||||
@@ -291,7 +296,7 @@ export function PlanPicker(
|
||||
variant={'success'}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={`billing.trialPeriod`}
|
||||
i18nKey={`billing:trialPeriod`}
|
||||
values={{
|
||||
period: plan.trialDays,
|
||||
}}
|
||||
@@ -303,11 +308,11 @@ export function PlanPicker(
|
||||
|
||||
<span className={'text-muted-foreground'}>
|
||||
<Trans
|
||||
i18nKey={`billing.plans.${product.id}.description`}
|
||||
i18nKey={`billing:plans.${product.id}.description`}
|
||||
defaults={product.description}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className={
|
||||
@@ -331,10 +336,10 @@ export function PlanPicker(
|
||||
plan.paymentType === 'recurring'
|
||||
}
|
||||
fallback={
|
||||
<Trans i18nKey={`billing.lifetime`} />
|
||||
<Trans i18nKey={`billing:lifetime`} />
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={`billing.perMonth`} />
|
||||
<Trans i18nKey={`billing:perMonth`} />
|
||||
</If>
|
||||
</span>
|
||||
</div>
|
||||
@@ -362,7 +367,6 @@ export function PlanPicker(
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
data-test="checkout-submit-button"
|
||||
disabled={props.pending ?? !form.formState.isValid}
|
||||
>
|
||||
@@ -404,7 +408,7 @@ function PlanDetails({
|
||||
selectedInterval: string;
|
||||
|
||||
selectedPlan: {
|
||||
lineItems: z.output<typeof LineItemSchema>[];
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
paymentType: string;
|
||||
};
|
||||
}) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
BillingConfig,
|
||||
@@ -122,14 +122,14 @@ function PricingItem(
|
||||
|
||||
selectable: boolean;
|
||||
|
||||
primaryLineItem: z.output<typeof LineItemSchema> | undefined;
|
||||
primaryLineItem: z.infer<typeof LineItemSchema> | undefined;
|
||||
|
||||
redirectToCheckout?: boolean;
|
||||
alwaysDisplayMonthlyPrice?: boolean;
|
||||
|
||||
plan: {
|
||||
id: string;
|
||||
lineItems: z.output<typeof LineItemSchema>[];
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
interval?: Interval;
|
||||
name?: string;
|
||||
href?: string;
|
||||
@@ -154,19 +154,19 @@ function PricingItem(
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
const t = useTranslations();
|
||||
const { t, i18n } = useTranslation();
|
||||
const highlighted = props.product.highlighted ?? false;
|
||||
const lineItem = props.primaryLineItem!;
|
||||
const isCustom = props.plan.custom ?? false;
|
||||
|
||||
const i18nKey = `billing.units.${lineItem.unit}` as never;
|
||||
const i18nKey = `billing:units.${lineItem.unit}`;
|
||||
|
||||
const unitLabel = lineItem?.unit
|
||||
? t.has(i18nKey)
|
||||
? i18n.exists(i18nKey)
|
||||
? t(i18nKey, {
|
||||
count: 1,
|
||||
defaultValue: lineItem.unit,
|
||||
} as never)
|
||||
})
|
||||
: lineItem.unit
|
||||
: '';
|
||||
|
||||
@@ -260,10 +260,10 @@ function PricingItem(
|
||||
<span>
|
||||
<If
|
||||
condition={props.plan.interval}
|
||||
fallback={<Trans i18nKey={'billing.lifetime'} />}
|
||||
fallback={<Trans i18nKey={'billing:lifetime'} />}
|
||||
>
|
||||
{(interval) => (
|
||||
<Trans i18nKey={`billing.billingInterval.${interval}`} />
|
||||
<Trans i18nKey={`billing:billingInterval.${interval}`} />
|
||||
)}
|
||||
</If>
|
||||
</span>
|
||||
@@ -279,10 +279,10 @@ function PricingItem(
|
||||
<If condition={lineItem?.type === 'per_seat'}>
|
||||
<If
|
||||
condition={Boolean(lineItem?.unit) && !isDefaultSeatUnit}
|
||||
fallback={<Trans i18nKey={'billing.perTeamMember'} />}
|
||||
fallback={<Trans i18nKey={'billing:perTeamMember'} />}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={'billing.perUnitShort'}
|
||||
i18nKey={'billing:perUnitShort'}
|
||||
values={{
|
||||
unit: unitLabel,
|
||||
}}
|
||||
@@ -294,7 +294,7 @@ function PricingItem(
|
||||
condition={lineItem?.type !== 'per_seat' && lineItem?.unit}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={'billing.perUnit'}
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: lineItem?.unit,
|
||||
}}
|
||||
@@ -343,7 +343,7 @@ function PricingItem(
|
||||
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<h6 className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing.detailsLabel'} />
|
||||
<Trans i18nKey={'billing:detailsLabel'} />
|
||||
</h6>
|
||||
|
||||
<LineItemDetails
|
||||
@@ -402,7 +402,7 @@ function Price({
|
||||
<span className={'text-muted-foreground text-sm leading-loose'}>
|
||||
<span>/</span>
|
||||
|
||||
<Trans i18nKey={'billing.perMonth'} />
|
||||
<Trans i18nKey={'billing:perMonth'} />
|
||||
</span>
|
||||
</If>
|
||||
</div>
|
||||
@@ -446,41 +446,41 @@ function PlanIntervalSwitcher(
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'hover:border-border border-border/50 flex gap-x-0 rounded-full border'
|
||||
'hover:border-border flex gap-x-1 rounded-full border border-transparent transition-colors'
|
||||
}
|
||||
>
|
||||
{props.intervals.map((plan, index) => {
|
||||
const selected = plan === props.interval;
|
||||
|
||||
const className = cn(
|
||||
'animate-in fade-in rounded-full transition-all focus:!ring-0',
|
||||
'animate-in fade-in rounded-full !outline-hidden transition-all focus:!ring-0',
|
||||
{
|
||||
'border-r-transparent': index === 0,
|
||||
['hover:text-primary text-muted-foreground']: !selected,
|
||||
['cursor-default']: selected,
|
||||
['cursor-default font-semibold']: selected,
|
||||
['hover:bg-initial']: !selected,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={'sm'}
|
||||
key={plan}
|
||||
variant={selected ? 'secondary' : 'custom'}
|
||||
size={'sm'}
|
||||
variant={selected ? 'secondary' : 'ghost'}
|
||||
className={className}
|
||||
onClick={() => props.setInterval(plan)}
|
||||
>
|
||||
<span className={'flex items-center'}>
|
||||
<CheckCircle
|
||||
className={cn(
|
||||
'animate-in fade-in zoom-in-50 mr-1 size-3 duration-200',
|
||||
{
|
||||
hidden: !selected,
|
||||
},
|
||||
)}
|
||||
className={cn('animate-in fade-in zoom-in-95 h-3', {
|
||||
hidden: !selected,
|
||||
'slide-in-from-left-4': index === 0,
|
||||
'slide-in-from-right-4': index === props.intervals.length - 1,
|
||||
})}
|
||||
/>
|
||||
|
||||
<span className={'text-xs capitalize'}>
|
||||
<Trans i18nKey={`billing.billingInterval.${plan}`} />
|
||||
<span className={'capitalize'}>
|
||||
<Trans i18nKey={`common:billingInterval.${plan}`} />
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
@@ -509,7 +509,7 @@ function DefaultCheckoutButton(
|
||||
highlighted?: boolean;
|
||||
}>,
|
||||
) {
|
||||
const t = useTranslations('billing');
|
||||
const { t } = useTranslation('billing');
|
||||
|
||||
const signUpPath = props.paths.signUp;
|
||||
|
||||
@@ -522,7 +522,7 @@ function DefaultCheckoutButton(
|
||||
const linkHref =
|
||||
props.plan.href ?? `${signUpPath}?${searchParams.toString()}`;
|
||||
|
||||
const label = props.plan.buttonLabel ?? 'common.getStartedWithPlan';
|
||||
const label = props.plan.buttonLabel ?? 'common:getStartedWithPlan';
|
||||
|
||||
return (
|
||||
<Link className={'w-full'} href={linkHref}>
|
||||
@@ -536,9 +536,9 @@ function DefaultCheckoutButton(
|
||||
i18nKey={label}
|
||||
defaults={label}
|
||||
values={{
|
||||
plan: t.has(props.product.name as never)
|
||||
? t(props.product.name as never)
|
||||
: props.product.name,
|
||||
plan: t(props.product.name, {
|
||||
defaultValue: props.product.name,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'server-only';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type BillingProviderSchema,
|
||||
@@ -20,7 +20,7 @@ export function createBillingEventHandlerFactoryService(
|
||||
// Create a registry for billing webhook handlers
|
||||
const billingWebhookHandlerRegistry = createRegistry<
|
||||
BillingWebhookHandlerService,
|
||||
z.output<typeof BillingProviderSchema>
|
||||
z.infer<typeof BillingProviderSchema>
|
||||
>();
|
||||
|
||||
// Register the Stripe webhook handler
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'server-only';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type BillingProviderSchema,
|
||||
@@ -11,7 +11,7 @@ import { createRegistry } from '@kit/shared/registry';
|
||||
// Create a registry for billing strategy providers
|
||||
export const billingStrategyRegistry = createRegistry<
|
||||
BillingStrategyProviderService,
|
||||
z.output<typeof BillingProviderSchema>
|
||||
z.infer<typeof BillingProviderSchema>
|
||||
>();
|
||||
|
||||
// Register the Stripe billing strategy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { BillingProviderSchema } from '@kit/billing';
|
||||
import {
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { billingStrategyRegistry } from './billing-gateway-registry';
|
||||
|
||||
export function createBillingGatewayService(
|
||||
provider: z.output<typeof BillingProviderSchema>,
|
||||
provider: z.infer<typeof BillingProviderSchema>,
|
||||
) {
|
||||
return new BillingGatewayService(provider);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function createBillingGatewayService(
|
||||
*/
|
||||
class BillingGatewayService {
|
||||
constructor(
|
||||
private readonly provider: z.output<typeof BillingProviderSchema>,
|
||||
private readonly provider: z.infer<typeof BillingProviderSchema>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ class BillingGatewayService {
|
||||
*
|
||||
*/
|
||||
async createCheckoutSession(
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = CreateBillingCheckoutSchema.parse(params);
|
||||
@@ -54,7 +54,7 @@ class BillingGatewayService {
|
||||
* @param {RetrieveCheckoutSessionSchema} params - The parameters to retrieve the checkout session.
|
||||
*/
|
||||
async retrieveCheckoutSession(
|
||||
params: z.output<typeof RetrieveCheckoutSessionSchema>,
|
||||
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
|
||||
) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = RetrieveCheckoutSessionSchema.parse(params);
|
||||
@@ -68,7 +68,7 @@ class BillingGatewayService {
|
||||
* @param {CreateBillingPortalSessionSchema} params - The parameters to create the billing portal session.
|
||||
*/
|
||||
async createBillingPortalSession(
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = CreateBillingPortalSessionSchema.parse(params);
|
||||
@@ -82,7 +82,7 @@ class BillingGatewayService {
|
||||
* @param {CancelSubscriptionParamsSchema} params - The parameters for cancelling the subscription.
|
||||
*/
|
||||
async cancelSubscription(
|
||||
params: z.output<typeof CancelSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = CancelSubscriptionParamsSchema.parse(params);
|
||||
@@ -95,7 +95,7 @@ class BillingGatewayService {
|
||||
* @description This is used to report the usage of the billing to the provider.
|
||||
* @param params
|
||||
*/
|
||||
async reportUsage(params: z.output<typeof ReportBillingUsageSchema>) {
|
||||
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = ReportBillingUsageSchema.parse(params);
|
||||
|
||||
@@ -107,7 +107,7 @@ class BillingGatewayService {
|
||||
* @description Queries the usage of the metered billing.
|
||||
* @param params
|
||||
*/
|
||||
async queryUsage(params: z.output<typeof QueryBillingUsageSchema>) {
|
||||
async queryUsage(params: z.infer<typeof QueryBillingUsageSchema>) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = QueryBillingUsageSchema.parse(params);
|
||||
|
||||
@@ -129,7 +129,7 @@ class BillingGatewayService {
|
||||
* @param params
|
||||
*/
|
||||
async updateSubscriptionItem(
|
||||
params: z.output<typeof UpdateSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
) {
|
||||
const strategy = await this.getStrategy();
|
||||
const payload = UpdateSubscriptionParamsSchema.parse(params);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'server-only';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
BillingConfig,
|
||||
@@ -24,7 +24,7 @@ export async function resolveProductPlan(
|
||||
currency: string,
|
||||
): Promise<{
|
||||
product: ProductSchema;
|
||||
plan: z.output<typeof PlanSchema>;
|
||||
plan: z.infer<typeof PlanSchema>;
|
||||
}> {
|
||||
// we can't always guarantee that the plan will be present in the local config
|
||||
// so we need to fallback to fetching the plan details from the billing provider
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* @name getLemonSqueezyEnv
|
||||
@@ -10,18 +10,18 @@ export const getLemonSqueezyEnv = () =>
|
||||
.object({
|
||||
secretKey: z
|
||||
.string({
|
||||
error: `The secret key you created for your store. Please use the variable LEMON_SQUEEZY_SECRET_KEY to set it.`,
|
||||
description: `The secret key you created for your store. Please use the variable LEMON_SQUEEZY_SECRET_KEY to set it.`,
|
||||
})
|
||||
.min(1),
|
||||
webhooksSecret: z
|
||||
.string({
|
||||
error: `The shared secret you created for your webhook. Please use the variable LEMON_SQUEEZY_SIGNING_SECRET to set it.`,
|
||||
description: `The shared secret you created for your webhook. Please use the variable LEMON_SQUEEZY_SIGNING_SECRET to set it.`,
|
||||
})
|
||||
.min(1)
|
||||
.max(40),
|
||||
storeId: z
|
||||
.string({
|
||||
error: `The ID of your store. Please use the variable LEMON_SQUEEZY_STORE_ID to set it.`,
|
||||
description: `The ID of your store. Please use the variable LEMON_SQUEEZY_STORE_ID to set it.`,
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getCustomer } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CreateBillingPortalSessionSchema } from '@kit/billing/schema';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk';
|
||||
* @param {object} params - The parameters required to create the billing portal session.
|
||||
*/
|
||||
export async function createLemonSqueezyBillingPortalSession(
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
await initializeLemonSqueezyClient();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
createCheckout,
|
||||
getCustomer,
|
||||
} from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CreateBillingCheckoutSchema } from '@kit/billing/schema';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk';
|
||||
* Creates a checkout for a Lemon Squeezy product.
|
||||
*/
|
||||
export async function createLemonSqueezyCheckout(
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
await initializeLemonSqueezyClient();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
listUsageRecords,
|
||||
updateSubscriptionItem,
|
||||
} from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BillingStrategyProviderService } from '@kit/billing';
|
||||
import type {
|
||||
@@ -40,7 +40,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async createCheckoutSession(
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -78,7 +78,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async createBillingPortalSession(
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -117,7 +117,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async cancelSubscription(
|
||||
params: z.output<typeof CancelSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -165,7 +165,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async retrieveCheckoutSession(
|
||||
params: z.output<typeof RetrieveCheckoutSessionSchema>,
|
||||
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -209,7 +209,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @description Reports the usage of the billing
|
||||
* @param params
|
||||
*/
|
||||
async reportUsage(params: z.output<typeof ReportBillingUsageSchema>) {
|
||||
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
@@ -248,7 +248,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async queryUsage(
|
||||
params: z.output<typeof QueryBillingUsageSchema>,
|
||||
params: z.infer<typeof QueryBillingUsageSchema>,
|
||||
): Promise<{ value: number }> {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -312,7 +312,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
||||
* @param params
|
||||
*/
|
||||
async updateSubscriptionItem(
|
||||
params: z.output<typeof UpdateSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ function EmbeddedCheckoutPopup({
|
||||
<Dialog
|
||||
defaultOpen
|
||||
open={open}
|
||||
disablePointerDismissal
|
||||
onOpenChange={(open) => {
|
||||
if (!open && onClose) {
|
||||
onClose();
|
||||
@@ -64,6 +63,9 @@ function EmbeddedCheckoutPopup({
|
||||
maxHeight: '98vh',
|
||||
}}
|
||||
className={className}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogTitle className={'hidden'}>Checkout</DialogTitle>
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const StripeClientEnvSchema = z
|
||||
.object({
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const StripeServerEnvSchema = z
|
||||
.object({
|
||||
secretKey: z
|
||||
.string({
|
||||
error: `Please provide the variable STRIPE_SECRET_KEY`,
|
||||
required_error: `Please provide the variable STRIPE_SECRET_KEY`,
|
||||
})
|
||||
.min(1),
|
||||
webhooksSecret: z
|
||||
.string({
|
||||
error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
|
||||
required_error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Stripe } from 'stripe';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CreateBillingPortalSessionSchema } from '@kit/billing/schema';
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { CreateBillingPortalSessionSchema } from '@kit/billing/schema';
|
||||
*/
|
||||
export async function createStripeBillingPortalSession(
|
||||
stripe: Stripe,
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
return stripe.billingPortal.sessions.create({
|
||||
customer: params.customerId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Stripe } from 'stripe';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CreateBillingCheckoutSchema } from '@kit/billing/schema';
|
||||
|
||||
@@ -17,7 +17,7 @@ const enableTrialWithoutCreditCard =
|
||||
*/
|
||||
export async function createStripeCheckout(
|
||||
stripe: Stripe,
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
// in MakerKit, a subscription belongs to an organization,
|
||||
// rather than to a user
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
|
||||
import type { Stripe } from 'stripe';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BillingStrategyProviderService } from '@kit/billing';
|
||||
import type {
|
||||
@@ -35,7 +35,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @param params
|
||||
*/
|
||||
async createCheckoutSession(
|
||||
params: z.output<typeof CreateBillingCheckoutSchema>,
|
||||
params: z.infer<typeof CreateBillingCheckoutSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
@@ -67,7 +67,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @param params
|
||||
*/
|
||||
async createBillingPortalSession(
|
||||
params: z.output<typeof CreateBillingPortalSessionSchema>,
|
||||
params: z.infer<typeof CreateBillingPortalSessionSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
@@ -96,7 +96,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @param params
|
||||
*/
|
||||
async cancelSubscription(
|
||||
params: z.output<typeof CancelSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof CancelSubscriptionParamsSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
@@ -139,7 +139,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @param params
|
||||
*/
|
||||
async retrieveCheckoutSession(
|
||||
params: z.output<typeof RetrieveCheckoutSessionSchema>,
|
||||
params: z.infer<typeof RetrieveCheckoutSessionSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
@@ -183,7 +183,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @description Reports usage for a subscription with the Metrics API
|
||||
* @param params
|
||||
*/
|
||||
async reportUsage(params: z.output<typeof ReportBillingUsageSchema>) {
|
||||
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -230,7 +230,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @name queryUsage
|
||||
* @description Reports the total usage for a subscription with the Metrics API
|
||||
*/
|
||||
async queryUsage(params: z.output<typeof QueryBillingUsageSchema>) {
|
||||
async queryUsage(params: z.infer<typeof QueryBillingUsageSchema>) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -287,7 +287,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
||||
* @param params
|
||||
*/
|
||||
async updateSubscriptionItem(
|
||||
params: z.output<typeof UpdateSubscriptionParamsSchema>,
|
||||
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
Reference in New Issue
Block a user