Hierarchical Checkout
import { createMachine, defineStates, effect, setup, withReset, matchina,} from "matchina";import { submachine, nestedHsmRoot } from "matchina/hsm";
// Hierarchical checkout: main flow contains a payment submachineexport const paymentStates = defineStates({ MethodEntry: undefined, Authorizing: undefined, AuthChallenge: undefined, AuthorizationError: undefined, Authorized: { final: true },});
// Create payment machine factoryfunction createPayment() { const m = matchina( paymentStates, { MethodEntry: { authorize: "Authorizing", exit: "MethodEntry", // Exit resets to initial state }, Authorizing: { authRequired: "AuthChallenge", authSucceeded: "Authorized", authFailed: "AuthorizationError", exit: "MethodEntry", // Exit from any payment state goes back to MethodEntry }, AuthChallenge: { authSucceeded: "Authorized", authFailed: "AuthorizationError", exit: "MethodEntry", }, AuthorizationError: { retry: "MethodEntry", exit: "MethodEntry", }, Authorized: { exit: "MethodEntry", }, }, paymentStates.MethodEntry() );
return withReset(nestedHsmRoot(m), paymentStates.MethodEntry());}
const paymentFactory = submachine(createPayment, { id: "payment" });
const checkoutStates = defineStates({ Cart: undefined, Shipping: undefined, ShippingPaid: undefined, Payment: paymentFactory, Review: undefined, Confirmation: undefined,});
export function createCheckoutMachine() { const checkout = createMachine( checkoutStates, { Cart: { proceed: "Shipping" }, Shipping: { back: "Cart", proceed: "Payment", }, Payment: { back: "Shipping", exit: "Shipping", "child.exit": "Review", }, Review: { back: "ShippingPaid", changePayment: "Payment", submitOrder: "Confirmation", }, ShippingPaid: { back: "Cart", proceed: "Review", changePayment: "Payment", }, Confirmation: { restart: "Cart" }, }, "Cart" );
const hierarchical = nestedHsmRoot(checkout);
// Get payment machine from state to wire up reset effect const getPayment = () => { const state = hierarchical.getState(); return state.is("Payment") ? state.data.machine : null; };
setup(hierarchical)( effect((ev) => { if (ev.type === "restart") { const payment = getPayment(); if (payment) { payment.reset!(); } return true; } }) );
return hierarchical;}
// Type export for payment machine (used by CheckoutViewNested context)export type PaymentMachine = ReturnType<typeof createPayment>;import { createHSM } from "matchina/hsm";
export function createFlatCheckoutMachine() { return createHSM({ initial: "Cart", states: { Cart: { on: { proceed: "Shipping" } }, Shipping: { on: { back: "Cart", proceed: "Payment" } }, Payment: { initial: "MethodEntry", states: { MethodEntry: { on: { authorize: "Authorizing" } }, Authorizing: { on: { authRequired: "AuthChallenge", authSucceeded: "Authorized", authFailed: "AuthorizationError" } }, AuthChallenge: { on: { authSucceeded: "Authorized", authFailed: "AuthorizationError" } }, AuthorizationError: { on: { retry: "MethodEntry" } }, Authorized: { // Final payment state - child.exit automatically triggered } }, on: { back: "Shipping", "child.exit": "Review" } }, Review: { on: { back: "ShippingPaid", changePayment: "Payment", submitOrder: "Confirmation" } }, ShippingPaid: { on: { back: "Cart", proceed: "Review", changePayment: "Payment" } }, Confirmation: { on: { restart: "Cart" } } } });}import React, { useContext } from "react";import { useMachine } from "matchina/react";import { eventApi } from "matchina";import type { FactoryMachine } from "matchina";import { createCheckoutMachine, type PaymentMachine } from "./machine";import { CartItems, ShippingForm, CardForm, StepIndicator, Confirmation, STATE_TO_STEP } from "./ui";
type CheckoutMachine = FactoryMachine<ReturnType<typeof createCheckoutMachine>>;type CheckoutActions = ReturnType<typeof eventApi<CheckoutMachine>>;
const CheckoutContext = React.createContext<{ machine: CheckoutMachine; paymentMachine?: PaymentMachine; actions: CheckoutActions;} | null>(null);
function useCheckoutContext() { const ctx = useContext(CheckoutContext); if (!ctx) throw new Error("useCheckoutContext must be used within CheckoutContext.Provider"); return ctx;}
interface CheckoutViewNestedProps { machine: FactoryMachine<ReturnType<typeof createCheckoutMachine>>;}
export function CheckoutViewNested({ machine }: CheckoutViewNestedProps) { useMachine(machine); const state = machine.getState() as { key: string; data?: any }; const parent = state.key;
let paymentMachine: PaymentMachine | undefined; if (state.data?.machine) paymentMachine = state.data.machine;
const actions = eventApi(machine); const currentStep = STATE_TO_STEP[parent] ?? "Cart";
return ( <CheckoutContext.Provider value={{ machine, paymentMachine, actions }}> <div className="max-w-xs mx-auto bg-card rounded-2xl border border-border p-5"> <StepIndicator currentStep={currentStep} /> <div className="space-y-5"> <MainContent parentState={parent} /> {parent === "Payment" && paymentMachine && ( <PaymentSection paymentMachine={paymentMachine} /> )} <CheckoutControls parentState={parent} /> </div> </div> </CheckoutContext.Provider> );}
function MainContent({ parentState }: { parentState: string }) { switch (parentState) { case "Cart": return <CartItems />; case "Shipping": return <ShippingForm />; case "Review": case "ShippingPaid": return ( <div> <p className="text-[9px] font-mono uppercase tracking-widest text-muted-foreground mb-2">Order Summary</p> <CartItems readOnly /> </div> ); case "Confirmation": return <Confirmation />; default: return null; }}
function PaymentSection({ paymentMachine }: { paymentMachine: PaymentMachine }) { useMachine(paymentMachine); const paymentActions = eventApi(paymentMachine); const pState = paymentMachine.getState();
return ( <div className="space-y-3"> {pState.match({ MethodEntry: () => ( <div className="space-y-3"> <CardForm /> <button onClick={() => paymentActions.authorize()} className="btn btn-primary w-full"> Authorize Payment </button> </div> ), Authorizing: () => ( <div className="space-y-3"> <div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="w-3.5 h-3.5 border-2 border-border border-t-primary rounded-full animate-spin" /> Authorizing payment </div> <div className="border border-border rounded-xl p-3 space-y-2"> <p className="text-[8px] font-mono uppercase tracking-widest text-muted-foreground">Simulate outcome</p> <div className="flex gap-2"> <button onClick={() => paymentActions.authSucceeded()} className="btn btn-outline btn-sm flex-1">Approve</button> <button onClick={() => paymentActions.authRequired()} className="btn btn-outline btn-sm flex-1">Challenge</button> <button onClick={() => paymentActions.authFailed()} className="btn btn-destructive btn-sm flex-1">Fail</button> </div> </div> </div> ), AuthChallenge: () => ( <div className="space-y-3"> <div className="bg-muted rounded-xl p-3"> <p className="text-sm font-medium mb-0.5">Authentication required</p> <p className="text-xs text-muted-foreground">Your bank is requesting additional verification.</p> </div> <div className="flex gap-2"> <button onClick={() => paymentActions.authSucceeded()} className="btn btn-primary btn-sm flex-1">Approve</button> <button onClick={() => paymentActions.authFailed()} className="btn btn-destructive btn-sm flex-1">Deny</button> </div> </div> ), AuthorizationError: () => ( <div className="space-y-3"> <div className="bg-destructive/10 border border-destructive/20 rounded-xl p-3"> <p className="text-sm text-destructive font-medium">Authorization failed</p> <p className="text-xs text-muted-foreground mt-0.5">Your payment could not be authorized.</p> </div> <div className="flex gap-2"> <button onClick={() => paymentActions.retry()} className="btn btn-outline btn-sm flex-1">Try Again</button> <button onClick={() => paymentActions.exit()} className="btn btn-ghost btn-sm flex-1">Cancel</button> </div> </div> ), Authorized: () => ( <div className="flex items-center gap-2 text-[oklch(0.55_0.16_142)] text-sm font-medium bg-[oklch(0.55_0.16_142)]/10 rounded-xl px-3 py-2.5"> <span>✓</span> <span>Payment authorized</span> </div> ), })} </div> );}
function CheckoutControls({ parentState }: { parentState: string }) { const { actions } = useCheckoutContext();
switch (parentState) { case "Cart": return ( <button onClick={() => actions.proceed()} className="btn btn-primary w-full"> Continue to Shipping </button> ); case "Shipping": return ( <div className="flex gap-2"> <button onClick={() => actions.back()} className="btn btn-outline flex-1">Back</button> <button onClick={() => actions.proceed()} className="btn btn-primary flex-1">Continue to Payment</button> </div> ); case "Payment": return ( <button onClick={() => actions.back()} className="btn btn-outline w-full">Back to Shipping</button> ); case "ShippingPaid": return ( <div className="space-y-2"> <button onClick={() => actions.proceed()} className="btn btn-primary w-full">Continue to Review</button> <div className="flex gap-2"> <button onClick={() => actions.back()} className="btn btn-outline btn-sm flex-1">Back to Cart</button> <button onClick={() => actions.changePayment()} className="btn btn-outline btn-sm flex-1">Change Payment</button> </div> </div> ); case "Review": return ( <div className="space-y-2"> <button onClick={() => actions.submitOrder()} className="btn btn-primary w-full">Place Order</button> <div className="flex gap-2"> <button onClick={() => actions.back()} className="btn btn-outline btn-sm flex-1">Back</button> <button onClick={() => actions.changePayment()} className="btn btn-outline btn-sm flex-1">Change Payment</button> </div> </div> ); case "Confirmation": return ( <button onClick={() => actions.restart()} className="btn btn-outline w-full">Start New Order</button> ); default: return null; }}import React, { useContext } from "react";import { useMachine } from "matchina/react";import { eventApi } from "matchina";import { parseFlatStateKey } from "matchina/hsm";import { createFlatCheckoutMachine } from "./machine-flat";import { CartItems, ShippingForm, CardForm, StepIndicator, Confirmation, STATE_TO_STEP } from "./ui";
type CheckoutMachine = ReturnType<typeof createFlatCheckoutMachine>;type CheckoutActions = ReturnType<typeof eventApi<CheckoutMachine>>;
const CheckoutContext = React.createContext<{ machine: CheckoutMachine; actions: CheckoutActions;} | null>(null);
function useCheckoutContext() { const ctx = useContext(CheckoutContext); if (!ctx) throw new Error("useCheckoutContext must be used within CheckoutContext.Provider"); return ctx;}
interface CheckoutViewFlatProps { machine: CheckoutMachine;}
export function CheckoutViewFlat({ machine }: CheckoutViewFlatProps) { useMachine(machine); const state = machine.getState() as { key: string; data?: any }; const parsed = parseFlatStateKey(state.key); const parent = parsed.parent; const child = parsed.child; const actions = eventApi(machine); const currentStep = STATE_TO_STEP[parent] ?? "Cart";
return ( <CheckoutContext.Provider value={{ machine, actions }}> <div className="max-w-xs mx-auto bg-card rounded-2xl border border-border p-5"> <StepIndicator currentStep={currentStep} /> <div className="space-y-5"> <MainContent parent={parent} child={child} /> <CheckoutControls parent={parent} /> </div> </div> </CheckoutContext.Provider> );}
function MainContent({ parent, child }: { parent: string; child: string | null }) { if (parent === "Cart") return <CartItems />; if (parent === "Shipping") return <ShippingForm />; if (parent === "Payment" && child) return <PaymentContent child={child} />; if (parent === "Review" || parent === "ShippingPaid") { return ( <div> <p className="text-[9px] font-mono uppercase tracking-widest text-muted-foreground mb-2">Order Summary</p> <CartItems readOnly /> </div> ); } if (parent === "Confirmation") return <Confirmation />; return null;}
function PaymentContent({ child }: { child: string }) { const { actions } = useCheckoutContext();
switch (child) { case "MethodEntry": return ( <div className="space-y-3"> <CardForm /> <button onClick={() => actions.authorize?.()} className="btn btn-primary w-full"> Authorize Payment </button> </div> ); case "Authorizing": return ( <div className="space-y-3"> <div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="w-3.5 h-3.5 border-2 border-border border-t-primary rounded-full animate-spin" /> Authorizing payment </div> <div className="border border-border rounded-xl p-3 space-y-2"> <p className="text-[8px] font-mono uppercase tracking-widest text-muted-foreground">Simulate outcome</p> <div className="flex gap-2"> <button onClick={() => actions.authSucceeded?.()} className="btn btn-outline btn-sm flex-1">Approve</button> <button onClick={() => actions.authRequired?.()} className="btn btn-outline btn-sm flex-1">Challenge</button> <button onClick={() => actions.authFailed?.()} className="btn btn-destructive btn-sm flex-1">Fail</button> </div> </div> </div> ); case "AuthChallenge": return ( <div className="space-y-3"> <div className="bg-muted rounded-xl p-3"> <p className="text-sm font-medium mb-0.5">Authentication required</p> <p className="text-xs text-muted-foreground">Your bank is requesting additional verification.</p> </div> <div className="flex gap-2"> <button onClick={() => actions.authSucceeded?.()} className="btn btn-primary btn-sm flex-1">Approve</button> <button onClick={() => actions.authFailed?.()} className="btn btn-destructive btn-sm flex-1">Deny</button> </div> </div> ); case "AuthorizationError": return ( <div className="space-y-3"> <div className="bg-destructive/10 border border-destructive/20 rounded-xl p-3"> <p className="text-sm text-destructive font-medium">Authorization failed</p> <p className="text-xs text-muted-foreground mt-0.5">Your payment could not be authorized.</p> </div> <button onClick={() => actions.retry?.()} className="btn btn-outline btn-sm">Try Again</button> </div> ); case "Authorized": return ( <div className="flex items-center gap-2 text-[oklch(0.55_0.16_142)] text-sm font-medium bg-[oklch(0.55_0.16_142)]/10 rounded-xl px-3 py-2.5"> <span>✓</span> <span>Payment authorized</span> </div> ); default: return null; }}
function CheckoutControls({ parent }: { parent: string }) { const { actions } = useCheckoutContext();
switch (parent) { case "Cart": return ( <button onClick={() => actions.proceed?.()} className="btn btn-primary w-full"> Continue to Shipping </button> ); case "Shipping": return ( <div className="flex gap-2"> <button onClick={() => actions.back?.()} className="btn btn-outline flex-1">Back</button> <button onClick={() => actions.proceed?.()} className="btn btn-primary flex-1">Continue to Payment</button> </div> ); case "Payment": return ( <button onClick={() => actions.back?.()} className="btn btn-outline w-full">Back to Shipping</button> ); case "ShippingPaid": return ( <div className="space-y-2"> <button onClick={() => actions.proceed?.()} className="btn btn-primary w-full">Continue to Review</button> <div className="flex gap-2"> <button onClick={() => actions.back?.()} className="btn btn-outline btn-sm flex-1">Back to Cart</button> <button onClick={() => actions.changePayment?.()} className="btn btn-outline btn-sm flex-1">Change Payment</button> </div> </div> ); case "Review": return ( <div className="space-y-2"> <button onClick={() => actions.submitOrder?.()} className="btn btn-primary w-full">Place Order</button> <div className="flex gap-2"> <button onClick={() => actions.back?.()} className="btn btn-outline btn-sm flex-1">Back</button> <button onClick={() => actions.changePayment?.()} className="btn btn-outline btn-sm flex-1">Change Payment</button> </div> </div> ); case "Confirmation": return ( <button onClick={() => actions.restart?.()} className="btn btn-outline w-full">Start New Order</button> ); default: return null; }}import { useMemo, useState } from "react";import { createFlatCheckoutMachine } from "./machine-flat";import { createCheckoutMachine } from "./machine";import { CheckoutViewFlat } from "./CheckoutViewFlat";import { CheckoutViewNested } from "./CheckoutViewNested";import { MachineVisualizer } from "@components/MachineVisualizer";
type Mode = "flat" | "nested";
export default function HSMCheckoutIndex() { const [mode, setMode] = useState<Mode>("flat");
// Create separate machines for each mode const flatMachine = useMemo(() => createFlatCheckoutMachine(), []); const nestedMachine = useMemo(() => createCheckoutMachine(), []); const machine = mode === "flat" ? flatMachine : nestedMachine;
return ( <div className="space-y-6"> {/* Mode Toggle - Sticky below header */} <div className="flex justify-center mb-6 sticky top-16 z-10 py-2 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm -mx-4 px-4"> <div className="inline-flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg"> <button onClick={() => setMode("flat")} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ mode === "flat" ? "bg-white dark:bg-gray-700 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" }`} > Flattened </button> <button onClick={() => setMode("nested")} className={`px-4 py-2 rounded text-sm font-medium transition-colors ${ mode === "nested" ? "bg-white dark:bg-gray-700 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" }`} > Nested (Hierarchical) </button> </div> </div>
{mode === "flat" ? ( <CheckoutViewFlat machine={flatMachine} /> ) : ( <CheckoutViewNested machine={nestedMachine} /> )}
<MachineVisualizer key={mode} // Force re-mount of visualizer when mode changes machine={machine} title={`Hierarchical Traffic Light (${mode === "flat" ? "Flattened" : "Nested"})`} defaultViz="sketch" interactive={true} layout="stacked" /> </div> );}