Update translations and add trial eligibility in plan picker
Added translations in "plan-picker" for better localization support. Introduced a new functionality to check trial eligibility where existing customers can't start a trial. Removed unnecessary billing line items like 'per-seat' and 'metered'. Also, made significant changes in multiple files to align with the updated internationalization best practices. The changes aim to make application more user-friendly across different locales and provide accurate trial period conditions.
This commit is contained in:
@@ -20,11 +20,16 @@ import billingConfig from '~/config/billing.config';
|
|||||||
|
|
||||||
import { createPersonalAccountCheckoutSession } from '../server-actions';
|
import { createPersonalAccountCheckoutSession } from '../server-actions';
|
||||||
|
|
||||||
export function PersonalAccountCheckoutForm() {
|
export function PersonalAccountCheckoutForm(props: {
|
||||||
|
customerId: string | null | undefined;
|
||||||
|
}) {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [checkoutToken, setCheckoutToken] = useState<string>();
|
const [checkoutToken, setCheckoutToken] = useState<string>();
|
||||||
|
|
||||||
|
// only allow trial if the user is not already a customer
|
||||||
|
const canStartTrial = !props.customerId;
|
||||||
|
|
||||||
// If the checkout token is set, render the embedded checkout component
|
// If the checkout token is set, render the embedded checkout component
|
||||||
if (checkoutToken) {
|
if (checkoutToken) {
|
||||||
return (
|
return (
|
||||||
@@ -40,10 +45,12 @@ export function PersonalAccountCheckoutForm() {
|
|||||||
<div>
|
<div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Manage your Plan</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'common:planCardLabel'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You can change your plan at any time.
|
<Trans i18nKey={'common:planCardDescription'} />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -55,6 +62,7 @@ export function PersonalAccountCheckoutForm() {
|
|||||||
<PlanPicker
|
<PlanPicker
|
||||||
pending={pending}
|
pending={pending}
|
||||||
config={billingConfig}
|
config={billingConfig}
|
||||||
|
canStartTrial={canStartTrial}
|
||||||
onSubmit={({ planId, productId }) => {
|
onSubmit={({ planId, productId }) => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ async function PersonalAccountBillingPage() {
|
|||||||
<div className={'flex flex-col space-y-8'}>
|
<div className={'flex flex-col space-y-8'}>
|
||||||
<If
|
<If
|
||||||
condition={subscription}
|
condition={subscription}
|
||||||
fallback={<PersonalAccountCheckoutForm />}
|
fallback={<PersonalAccountCheckoutForm customerId={customerId} />}
|
||||||
>
|
>
|
||||||
{(subscription) => (
|
{(subscription) => (
|
||||||
<CurrentPlanCard
|
<CurrentPlanCard
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import billingConfig from '~/config/billing.config';
|
|||||||
|
|
||||||
import { createTeamAccountCheckoutSession } from '../server-actions';
|
import { createTeamAccountCheckoutSession } from '../server-actions';
|
||||||
|
|
||||||
export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
export function TeamAccountCheckoutForm(params: {
|
||||||
|
accountId: string;
|
||||||
|
customerId: string | null | undefined;
|
||||||
|
}) {
|
||||||
const routeParams = useParams();
|
const routeParams = useParams();
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const [checkoutToken, setCheckoutToken] = useState<string | null>(null);
|
const [checkoutToken, setCheckoutToken] = useState<string | null>(null);
|
||||||
@@ -33,6 +36,9 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only allow trial if the user is not already a customer
|
||||||
|
const canStartTrial = !params.customerId;
|
||||||
|
|
||||||
// Otherwise, render the plan picker component
|
// Otherwise, render the plan picker component
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -50,6 +56,7 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
|||||||
<PlanPicker
|
<PlanPicker
|
||||||
pending={pending}
|
pending={pending}
|
||||||
config={billingConfig}
|
config={billingConfig}
|
||||||
|
canStartTrial={canStartTrial}
|
||||||
onSubmit={({ planId, productId }) => {
|
onSubmit={({ planId, productId }) => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const slug = routeParams.account as string;
|
const slug = routeParams.account as string;
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ async function TeamAccountBillingPage({ params }: Params) {
|
|||||||
condition={subscription}
|
condition={subscription}
|
||||||
fallback={
|
fallback={
|
||||||
<If condition={canManageBilling}>
|
<If condition={canManageBilling}>
|
||||||
<TeamAccountCheckoutForm accountId={accountId} />
|
<TeamAccountCheckoutForm
|
||||||
|
customerId={customerId}
|
||||||
|
accountId={accountId}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -89,6 +92,7 @@ function CannotManageBillingAlert() {
|
|||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'billing:cannotManageBillingAlertTitle'} />
|
<Trans i18nKey={'billing:cannotManageBillingAlertTitle'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'billing:cannotManageBillingAlertDescription'} />
|
<Trans i18nKey={'billing:cannotManageBillingAlertDescription'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -28,22 +28,6 @@ export default createBillingSchema({
|
|||||||
cost: 9.99,
|
cost: 9.99,
|
||||||
type: 'base',
|
type: 'base',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe6',
|
|
||||||
name: 'Per Seat',
|
|
||||||
description: 'Add-on plan',
|
|
||||||
cost: 1.99,
|
|
||||||
type: 'per-seat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe7',
|
|
||||||
name: 'Metered',
|
|
||||||
description: 'Metered plan',
|
|
||||||
cost: 0.99,
|
|
||||||
type: 'metered',
|
|
||||||
unit: 'GB',
|
|
||||||
included: 10,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,23 @@
|
|||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.",
|
||||||
"manageTeamPlan": "Manage your Team Plan",
|
"manageTeamPlan": "Manage your Team Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
|
"flatSubscription": "Flat Subscription",
|
||||||
|
"billingInterval": {
|
||||||
|
"label": "Choose your billing interval",
|
||||||
|
"month": "Billed monthly",
|
||||||
|
"year": "Billed yearly"
|
||||||
|
},
|
||||||
|
"trialPeriod": "{{period}} day trial",
|
||||||
|
"perPeriod": "per {{period}}",
|
||||||
|
"processing": "Processing...",
|
||||||
|
"proceedToPayment": "Proceed to Payment",
|
||||||
|
"startTrial": "Start Trial",
|
||||||
|
"perTeamMember": "Per team member",
|
||||||
|
"perUnitIncluded": "({{included}} included)",
|
||||||
|
"featuresLabel": "Features",
|
||||||
|
"detailsLabel": "Details",
|
||||||
|
"planPickerLabel": "Pick your preferred plan",
|
||||||
|
"planCardLabel": "Manage your Plan",
|
||||||
"status": {
|
"status": {
|
||||||
"free": {
|
"free": {
|
||||||
"badge": "Free Plan",
|
"badge": "Free Plan",
|
||||||
|
|||||||
@@ -57,9 +57,5 @@
|
|||||||
"label": "Member",
|
"label": "Member",
|
||||||
"description": "Cannot invite members or change settings"
|
"description": "Cannot invite members or change settings"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"billingInterval": {
|
|
||||||
"month": "Billed monthly",
|
|
||||||
"year": "Billed yearly"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { ArrowRight, CheckCircle } from 'lucide-react';
|
import { ArrowRight, CheckCircle } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +33,6 @@ import {
|
|||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
RadioGroupItemLabel,
|
RadioGroupItemLabel,
|
||||||
} from '@kit/ui/radio-group';
|
} from '@kit/ui/radio-group';
|
||||||
import { Separator } from '@kit/ui/separator';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ export function PlanPicker(
|
|||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
config: BillingConfig;
|
config: BillingConfig;
|
||||||
onSubmit: (data: { planId: string; productId: string }) => void;
|
onSubmit: (data: { planId: string; productId: string }) => void;
|
||||||
|
canStartTrial?: boolean;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
@@ -94,6 +95,8 @@ export function PlanPicker(
|
|||||||
}
|
}
|
||||||
}, [props.config, planId]);
|
}, [props.config, planId]);
|
||||||
|
|
||||||
|
const { t } = useTranslation(`billing`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<div
|
<div
|
||||||
@@ -111,7 +114,7 @@ export function PlanPicker(
|
|||||||
return (
|
return (
|
||||||
<FormItem className={'rounded-md border p-4'}>
|
<FormItem className={'rounded-md border p-4'}>
|
||||||
<FormLabel htmlFor={'plan-picker-id'}>
|
<FormLabel htmlFor={'plan-picker-id'}>
|
||||||
Choose your billing interval
|
<Trans i18nKey={'common:billingInterval.label'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl id={'plan-picker-id'}>
|
<FormControl id={'plan-picker-id'}>
|
||||||
@@ -148,7 +151,7 @@ export function PlanPicker(
|
|||||||
|
|
||||||
<span className={'text-sm font-bold'}>
|
<span className={'text-sm font-bold'}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={`common:billingInterval.${interval}`}
|
i18nKey={`billing:billingInterval.${interval}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -167,7 +170,9 @@ export function PlanPicker(
|
|||||||
name={'planId'}
|
name={'planId'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Pick your preferred plan</FormLabel>
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'common:planPickerLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup name={field.name}>
|
<RadioGroup name={field.name}>
|
||||||
@@ -215,10 +220,18 @@ export function PlanPicker(
|
|||||||
'flex flex-col justify-center space-y-2'
|
'flex flex-col justify-center space-y-2'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="font-bold">{product.name}</span>
|
<span className="font-bold">
|
||||||
|
<Trans
|
||||||
|
i18nKey={`billing:products.${product.id}.name`}
|
||||||
|
defaults={product.name}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className={'text-muted-foreground'}>
|
<span className={'text-muted-foreground'}>
|
||||||
{product.description}
|
<Trans
|
||||||
|
i18nKey={`billing:products.${product.id}.description`}
|
||||||
|
defaults={product.description}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
@@ -227,10 +240,19 @@ export function PlanPicker(
|
|||||||
'flex items-center space-x-4 text-right'
|
'flex items-center space-x-4 text-right'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<If condition={plan.trialPeriod}>
|
<If
|
||||||
|
condition={
|
||||||
|
plan.trialPeriod && props.canStartTrial
|
||||||
|
}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Badge variant={'success'}>
|
<Badge variant={'success'}>
|
||||||
{plan.trialPeriod} day trial
|
<Trans
|
||||||
|
i18nKey={`billing:trialPeriod`}
|
||||||
|
values={{
|
||||||
|
period: plan.trialPeriod,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
@@ -247,7 +269,12 @@ export function PlanPicker(
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className={'text-muted-foreground'}>
|
<span className={'text-muted-foreground'}>
|
||||||
per {selectedInterval}
|
<Trans
|
||||||
|
i18nKey={`billing:perPeriod`}
|
||||||
|
values={{
|
||||||
|
period: selectedInterval,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,14 +294,14 @@ export function PlanPicker(
|
|||||||
<div>
|
<div>
|
||||||
<Button disabled={props.pending ?? !form.formState.isValid}>
|
<Button disabled={props.pending ?? !form.formState.isValid}>
|
||||||
{props.pending ? (
|
{props.pending ? (
|
||||||
'Processing...'
|
t('processing')
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<If
|
<If
|
||||||
condition={selectedPlan?.trialPeriod}
|
condition={selectedPlan?.trialPeriod && props.canStartTrial}
|
||||||
fallback={'Proceed to payment'}
|
fallback={t(`proceedToPayment`)}
|
||||||
>
|
>
|
||||||
<span>Start {selectedPlan?.trialPeriod} day trial</span>
|
<span>{t(`startTrial`)}</span>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<ArrowRight className={'ml-2 h-4 w-4'} />
|
<ArrowRight className={'ml-2 h-4 w-4'} />
|
||||||
@@ -292,18 +319,32 @@ export function PlanPicker(
|
|||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-0.5'}>
|
<div className={'flex flex-col space-y-0.5'}>
|
||||||
<Heading level={5}>
|
<Heading level={5}>
|
||||||
<b>{selectedProduct?.name}</b>
|
<b>
|
||||||
|
<Trans
|
||||||
|
i18nKey={`billing:products.${selectedProduct?.id}.name`}
|
||||||
|
defaults={selectedProduct?.name}
|
||||||
|
/>
|
||||||
|
</b>{' '}
|
||||||
|
/{' '}
|
||||||
|
<Trans
|
||||||
|
i18nKey={`billing:billingInterval.${selectedInterval}`}
|
||||||
|
/>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<span className={'text-muted-foreground'}>
|
<span className={'text-muted-foreground'}>
|
||||||
{selectedProduct?.description}
|
<Trans
|
||||||
|
i18nKey={`billing:products.${selectedProduct?.id}.description`}
|
||||||
|
defaults={selectedProduct?.description}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-1'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<span className={'font-semibold'}>Details</span>
|
<span className={'font-semibold'}>
|
||||||
|
<Trans i18nKey={'billing:detailsLabel'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className={'flex flex-col divide-y'}>
|
<div className={'flex flex-col divide-y'}>
|
||||||
{selectedPlan?.lineItems.map((item) => {
|
{selectedPlan?.lineItems.map((item) => {
|
||||||
@@ -316,7 +357,20 @@ export function PlanPicker(
|
|||||||
'flex items-center justify-between py-1.5 text-sm'
|
'flex items-center justify-between py-1.5 text-sm'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>{item.name}</span>
|
<span className={'flex space-x-2'}>
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey={'billing:flatSubscription'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>/</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<Trans
|
||||||
|
i18nKey={`billing:billingInterval.${selectedInterval}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className={'font-semibold'}>
|
<span className={'font-semibold'}>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
selectedProduct?.currency.toLowerCase(),
|
selectedProduct?.currency.toLowerCase(),
|
||||||
@@ -334,7 +388,10 @@ export function PlanPicker(
|
|||||||
'flex items-center justify-between py-1.5 text-sm'
|
'flex items-center justify-between py-1.5 text-sm'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>Per team member</span>
|
<span>
|
||||||
|
<Trans i18nKey={'billing:perTeamMember'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className={'font-semibold'}>
|
<span className={'font-semibold'}>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
selectedProduct?.currency.toLowerCase(),
|
selectedProduct?.currency.toLowerCase(),
|
||||||
@@ -353,11 +410,25 @@ export function PlanPicker(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Per {item.unit}
|
<Trans
|
||||||
{item.included
|
i18nKey={'billing:perUnit'}
|
||||||
? ` (${item.included} included)`
|
values={{
|
||||||
: ''}
|
unit: item.unit,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{item.included ? (
|
||||||
|
<Trans
|
||||||
|
i18nKey={'billing:perUnitIncluded'}
|
||||||
|
values={{
|
||||||
|
included: item.included,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className={'font-semibold'}>
|
<span className={'font-semibold'}>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
selectedProduct?.currency.toLowerCase(),
|
selectedProduct?.currency.toLowerCase(),
|
||||||
@@ -372,7 +443,9 @@ export function PlanPicker(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-2'}>
|
||||||
<span className={'font-semibold'}>Features</span>
|
<span className={'font-semibold'}>
|
||||||
|
<Trans i18nKey={'billing:featuresLabel'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
{selectedProduct?.features.map((item) => {
|
{selectedProduct?.features.map((item) => {
|
||||||
return (
|
return (
|
||||||
@@ -382,7 +455,12 @@ export function PlanPicker(
|
|||||||
>
|
>
|
||||||
<CheckCircle className={'h-4 text-green-500'} />
|
<CheckCircle className={'h-4 text-green-500'} />
|
||||||
|
|
||||||
<span className={'text-muted-foreground'}>{item}</span>
|
<span className={'text-muted-foreground'}>
|
||||||
|
<Trans
|
||||||
|
i18nKey={`billing:features.${item}`}
|
||||||
|
defaults={item}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -13,13 +13,39 @@ export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
|
|||||||
|
|
||||||
export const LineItemSchema = z
|
export const LineItemSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().min(1),
|
id: z
|
||||||
name: z.string().min(1),
|
.string({
|
||||||
description: z.string().optional(),
|
description:
|
||||||
cost: z.number().positive(),
|
'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.',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
cost: z
|
||||||
|
.number({
|
||||||
|
description: 'Cost of the line item. Displayed to the user.',
|
||||||
|
})
|
||||||
|
.min(0),
|
||||||
type: LineItemTypeSchema,
|
type: LineItemTypeSchema,
|
||||||
unit: z.string().optional(),
|
unit: z
|
||||||
included: z.number().optional(),
|
.string({
|
||||||
|
description:
|
||||||
|
'Unit of the line item. Displayed to the user. Example "seat" or "GB"',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
included: z
|
||||||
|
.number({
|
||||||
|
description: 'Included amount of the line item. Displayed to the user.',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.refine((data) => data.type !== 'metered' || (data.unit && data.included), {
|
.refine((data) => data.type !== 'metered' || (data.unit && data.included), {
|
||||||
message: 'Metered line items must have a unit and included amount',
|
message: 'Metered line items must have a unit and included amount',
|
||||||
@@ -28,12 +54,25 @@ export const LineItemSchema = z
|
|||||||
|
|
||||||
export const PlanSchema = z
|
export const PlanSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().min(1),
|
id: z
|
||||||
name: z.string().min(1),
|
.string({
|
||||||
description: z.string().optional(),
|
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(),
|
interval: BillingIntervalSchema.optional(),
|
||||||
lineItems: z.array(LineItemSchema),
|
lineItems: z.array(LineItemSchema),
|
||||||
trialPeriod: z.number().optional(),
|
trialPeriod: z
|
||||||
|
.number({
|
||||||
|
description:
|
||||||
|
'Number of days for the trial period. Leave empty for no trial.',
|
||||||
|
})
|
||||||
|
.positive()
|
||||||
|
.optional(),
|
||||||
paymentType: PaymentTypeSchema,
|
paymentType: PaymentTypeSchema,
|
||||||
})
|
})
|
||||||
.refine((data) => data.lineItems.length > 0, {
|
.refine((data) => data.lineItems.length > 0, {
|
||||||
@@ -68,13 +107,40 @@ export const PlanSchema = z
|
|||||||
|
|
||||||
const ProductSchema = z
|
const ProductSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().min(1),
|
id: z
|
||||||
name: z.string().min(1),
|
.string({
|
||||||
description: z.string().min(1),
|
description:
|
||||||
currency: z.string().min(1),
|
'Unique identifier for the product. Defined by th Provider.',
|
||||||
badge: z.string().optional(),
|
})
|
||||||
|
.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()).nonempty(),
|
features: z.array(z.string()).nonempty(),
|
||||||
highlighted: z.boolean().optional(),
|
highlighted: z
|
||||||
|
.boolean({
|
||||||
|
description: 'Highlight this product. Displayed to the user.',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
plans: z.array(PlanSchema),
|
plans: z.array(PlanSchema),
|
||||||
})
|
})
|
||||||
.refine((data) => data.plans.length > 0, {
|
.refine((data) => data.plans.length > 0, {
|
||||||
|
|||||||
Reference in New Issue
Block a user