Headless integration
Headless is the "own everything UI-side" tier: the Kit owns the FSM and driver snapshot, and you render every pixel. Reach for it when the skin tier (restyle) and compound tier (restructure) do not fit your UI. Public controller, state, and snapshot hooks let you build that UI inside the configured provider. Dialogs also use private hooks that are not consumer exports.
"use client"
import { useDeposit, useDepositState, useDepositSnapshot } from "@stridge/gateway-kit"
export function CustomDepositPanel() {
const { open, close } = useDeposit()
const state = useDepositState()
const snapshot = useDepositSnapshot()
// Render your own UI driven by `state` and `snapshot`.
return null
}The Kit's root entry exports no UI — the bundle stays clean when you stay headless. Dialogs and compound parts live at separate subpaths so tree-shaking does the right thing.
A live worked example: demo.stridge.com/headless.
Controllers
The imperative surface. Returned from useDeposit() / useWithdraw() / useActivity().
const { open, close } = useDeposit()
const { open, close } = useWithdraw()
const { open, close } = useActivity()| Method | Effect |
|---|---|
open() | Open the flow. The FSM advances from "closed" to its first non-closed step. |
open({ method, assetId }) | (Deposit only) Skip the method picker or jump straight to a chosen asset. |
open({ method: "onramp", amount?, currency? }) | (Deposit only) Open straight into the Cash flow, optionally preselecting the amount + currency. |
open({ settlementId }) / open({ txHash }) | (Activity only) Open straight into a settlement's detail view. See Activity. |
close() | Force-close. Useful for "Cancel" buttons or programmatic dismissal. |
Non-throwing variants
useDeposit(), useWithdraw(), and useActivity() throw when their controllers are absent. Activity also needs a resolved owner, even inside a provider with a configured money flow. For components that render before a controller exists, use the optional variants:
import { useOptionalDeposit, useOptionalWithdraw, useOptionalActivity } from "@stridge/gateway-kit"
const deposit = useOptionalDeposit() // → DepositController | null
const withdraw = useOptionalWithdraw() // → WithdrawController | null
const activity = useOptionalActivity() // → ActivityController | nullFSM state
useDepositState() and useWithdrawState() each return the typed FSM state directly. Read imperative actions from useDeposit() or useWithdraw().
const state = useDepositState()
if (state.name === "success") {
state.ctx
}Deposit state names include "closed", "deposit", "assetPicker", "amountEntry", "confirmDeposit", "transferCrypto", the "onramp*" Cash steps, "processing", "success", "error", "activityList", and "activityDetail". Narrow state.name before reading the matching ctx.
const state = useWithdrawState()
if (state.name === "submitting") {
state.ctx
}Withdraw state names are "closed", "form", "submitting", "inProgress", "success", "error", "activityList", and "activityDetail".
The state value updates synchronously as the FSM advances; rendering off state.name keeps your UI in lockstep with the Kit.
Discriminated context
Every non-closed state carries a ctx payload typed against the active screen. For example, the deposit success state's ctx contains the settled asset and settlement tx; the deposit error state's ctx contains a FailureInfo. Reach into state.ctx only after narrowing on state.name — TypeScript will refuse the unsafe access otherwise.
Driver snapshots
useDepositSnapshot() and useWithdrawSnapshot() return the full driver state: balances, supported assets, the current quote, settlement progress, wallet readiness.
const snapshot = useDepositSnapshot()
snapshot.target // settlement target — networkId / symbol / destination
snapshot.brand // home-asset metadata read from the gateway
snapshot.balances // user's balances grouped by chain, with `.status` and `.items`
snapshot.addresses // provisioned UDA per chain, with `.status`
snapshot.settlement // active settlement record + watcher status
snapshot.quote // last resolved quote with fees and route breakdown
snapshot.wallet // wallet readiness / chain matchEntities use status: "idle" | "loading" | "ready" | "stale" | "error". A stale entity keeps its last payload while a new read runs; an error entity carries an error.
The snapshot is reactive — re-rendering happens whenever any entity advances. The Kit batches transitions to keep React's render budget honest.
Prefetch
The Kit lazily fires Stridge requests on first open. For pages where you know the dialog will open (a checkout page, a dedicated funding screen), arm the driver earlier so the picker has nothing to wait on:
import { useEffect } from "react"
import { usePrefetchDeposit, usePrefetchWithdraw } from "@stridge/gateway-kit"
function FundingPage() {
const prefetch = usePrefetchDeposit()
useEffect(() => {
prefetch() // fire-and-forget; subsequent calls are no-ops
}, [prefetch])
return <YourFundingUI />
}usePrefetchDeposit() / usePrefetchWithdraw() return a stable function — call it on mount, on hover, when a route transitions, whenever it fits. The driver bootstraps once.
For an always-eager strategy, set <StridgeProvider prefetch={…}> on the provider instead.
End-to-end headless deposit
"use client"
import { useDeposit, useDepositState, useDepositSnapshot } from "@stridge/gateway-kit"
export function CustomDeposit() {
const { open, close } = useDeposit()
const state = useDepositState()
const { balances, settlement, quote } = useDepositSnapshot()
if (state.name === "closed") {
return <button onClick={() => open()}>Fund</button>
}
if (state.name === "assetPicker") {
if (balances.status !== "ready") return <Spinner />
return (
<YourPicker
items={balances.items}
onSelect={(id) => { /* drive your custom picker; the controller exposes the per-FSM-state actions */ }}
/>
)
}
if (state.name === "processing") {
return <YourWatcher settlement={settlement} />
}
if (state.name === "success") {
return <YourSuccess details={state.ctx} onClose={close} />
}
if (state.name === "error") {
return <YourError failure={state.ctx.failure} onRetry={() => open()} />
}
return null
}For imperative actions, use the controller from useDeposit(); useDepositState() supplies the state you narrow before rendering. The root entry exports DepositState for typed narrowing, while @stridge/gateway-kit/types carries other public payload and context types.
Headless + UI primitives
If you want to skip the FSM-level composition but reuse the Kit's visual building blocks, import primitives from @stridge/gateway-kit/ui. The hooks and the primitives are designed to compose — render <AmountInput> and <Select> driven by your own state, then dispatch through useDeposit() when the user is ready. The primitives respond to the same --stridge-gk-* tokens as the rest of the Kit — Kitweak, the live theme editor on the demo site, previews the full primitive catalogue under your token tweaks before you compose against them.
Activity
Alongside Deposit and Withdraw, the Kit ships a controller-backed Activity flow for an owner's settlements and Cash sessions. Configure Deposit or Withdraw on StridgeProvider with a resolved owner; there is no flows.activity key. Neither money flow needs to be open. The built-in controller can then open <ActivityDialog /> or drive a custom headless view. For host-owned history without either money flow, use the public bare Activity compounds and ActivitySourceProvider instead of the controller-backed hooks.
"use client"
import { useActivity, useActivityState, useActivitySnapshot } from "@stridge/gateway-kit"
function ActivityButton() {
const { open } = useActivity()
return <button onClick={() => open()}>Activity</button>
}Direct-entry
Direct-entry opens a settlement receipt when the record is already in the current Activity snapshot. Pass a settlementId (the Stridge canonical id) or a txHash. The controller resolves the current cached snapshot synchronously; a cold snapshot or a miss opens the list. It does not wait for history to load and retry the detail selection.
const { open } = useActivity()
open() // → activityList
open({ settlementId }) // → activityDetail (or activityList if unresolved)
open({ txHash }) // → activityDetail, matched on the source txFor a route or notification that targets one receipt, load the owner's Activity history before direct-entry if the detail must open immediately.
FSM state
useActivityState() returns the typed Activity FSM state directly; useActivity() returns the controller.
const state = useActivityState()
state.name // "closed" | "activityList" | "activityDetail"
state.ctx // discriminated context — `activityDetail` carries `{ settlementId, viaList }`useActivitySnapshot() returns the driver state. snapshot.activity is the unified history entity with status: "idle" | "loading" | "ready" | "stale" | "error" and a payload.rows list in ready or stale states. usePrefetchActivity() (and the non-throwing useOptionalPrefetchActivity()) arm the driver early, like the Deposit and Withdraw prefetch hooks.
With <ActivityDialog /> mounted, the built-in flow emits activity.opened, activity.step.changed, activity.settlement.selected (with via: "list" | "direct"), and activity.closed through the event bus. They are UI-tier events; subscribe via useStridgeEvents. A custom headless composition does not mount the dialog's emission hook, so it does not emit those events automatically.
Next
- Compound components — same FSM, more structure than raw hooks.
- Events — typed event bus for analytics and behavioural reactions.
- UI primitives & types — 23 lower-level building blocks.