Subscription Checkout
Pick a plan, enter billing details and confirm the subscription.
Preview
File: app.tsx
"use client";import React, {useState} from "react";import {Button,ButtonGroup,Checkbox,Container,FormDivider,FormField,FormSpacing,IconCheck,InlineMessage,Input,PricingPlan,PricingPlanSelect,SwitchSelect,Select,StackLayout,Tile,TileContent,TileFooter,TileHeader,} from "@brynlabs/fusion-ui";import styles from "./checkout.module.scss";type Billing = "monthly" | "yearly";const prices = {starter: {monthly: 0, yearly: 0},team: {monthly: 29, yearly: 24},business: {monthly: 79, yearly: 66},} as const;type PlanId = keyof typeof prices;const feature = (text: string) => ({icon: <IconCheck />, text});const createPlans = (billing: Billing): PricingPlan[] => [{id: "starter",name: "Starter",priceLabel: "Free",priceUnitLabel: "",description: "For trying things out on your own.",featuresTitle: "Includes:",features: [feature("1 project"), feature("Community support")],},{id: "team",name: "Team",priceLabel: `${prices.team[billing]} €`,priceUnitLabel: "/ seat / month",description: "For small teams that work together every day.",featuresTitle: "Everything in Starter, plus:",features: [feature("Unlimited projects"),feature("Shared dashboards"),feature("Email support"),],},{id: "business",name: "Business",priceLabel: `${prices.business[billing]} €`,priceUnitLabel: "/ seat / month",description: "For organizations with advanced needs.",featuresTitle: "Everything in Team, plus:",features: [feature("Single sign-on"), feature("Audit log"), feature("Priority support")],},];const countries = ["Germany", "Austria", "Switzerland", "France", "Netherlands"];export const CheckoutApp = () => {const [billing, setBilling] = useState<Billing>("yearly");const [plan, setPlan] = useState<PlanId>("team");const [seats, setSeats] = useState("5");const [company, setCompany] = useState("");const [email, setEmail] = useState("");const [country, setCountry] = useState<string | null>("Germany");const [vatId, setVatId] = useState("");const [terms, setTerms] = useState(false);const [submitted, setSubmitted] = useState(false);const [loading, setLoading] = useState(false);const [done, setDone] = useState(false);const seatCount = Math.max(1, Number(seats) || 0);const monthly = prices[plan][billing] * seatCount;const total = billing === "yearly" ? monthly * 12 : monthly;// Errors appear after the first submit, then update while typingconst errors = {company: company.trim() === "" ? "Enter the name of your company" : undefined,email: /^\S+@\S+\.\S+$/.test(email) ? undefined : "Enter a valid email address",seats: Number(seats) >= 1 ? undefined : "At least one seat is needed",terms: terms ? undefined : "Accept the terms to continue",};const valid = Object.values(errors).every(error => !error);const submit = () => {setSubmitted(true);if (!valid) return;setLoading(true);setTimeout(() => {setLoading(false);setDone(true);}, 1500);};if (done) {return (<Container direction={"both"}><InlineMessage inlineMessageStyle={"success"} inlineMessageTitle={"Welcome aboard!"}>Your {plan} plan for {seatCount} seats is active. We sent the invoice to {email}.</InlineMessage></Container>);}return (<Container direction={"both"}><StackLayout><div className={styles.intro}><h1 className={"heading-1-font"}>Choose your plan</h1><SwitchSelectaria-label={"Billing period"}options={[{label: "Monthly", value: "monthly" as Billing},{label: "Yearly −15%", value: "yearly" as Billing},]}value={billing}onChange={setBilling}/></div><PricingPlanSelectplans={createPlans(billing)}activePlan={plan}onActivePlanChange={id => setPlan(id as PlanId)}/><div className={styles.layout}><Tile><TileHeader>Billing details</TileHeader><TileContent><FormSpacing><Inputlabel={"Company"}requiredvalue={company}onChange={setCompany}invalid={submitted && !!errors.company}errorMessage={submitted ? errors.company : undefined}/><Inputlabel={"Billing email"}type={"email"}requiredvalue={email}onChange={setEmail}hint={"Invoices are sent to this address."}invalid={submitted && !!errors.email}errorMessage={submitted ? errors.email : undefined}/><Inputlabel={"Seats"}type={"number"}requiredvalue={seats}onChange={setSeats}invalid={submitted && !!errors.seats}errorMessage={submitted ? errors.seats : undefined}/><FormDivider>Address</FormDivider><FormField label={"Country"} required><Selectstyle={"FULL_SIZE"}values={countries}value={country}placeholder={"Choose a country"}onChange={setCountry}/></FormField><Inputlabel={"VAT ID"}value={vatId}onChange={setVatId}hint={"Optional, for invoices without VAT within the EU."}/><Checkboxvalue={terms}onChange={setTerms}label={"I accept the terms of service"}invalid={submitted && !!errors.terms}/></FormSpacing></TileContent></Tile><Tile className={styles.summary}><TileHeader>Summary</TileHeader><TileContent><dl className={styles.lines}><dt>Plan</dt><dd>{createPlans(billing).find(p => p.id === plan)?.name}</dd><dt>Seats</dt><dd>{seatCount}</dd><dt>Billing</dt><dd>{billing === "yearly" ? "Yearly" : "Monthly"}</dd><dt className={"strong-font"}>Total</dt><dd className={"strong-font"}>{total} € {billing === "yearly" ? "/ year" : "/ month"}</dd></dl>{submitted && !valid && (<InlineMessage inlineMessageStyle={"critical"}>Please check the highlighted fields.</InlineMessage>)}</TileContent><TileFooter><ButtonGroup><Button fullWidth loading={loading} onClick={submit}>{plan === "starter" ? "Start for free" : "Subscribe"}</Button></ButtonGroup></TileFooter></Tile></div></StackLayout></Container>);};
What it shows
- A pricing plan select with a segmented button for the billing period
- A form with field validation, inline hints and a summary tile
- Loading state of the submit button and a success message
Components used
Read more about each component and when to use it.

