// ── usePaymentFields hook ─────────────────────────────────────── // // React 18+ binding for `PaymentFieldsMachine`. Uses `useSyncExternalStore` // so React stays in sync with the machine's snapshot without unnecessary // re-renders or tearing in concurrent mode. // // Lifecycle: one machine per hook invocation. The machine is created lazily // on first render and destroyed on unmount. Re-configuring requires unmount // (e.g., key change on the parent) — the SDK does not support config swaps // because Clover iframes can't be re-themed after mount. import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; import { PaymentFieldsMachine, type MountTargets, type PaymentFieldsConfig, type PaymentFieldsError, type PaymentFieldsSnapshot, type PaymentFieldsState, type ThreeDsInterim, type ThreeDsResult, type TokenizationResult, type ValidationSnapshot, } from '../core/index'; export interface UsePaymentFieldsOptions extends PaymentFieldsConfig {} /** * Input to `usePaymentFields`. Either a config (hook constructs the machine * and destroys it on unmount) or an existing machine (hook subscribes only, * leaves lifecycle to the caller — used by the web-component layer). */ export type UsePaymentFieldsInput = UsePaymentFieldsOptions | PaymentFieldsMachine; export interface UsePaymentFieldsReturn { // ── Flattened snapshot fields (most common reads) ── readonly state: PaymentFieldsState; readonly validation: ValidationSnapshot; readonly result?: TokenizationResult; readonly error?: PaymentFieldsError; readonly threeDs?: ThreeDsInterim; /** Full snapshot, for hosts that want everything at once. */ readonly snapshot: PaymentFieldsSnapshot; // ── Actions (stable references) ── readonly mount: (targets: MountTargets) => Promise; readonly tokenize: () => Promise; readonly runThreeDs: ( interim: ThreeDsInterim, options?: { readonly extraBody?: Record }, ) => Promise; readonly resetToIdle: () => void; /** Escape hatch for advanced cases (the underlying machine instance). */ readonly machine: PaymentFieldsMachine; } export function usePaymentFields(input: UsePaymentFieldsInput): UsePaymentFieldsReturn { // Lazy-init pattern: capture the machine on first render, persist across renders. // In React Strict Mode the function body runs twice; the ref persists, so we // only construct one machine. `ownsRef` tracks whether we should destroy it on // unmount (true when we constructed it; false when the caller passed one in). const machineRef = useRef(null); const ownsRef = useRef(false); if (machineRef.current === null) { if (input instanceof PaymentFieldsMachine) { machineRef.current = input; ownsRef.current = false; } else { machineRef.current = new PaymentFieldsMachine(input); ownsRef.current = true; } } const machine = machineRef.current; // Subscribe + getSnapshot must have stable identity for useSyncExternalStore. const subscribe = useCallback( (onChange: () => void) => machine.subscribe(() => onChange()), [machine], ); const getSnapshot = useCallback(() => machine.getSnapshot(), [machine]); const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); // Destroy on unmount only if we own the machine. Externally-supplied machines // are the caller's responsibility (web-component layer destroys in disconnectedCallback). useEffect(() => { return () => { if (ownsRef.current) { machine.destroy(); } machineRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const mount = useCallback((targets: MountTargets) => machine.mount(targets), [machine]); const tokenize = useCallback(() => machine.tokenize(), [machine]); const runThreeDs = useCallback( (interim: ThreeDsInterim, options?: { readonly extraBody?: Record }) => machine.runThreeDs(interim, options), [machine], ); const resetToIdle = useCallback(() => machine.resetToIdle(), [machine]); return useMemo( () => ({ state: snapshot.state, validation: snapshot.validation, result: snapshot.result, error: snapshot.error, threeDs: snapshot.threeDs, snapshot, mount, tokenize, runThreeDs, resetToIdle, machine, }), [snapshot, mount, tokenize, runThreeDs, resetToIdle, machine], ); }