Kaiten

Embedded Components

Checkout

CheckoutFlow for a customer you know, through a customer session; AnonymousCheckoutFlow for a visitor with no account; CheckoutSummary, TrialBanner and PaymentMethodCard around them — the server prices, the payment provider's page takes the card, and a paid License applies once it is paid.

A checkout turns a choice on your pricing page into a subscription. The SDK draws two: CheckoutFlow, for a customer you know, signed in to your product and acting through a customer session; and AnonymousCheckoutFlow, for a visitor your backend does not know yet, who buys with an e-mail and a security check. Both keep the same rules. The server prices every amount, and the components show its figures as it states them. A card is entered only on the payment provider's own page. And nothing bought is granted before it is paid: with an amount due, the License changes when the first invoice is paid, not when the visitor clicks. Self-serve checkout describes the routes underneath, and Sell a License online wires the anonymous flow end to end.

ComponentProviderFor
CheckoutFlowmode="customer-session"A signed-in customer buying a License for the Instance their session is bound to
AnonymousCheckoutFlowmode="public" or mode="static", given an anonymousCheckoutClientA visitor with no account
CheckoutSummarymode="customer-session"What is due today for a selection
TrialBannermode="customer-session"The trial the session's Instance is in
PaymentMethodCardmode="customer-session"The payment method the customer's invoices are charged to

All five are in @kaitencloud/react-ui, and, like every styled component, experimental: their rendering may change in a minor release. Each is a set of hooks of @kaitencloud/react/billing, drawn — useCheckout, useCheckoutPreview, useAnonymousCheckout, usePaymentMethodSession — and the hooks are the stable surface, for a checkout drawn in your own design system.

Mount it where the visitor comes back

Four of these components send the browser to the payment provider's page — to save a card, or to authorize a payment — and pick up where they left off when the visitor is back. The two flows open it in a popup, or in place of your page when the browser blocks popups; TrialBanner and PaymentMethodCard go there in place of your page. The way back is returnUrl: by default the current page, minus its fragment and any kaiten_* parameter, so mount the component on the page the visitor returns to, and give a checkout an address of its own rather than a state of the page. A returnUrl must be on an origin one of your publishable keys allows. Meanwhile, what was chosen waits in the tab's sessionStorage, and is forgotten once the checkout is done.

CheckoutFlow

CheckoutFlow subscribes the session's Instance to a License of the public catalogue: the billing period, the add-ons, a voucher code, what is due today, the payment method when one is needed, and what became of the payment. It takes the License from the catalogue — what PricingTable hands to onSelectPlan, or one of usePlans() — so the provider reads the catalogue as well: give it your publishable key beside the session client.

import { CheckoutFlow, usePlans } from "@kaitencloud/react-ui";

export function SubscribePage({ slug, priceId }: { slug: string; priceId?: string }) {
  const { data: plans } = usePlans();
  const plan = plans?.find((each) => each.slug === slug);
  if (!plan) return null;

  return (
    <CheckoutFlow
      plan={plan}
      priceId={priceId}
      portalHref="/settings/billing"
      onSubscribed={() => navigate("/settings/billing")}
    />
  );
}
PropEffect
planRequired. The License to buy, as the catalogue served it, with the prices that can be bought
priceIdThe price chosen at first — the billing period — as onSelectPlan hands it
addOns, initialQuantitiesThe add-ons to offer, and the quantities to start from. By default, the catalogue's public add-ons that fit the License
returnUrlWhere the provider's page sends the customer back. Default: this page
portalHref, onOpenPortalWhere a customer who is already subscribed is sent
onSubscribed(result)Called once, when the Instance is subscribed — whatever became of the payment
onCancelAdds a cancel button while the customer chooses
appearance, unstyledTheming, or bare semantic HTML

What the flow does, in order:

  • It asks the server what is due. Each selection is priced by a checkout dryRun once it has stood still for 300 ms, and the flow shows the lines and the total as the server returns them — they need not add up, and the browser never sums them. A trial is nothing due today, and the day it ends.
  • It asks for what the customer lacks. A billing e-mail when the customer has none. A voucher code is typed once and sent in the body of the checkout only — not kept in the field, the address, or a log; a code the server refuses is one sentence, with no reason.
  • It sends the customer to the provider's page when a payment method is needed, and subscribes when they are back.
  • It says what became of the payment. The Instance is subscribed whatever happened: the payment is paid, not_required, requires_action — confirmed on the provider's hosted invoice —, failed — another card, or payment on that page —, or processing. While the first payment is not confirmed, the subscription is INCOMPLETE and the flow says the License starts once the payment is confirmed: the License and the add-ons bought apply then. While a payment is open, the flow reads the invoices every 5 seconds for 10 minutes, then each time the window takes focus.

A customer on a free License can upgrade through the flow: the free subscription keeps running until the first payment, then is replaced. A customer with any other live subscription is told they already have one, with the way to their portal — a change between paid Licenses is a change you schedule. A License with a trial is subscribed with its trial, unless this Customer already had a trial of the family: one per Customer and License family, and the flow offers no way to start without it.

AnonymousCheckoutFlow

AnonymousCheckoutFlow sells a License to a visitor with no account. It draws nothing unless the catalogue says the provider's publishable key may start an anonymous checkout, with the site key of its Cloudflare Turnstile widget — so the provider is given both the key and an anonymousCheckoutClient, made once, outside render:

import { KaitenAnonymousCheckoutClient } from "@kaitencloud/client";
import { AnonymousCheckoutFlow, KaitenProvider, KaitenStyleRoot } from "@kaitencloud/react-ui";

const anonymousCheckoutClient = new KaitenAnonymousCheckoutClient({ apiUrl, publishableKey });

<KaitenProvider
  mode="public"
  apiUrl={apiUrl}
  publishableKey={publishableKey}
  anonymousCheckoutClient={anonymousCheckoutClient}
>
  <KaitenStyleRoot>
    <AnonymousCheckoutFlow plan={plan} priceId={priceId} clientReferenceId={orderId} onSubscribed={showThankYou} />
  </KaitenStyleRoot>
</KaitenProvider>;
PropEffect
plan, priceIdThe License and the price chosen, as PricingTable hands them. Optional on the page the visitor comes back to, which finds them from the session
collectCompany, collectNameAsk for the visitor's company and name, above the e-mail: false (the default), "optional", or true / "required", which holds the button until it is given
clientReferenceIdYour own reference, at most 255 characters, echoed on the checkout's events
returnUrlWhere the provider's page sends the visitor back. Default: this page
successUrlWhere Continue sends the visitor once the checkout completed. Without it, the flow says the order is registered for their address
onStarted, onSubscribedCalled once when the checkout starts, and once when it completes — with the e-mail, the company and the name typed, and the session
onCancelAdds a cancel button while the visitor chooses, and beside the invoice of a declined card
appearance, unstyledTheming, or bare semantic HTML

The visitor chooses the billing period, gives an e-mail — and the company and the name, when you ask —, and passes the Turnstile check, whose widget the flow loads from challenges.cloudflare.com: a page with a Content-Security-Policy allows that host in script-src, frame-src and connect-src. A free License then starts with one button; a trial that asks for no card, after a review that says nothing is charged; a paid License, after a review of what the server says is due today, through the payment provider's page and a wait for the first payment. Each start spends one Turnstile token, so the flow never previews on its own.

  • The company and the name are your page's. Kaiten has no field for either: they go to onStarted and onSubscribed, and never to Kaiten. Your backend names the Customer after them, once it has checked the Customer the checkout created.
  • What the browser reports is the visitor's. The outcome onSubscribed receives, and a clientReferenceId read back in the page, can be changed by the visitor: use them for the screen, give clientReferenceId a value of your own — never one taken from a request parameter —, and link the buyer from the signed checkout_session.v1.completed event your backend receives.
  • An amount that changed is said. When the confirm finds another amount before any payment method is saved — a trial used up meanwhile — the review shows both, focused, and confirming again accepts the new one. After a payment method was saved, a changed amount is refused: nothing was charged, and the visitor starts again.
  • The styled flow sells the License alone. It has no field for a voucher code and none for add-ons. A page that sells either builds its form on useAnonymousCheckout, which takes both and names their refusals.

Kaiten sends the buyer no e-mail. If yours follows the purchase, say so in the flow's last words — localization.anonymousCheckout.completedFor.

🎬 TODO media (recording, ~45s): the SDK repository's examples/storefront against a Kaiten with a publishable key that opens the anonymous checkout, Stripe connected in test mode. A new tab on /: choose Standard, monthly. On /checkout/standard, type a company and the e-mail orders@izakaya-hanami.example, let the Turnstile test widget pass, Continue, show the review with 49.00 EUR due today, Continue to payment, enter the card 4242 4242 4242 4242 in Stripe's window, and end on the completed state with the order registered for the address.

When the browser loses the checkout

An anonymous checkout's token — the ksc_ that reads and confirms that one session — lives in the client's memory and in the tab's sessionStorage, never in a URL or in localStorage. It survives the full-page trip to the provider's page, and a reload. It does not survive another tab or browser, cleared or blocked storage — a private window —, or more than 24 hours: a page that comes back without it is lost, and the flow says the checkout may have gone through, and starts nothing by itself.

A lost token loses nothing on the server, and never creates a second Customer. A visitor who saved a card and never came back is completed by Kaiten itself, through its sync with the payment provider, and the session's outcome is in Customers › Checkouts — by e-mail or by your reference. What creates a second Customer is a second checkout: the visitor who starts again with the same e-mail gets a second Customer, Instance and subscription when it completes. Let the visitor decide, after you have looked the first one up — which is why the flow never restarts on its own. The confirm itself is safe to send again: it answers the session as it stands.

CheckoutSummary

CheckoutSummary shows what is due today for a selection, as the server computed it: its lines, and the total it states. Give it a selection — a licensePriceId, and the add-ons and voucher code that go with it —, which it has the server price with a dryRun once it stands still for 300 ms, keeping the last summary dimmed meanwhile; or a preview that useCheckoutPreview or useCheckout already holds. A trial reads "€0.00 today" and the day it ends; a refused voucher leaves the summary it had; a License the Instance already uses more of than it grants is refused with the entitlements over it. It needs mode="customer-session", and is the summary CheckoutFlow draws.

TrialBanner

TrialBanner shows the trial of the Instance a customer session is bound to: a countdown to its end — in days, then hours, then minutes —, the date, what the end charges — the server's figure, with its billing period —, and the card it goes to. While no card is on file it says what the end would charge, and offers Add a card on the provider's page — mount the banner where the customer comes back. Cancel is offered while the portal lets this customer cancel: it asks first, says the trial ends at once with nothing charged, and calls onTrialEnded once the portal was read again. The banner draws nothing outside a trial. It is not part of CustomerPortal: place it where the customer should see it, at the top of your app for one. now fixes the instant the countdown counts from — a server render, a test —, and trial draws a trial you hold.

PaymentMethodCard

PaymentMethodCard shows the payment method the customer's invoices are charged to, in the labels the provider gave — "Visa •••• 4242", its expiry, an Expired or Failed badge when it is —, never a number. Adding or replacing one happens on the provider's hosted page, in place of yours: mount the card on the page the customer comes back to, where it confirms with the API that a payment method was saved and shows the new labels. The provider keeps one currency per customer, so a customer with no live subscription is set up in the currency of their last subscription, or the catalogue's: pass currencyCode otherwise. The card is absent when your payment provider cannot capture payment methods, and is an element of CustomerPortal under a customer session.

Where to go next

On this page