import React from 'react'; import { Loader2Icon } from 'lucide-react'; import { classNames } from '../../lib/utils'; import { Button } from '../ui/button'; import type { IPlanSelection } from '../../service/user/user.interface'; import { PlanPeriod } from '../../utils/plan'; import { formatCount, formatUSD, limitWords, stopBillNote, stopMonthlyPrice, type PricingModel, type PricingPlan, } from '../../utils/pricingModel'; import { voucherAppliesTo, voucherDurationNote, voucherLabel, voucherMonthlyPrice, type AppliedVoucher, } from '../../utils/voucher'; import { PlanDots } from './PlanDots'; /** What the chosen plan's button says and whether it is live. */ export interface PlanSelectorCta { label: string; disabled: boolean; loading: boolean; /** The plan is the account's current one. */ current: boolean; } interface PlanSelectorProps { model: PricingModel; planIndex: number; period: PlanPeriod; onPeriodChange: (period: PlanPeriod) => void; /** Agents ticked on the per-agent plan. */ pickedAgents: string[]; onPickedAgentsChange: (agents: string[]) => void; /** Tapping an agent on Free jumps to the per-agent plan with it ticked. */ onJumpToStarter: (agent: string) => void; cta: PlanSelectorCta; /** The merchant is on the launch trial of this very plan: the launch price shows. */ launchMonthly: number | null; launchMonths: number; /** The list price the launch trial reverts to on this plan; the catalog's when null. */ launchListMonthly: number | null; /** A priced voucher applied on the page; the price shows it when it names this plan. */ voucher: AppliedVoucher | null; onCheckout: (selection: IPlanSelection) => void; } const LIMIT_PREFIX = 'up to '; /** * The one plan the slider stopped on, the way recomaze.ai/pricing shows it: * price and period on the left, how hard each agent runs on the right, the * agent picker and the plan's limits below. */ export const PlanSelector = ({ model, planIndex, period, onPeriodChange, pickedAgents, onPickedAgentsChange, onJumpToStarter, cta, launchMonthly, launchMonths, launchListMonthly, voucher, onCheckout, }: PlanSelectorProps): JSX.Element => { const plan: PricingPlan = model.plans[planIndex]; const isFree = !plan.soldOn[PlanPeriod.MONTHLY] && !plan.soldOn[PlanPeriod.YEARLY]; const isStarter = plan.perAgent; const shownPeriod = plan.soldOn[period] || isFree ? period : PlanPeriod.MONTHLY; const isYearly = shownPeriod === PlanPeriod.YEARLY; const units = isStarter ? Math.max(pickedAgents.length, 1) : 1; const deal = isStarter && !!plan.bundle && plan.bundle.period === shownPeriod && pickedAgents.length === plan.bundle.units; const price = stopMonthlyPrice(plan, shownPeriod, units); const listMonthly = stopMonthlyPrice(plan, PlanPeriod.MONTHLY, units); const wasPrice = isYearly && price < listMonthly ? listMonthly : deal && plan.bundle ? plan.bundle.was : 0; const credits = isStarter ? plan.credits * units : plan.credits; const showLaunch = launchMonthly !== null && cta.current && !isYearly; const launchRevertsTo: number = launchListMonthly ?? listMonthly; const voucherOn: boolean = !isFree && !cta.current && voucherAppliesTo(voucher, plan.key); const voucherPrice: number = voucherOn && voucher ? voucherMonthlyPrice(price, shownPeriod, voucher) : price; const agentFill = (agentKey: string): number => { if (isFree) return 0; if (isStarter && !deal) return pickedAgents.includes(agentKey) ? plan.fill : 0; return plan.fill; }; const limitRows = (() => { if (isFree) return []; let names: string[]; if (isStarter) { names = model.agents .filter(agent => pickedAgents.includes(agent.key)) .flatMap(agent => agent.solo); } else { const unlocked = new Set(model.agents.flatMap(agent => agent.rows)); names = model.limitRows.filter(row => unlocked.has(row)); } return names .map(row => ({ row, value: model.rowLimits[row]?.[planIndex] ?? '', prefix: model.fixedLimitRows.includes(row) ? '' : LIMIT_PREFIX, })) .filter(limit => limit.value !== ''); })(); const toggleAgent = (agentKey: string): void => { if (isFree) { onJumpToStarter(agentKey); return; } if (!isStarter) return; const on = pickedAgents.includes(agentKey); if (on && pickedAgents.length === 1) return; onPickedAgentsChange( on ? pickedAgents.filter(key => key !== agentKey) : [...pickedAgents, agentKey] ); }; const agentsTitle = isFree ? 'Add an agent to start' : isStarter ? 'Pick your agents, bundled at one price' : 'All six agents, included'; return (
{deal ? 'All six agents, for the price of one plan.' : plan.fit}
{showLaunch ? `Your launch price for the first ${launchMonths} months, then ${formatUSD(launchRevertsTo)}/mo.` : isFree ? plan.billMonthly : voucherOn && voucher ? `${voucherLabel(voucher)} ${voucherDurationNote(voucher)}. ${stopBillNote(plan, shownPeriod, units, deal)}` : stopBillNote(plan, shownPeriod, units, deal)}
{!isFree && (A month on this plan
{agentsTitle}
{isFree ? 'What you get, free' : 'What the plan does'}
{formatCount(credits)} AI credits {' '} {isFree ? 'a month to test everything' : isStarter ? `a month, ${formatCount(plan.credits)} per agent you pick` : 'a month'}
All limits are per month. "Up to" means the most the plan does; every job shows its cost before it runs.