Headless integration
Headless is the "own everything UI-side" tier — the Kit only owns the FSM and the driver snapshot; you render every pixel. Reach for it when the skin tier (restyle) and the compound tier (restructure) still don't fit the UI you want to build. The Kit exports every hook the dialogs use internally; mount the provider, skip the dialog, render your own UI driven by the same controllers and snapshots.
"use client"
import { useDeposit, useDepositState, useDepositSnapshot } from "@stridge/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() and useWithdraw() throw when called outside a configured provider. For components that should render whether or not the flow is mounted, use the optional variants:
import { useOptionalDeposit, useOptionalWithdraw, useOptionalActivity } from "@stridge/kit"
const deposit = useOptionalDeposit() // → DepositController | null
const withdraw = useOptionalWithdraw() // → WithdrawController | null
const activity = useOptionalActivity() // → ActivityController | nullFSM state
useDepositState() and useWithdrawState() return both the controller and a typed FSM snapshot.
const { state, open, close } = useDepositState()
state.name // "closed" | "deposit" | "assetPicker" | "amountEntry" |
// "confirmDeposit" | "transferCrypto" |
// "onrampAmountEntry" | "onrampCurrencyPicker" |
// "onrampProviderPicker" | "onrampConfirm" |
// "onrampPaymentPending" | "processing" |
// "success" | "error"
state.ctx // discriminated context — narrows on `state.name`const { state } = useWithdrawState()
state.name // "closed" | "form" | "inProgress" | "success" | "error"
state.ctx // discriminated contextThe 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 matchEvery entity exposes a status: "idle" | "loading" | "ready" | "failed" field so your UI can render loading shimmer, error states, or live values uniformly.
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/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/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 per-FSM-state action handles (advance, retry, dismiss), reach for the orchestrator hooks on a per-flow basis — useDepositState() is the bread-and-butter entry; the type catalog at @stridge/kit/types exposes the per-state context shapes if you need exhaustive narrowing.
Headless + UI primitives
If you want to skip the FSM-level composition but reuse the Kit's visual building blocks, import primitives from @stridge/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.
Activity
Alongside deposit and withdraw, the Kit ships a standalone activity surface — a read-only viewer over the user's settlement history (deposits and withdrawals, unified). It's a top-level flow like the other two: configure it on the provider, then open it imperatively or drop in <ActivityDialog>. <StridgeProvider> derives the activity driver automatically whenever any flow is enabled; for a custom driver, pass activity to <KitProvider>.
"use client"
import { useActivity, useActivityState, useActivitySnapshot } from "@stridge/kit"
function ActivityButton() {
const { open } = useActivity()
return <button onClick={() => open()}>Activity</button>
}Direct-entry
The headline of the activity flow is imperative direct-entry: open straight into a single settlement's receipt, skipping the list. Pass a settlementId (the Stridge canonical id) or a txHash — the controller resolves it against the loaded history and, on a miss, falls through to the list rather than silently no-op'ing.
const { open } = useActivity()
open() // → activityList
open({ settlementId }) // → activityDetail (or activityList if unresolved)
open({ txHash }) // → activityDetail, matched on the source txThis makes the surface URL- and push-notification-friendly: a /activity/:id route or a "View receipt" deep link maps one-to-one onto open({ settlementId }).
FSM state
useActivityState() returns the controller plus the typed FSM snapshot.
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 the standard status: "idle" | "loading" | "ready" | "failed" field and a payload.rows list. usePrefetchActivity() (and the non-throwing useOptionalPrefetchActivity()) arm the driver early, exactly like the deposit / withdraw prefetch hooks.
Lifecycle is observable through the event bus: activity.opened, activity.step.changed, activity.settlement.selected (with via: "list" | "direct"), and activity.closed. The surface is read-only, so those events are UI-tier — subscribe via useStridgeEvents.
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.