'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, 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 { 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; value?: { interval: string; planId: string; productId: string; }; }>, ) { 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: props.value?.interval ?? intervals[0], planId: props.value?.planId ?? '', productId: props.value?.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 (
} >
); })}
)} /> {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 // eslint-disable-next-line react-hooks/purity const key = Math.random(); return (
0}>
{selectedProduct.features.map((item) => { return ( ); })}
); } function Price(props: React.PropsWithChildren) { return ( {props.children} ); }