Fusion UI LogoFusion UI Logo
Fusion UI LogoFusion UI Logo
/
Getting Started
Concepts
Components
Example Apps
Getting Started
Concepts
Components
Example Apps
Fusion UI LogoFusion UI Logo
Getting Started
Concepts
Components
Example Apps
Overview
Example Apps
Analytics Dashboard
Customer Management
Account Settings
Support Inbox
Subscription Checkout

Subscription Checkout

Pick a plan, enter billing details and confirm the subscription.

Preview

/preview/checkout

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 typing
const 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>
<SwitchSelect
aria-label={"Billing period"}
options={[
{label: "Monthly", value: "monthly" as Billing},
{label: "Yearly −15%", value: "yearly" as Billing},
]}
value={billing}
onChange={setBilling}
/>
</div>
<PricingPlanSelect
plans={createPlans(billing)}
activePlan={plan}
onActivePlanChange={id => setPlan(id as PlanId)}
/>
<div className={styles.layout}>
<Tile>
<TileHeader>Billing details</TileHeader>
<TileContent>
<FormSpacing>
<Input
label={"Company"}
required
value={company}
onChange={setCompany}
invalid={submitted && !!errors.company}
errorMessage={submitted ? errors.company : undefined}
/>
<Input
label={"Billing email"}
type={"email"}
required
value={email}
onChange={setEmail}
hint={"Invoices are sent to this address."}
invalid={submitted && !!errors.email}
errorMessage={submitted ? errors.email : undefined}
/>
<Input
label={"Seats"}
type={"number"}
required
value={seats}
onChange={setSeats}
invalid={submitted && !!errors.seats}
errorMessage={submitted ? errors.seats : undefined}
/>
<FormDivider>Address</FormDivider>
<FormField label={"Country"} required>
<Select
style={"FULL_SIZE"}
values={countries}
value={country}
placeholder={"Choose a country"}
onChange={setCountry}
/>
</FormField>
<Input
label={"VAT ID"}
value={vatId}
onChange={setVatId}
hint={"Optional, for invoices without VAT within the EU."}
/>
<Checkbox
value={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.

Pricing Plan SelectButtonFormInputSelectCheckboxTileInline MessageContainerStack LayoutSegmented Button
On this page
  • Preview
  • What it shows
  • Components used