'use client'; import { useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { ArrowRight, CheckCircle } from 'lucide-react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { BillingConfig, type LineItemSchema, getPlanIntervals, getPrimaryLineItem, getProductPlanPair, } from '@kit/billing'; import { Badge } from '@kit/ui/badge'; import { Button } from '@kit/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@kit/ui/form'; import { If } from '@kit/ui/if'; import { Label } from '@kit/ui/label'; import { RadioGroup, RadioGroupItem, RadioGroupItemLabel, } from '@kit/ui/radio-group'; import { Separator } from '@kit/ui/separator'; import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; import { LineItemDetails } from './line-item-details'; import { PlanCostDisplay } from './plan-cost-display'; export function PlanPicker( props: React.PropsWithChildren<{ config: BillingConfig; onSubmit: (data: { planId: string; productId: string }) => void; canStartTrial?: boolean; pending?: boolean; }>, ) { const { t } = useTranslation(`billing`); const intervals = useMemo( () => getPlanIntervals(props.config), [props.config], ) as string[]; const form = useForm({ reValidateMode: 'onChange', mode: 'onChange', resolver: zodResolver( z .object({ planId: z.string(), productId: z.string(), interval: z.string().optional(), }) .refine( (data) => { try { const { product, plan } = getProductPlanPair( props.config, data.planId, ); return product && plan; } catch { return false; } }, { message: t('noPlanChosen'), path: ['planId'] }, ), ), defaultValues: { interval: intervals[0], planId: '', productId: '', }, }); const selectedInterval = useWatch({ name: 'interval', control: form.control, }); const planId = form.getValues('planId'); const { plan: selectedPlan, product: selectedProduct } = useMemo(() => { try { return getProductPlanPair(props.config, planId); } catch { return { plan: null, product: null, }; } }, [props.config, planId]); // display the period picker if the selected plan is recurring or if no plan is selected const isRecurringPlan = selectedPlan?.paymentType === 'recurring' || !selectedPlan; // Always filter out hidden products const visibleProducts = props.config.products.filter( (product) => !product.hidden, ); return (
{ return (
{intervals.map((interval) => { const selected = field.value === interval; return ( ); })}
); }} />
( {visibleProducts.map((product) => { const plan = product.plans.find((item) => { if (item.paymentType === 'one-time') { return true; } return item.interval === selectedInterval; }); if (!plan || plan.custom) { return null; } const planId = plan.id; const selected = field.value === planId; const primaryLineItem = getPrimaryLineItem( props.config, planId, ); if (!primaryLineItem) { throw new Error(`Base line item was not found`); } return ( { if (selected) { return; } form.setValue('planId', planId, { shouldValidate: true, }); form.setValue('productId', product.id, { shouldValidate: true, }); }} />
} >
); })}
)} />
{selectedPlan && selectedInterval && selectedProduct ? ( ) : null}
); } function PlanDetails({ selectedProduct, selectedInterval, selectedPlan, }: { selectedProduct: { id: string; name: string; description: string; currency: string; features: string[]; }; selectedInterval: string; selectedPlan: { lineItems: z.infer[]; paymentType: string; }; }) { const isRecurring = selectedPlan.paymentType === 'recurring'; // trick to force animation on re-render const key = Math.random(); return (
{' '} /

0}>
{selectedProduct.features.map((item) => { return (
); })}
); } function Price(props: React.PropsWithChildren) { return ( {props.children} ); }