Developers

Drop-in dialogs

The fastest path to a working deposit or withdraw flow: mount one component per direction, drive it from a hook, ship.

App.tsx
import { StridgeProvider, useDeposit } from "@stridge/kit"
import { DepositDialog } from "@stridge/kit/deposit/dialog"

function DepositButton() {
  const { open } = useDeposit()
  return <button onClick={() => open()}>Deposit</button>
}

export function App({ userWallet }: { userWallet: `0x${string}` }) {
  return (
    <StridgeProvider
      gatewayKey={process.env.NEXT_PUBLIC_STRIDGE_GATEWAY_KEY!}
      asset={{ networkId: "9001", symbol: "USDC" }} // 9001 = Arbitrum One
      flows={{ deposit: { destination: { address: userWallet } } }}
    >
      <DepositButton />
      <DepositDialog />
    </StridgeProvider>
  )
}

Mount the dialog once anywhere inside the provider. The dialog renders nothing until the controller transitions out of "closed", so it's safe to mount at the layout root.

How dialogs are opened and closed

Open state lives on the FSM controller, not on the dialog. Dialogs do not accept open, defaultOpen, onOpenChange, or trigger props.

ActionAPI
OpenuseDeposit().open() / useWithdraw().open() / useActivity().open()
CloseuseDeposit().close() / useWithdraw().close() / useActivity().close(), or user dismisses (escape / overlay click / close button)
Open at a specific stepuseDeposit().open({ method: "wallet" }) — jumps past the picker; useActivity().open({ settlementId }) — opens a receipt directly
Read open stateuseDepositState().state.name !== "closed"

Because state is controller-driven, you can open the dialog from anywhere in the provider subtree without lifting any open-state to the host. Lifecycle events (open, picker selection, quote resolved, settlement confirmed, error, …) are delivered through the typed event bus, not via dialog callbacks.

<DepositDialog />

import { DepositDialog } from "@stridge/kit/deposit/dialog"

<DepositDialog />

That's it for the default case. All three props are optional.

interface DepositDialog.Props {
  container?: HTMLElement | null
  metadata?: Record<string, unknown>
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void
  presentation?: Presentation
}
PropEffect
containerElement the dialog portals into. Defaults to document.body. Pass a transformed ancestor with overflow: clip — e.g. a mobile-mockup frame — to scope the dialog's width, height, and backdrop to that rect. Also the box measured for "auto" presentation.
metadataConsumer-attached metadata rides on every bus event from this session. Snapshotted at deposit.opened and frozen for the session; mid-flow changes are ignored to keep correlation consistent. The Kit does not validate the shape.
onErrorForwarded to the dialog's internal <Deposit.Boundary> for crash reporting. The boundary's onReset is wired automatically through useDeposit().close().
presentationPer-dialog surface override — "auto" (default) / "dialog" / "drawer", or { mode, breakpoint }. Wins over the provider's appearance.presentation. See Responsive presentation.

Internally the dialog is the canonical compound composition — <Deposit.Boundary><Deposit.Guards><Deposit.Steps> with one <Deposit.Step> per FSM screen, plus the inline <DepositStatusBanner />. See Compound components if you want to author that composition yourself.

<WithdrawDialog />

Withdraw is the inverse direction. The user picks a destination chain and token; the Kit hands your host a fresh UDA target; your backend broadcasts the source-chain transfer to that UDA; the Kit watches for settlement and renders the final state.

import { WithdrawDialog } from "@stridge/kit/withdraw/dialog"

<WithdrawDialog
  balance={marginBalance}
  onSubmit={async (input, actions) => {
    try {
      const { hash } = await yourBackend.broadcast(input.depositTarget)
      actions.beginProcessing({ hash })
    } catch (err) {
      actions.fail({
        reason: err instanceof Error ? err.message : String(err),
      })
    }
  }}
/>
interface WithdrawDialog.Props {
  container?: HTMLElement | null
  metadata?: Record<string, unknown>
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void
  balance?: WithdrawBalanceInput
  onSubmit?: WithdrawSubmitCallback
  suggestedRecipient?: WithdrawSuggestedRecipient
  presentation?: Presentation
}
PropNotes
containerSame as deposit. Also the box measured for "auto" presentation.
metadataSame as deposit. Frozen at withdraw.opened.
onErrorSame as deposit.
balanceWithdrawable balance for the form to display. Either a bare amount in display units (Kit short-circuits known stablecoins at $1) or { amount, amountUsd? }. Pass undefined while loading — the form renders a skeleton. Reactive — the form re-renders when the value changes.
onSubmitRequired when you mount the dialog. The Kit hands you a (input, actions) pair; your code broadcasts the source-chain transfer to input.depositTarget and calls one of actions.beginProcessing, actions.setTxHash, actions.succeed, actions.fail, actions.decline to advance the FSM. Missing the callback lands the user on the error screen instead of an infinite spinner.
suggestedRecipientTrusted recipient address surfaced as a one-click prefill chip next to the recipient input. Omit to hide the chip.
presentationSame as deposit — per-dialog surface override. See Responsive presentation.

The withdraw submit contract

onSubmit is the only place the Kit hands work back to your host — the rest is automatic.

type WithdrawSubmitCallback = (
  input: WithdrawSubmitInput,
  actions: WithdrawSubmitActions,
) => Promise<void> | void

interface WithdrawSubmitInput {
  depositTarget: {
    address: `0x${string}` | string
    networkId: string
    symbol: string
  }
  amount: { value: number; formatted: string }
  correlation: { owner: `0x${string}` /* …other ids… */ }
  recipient: { address: `0x${string}` | string }
}

interface WithdrawSubmitActions {
  beginProcessing(tx?: { hash?: string }): void
  setTxHash(tx: { hash: string }): void
  succeed(): void
  fail(failure: { reason: string }): void
  decline(failure?: { reason?: string }): void
}

Action call order. Pick one, in this order, depending on what you know:

WhenCall
Your backend accepted the request, broadcast pendingactions.beginProcessing() — moves the user to the processing screen.
Broadcast resolved and you have a tx hashactions.beginProcessing({ hash }) — upgrades the watcher from best-match to tx-anchored.
You learn the hash later (e.g. wallet signs after a delay)actions.setTxHash({ hash }) — upgrades the watcher mid-processing.
Stridge confirms settlement (default)Do nothing — the dialog resolves itself.
You want to short-circuit to successactions.succeed() — optional terminal success.
Broadcast failed or was rejectedactions.fail({ reason }) — terminal failure with a reason string.
User cancelled at the walletactions.decline() — soft-fail; the dialog offers retry.
Note

The onSubmit contract is distinct from the event bus. Bus events are for analytics ("the user reached confirm"); onSubmit's return is what drives the FSM. Don't try to drive the FSM from a bus subscriber — the bus fires after state transitions, not before.

<ActivityDialog />

The drop-in for the read-only activity surface — a unified settlement-history viewer (deposits + withdrawals). Mount it once inside the provider; open it with useActivity().open(), or open straight into a receipt with useActivity().open({ settlementId }) / open({ txHash }). There's no onSubmit and no money path — it only reads history.

import { ActivityDialog } from "@stridge/kit/activity/dialog"

<ActivityDialog />
interface ActivityDialog.Props {
  container?: HTMLElement | null
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void
  presentation?: Presentation
  metadata?: Record<string, unknown>
}
PropEffect
containerSame as deposit — the portal target and the box measured for "auto" presentation.
onErrorForwarded to the dialog's internal <ActivityFlow.Boundary>; recovers via useActivity().close().
presentationPer-dialog surface override — see Responsive presentation.
metadataConsumer-attached metadata, frozen at activity.opened and attached to every activity.* event in the session.

For driving the surface from your own UI (FSM state, direct-entry, prefetch), see Headless → Activity.

Portal containers

By default the dialog portals into document.body, painting a full-page backdrop. To scope it to a specific region (mobile preview frame, embedded mock), pass container:

const frameRef = useRef<HTMLDivElement>(null)

<div ref={frameRef} className="phone-frame" style={{ overflow: "clip" }}>

  <DepositDialog container={frameRef.current} />
</div>

The Kit handles the rest — the dialog's width, backdrop, and animation all clip to the container.

Responsive presentation

A centred modal is the wrong shape on a phone-width surface. Below a breakpoint the dialogs render as a bottom-sheet drawer instead — the native mobile pattern — and as the centred dialog above it. You don't branch on viewport: the Kit measures the dialog's containing block (its portal container, else the viewport) and picks the surface for you.

// Global default for every gateway dialog.
<StridgeProvider appearance={{ presentation: "auto" }}>

// Per-dialog override — wins over the provider default.
<WithdrawDialog presentation="dialog" />

// Tune the breakpoint (default 600px).
<DepositDialog presentation={{ mode: "auto", breakpoint: 720 }} />
ValueSurface
"auto" (default)Dialog at/above the breakpoint, drawer below it.
"dialog"Always the centred dialog.
"drawer"Always the bottom-sheet drawer.
{ mode, breakpoint }Object form to tune the px threshold (default 600).

Precedence: per-dialog presentation prop > appearance.presentation > "auto". Set it once on the provider and override a single dialog when you need to.

Note

"auto" is reactive. Resizing across the breakpoint (rotating a device, dragging a window) live-swaps dialog↔drawer mid-flow — only the surface subtree remounts, so flow state survives because the FSM controllers sit above the surface. Focus is the one transient that re-runs on the swap.

A drawer has no close (X) button — it dismisses via the grabber, swipe-down, backdrop tap, or escape. The X is dialog-only; the Kit hides it automatically in drawer mode, so your composition needs no change. The drawer's surface and its grabber are targetable from CSS — see the drawer-content / drawer-handle slots and the --stridge-kit-drawer-* tokens.

Custom FSM screens

Both dialogs are pre-composed from the compound parts — they exist for the 80% who want zero composition work. The moment you want to swap a screen (replace the success state, redesign the asset picker, embed deposit inline rather than modal), drop down to the compound surface. The provider, the FSM, and the settlement watcher don't change.

Next

Was this page helpful?