'use client'; import { PlusSquare } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import * as z from 'zod'; import type { LineItemSchema } from '@kit/billing'; import { formatCurrency } from '@kit/shared/utils'; import { If } from '@kit/ui/if'; import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; const className = 'flex text-secondary-foreground items-center text-sm'; export function LineItemDetails( props: React.PropsWithChildren<{ lineItems: z.output[]; currency: string; selectedInterval?: string | undefined; alwaysDisplayMonthlyPrice?: boolean; }>, ) { const t = useTranslations('billing'); const locale = useLocale(); const currencyCode = props?.currency.toLowerCase(); const shouldDisplayMonthlyPrice = props.alwaysDisplayMonthlyPrice && props.selectedInterval === 'year'; const getUnitLabel = (unit: string | undefined, count: number) => { if (!unit) { return ''; } const i18nKey = `units.${unit}` as never; if (!t.has(i18nKey)) { return unit; } return t(i18nKey, { count, defaultValue: unit, } as never); }; const getDisplayCost = (cost: number, hasTiers: boolean) => { if (shouldDisplayMonthlyPrice && !hasTiers) { return cost / 12; } return cost; }; return (
{props.lineItems.map((item, index) => { // If the item has a description, we render it as a simple text // and pass the item as values to the translation so we can use // the item properties in the translation. if (item.description) { return (
); } const SetupFee = () => (
); const unit = item.unit ?? (item.type === 'per_seat' ? 'member' : undefined); const hasTiers = Boolean(item.tiers?.length); const isDefaultSeatUnit = unit === 'member'; const FlatFee = () => (
} > ( ) - {formatCurrency({ currencyCode, value: getDisplayCost(item.cost, hasTiers), locale, })}
); const PerSeat = () => (
} > - {formatCurrency({ currencyCode, value: getDisplayCost(item.cost, hasTiers), locale, })}
); const Metered = () => (
{/* If there are no tiers, there is a flat cost for usage */} {formatCurrency({ currencyCode, value: getDisplayCost(item.cost, hasTiers), locale, })}
{/* If there are tiers, we render them as a list */}
); switch (item.type) { case 'flat': return ; case 'per_seat': return ; case 'metered': { return ; } } })}
); } function Tiers({ currency, item, unit, }: { currency: string; unit?: string; item: z.output; }) { const t = useTranslations('billing'); const locale = useLocale(); // Helper to safely convert tier values to numbers for pluralization // Falls back to plural form (2) for 'unlimited' values const getSafeCount = (value: number | 'unlimited' | string): number => { if (value === 'unlimited') return 2; const num = typeof value === 'number' ? value : Number(value); return Number.isNaN(num) ? 2 : num; }; const getUnitLabel = (count: number) => { if (!unit) return ''; return t( `units.${unit}` as never, { count, defaultValue: unit, } as never, ); }; const tiers = item.tiers?.map((tier, index) => { const tiersLength = item.tiers?.length ?? 0; const previousTier = item.tiers?.[index - 1]; const isLastTier = tier.upTo === 'unlimited'; const previousTierFrom = previousTier?.upTo === 'unlimited' ? 'unlimited' : previousTier === undefined ? 0 : previousTier.upTo + 1 || 0; const upTo = tier.upTo; const previousTierUpTo = typeof previousTier?.upTo === 'number' ? previousTier.upTo : undefined; const currentTierUpTo = typeof upTo === 'number' ? upTo : undefined; const rangeCount = previousTierUpTo !== undefined && currentTierUpTo !== undefined ? currentTierUpTo - previousTierUpTo : undefined; const isIncluded = tier.cost === 0; return ( -{' '} {formatCurrency({ currencyCode: currency.toLowerCase(), value: tier.cost, locale, })} {' '} 1}> {' '} {' '} {formatCurrency({ currencyCode: currency.toLowerCase(), value: tier.cost, locale, })} {' '} ); }); return
{tiers}
; }