npm package: @swype-org/checkout-mobile
Description: Lightweight mobile checkout SDK — open a hosted Swype payment flow via in-app browser, handle deep-link completion, zero runtime dependencies
Latest version: 0.1.7

# @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`](../checkout-ios-sdk/) — Swift, ASAuthorization, direct passkey signing
> - **Android:** [`checkout-android-sdk`](../checkout-android-sdk/) — Kotlin, Credential Manager, direct passkey signing
>
> 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 checkout SDK — open a hosted Swype payment flow via in-app browser, handle deep-link completion, zero runtime dependencies.

## Quick Start

```bash
npm install @swype-org/checkout-mobile
```

```typescript
import { 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:

```bash
npm install @swype-org/checkout-mobile react
```

```tsx
import { 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:

```typescript
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:

```typescript
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:

```json
{
  "amount": 50,
  "chainId": 8453,
  "address": "0x...",
  "token": "USDC",
  "callbackScheme": "myapp",
  "url": "https://pay-staging.tryblink.xyz",
  "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:

```json
{
  "amount": 50,
  "chainId": 8453,
  "address": "0x...",
  "token": "USDC",
  "idempotencyKey": "generated-uuid",
  "callbackScheme": "myapp",
  "expiresAt": "2026-03-07T12:00:00Z",
  "version": "v1"
}
```

4. **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:

```json
{
  "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

```typescript
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://pay-staging.tryblink.xyz'
  webviewBaseUrl: 'https://pay-staging.tryblink.xyz',

  // 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`:

```json
{
  "expo": {
    "scheme": "myapp"
  }
}
```

### iOS (native)

Register your URL scheme in `Info.plist`:

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

### Android (native)

Add an intent filter in `AndroidManifest.xml`:

```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()`:

```typescript
// 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                            |

```typescript
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-active
```

## Error 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:

```typescript
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

```typescript
checkout.on('complete', (result) => { /* DepositResult */ });
checkout.on('error', (error) => { /* CheckoutError */ });
checkout.on('dismiss', () => { /* browser dismissed */ });
checkout.on('status-change', (status) => { /* MobileCheckoutStatus */ });
```

## Lifecycle

```typescript
// 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:

```typescript
import type {
  MobileCheckoutConfig,
  MobileCheckoutStatus,
  DepositRequest,
  DepositResult,
  SignerFunction,
  SignerRequest,
  SignerResponse,
  TransferSummary,
} from '@swype-org/checkout-mobile';

import type { CheckoutErrorCode } from '@swype-org/checkout-mobile';
```