# Proration Preview URL: /docs/components/proration-preview Interactive component that shows billing adjustments when users change subscription plans, featuring cost breakdowns, credits, and prorated charges with immediate or next-cycle options. *** title: Proration Preview description: Interactive component that shows billing adjustments when users change subscription plans, featuring cost breakdowns, credits, and prorated charges with immediate or next-cycle options. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ## Default ```tsx title="src/components/proration-preview-demo.tsx" "use client"; import { ProrationPreview } from "@/components/billingsdk/proration-preview"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const currentPlan = { plan: { id: "pro", title: "Pro", description: "Best for small teams", monthlyPrice: "29.99", yearlyPrice: "299.99", currency: "$", buttonText: "Current Plan", features: [ { name: "Advanced features", icon: "check" }, { name: "Priority support", icon: "check" }, ], }, type: "monthly" as const, price: "29.99", nextBillingDate: "2024-01-15", paymentMethod: "•••• 4242", status: "active" as const, }; const yearlyCurrentPlan = { plan: { id: "pro-yearly", title: "Pro", description: "Best for small teams", monthlyPrice: "29.99", yearlyPrice: "299.99", currency: "$", buttonText: "Current Plan", features: [ { name: "Advanced features", icon: "check" }, { name: "Priority support", icon: "check" }, ], }, type: "yearly" as const, price: "299.99", nextBillingDate: "2024-12-15", paymentMethod: "•••• 4242", status: "active" as const, }; const basicPlan = { id: "basic", title: "Basic", description: "Perfect for getting started", monthlyPrice: "9.99", yearlyPrice: "99.99", currency: "$", buttonText: "Downgrade", features: [ { name: "Basic features", icon: "check" }, { name: "Email support", icon: "check" }, ], }; const enterprisePlan = { id: "enterprise", title: "Enterprise", description: "For large organizations", monthlyPrice: "99.99", yearlyPrice: "999.99", currency: "$", buttonText: "Upgrade", features: [ { name: "All features", icon: "check" }, { name: "Priority support", icon: "check" }, { name: "Custom integrations", icon: "check" }, ], }; const customPlan = { id: "custom", title: "Custom", description: "Tailored for your needs", monthlyPrice: "Custom", yearlyPrice: "Custom", currency: "$", buttonText: "Contact Sales", features: [ { name: "Custom features", icon: "check" }, { name: "Dedicated support", icon: "check" }, ], }; export function ProrationPreviewDemo() { return (
Upgrade Downgrade Cycle Change Next Cycle Monthly to Enterprise Upgrade Upgrading from Pro monthly to Enterprise monthly with 15 days remaining {}} onCancel={() => {}} /> Enterprise to Basic Downgrade Downgrading from Enterprise to Basic with account credit {}} onCancel={() => {}} /> Monthly to Yearly Switch Switching from monthly to yearly billing for the same plan {}} onCancel={() => {}} /> Yearly to Monthly Switch Switching from yearly to monthly billing {}} onCancel={() => {}} /> Next Cycle Change Plan change effective at the next billing cycle with no immediate charge {}} onCancel={() => {}} /> Custom Pricing Scenario Handling custom/enterprise pricing that doesn't follow standard rates {}} onCancel={() => {}} />
); } ```
## Installation ```bash yarn dlx @billingsdk/cli add proration-preview ``` **Step 1**: Install the following dependencies: ```bash npm install motion lucide-react class-variance-authority ``` **Step 2**: Copy and paste the following code into your project. ```tsx title="src/components/billingsdk/proration-preview\.tsx" "use client"; import { motion } from "motion/react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { ArrowRight, Calendar, CreditCard, Calculator, Clock, } from "lucide-react"; import { type Plan, type CurrentPlan } from "@/lib/billingsdk-config"; import { cn } from "@/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; const prorationPreviewVariants = cva("w-full max-w-3xl mx-auto", { variants: { theme: { minimal: "", classic: "relative overflow-hidden rounded-xl border border-border/50 bg-gradient-to-br from-card/50 to-muted/30 backdrop-blur-sm", }, size: { small: "text-sm", medium: "text-base", large: "text-lg", }, }, defaultVariants: { theme: "minimal", size: "medium", }, }); const cardVariants = cva("transition-all duration-300", { variants: { theme: { minimal: "border border-border bg-card", classic: "border border-border/30 bg-gradient-to-br from-card/80 to-muted/20 backdrop-blur-sm shadow-md", }, }, defaultVariants: { theme: "minimal", }, }); /** * Props for `ProrationPreview`. * * Combines visual variants from `prorationPreviewVariants` with * the billing context needed to compute proration math. * * - `currentPlan` and `newPlan` supply pricing and labels * - `billingCycle` controls the target cycle for the new plan * - `daysRemaining` and `effectiveDate` influence credit/charge math * - `onConfirm`/`onCancel` wire user actions */ export interface ProrationPreviewProps extends VariantProps< typeof prorationPreviewVariants > { className?: string; currentPlan: CurrentPlan; newPlan: Plan; billingCycle: "monthly" | "yearly"; daysRemaining?: number; effectiveDate?: string; onConfirm?: () => void; onCancel?: () => void; confirmText?: string; cancelText?: string; } /** * ProrationPreview * * Renders a detailed, accessible preview of billing changes when a user switches * subscription plans. It calculates and displays credits for unused time, * prorated charges for the new plan, and the resulting net amount, with clear * visual hierarchy and responsive styles. * * Key UI sections: * - From/To plan summary with upgrade/downgrade badges * - Billing breakdown (credit, prorated charge, net amount) * - Timeline note describing when changes take effect * - Primary/secondary actions to confirm or cancel * * Props accept both monthly and yearly cycles and support custom pricing * scenarios (e.g., enterprise). Visual appearance can be adjusted via * `theme` and `size` variants. * * @param props.className Optional container className override. * @param props.currentPlan Current subscription context including plan and cycle. * @param props.newPlan Target plan the user is moving to. * @param props.billingCycle Billing cycle for the new plan (monthly/yearly). * @param props.daysRemaining Remaining days in the current cycle (defaults to 15). * @param props.effectiveDate When the change takes effect (e.g., "immediately" or "next billing cycle"). * @param props.onConfirm Callback invoked when user confirms the change. * @param props.onCancel Callback invoked when user cancels the change. * @param props.confirmText Custom label for the confirm action. * @param props.cancelText Custom label for the cancel action. * @param props.theme Visual theme variant (minimal | classic). Defaults to minimal. * @param props.size Component size (small | medium | large). Defaults to medium. */ export function ProrationPreview({ className, currentPlan, newPlan, billingCycle, daysRemaining = 15, effectiveDate = "immediately", onConfirm, onCancel, confirmText = "Confirm Change", cancelText = "Cancel", theme = "minimal", size = "medium", }: ProrationPreviewProps) { // Prices & proration (robust) - Fixed CodeRabbit issues const currentCycleDays = currentPlan.type === "yearly" ? 365 : 30; const newCycleDays = billingCycle === "yearly" ? 365 : 30; const isNumericValue = (v?: string) => { if (v == null) return false; const s = String(v).replace(/[^\d.\-]/g, ""); const n = Number.parseFloat(s); return Number.isFinite(n); }; const toNumber = (v?: string) => { if (v == null) return undefined; const s = String(v).replace(/[^\d.\-]/g, ""); const n = Number.parseFloat(s); return Number.isFinite(n) ? n : undefined; }; const currentRaw = currentPlan.type === "monthly" ? currentPlan.plan.monthlyPrice : currentPlan.type === "yearly" ? currentPlan.plan.yearlyPrice : currentPlan.price; const newRaw = billingCycle === "monthly" ? newPlan.monthlyPrice : newPlan.yearlyPrice; const currentPrice = toNumber(currentRaw); const newPrice = toNumber(newRaw); const isCustomCurrent = !isNumericValue(currentRaw); const isCustomNew = !isNumericValue(newRaw); const chargeCurrency = newPlan.currency ?? currentPlan.plan.currency ?? "$"; const creditCurrency = currentPlan.plan.currency ?? newPlan.currency ?? "$"; const clampedUnusedDays = Math.max( 0, Math.min(daysRemaining, currentCycleDays), ); const isNextCycle = typeof effectiveDate === "string" && effectiveDate.toLowerCase().includes("next"); const prorationDays = isNextCycle ? 0 : clampedUnusedDays; const canCompute = !isNextCycle && !isCustomCurrent && !isCustomNew && currentPrice !== undefined && newPrice !== undefined; const creditAmount = canCompute ? (currentPrice! / currentCycleDays) * clampedUnusedDays : 0; const proratedCharge = canCompute ? (newPrice! / newCycleDays) * prorationDays : 0; const netAmount = proratedCharge - creditAmount; const normalizedCurrentMonthly = currentPlan.type === "yearly" && currentPrice !== undefined ? currentPrice / 12 : (currentPrice ?? 0); const normalizedNewMonthly = billingCycle === "yearly" && newPrice !== undefined ? newPrice / 12 : (newPrice ?? 0); const hasComparablePrices = !isCustomCurrent && !isCustomNew && currentPrice !== undefined && newPrice !== undefined; const isUpgrade = hasComparablePrices && normalizedNewMonthly > normalizedCurrentMonthly; const isDowngrade = hasComparablePrices && normalizedNewMonthly < normalizedCurrentMonthly; return (
{theme === "classic" && ( <>
)}
Plan Change Preview

Review the charges and credits for your plan change

{/* From/To Plans */}
{/* Current Plan */}
Current {isDowngrade && ( Downgrading )}

{currentPlan.plan.title}

{isCustomCurrent ? "Custom" : `${creditCurrency}${currentPrice}/${currentPlan.type}`}

{daysRemaining} days remaining
{/* Arrow */}
{/* New Plan */}
New Plan {isUpgrade && ( Upgrading )}

{newPlan.title}

{isCustomNew ? "Custom" : `${chargeCurrency}${newPrice}/${billingCycle}`}

Effective {effectiveDate}
{/* Calculation Breakdown */}

Billing Breakdown

Credit for unused time {canCompute ? `-${creditCurrency}${Math.abs(creditAmount).toFixed(2)}` : "—"}
Prorated charge ({prorationDays} days) {canCompute ? `+${chargeCurrency}${proratedCharge.toFixed(2)}` : "—"}
{canCompute ? netAmount >= 0 ? "Amount to charge" : "Credit to account" : "Amount due will be calculated at checkout"} = 0 ? "text-foreground" : "text-green-600" : "text-muted-foreground", )} > {canCompute ? `${netAmount >= 0 ? "+" : ""}${chargeCurrency}${netAmount.toFixed(2)}` : "—"}
{/* Timeline */}

Your plan will change {effectiveDate}. {isNextCycle ? " No immediate charge." : hasComparablePrices ? netAmount >= 0 ? ` You'll be charged ${chargeCurrency}${Math.abs(netAmount).toFixed(2)}.` : ` You'll receive a ${chargeCurrency}${Math.abs(netAmount).toFixed(2)} credit.` : " Amount will be finalized at checkout."}

{/* Action Buttons */}
); } ``` **Step 3**: Update the import paths to match your project setup. ## Usage ```tsx import { ProrationPreview } from "@/components/billingsdk/proration-preview" ``` ```tsx console.log("Plan change confirmed")} onCancel={() => console.log("Plan change cancelled")} /> ``` ## Examples ### Upgrade Scenario Shows cost breakdown when upgrading from Pro to Enterprise with remaining billing cycle. ### Downgrade Scenario Displays credit calculations when downgrading plans with unused time refunds. ### Billing Cycle Changes Handles switching between monthly and yearly billing for the same plan. ### Next Cycle Changes Shows plan changes that take effect at the next billing cycle with no immediate charges. ## API Reference ### ProrationPreview | Prop | Type | Default | Description | | --------------- | -------------------------------- | ------------------ | -------------------------------------------- | | `currentPlan` | `CurrentPlan` | - | The user's current subscription plan details | | `newPlan` | `Plan` | - | The target plan they want to switch to | | `billingCycle` | `'monthly' \| 'yearly'` | - | The billing cycle for the new plan | | `daysRemaining` | `number` | `15` | Days remaining in current billing cycle | | `effectiveDate` | `string` | `"immediately"` | When the plan change takes effect | | `onConfirm` | `() => void` | - | Callback when user confirms the change | | `onCancel` | `() => void` | - | Callback when user cancels the change | | `confirmText` | `string` | `"Confirm Change"` | Text for the confirm button | | `cancelText` | `string` | `"Cancel"` | Text for the cancel button | | `theme` | `'minimal' \| 'classic'` | `'minimal'` | Visual theme variant | | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Component size | | `className` | `string` | - | Additional CSS classes | ### CurrentPlan ```tsx interface CurrentPlan { plan: Plan; type: 'monthly' | 'yearly' | 'custom'; price?: string; nextBillingDate: string; paymentMethod: string; status: 'active' | 'inactive' | 'past_due' | 'cancelled'; } ``` ### Plan ```tsx interface Plan { id: string; title: string; description: string; monthlyPrice: string; yearlyPrice: string; currency?: string; buttonText: string; features: { name: string; icon: string; iconColor?: string; }[]; } ``` ## Features * **Accurate Proration Calculations** - Handles complex billing scenarios with precise math * **Credit & Charge Breakdown** - Shows detailed billing adjustments and credits * **Immediate vs Next Cycle** - Supports both immediate and next-cycle plan changes * **Theme Variants** - Minimal and classic visual themes * **Responsive Design** - Works seamlessly across all device sizes * **Accessibility Ready** - Full keyboard navigation and screen reader support * **Animation Effects** - Smooth transitions using Motion * **Custom Pricing Support** - Handles non-standard pricing like "Custom" or "Enterprise" * **Robust Error Handling** - Graceful handling of invalid pricing data ## Use Cases * **Plan Upgrades** - Show costs when upgrading to higher-tier plans * **Plan Downgrades** - Display credits when downgrading plans * **Billing Cycle Changes** - Handle monthly/yearly billing switches * **Enterprise Sales** - Support custom pricing and next-cycle changes * **Billing Transparency** - Provide clear breakdown of charges and credits ## Billing Logic The component implements sophisticated proration logic: * **Credits** - Calculated based on unused time in current plan (remaining days, not elapsed) * **Prorated Charges** - New plan charges based on remaining cycle days * **Net Amount** - Final charge or credit after calculations * **Cycle Handling** - Different logic for immediate vs next-cycle changes * **Price Validation** - Robust parsing of pricing strings with fallbacks * **Currency Consistency** - Uses current plan currency for credits, new plan currency for charges