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

@heliofi/checkout-react-native

v0.2.1

Published

MoonPay Commerce SDK for React Native — accept crypto payments in your app with one hook.

Readme

@heliofi/checkout-react-native

Accept crypto payments in your React Native app with MoonPay Commerce. Drop-in provider with composable UI components — from zero-config to fully custom.

Installation

npm install @heliofi/checkout-react-native

Peer dependencies

npm install \
  react-native-gesture-handler \
  react-native-reanimated \
  react-native-safe-area-context \
  react-native-svg \
  lucide-react-native

Quick Start

The simplest integration — 4 lines of code:

import {
  MoonpayCommerceProvider,
  usePayWithCrypto,
} from "@heliofi/checkout-react-native";

// 1. Wrap your app
export default function App() {
  return (
    <MoonpayCommerceProvider chargeToken="your-charge-token" network="main">
      <Checkout />
    </MoonpayCommerceProvider>
  );
}

// 2. Use the hook
function Checkout() {
  const { payWithCrypto } = usePayWithCrypto();

  return <Button onPress={() => payWithCrypto()} title="Pay with Crypto" />;
}

That's it. The SDK shows a wallet selector, deeplinks to the chosen wallet, polls the charge for confirmation, and displays success/error states — all automatically.

Creating a Charge

Charges are single-use checkout sessions. Always create them on your backend — never expose your API key to the client. See the full Charges API docs: https://docs.hel.io/docs/charges. The response contains a token (the charge token). Send that to your client and pass it to the provider:

<MoonpayCommerceProvider chargeToken={chargeTokenFromBackend} ...>

Why a charge instead of a pay link? A charge is one-time-use, tied to a specific user/order, and produces a polling endpoint the SDK uses to confirm the on-chain transaction. The SDK does not create charges — that's a server responsibility.

Provider API

<MoonpayCommerceProvider
  chargeToken="your-charge-token" // Required — charge token from your backend
  network="main" // Optional — "main" (default) or "test" for devnet
  theme="system" // Optional — "light" | "dark" | "system" (default)
  onProcessing={({ showDefaultUI }) => {}} // Optional — suppresses built-in processing UI
  onVerifying={({ showDefaultUI }) => {}} // Optional — suppresses built-in verifying UI
  onSuccess={(result, { showDefaultUI }) => {}} // Optional — suppresses built-in success UI
  onError={(result, { showDefaultUI }) => {}} // Optional — suppresses built-in error UI
>
  {children}
</MoonpayCommerceProvider>

Props

| Prop | Type | Required | Default | Description | | -------------- | ------------------------------- | -------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | chargeToken | string | Yes | — | Single-use charge token created on your backend via the Charges API. The post-payment redirect target is owned by the charge — set successRedirectUrl (and optionally cancelRedirectUrl) when you create it. | | network | "main" \| "test" | No | "main" | API environment. Use "test" for development | | theme | "light" \| "dark" \| "system" | No | "system" | Theme for SDK UI components | | onProcessing | StatusCallback | No | — | Called when wallet deeplink opens. Suppresses built-in processing UI. | | onVerifying | StatusCallback | No | — | Called when polling starts. Suppresses built-in verifying UI. | | onSuccess | PaymentCallback | No | — | Called when payment is confirmed. Suppresses built-in success UI. | | onError | PaymentCallback | No | — | Called when payment fails. Suppresses built-in error UI. |

Note: The SDK does not depend on expo-linking or any Expo-specific modules. It uses React Native's built-in Linking API. The redirect after payment is controlled by the charge's successRedirectUrl/cancelRedirectUrl (set on your backend when you create the charge) — your app just needs to register the matching URL scheme so the OS routes the deeplink back.

Note on chargeToken: Because the SDK polls for transaction status (and may need to re-poll after the user closes and reopens the app), the charge token must live on the Provider so it's stable across the entire payment flow. It's not a per-call option.

Hook API

const {
  payWithCrypto, // Trigger a payment
  wallets, // Available wallets (from API)
  status, // Current payment status
  isLoading, // True while wallet list is being fetched
  reset, // Reset status to idle
  transactionResult, // Transaction details after completion
  resolvedTheme, // Current resolved theme ("light" | "dark")
} = usePayWithCrypto({
  onProcessing: ({ showDefaultUI }) => {}, // Optional — suppresses built-in processing UI
  onVerifying: ({ showDefaultUI }) => {}, // Optional — suppresses built-in verifying UI
  onSuccess: (result, { showDefaultUI }) => {}, // Optional — suppresses built-in success UI
  onError: (result, { showDefaultUI }) => {}, // Optional — suppresses built-in error UI
});

Return values

| Field | Type | Description | | ------------------- | --------------------------- | ------------------------------------------------------------------------------ | | payWithCrypto | (options?) => void | Triggers the payment flow. Without options, shows the built-in wallet selector | | wallets | Wallet[] | Available wallets, filtered by the charge's blockchain. Prefetched on mount | | status | PaymentStatus | "idle" | "processing" | "verifying" | "success" | "error" | | isLoading | boolean | true while the wallet list is being fetched from the API | | reset | () => void | Resets status back to "idle" | | transactionResult | TransactionResult \| null | Contains status and optional transactionSignature after completion | | resolvedTheme | "light" \| "dark" | The resolved theme (accounts for "system" preference) |

payWithCrypto() options

// Show built-in wallet selector
payWithCrypto();

// Skip wallet selector — go directly to a specific wallet
// (use the `id` from the wallets list returned by usePayWithCrypto())
payWithCrypto({ wallet: "PHANTOM" });

Components

The SDK exports composable UI components. Mix and match to build your ideal checkout experience.

PaymentDrawer

A bottom sheet that handles the full payment lifecycle. Shows your wallet selection UI when idle, then automatically transitions to status displays (processing, verifying, success, error) during payment.

import {
  PaymentDrawer,
  DrawerHeader,
  WalletButtons,
} from "@heliofi/checkout-react-native";

function Checkout() {
  const [visible, setVisible] = useState(false);

  return (
    <>
      <Button onPress={() => setVisible(true)} title="Pay" />

      <PaymentDrawer visible={visible} onClose={() => setVisible(false)}>
        <DrawerHeader
          title="Connect Wallet"
          onClose={() => setVisible(false)}
        />
        <WalletButtons />
      </PaymentDrawer>
    </>
  );
}

DrawerHeader

Styled header with title and close button. Use inside PaymentDrawer or your own modal.

<DrawerHeader title="Pay with Crypto" onClose={handleClose} />

WalletButtons

Renders all available wallets as a scrollable list with detection badges. Reads wallet data from context.

<WalletButtons />

WalletButton

Renders a single wallet row. Use when you want to filter, reorder, or wrap individual wallet items.

import { WalletButton } from "@heliofi/checkout-react-native";

const { wallets } = usePayWithCrypto();

{
  wallets
    .filter((w) => w.enabled)
    .map((wallet) => <WalletButton key={wallet.id} wallet={wallet} />);
}

StatusContent

Pre-built status display with icons, titles, and action buttons. Use it in your own modal, inline on your page, or anywhere you want status UI.

import { StatusContent } from "@heliofi/checkout-react-native";

const { status, reset } = usePayWithCrypto();

{
  status !== "idle" && (
    <StatusContent status={status} onDone={reset} onRetry={reset} />
  );
}

Composition Examples

Level 1: Zero Config

The SDK handles everything — wallet modal, deeplinks, polling, status UI.

const { payWithCrypto } = usePayWithCrypto();

<Button onPress={() => payWithCrypto()} title="Pay with Crypto" />;

Level 2: Custom Drawer with SDK Components

Use PaymentDrawer + WalletButtons for a custom bottom sheet that still handles status transitions automatically.

const [visible, setVisible] = useState(false);

<Button onPress={() => setVisible(true)} title="Pay" />

<PaymentDrawer visible={visible} onClose={() => setVisible(false)}>
  <DrawerHeader title="Choose Wallet" onClose={() => setVisible(false)} />
  <WalletButtons />
</PaymentDrawer>

Level 3: Individual Wallet Buttons

Cherry-pick which wallets to show, filter by enabled status, or add your own UI around each wallet.

const { wallets } = usePayWithCrypto();

<PaymentDrawer visible={visible} onClose={close}>
  <DrawerHeader title="Choose Wallet" onClose={close} />
  {wallets
    .filter((w) => w.enabled)
    .map((wallet) => (
      <WalletButton key={wallet.id} wallet={wallet} />
    ))}
</PaymentDrawer>;

Level 4: Fully Custom UI

Build your own wallet list using the wallets array and payWithCrypto({ wallet }).

const { payWithCrypto, wallets } = usePayWithCrypto({
  onSuccess: (result) => console.log("Paid!", result.transactionSignature),
});

<Modal visible={visible}>
  {wallets.map((wallet) => (
    <Pressable
      key={wallet.id}
      onPress={() => payWithCrypto({ wallet: wallet.id })}
    >
      <Text>{wallet.name}</Text>
    </Pressable>
  ))}
</Modal>;

Level 5: Custom Status Display

Use StatusContent in your own layout, or read status directly for a completely custom experience.

const { status, reset } = usePayWithCrypto();

// Option A: Use the pre-built StatusContent component
{
  status !== "idle" && (
    <StatusContent status={status} onDone={reset} onRetry={reset} />
  );
}

// Option B: Build your own from the status value
{
  status === "verifying" && <MySpinner text="Checking blockchain..." />;
}
{
  status === "success" && <MyCheckmark text="Payment confirmed!" />;
}
{
  status === "error" && (
    <MyError
      onRetry={() => {
        reset();
        payWithCrypto();
      }}
    />
  );
}

Theming

The SDK supports light, dark, and system-auto themes. Pass the theme prop to the provider:

<MoonpayCommerceProvider theme="dark" chargeToken="..." network="main">

All SDK components (WalletButtons, DrawerHeader, PaymentDrawer, StatusContent) automatically adapt to the active theme using internal design tokens (plain StyleSheet.create() under the hood — no nativewind, no CSS, no setup).

Theme-matching custom UI

If you're building custom UI alongside the SDK and want it to visually match, use the useTokens hook to read the same color/spacing/radius values the SDK uses:

import { useTokens } from "@heliofi/checkout-react-native";

function MyButton() {
  const t = useTokens();
  return (
    <Pressable
      style={{
        backgroundColor: t.colors.primary,
        paddingHorizontal: t.spacing.lg,
        paddingVertical: t.spacing.md,
        borderRadius: t.radii.md,
      }}
    >
      <Text
        style={{ color: t.colors.textInverse, fontWeight: t.fontWeight.bold }}
      >
        Pay
      </Text>
    </Pressable>
  );
}

useTokens() automatically returns the right tokens for the current theme prop (light/dark). The Tokens type is also exported if you need to type a styles factory.

Callbacks

Callbacks can be set at two levels (Provider and hook). Both fire if both are set.

Key behavior: Providing a callback for a status suppresses the built-in UI for that specific status. Each status is independently controllable — cherry-pick which ones you handle and which ones the SDK handles.

// Global (Provider level)
<MoonpayCommerceProvider
  onVerifying={({ showDefaultUI }) => {
    analytics.track("payment_verifying");
    showDefaultUI(); // still show the spinner
  }}
  onSuccess={(result) => analytics.track("payment_success")}
  onError={(result) => Sentry.captureMessage("payment_failed")}
>

// Per-hook
const { payWithCrypto } = usePayWithCrypto({
  onSuccess: (result) => router.push("/order-confirmed"),
  onError: (result) => Alert.alert("Failed", result.status),
});

Available callbacks

| Callback | Signature | When it fires | | -------------- | --------------------------- | ---------------------------------------- | | onProcessing | (helpers) => void | Wallet deeplink opened, waiting for user | | onVerifying | (helpers) => void | User returned, polling charge status | | onSuccess | (result, helpers) => void | Transaction confirmed on-chain | | onError | (result, helpers) => void | Transaction failed or timed out |

Showing built-in UI from a callback

Each callback receives a helpers object with showDefaultUI(). Call it to show the SDK's built-in drawer for that status even when you have a custom callback:

const { payWithCrypto } = usePayWithCrypto({
  // Track + still show built-in verifying spinner
  onVerifying: ({ showDefaultUI }) => {
    analytics.track("payment_verifying");
    showDefaultUI();
  },

  // Custom success — built-in success UI suppressed
  onSuccess: (result) => {
    router.push("/order-confirmed");
  },

  // Custom logic + still show built-in error drawer
  onError: (result, { showDefaultUI }) => {
    Sentry.captureException(result);
    showDefaultUI();
  },
});

Summary

| Scenario | Built-in UI | | -------------------------------- | ----------------------- | | No callback provided | Shown automatically | | Callback provided | Suppressed — you own it | | Callback calls showDefaultUI() | Shown on demand | | Callback is () => {} (no-op) | Suppressed |

Types

type TransactionResult = {
  status: "SUCCESS" | "FAILED" | "UNKNOWN";
  transactionSignature?: string;
};

type CallbackHelpers = {
  showDefaultUI: () => void;
};

// For terminal states (success/error) — receives the result
type PaymentCallback = (
  result: TransactionResult,
  helpers: CallbackHelpers,
) => void;

// For transient states (processing/verifying) — no result yet
type StatusCallback = (helpers: CallbackHelpers) => void;

Polling Behaviour

The SDK polls GET /charge/{chargeToken} to determine when a payment has been confirmed on-chain.

| Trigger | Behaviour | | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provider mounts (or chargeToken changes) | One-shot poll. Catches the case where the charge was already paid (e.g., user closed the app after paying). | | Status is processing or verifying AND app is foregrounded | Continuous polling with staged backoff: every 3s for the first minute (catches Solana, Base, Arbitrum, BSC), then every 15s up to 10 minutes (catches Ethereum, Tron, Polygon), then every 60s up to 30 minutes (catches Bitcoin, Doge, deep ETH confirmations). | | Status leaves processing/verifying (terminal/idle/reset) | Polling stops. | | App goes to background | Polling stops; resumes on next foreground. | | 30-minute total timeout | Polling stops with UNKNOWN result. Note: this does NOT fire onError (the transaction may still confirm on-chain after this point — it's just no longer being watched). The status returns to idle and the user can retry by calling payWithCrypto again. |

Hydration vs witnessed transitions

Because charges are persistent (the same chargeToken may be paid hours before the app is opened again), the SDK distinguishes between witnessed payments and hydrated state:

  • Witnessed payment: the SDK saw the user start the flow in this session — status went through processing and/or verifying. When the poll then returns SUCCESS/FAILED, onSuccess/onError fires.
  • Hydrated state: the SDK boots, polls, and discovers the charge is already in a terminal state without any user action in this session. status updates so you can read it (and transactionResult is populated), but onSuccess/onError does not fire.

Without this distinction, onSuccess would fire on every cold launch with a paid charge, causing duplicate route pushes, analytics events, or order fulfilment.

If you need to react to a hydrated terminal state (e.g., navigate the user to the order confirmation page), read status in your own effect:

const { status, transactionResult } = usePayWithCrypto();

useEffect(() => {
  if (status === "success") {
    router.replace("/order-confirmed");
  }
}, [status]);

Deeplink returns are always treated as witnessed signals — ?status=error from the dashboard fires onError even on a cold start.

Best practice: rotate chargeToken between checkouts. A charge represents a single checkout session, and the SDK's behaviour is cleanest when each session has its own token.

Platform Support

| Platform | Behavior | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | iOS / Android | Opens the wallet via deeplink (Phantom, MetaMask, Solflare, etc.). After payment the hosted page redirects to the charge's successRedirectUrl — your app catches that deeplink and the SDK polls the on-chain result. | | Web | Redirects to the hosted payment page. After payment the page navigates to the charge's successRedirectUrl. |

Supported Wallets

Wallet availability is determined by the charge's blockchain configuration and returned dynamically from the API.

Solana: Phantom, Solflare, Backpack, Coinbase Wallet, Trust Wallet, OKX Wallet

EVM: MetaMask, Coinbase Wallet, Trust Wallet, Rainbow, OKX Wallet, Phantom

TypeScript

All types are exported:

import type {
  CallbackHelpers,
  MoonpayCommerceProviderProps,
  Network,
  PaymentCallback,
  PaymentStatus,
  PayWithCryptoHookOptions,
  PayWithCryptoOptions,
  StatusCallback,
  ThemeMode,
  TransactionResult,
  Wallet,
} from "@heliofi/checkout-react-native";

License

MIT