Developers

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()
MethodEffect
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 | null

FSM 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 context

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 match

Every 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

CustomDeposit.tsx
"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 tx

This 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

Was this page helpful?