@swype-org/checkout-mobile
v0.1.4
Published
Lightweight mobile checkout SDK — open a hosted Swype payment flow via in-app browser, handle deep-link completion, zero runtime dependencies
Maintainers
Readme
@swype-org/checkout-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 checkout SDK — open a hosted Swype payment flow via in-app browser, handle deep-link completion, zero runtime dependencies.
Quick Start
npm install @swype-org/checkout-mobileimport { MobileCheckout } from '@swype-org/checkout-mobile';
const checkout = new MobileCheckout({
signer: 'https://api.merchant.com/sign-payment',
callbackScheme: 'myapp',
openUrl: (url) => openInAppBrowser(url),
});
// Listen for deep links and pass them to the SDK
onDeepLink((url) => checkout.handleDeepLink(url));
// Start a deposit
const { transfer } = await checkout.requestDeposit({
amount: 50,
chainId: 8453,
address: '0x...',
token: 'USDC',
});
console.log('Transfer complete:', transfer.id, transfer.status);React Native Hook
A dedicated React Native entry point eliminates all boilerplate:
npm install @swype-org/checkout-mobile reactimport { useSwypeMobileCheckout } from '@swype-org/checkout-mobile/react-native';
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';
function DepositButton() {
const { status, result, error, displayMessage, requestDeposit, handleDeepLink } =
useSwypeMobileCheckout({
signer: 'https://api.merchant.com/sign-payment',
callbackScheme: 'myapp',
openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
});
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: 'USDC' })
}
/>
{error && <Text>{displayMessage}</Text>}
{result && <Text>Transfer {result.transfer.id} complete!</Text>}
</>
);
}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, openUrl() → in-app browser │
│──────────────────────────────────────────────────────────────►│
│ │
│ 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 contract as @swype-org/checkout (web), except callbackScheme is set to the mobile app's URL scheme instead of null.
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 checkout = new MobileCheckout({
signer: 'https://api.merchant.com/sign-payment',
callbackScheme: 'myapp',
openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
});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/checkout-mobile';
const checkout = new MobileCheckout({
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',
openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
});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 | EVM chain ID for the destination (e.g. 8453 for Base). |
| address | string | Destination wallet address (0x-prefixed, 40 hex chars). |
| token | string | Token symbol on the destination chain (e.g. "USDC"). |
| 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": "USDC",
"callbackScheme": "myapp",
"url": "https://webview-app-staging.staging-swype.network",
"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": "USDC",
"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 symbol. |
| 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": "USDC",
"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 checkout = new MobileCheckout({
// 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',
// Required: function that opens a URL in an in-app browser
openUrl: (url) => WebBrowser.openBrowserAsync(url).then(() => {}),
// Optional: base URL of the hosted payment webview app.
// Default: 'https://webview-app-staging.staging-swype.network'
webviewBaseUrl: 'https://webview-app-staging.staging-swype.network',
// Optional: path for the callback deep link (default: '/swype/callback')
callbackPath: '/swype/callback',
// 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 }) => checkout.handleDeepLink(url));
// React Native (bare)
import { Linking } from 'react-native';
Linking.addEventListener('url', ({ url }) => checkout.handleDeepLink(url));
// iOS native (AppDelegate)
func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {
// bridge call to checkout.handleDeepLink(url.absoluteString)
}
// Android native (Activity)
override fun onNewIntent(intent: Intent) {
intent.data?.toString()?.let { checkout.handleDeepLink(it) }
}handleDeepLink() returns true if the URL was a Swype callback, false otherwise.
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 |
checkout.on('status-change', (status) => console.log('Status:', status));
checkout.status; // current status
checkout.result; // last DepositResult (when completed)
checkout.error; // last CheckoutError (when error)
checkout.isActive; // true during signer-loading or browser-activeError Handling
Every error is a CheckoutError with a machine-readable code:
| Code | Meaning |
| ------------------------ | ----------------------------------------------- |
| BROWSER_FAILED | Failed to open the in-app browser |
| BROWSER_DISMISSED | User dismissed the browser before completing |
| 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 { CheckoutError, getDisplayMessage } from '@swype-org/checkout-mobile';
try {
await checkout.requestDeposit({ ... });
} catch (err) {
if (err instanceof CheckoutError) {
Alert.alert('Payment Error', getDisplayMessage(err));
}
}Events
checkout.on('complete', (result) => { /* DepositResult */ });
checkout.on('error', (error) => { /* CheckoutError */ });
checkout.on('dismiss', () => { /* browser dismissed */ });
checkout.on('status-change', (status) => { /* MobileCheckoutStatus */ });Lifecycle
// Cancel the current flow and reset to idle
checkout.close();
// Tear down and release all resources (call on unmount)
checkout.destroy();Comparison with Other Swype SDKs
| Aspect | @swype-org/checkout (web) | @swype-org/checkout-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 {
MobileCheckoutConfig,
MobileCheckoutStatus,
DepositRequest,
DepositResult,
SignerFunction,
SignerRequest,
SignerResponse,
TransferSummary,
} from '@swype-org/checkout-mobile';
import type { CheckoutErrorCode } from '@swype-org/checkout-mobile';