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

@swype-org/deposit-mobile

v1.0.9

Published

Lightweight mobile deposit SDK — opens a hosted Swype payment flow in an in-app browser sheet, handles deep-link completion, verifies a user is human

Readme

@swype-org/deposit-mobile

Note: For the best native UX (biometric passkey prompts with no browser chrome), use the platform-specific SDKs instead:

This SDK remains available as a fallback for React Native / Expo apps that prefer the simpler in-app browser integration without native bridging.

Lightweight mobile deposit SDK — opens a hosted Swype payment flow in an in-app browser sheet and handles deep-link completion. No hard dependencies; expo-web-browser is an optional peer.

Quick Start

npm install @swype-org/deposit-mobile
import { MobileDeposit } from '@swype-org/deposit-mobile';
import * as WebBrowser from 'expo-web-browser';

const deposit = new MobileDeposit({
  signer: 'https://api.merchant.com/sign-payment',
  callbackScheme: 'myapp',
  // Lets the SDK open (and dismiss) the browser itself, as a sheet.
  webBrowserModule: WebBrowser,
});

// Listen for deep links and pass them to the SDK
onDeepLink((url) => deposit.handleDeepLink(url));

// Start a deposit
const { transfer } = await deposit.requestDeposit({
  amount: 50,
  chainId: 8453,
  address: '0x...',
  token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
});

console.log('Transfer complete:', transfer.id, transfer.status);

React Native Hook

A dedicated React Native entry point eliminates all boilerplate:

npm install @swype-org/deposit-mobile react
import { useBlinkMobileDeposit } from '@swype-org/deposit-mobile/react-native';
import { useEffect } from 'react';
import { Button, Text } from 'react-native';
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';

function DepositButton() {
  const { status, result, error, displayMessage, requestDeposit, handleDeepLink } =
    useBlinkMobileDeposit({
      signer: 'https://api.merchant.com/sign-payment',
      callbackScheme: 'myapp',
      webBrowserModule: WebBrowser,
    });

  useEffect(() => {
    const sub = Linking.addEventListener('url', ({ url }) => handleDeepLink(url));
    return () => sub.remove();
  }, [handleDeepLink]);

  return (
    <>
      <Button
        title={status === 'signer-loading' ? 'Preparing…' : 'Deposit $50'}
        disabled={status === 'signer-loading'}
        onPress={() =>
          requestDeposit({
            amount: 50,
            chainId: 8453,
            address: '0x...',
            token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
          })
        }
      />
      {error && <Text>{displayMessage}</Text>}
      {result && <Text>Transfer {result.transfer.id} complete!</Text>}
    </>
  );
}

Browser Presentation (iOS)

The SDK opens the in-app browser itself, so the presentation is consistent for every merchant. On iOS that is a sheet: the SFSafariViewController stops short of the status bar, leaving a sliver of your app visible behind it, and the user can swipe it down to dismiss. Passkey prompts render over the sheet as normal.

expo-web-browser is an optional peer dependency. Install it and hand it to the SDK:

npx expo install expo-web-browser
import * as WebBrowser from 'expo-web-browser';

new MobileDeposit({
  signer, callbackScheme,
  webBrowserModule: WebBrowser,
  presentation: 'sheet', // default; 'fullScreen' covers the screen instead
});

Why pass the module? Metro resolves imports statically, so a bundled library cannot reliably require an optional dependency out of the host app. The SDK does attempt a lookup, but webBrowserModule is the only path guaranteed to work — pass it.

⚠️ Upgrading from 1.0.5 or earlier

Once the SDK has a browser module (webBrowserModule), your openUrl and closeBrowser hooks are no longer called — the SDK opens and dismisses the browser itself. This is deliberate, so the presentation is consistent for every merchant, but it means any extra configuration you did inside openUrl silently stops applying: toolbar tint (toolbarColor / controlsColor), dismissButtonStyle, Android browserPackage or share-menu options, and analytics wrappers.

If you relied on any of those, pass your module explicitly and keep control:

import * as WebBrowser from 'expo-web-browser';

new MobileDeposit({
  signer, callbackScheme,
  webBrowserModule: {
    openBrowserAsync: (url, options) =>
      WebBrowser.openBrowserAsync(url, { ...options, toolbarColor: '#101010' }),
    dismissBrowser: () => WebBrowser.dismissBrowser(),
  },
});

Hosts without expo-web-browser

Bare React Native apps have no module to pass, so openUrl still runs exactly as before. Forward the second argument to honour presentation:

openUrl: (url, options) => openInAppBrowser(url, options),

With neither expo-web-browser nor openUrl, requestDeposit rejects with BROWSER_FAILED rather than failing silently.

Two limits worth knowing

  • iOS only. Android Custom Tabs expose no equivalent presentation control, so the setting is ignored there.
  • One height. expo-web-browser has no API for sheet detents, so the sheet is the system's tall pageSheet — it cannot rest at a partial height or be dragged between sizes. That needs a native module presenting SFSafariViewController with sheetPresentationController.detents.

How It Works

Merchant App / SDK              Merchant Signer                Hosted Flow (in-app browser)
     │                               │                               │
     │  1. requestDeposit(request)   │                               │
     │──────────────────────────────►│                               │
     │                               │  2. signer(data) or POST URL  │
     │                               │  (includes callbackScheme     │
     │                               │   and webviewBaseUrl)         │
     │  3. { signature, payload, ...}│                               │
     │◄──────────────────────────────│                               │
     │                                                               │
     │  4. SDK builds URL, opens the in-app browser sheet            │
     │──────────────────────────────────────────────────────────────►│
     │                                                               │
     │  5. User completes payment in hosted flow                     │
     │                                                               │
     │  6. Redirect → myapp://swype/callback?transferId=...          │
     │◄─────────────────────────────────────────────────────────────│
     │                                                               │
     │  7. handleDeepLink(url) → Promise resolves with DepositResult │

Signer Contract

The signer config option controls how the SDK obtains a signed payment link. It accepts either a URL string or a custom function, giving you full control over authentication, HTTP method, and request shape. This is the same v1 signer contract as @swype-org/deposit (web), except callbackScheme is set to the mobile app's URL scheme so the hosted flow can return to the app.

Using a URL string (simple)

When signer is a string, the SDK sends a POST with a JSON body to that URL and expects a SignerResponse back:

const deposit = new MobileDeposit({
  signer: 'https://api.merchant.com/sign-payment',
  callbackScheme: 'myapp',
});

Using a custom function (full control)

When signer is a function, the SDK calls it with a SignerRequest object and expects a Promise<SignerResponse>. Use this when you need control over the HTTP method, authentication, request transformation, or any other aspect of the signing call:

import type { SignerFunction } from '@swype-org/deposit-mobile';

const deposit = new MobileDeposit({
  signer: async (data) => {
    const res = await fetch('https://api.merchant.com/sign-payment', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`,
      },
      body: JSON.stringify({ ...data, orderId: 'order-123' }),
    });
    if (!res.ok) throw new Error(`Signer error: ${res.status}`);
    return res.json();
  },
  callbackScheme: 'myapp',
});

SignerRequest (input)

The data the SDK passes to your signer (as the JSON body for URL mode, or as the function argument for function mode):

| Field | Type | Description | | --- | --- | --- | | amount | number | USD amount to deposit (always > 0). | | chainId | number | Numeric chain ID for the destination (e.g. 8453 for Base, 792703809 for Solana). | | address | string | Destination wallet address: EVM 0x... address or Solana Base58 address. | | token | string | Token address on the destination chain: EVM contract address or Solana SPL mint address. | | callbackScheme | string | URL scheme registered by the mobile app (e.g. "myapp"). The hosted flow redirects to {callbackScheme}://swype/callback?... on completion. | | url | string | Base webview URL the SDK will navigate to. Provided for logging/validation — your signer does not construct the final URL. | | version | string | Protocol version (currently "v1"). | | reference | string? | Merchant order or invoice ID for reconciliation. | | metadata | object? | Arbitrary key-value pairs forwarded from the merchant app. |

Example:

{
  "amount": 50,
  "chainId": 8453,
  "address": "0x...",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "callbackScheme": "myapp",
  "url": "https://pay.blink.cash",
  "version": "v1"
}

What the signer must do

  1. Validate the request fields.
  2. Generate an idempotency key (UUID) for this payment.
  3. Build a payload — a base64url-encoded JSON string containing the payment parameters:
{
  "amount": 50,
  "chainId": 8453,
  "address": "0x...",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "idempotencyKey": "generated-uuid",
  "callbackScheme": "myapp",
  "expiresAt": "2026-03-07T12:00:00Z",
  "version": "v1"
}
  1. Sign the payload string with your merchant private key (SHA-256) and base64url-encode the signature.

SignerResponse (output)

The response your signer must return (as JSON for URL mode, or as the resolved value for function mode):

| Field | Type | Description | | --- | --- | --- | | merchantId | string | Your merchant UUID. | | payload | string | Base64url-encoded payment payload (see above). | | signature | string | Base64url-encoded signature of the payload string. | | expiresAt | string | ISO 8601 expiration timestamp for this payment link. | | preview | object | Echo of the payment parameters for client-side display. | | preview.amount | number | Deposit amount. | | preview.chainId | number | Destination chain ID. | | preview.address | string | Destination wallet address. | | preview.token | string | Destination token address or mint. | | preview.idempotencyKey | string | The generated idempotency key. |

Example:

{
  "merchantId": "uuid",
  "payload": "base64url-encoded-payload",
  "signature": "base64url-encoded-signature",
  "expiresAt": "2026-03-07T12:00:00Z",
  "preview": {
    "amount": 50,
    "chainId": 8453,
    "address": "0x...",
    "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "idempotencyKey": "uuid"
  }
}

The SDK constructs the hosted flow URL by appending merchantId, payload, and signature as query parameters to the webviewBaseUrl, then opens it in the in-app browser. When the user completes payment the hosted flow redirects to {callbackScheme}://swype/callback?... and the SDK resolves the promise.

Configuration

const deposit = new MobileDeposit({
  // Required: URL string or custom async function (see "Signer Contract")
  signer: 'https://api.merchant.com/sign-payment',

  // Required: URL scheme registered by your mobile app (e.g. 'myapp')
  callbackScheme: 'myapp',

  // Optional (iOS): 'sheet' presents the browser as a card with a sliver of
  // your app visible behind it; 'fullScreen' covers the screen.
  // Default: 'sheet'
  presentation: 'sheet',

  // Optional: only used when expo-web-browser is NOT installed (bare RN).
  // Forward the second argument so `presentation` still applies.
  openUrl: (url, options) => openInAppBrowser(url, options),

  // Optional: dismiss hook for that same fallback path
  closeBrowser: () => dismissInAppBrowser(),

  // Optional: supply/wrap the expo-web-browser module yourself
  webBrowserModule: WebBrowser,

  // Optional: base URL of the hosted payment webview app.
  // Default: 'https://pay.blink.cash'
  webviewBaseUrl: 'https://pay.blink.cash',

  // Optional: path for the callback deep link (default: '/swype/callback')
  callbackPath: '/swype/callback',

  // Optional: hosted UI color scheme. { theme: 'light' | 'dark' | 'system' }
  // Default: { theme: 'light' }
  appearance: { theme: 'dark' },

  // Optional: max ms to wait for signer response (default: 15000)
  signerTimeoutMs: 15_000,

  // Optional: max ms for entire flow (signer + user completion)
  flowTimeoutMs: 300_000,

  // Optional: enable debug logging to console
  debug: false,
});

Deep Link Setup

Your mobile app must be configured to handle the callback URL scheme.

Expo / React Native

In app.json:

{
  "expo": {
    "scheme": "myapp"
  }
}

iOS (native)

Register your URL scheme in Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

Android (native)

Add an intent filter in AndroidManifest.xml:

<activity ...>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" />
  </intent-filter>
</activity>

Handling Deep Links

The SDK does not set up deep link listeners itself — this keeps it platform-agnostic. You must forward incoming URLs to handleDeepLink():

// Expo
import * as Linking from 'expo-linking';
Linking.addEventListener('url', ({ url }) => deposit.handleDeepLink(url));

// React Native (bare)
import { Linking } from 'react-native';
Linking.addEventListener('url', ({ url }) => deposit.handleDeepLink(url));

// iOS native (AppDelegate)
func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {
    // bridge call to deposit.handleDeepLink(url.absoluteString)
}

// Android native (Activity)
override fun onNewIntent(intent: Intent) {
    intent.data?.toString()?.let { deposit.handleDeepLink(it) }
}

handleDeepLink() returns true if the URL was a Swype callback, false otherwise.

Two callback shapes exist:

  • Transfer callback — {scheme}://swype/callback?transferId=...&transferStatus=...: the deposit finished; the pending requestDeposit() promise resolves with the DepositResult.
  • Dismissal callback — {scheme}://swype/callback?transferStatus=DISMISSED (no transferId): the user closed the hosted flow from an in-page control before any transfer was created; the pending promise rejects with DEPOSIT_DISMISSED.

After handling either shape the SDK dismisses the in-app browser automatically — through expo-web-browser when it is installed, otherwise through the optional closeBrowser config hook. On a bare React Native host with no closeBrowser, the browser stays presented until the user taps the OS chrome's Done button.

Human Verification

Prove the signed-in user is a real, unique human with a palm scan; Blink guarantees one palm ↔ one of your user ids. No Blink account is involved.

const deposit = new MobileDeposit({
  signer: 'https://api.merchant.com/sign-payment',
  // Optional: a separate signer for verification. When omitted, `signer` is
  // called with `{ kind: 'human-verification', callbackScheme, url, version }`.
  verificationSigner: 'https://api.merchant.com/sign-verification',
  callbackScheme: 'myapp',
  ...expoBrowser(WebBrowser),
});

try {
  const { sessionId, status } = await deposit.requestVerification(); // a HINT
  await refreshFromMyServer();                                       // the ANSWER
} catch (e) {
  if (e instanceof VerificationError && e.code === 'ALREADY_VERIFIED_OTHER_ACCOUNT') { /* … */ }
}

The hosted page opens top-level in the same in-app browser as a deposit and, when the scan finishes, deep-links {callbackScheme}://swype/callback?verificationSessionId=…&verificationStatus=verified|failed&failureCode=… — pass it to handleDeepLink exactly as for deposits. Older SDK versions ignore that shape (it never carries transferId).

The signer must run behind your own user auth and sign a grant naming your signed-in user as merchantUserId; the SDK never sends one. Your server then reads GET /v1/identity/human-verifications?merchantUserId=… with your merchant key; that read, never the promise, is what unlocks anything. The hook exposes requestVerification, verificationResult, verificationError and verificationDisplayMessage.

Observable Status

| Status | Meaning | | ---------------- | ------------------------------------------- | | idle | No active flow | | signer-loading | Calling the merchant signer endpoint | | browser-active | In-app browser is open, waiting for user | | completed | Transfer succeeded | | error | Something failed |

deposit.on('status-change', (status) => console.log('Status:', status));
deposit.status;   // current status
deposit.result;   // last DepositResult (when completed)
deposit.error;    // last DepositError (when error)
deposit.isActive; // true during signer-loading or browser-active

Error Handling

Every error is a DepositError with a machine-readable code:

| Code | Meaning | | ------------------------ | ----------------------------------------------- | | DEPOSIT_DISMISSED | User closed the deposit flow before completing | | BROWSER_FAILED | Failed to open the in-app browser | | DEEP_LINK_INVALID | Callback deep link was malformed | | SIGNER_REQUEST_FAILED | Signer returned a non-2xx response | | SIGNER_NETWORK_ERROR | Network failure reaching the signer | | SIGNER_RESPONSE_INVALID| Signer response missing required fields | | SIGNER_TIMEOUT | Signer did not respond within signerTimeoutMs | | FLOW_TIMEOUT | Entire flow exceeded flowTimeoutMs | | INVALID_REQUEST | Bad input (amount, address, etc.) |

Use getDisplayMessage() for user-facing strings:

import { DepositError, getDisplayMessage } from '@swype-org/deposit-mobile';

try {
  await deposit.requestDeposit({ ... });
} catch (err) {
  if (err instanceof DepositError) {
    Alert.alert('Payment Error', getDisplayMessage(err));
  }
}

Events

deposit.on('complete', (result) => { /* DepositResult */ });
deposit.on('error', (error) => { /* DepositError */ });
deposit.on('close', () => { /* browser closed or flow cancelled */ });
deposit.on('status-change', (status) => { /* MobileDepositStatus */ });

Lifecycle

// Cancel the current flow
deposit.close();

// No-op retained for API compatibility with @swype-org/deposit
deposit.focus();

// Tear down and release all resources (call on unmount)
deposit.destroy();

Comparison with Other Swype SDKs

| Aspect | @swype-org/deposit (web) | @swype-org/deposit-mobile | checkout-ios-sdk | checkout-android-sdk | | ----------------- | ------------------------------ | ---------------------------------- | -------------------------------- | -------------------------------- | | Platform | Browser | React Native / iOS / Android | iOS 16+ | Android 9+ (API 28) | | Language | TypeScript | TypeScript | Swift | Kotlin | | Passkey handling | Hosted flow (iframe) | Hosted flow (in-app browser) | Native ASAuthorization | Native Credential Manager | | UX | Modal iframe overlay | In-app browser with browser chrome | Direct biometric prompt | Direct biometric prompt | | Flow mechanism | iframe + postMessage | In-app browser + deep link | Direct API calls + passkey | Direct API calls + passkey | | Completion signal | postMessage | URL scheme callback | async return value | Coroutine return value | | Dependencies | None | None | None (Apple frameworks only) | Credential Manager, OkHttp |

TypeScript

All types are exported:

import type {
  MobileDepositConfig,
  MobileDepositStatus,
  DepositRequest,
  DepositResult,
  SignerFunction,
  SignerRequest,
  SignerResponse,
  TransferSummary,
} from '@swype-org/deposit-mobile';

import type { DepositMobileErrorCode } from '@swype-org/deposit-mobile';