@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
Maintainers
Readme
@swype-org/deposit-mobile
Note: For the best native UX (biometric passkey prompts with no browser chrome), use the platform-specific SDKs instead:
- iOS:
checkout-ios-sdk— Swift, ASAuthorization, direct passkey signing- Android:
checkout-android-sdk— Kotlin, Credential Manager, direct passkey signingThis 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-mobileimport { 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 reactimport { 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-browserimport * 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
webBrowserModuleis 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-browserhas no API for sheet detents, so the sheet is the system's tallpageSheet— it cannot rest at a partial height or be dragged between sizes. That needs a native module presentingSFSafariViewControllerwithsheetPresentationController.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
- Validate the request fields.
- Generate an idempotency key (UUID) for this payment.
- 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"
}- 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 pendingrequestDeposit()promise resolves with theDepositResult. - Dismissal callback —
{scheme}://swype/callback?transferStatus=DISMISSED(notransferId): the user closed the hosted flow from an in-page control before any transfer was created; the pending promise rejects withDEPOSIT_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-activeError 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';