npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@unifold/headless-react

v0.1.70-beta.1

Published

Unifold Headless React SDK - hooks-only (no UI) crypto deposit flows

Readme

@unifold/headless-react

Hooks-only (no UI) React SDK for Unifold crypto deposits. You own 100% of the rendering; the SDK owns the data flow, session lifecycle, polling, and events.

  • Zero UI, zero CSS — no components, no Tailwind, no portals.
  • Layered like Stripe.js — the flow logic lives in a framework-agnostic DepositSession controller in @unifold/core; this package is a thin React binding over it.
  • Events first — every state transition is observable as a typed event using the same webhook-style envelope ({ id, type, created, data: { object } }) as @unifold/connect-react's onEvent. Callbacks (onSuccess, …) are sugar over the event stream.

Already using @unifold/connect-react (the modal SDK)? Don't install this package — import the same hooks from @unifold/connect-react/headless instead. Install exactly one of the two packages.

Install

npm install @unifold/headless-react
# or
pnpm add @unifold/headless-react

Setup

Same provider as the modal SDK — the two SDKs can coexist in one app, sharing the provider and QueryClient:

import { UnifoldProvider } from '@unifold/headless-react';

function App() {
  return (
    <UnifoldProvider publishableKey="pk_live_...">
      <YourApp />
    </UnifoldProvider>
  );
}

Quickstart — a custom deposit screen

import { useDeposit } from '@unifold/headless-react';

function DepositUSDC({ externalUserId }: { externalUserId: string }) {
  const { status, getAddress, latestExecution } = useDeposit({
    externalUserId,
    destination: {
      chainType: 'ethereum',
      chainId: '8453',
      tokenAddress: USDC_BASE,
      recipientAddress: userTreasuryAddress,
    },
    onSuccess: (execution) => toast.success(`Received $${execution.destinationAmountUsd}`),
  });

  const eth = getAddress({ chainType: 'ethereum' });
  if (!eth) return <Spinner />;

  return (
    <div>
      <MyQrCode value={eth.address} />
      <CopyField value={eth.address} />
      {status === 'processing' && <MyProgress execution={latestExecution!} />}
    </div>
  );
}

useDeposit drives an explicit status state machine:

idle → creating_addresses → awaiting_funds → processing → succeeded | failed
                          ↘ error (fatal: address creation / invalid recipient)

The headless SDK is deliberately not IP-awareuseDeposit never geo-gates address creation. If you want the modal's region gate, opt in with useAllowedCountry and gate your own UI on isAllowed.

Manual confirmation + full event wiring

const deposit = useDeposit({
  externalUserId,
  destination,
  confirmationMode: 'manual',
  onEvent: (event) => analytics.track(event.type, event),
  onExecutionUpdated: (execution) => setTimeline((t) => [...t, execution]),
  onError: (error) => {
    if (error.code === 'POLLING_ERROR') showBanner('Connection hiccup — still watching');
    else showFailure(error);
  },
});

<Button onClick={deposit.confirmFundsSent} disabled={deposit.isCheckingDeposit}>
  I've made the transfer
</Button>;

Promise waiters

For imperative flows, the session exposes await-style sugar over the event stream (they only listen — neither starts nor stops the session):

// Generic primitive over the status state machine:
await session.waitForStatus('processing');                 // deposit detected
await session.waitForStatus(['succeeded', 'failed']);      // terminal outcome

// The 90% case — mirrors beginDeposit()'s promise contract
// (resolve on success, reject on failure):
try {
  const execution = await session.waitForSuccess();
  creditUser(execution.destinationAmountUsd);
} catch (error) {
  // DepositSessionWaitError: DEPOSIT_FAILED | SESSION_ERROR | ABORTED | DESTROYED
}

Both accept { signal } — an AbortSignal cancels the wait, never the session (a deposit isn't cancelable: once the user sends funds, they arrive whether or not anyone is awaiting). For a deadline, compose the platform primitive — waitForSuccess({ signal: AbortSignal.timeout(60_000) }) — and treat it as "outcome still unknown", not failure: keep the session (and your UI) watching. From useDeposit, reach the waiters via the session escape hatch.

One session, many executions. Unlike quote-scoped models (e.g. Privy's, where one address maps to one order), a Unifold session can observe multiple executions — the user may send twice, or on two different chains, to the same universal addresses. waitForSuccess is one-shot first-completion detection; the session keeps polling afterwards, and every settlement fires its own direct_execution.succeeded event.

Live outcomes only. The 60s lookback window exists to catch deposits sent moments before the session started — it only admits executions still in-flight at first sight (their settlement then fires live). An execution that already settled before the session began is history and never re-fires success/failure events, so reopening a deposit screen right after a success cannot double-credit. Render history with useExecutions instead. To credit each deposit, subscribe to events (dedupe on execution.id) or render executions from the hook:

session.on(DepositSessionEventType.EXECUTION_SUCCEEDED, ({ data }) => {
  creditUser(data.object); // fires once per settled execution
});

Vanilla JS (no React)

The controller is usable without React from @unifold/core:

import { createUnifoldClient } from '@unifold/core';

const unifold = createUnifoldClient({ publishableKey: 'pk_live_...' });
const session = unifold.createDepositSession({ externalUserId, destination });

await session.start();
renderQr(session.getSnapshot().addresses);

const execution = await session.waitForSuccess();
showSuccess(execution);
session.destroy();

Hooks

| Hook | Purpose | | --- | --- | | useDeposit | Flagship flow hook: deposit addresses + execution detection + status state machine + events | | useDepositAddresses | Addresses without a live session (cached, idempotent create) | | useSupportedDepositTokens | Source token/chain list for custom pickers | | useExecutions | Deposit history (tracker screens) | | useAllowedCountry | Geo gate the modal uses; decide what to render when blocked | | useAddressValidation | Inline recipient validation (e.g. Algorand opt-in) | | useUnifoldClient | Escape hatch to the configured vanilla client |

Events

resource.action names with webhook-mirroring envelopes; direct_execution.succeeded is byte-compatible with the modal SDK's onEvent:

  • deposit_session.started / .addresses_created / .confirmation_started / .stopped / .errored
  • direct_execution.detected / .updated / .succeeded / .failed

When mixing the modal and headless surfaces, dedupe on execution.id (envelope sevt_ ids are minted per emitter), and keep one active surface per flow at a time.

License

Apache-2.0