# Glomo React Native SDK v5

Official React Native SDK for integrating Glomo payment checkout flows into your mobile applications.

> **Upgrading from v4? Read the [Migration Guide](/platform/sdk/react-native-sdk/migration) first.** `jail-monkey` is now a required peer dependency and the device compliance gate fails closed, so an upgrade that skips it takes live checkouts off the air. Mock and test keys are never blocked, so a `test_` key QA pass will not catch it.
Full [Changelog](/platform/sdk/react-native-sdk/changelog) is also available.


## Prerequisites

Before using this SDK, you need:

- API credentials (Public Key) from your Glomo dashboard
- An order ID created via the Glomo API (or a subscription ID for subscription payments)


## System Requirements

| Requirement | Version |
|  --- | --- |
| Node.js | >= 16.0.0 |
| npm | >= 8.0.0 |
| React | >= 17.0.0 |
| React Native | >= 0.68.0 |
| react-native-webview | ^13.0.0 |
| jail-monkey | ^2.6.0 |
| react-native-blob-util (optional) | ^0.25.0 |
| @react-native-documents/picker (optional) | >= 10.1.3 |


`jail-monkey` was optional in v4 and is required in v5. The two optional packages enable [document downloads](#document-downloads); leaving them out is supported.

## Installation

### React Native CLI

```bash
npm install @glomopay/react-native-sdk react-native-webview jail-monkey
cd ios && pod install
```

### Expo

```bash
npx expo install @glomopay/react-native-sdk react-native-webview jail-monkey
```

### Required: Device Security Compliance

`jail-monkey` is a **required** peer dependency as of v5.

**Without it, live checkouts are blocked.** The SDK cannot tell a clean device from a compromised one, and on a `live_` key it refuses to start rather than assume the device is safe: `start()` returns `false` and `onSdkError` fires with `type: "device_forbidden"`.

Installing is not enough - `jail-monkey` ships native code, so the app must be rebuilt. An installed but unlinked module is treated exactly as if the package were absent.

Test and mock keys are never blocked, so QA on a rooted handset or an emulator still works.

### Optional: Document Downloads

Some checkout flows offer a document - an agreement PDF, for example - for the customer to save. A web page cannot write a file to the device from inside a WebView, so the SDK does it, but only if both of these are installed:

```bash
npm install react-native-blob-util @react-native-documents/picker
cd ios && pod install
```

Leaving them out is a supported choice: the SDK does not advertise the capability, the checkout page keeps its own download behaviour, and nothing breaks.

Five things worth knowing before you install them:

1. **Installing is not enabling.** Both ship native code, so the app must be rebuilt. Until then the SDK treats them exactly as if they were absent.
2. **Reset the Metro cache** after installing (`npx react-native start --reset-cache`). A stale bundle resolves the old module graph and the capability stays off.
3. **Expo needs a development build.** Neither package works in Expo Go. `react-native-blob-util` ships a config plugin; `@react-native-documents/picker` does not, so add it to your config plugins list yourself if your setup needs one.
4. **`@react-native-documents/picker` 11 and above requires React Native >= 0.79.** On older React Native, pin `10.1.x` - the API this SDK uses is identical.
5. **Degradation is silent by design**, which is why it is noisy in development: `start()` logs an ungated warning naming the missing package, and `onSdkError` fires once per mount with `field: "file.save"`. See [Document Downloads](#document-downloads) for what that means for your error handler.


## Quick Start

```tsx
import React, { useRef } from "react";
import { View, Button } from "react-native";
import {
  GlomoCheckout,
  ASYNC_PAYMENT_EVENTS,
  type GlomoCheckoutRef,
  type GlomoCheckoutPayload,
  type GlomoTerminationPayload,
  type SdkError,
} from "@glomopay/react-native-sdk";

export default function PaymentScreen() {
  const checkoutRef = useRef<GlomoCheckoutRef>(null);

  const handlePay = async () => {
    const started = await checkoutRef.current?.start();
    if (!started) {
      console.log("Checkout could not start - check onSdkError for details");
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <Button title="Pay Now" onPress={handlePay} />

      <GlomoCheckout
        ref={checkoutRef}
        publicKey="live_pk_abc123"
        orderId="order_xyz789"
        onPaymentSuccess={(payload: GlomoCheckoutPayload) => {
          // paymentId and signature are optional in v5 - narrow before forwarding
          if (payload.paymentId && payload.signature) {
            confirmOnOurBackend(payload.paymentId, payload.signature);
          } else {
            awaitWebhookFor(payload.orderId);
          }
        }}
        onPaymentFailure={(payload: GlomoCheckoutPayload) => {
          console.log("Payment failed:", payload.orderId);
        }}
        onPaymentTerminate={(payload: GlomoTerminationPayload) => {
          console.log("Checkout ended without a payment outcome:", payload.source);
        }}
        onConnectionError={(error) => {
          console.log("Connection error:", error);
        }}
        onUserJourneyCompleted={(payload) => {
          switch (payload.journeyType) {
            case ASYNC_PAYMENT_EVENTS.BANK_TRANSFER_SUBMITTED:
              console.log("Bank transfer submitted:", payload.orderId);
              break;
            case ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED:
              console.log("Pay via bank completed:", payload.status);
              break;
          }
        }}
        onUserRefusedDevicePermissions={() => {
          console.log("User refused a device permission the bank's page asked for");
        }}
        onSdkError={(errors: SdkError[]) => {
          // field: "file.save" is not a misconfiguration - see Document Downloads
          const blocking = errors.filter((e) => e.field !== "file.save");
          blocking.forEach((e) => console.error(e.type, e.message, e.field));
        }}
      />
    </View>
  );
}
```

## Subscriptions Checkout

To process subscription payments, pass a `subscriptionId` instead of an `orderId`:

```tsx
<GlomoCheckout
  ref={checkoutRef}
  publicKey="live_pk_abc123"
  subscriptionId="sub_xyz789"
  onPaymentSuccess={(payload) => {
    console.log("Subscription payment success:", payload.orderId);
  }}
  onPaymentFailure={(payload) => {
    console.log("Subscription payment failed:", payload.orderId);
  }}
  onSdkError={(errors) => {
    errors.forEach((e) => console.error(e.type, e.message));
  }}
/>
```

When `subscriptionId` is provided:

- The SDK skips order type detection (no API call)
- The `subscriptionId` must start with `sub_`
- `onUserJourneyCompleted` is not applicable - subscriptions are card-only (`onPaymentSuccess` / `onPaymentFailure`)
- Do not pass both `orderId` and `subscriptionId` - the SDK will fire `onSdkError`
- The `detecting_order_type` status is not emitted for subscription flows


## Features

- **Cross-platform** - works on both iOS and Android
- **TypeScript** - full type definitions for all exports
- **Unified checkout** - renders the correct checkout flow automatically based on your order
- **Asynchronous payment flows** - single `onUserJourneyCompleted` callback for bank transfer and pay via bank events
- **Device permissions** - built-in camera handling for bank authentication, on every checkout flow. **Now also on LRS**
- **Document downloads** - saves a document the page offers to the customer's device, on every checkout flow, behind two optional peer dependencies. **New in v5**
- **Device security** - required `jail-monkey` integration for rooted/jailbroken device detection, failing closed on live keys
- **Input validation** - publicKey and orderId format enforcement before checkout starts
- **Mock mode** - test with `test_` / `mock_` prefixed keys without hitting production


## API Reference

### GlomoCheckout Component

The unified checkout component. Place it in your render tree and control it via a ref.

```tsx
<GlomoCheckout ref={checkoutRef} {...props} />
```

#### Props (`GlomoCheckoutProps`)

| Prop | Type | Required | Description |
|  --- | --- | --- | --- |
| `publicKey` | `string` | Yes | Your Glomo public key (must start with `live_`, `mock_`, or `test_`) |
| `orderId` | `string` | No* | Order ID from the Glomo API (must start with `order_`) |
| `subscriptionId` | `string` | No* | Subscription ID (must start with `sub_`). Bypasses order type detection. |
| `onPaymentSuccess` | `(payload: GlomoCheckoutPayload) => void` | Yes | Called when payment succeeds |
| `onPaymentFailure` | `(payload: GlomoCheckoutPayload) => void` | Yes | Called when payment fails |
| `onConnectionError` | `(error: unknown) => void` | No | Called on network/connection errors |
| `onPaymentTerminate` | `(payload: GlomoTerminationPayload) => void` | No | Called when the checkout ends without a payment outcome. `payload.source` names how. An existing `() => void` handler stays assignable. **Changed in v5** |
| `onSdkError` | `(errors: SdkError[]) => void` | Yes | Called on validation errors, device compliance failures, and a missing download capability. Filter on `field`, not `type` - see [Document Downloads](#document-downloads) |
| `onUserJourneyCompleted` | `(payload: GlomoUserJourneyCompletedPayload) => void` | No | Called when an asynchronous payment flow completes (bank transfer submission, pay via bank completion) - not a confirmed payment. Check `payload.journeyType` to determine the flow. Accepted on `GlomoCheckout` but silently ignored on LRS orders, which never produce an asynchronous journey. |
| `onUserRefusedDevicePermissions` | `() => void` | No | Called when the user denies a device permission the bank's page asked for - camera today. Android only; see [Device Permissions](#device-permissions). **Renamed in v5** from `onUserRefusedCameraPermissions` |


*Exactly one of `orderId` or `subscriptionId` must be provided. If both or neither are set, `onSdkError` fires and `start()` returns `false`.

#### Ref Methods (`GlomoCheckoutRef`)

| Method | Signature | Description |
|  --- | --- | --- |
| `start()` | `() => Promise<boolean>` | Validates inputs, and opens the checkout modal. Resolves `true` if successful, `false` otherwise. |
| `getStatus()` | `() => CheckoutStatus` | Returns the current checkout status. |


#### CheckoutStatus

| Status | Description |
|  --- | --- |
| `ready` | Initial state - checkout can be started |
| `detecting_order_type` | Order type detection in progress (after `start()`, before checkout opens) |
| `payment_in_progress` | Checkout modal is open, user is interacting |
| `payment_successful` | Payment completed successfully. Cannot restart with same order |
| `payment_failed` | Payment failed. Can retry with same order |
| `payment_cancelled` | User dismissed checkout mid-flow. Can retry |
| `bank_transfer_submitted` | User submitted bank transfer details |
| `pay_via_bank_completed` | Pay via bank flow completed |


### Type Definitions

#### GlomoCheckoutPayload

```ts
interface GlomoCheckoutPayload {
  orderId: string;
  paymentId?: string;
  signature?: string;
}
```

`paymentId` and `signature` became optional in v5. The checkout page can report a terminal outcome before the backend has resolved them - open banking completes on the bank's side first - and the SDK previously dropped those outcomes rather than report an incomplete one. Narrow before forwarding either value.

#### ASYNC_PAYMENT_EVENTS

```ts
enum ASYNC_PAYMENT_EVENTS {
  BANK_TRANSFER_SUBMITTED = "bank_transfer_submitted",
  PAY_VIA_BANK_COMPLETED = "pay_via_bank_completed",
}
```

#### GlomoUserJourneyCompletedPayload

```ts
interface GlomoUserJourneyCompletedPayload {
  journeyType: ASYNC_PAYMENT_EVENTS;
  orderId?: string;
  status?: string;
  senderAccountNumber?: string;
  transactionReference?: string; // Always undefined: the checkout page does not send it
}
```

#### TERMINATION_SOURCES

**New in v5.**

```ts
enum TERMINATION_SOURCES {
  CHECKOUT_CLOSED = "checkout_closed",        // the page's own close control
  DEVICE_BACK_BUTTON = "device_back_button",  // Android hardware back or back gesture
  MODAL_DISMISS = "modal_dismiss",            // iOS swipe-down on the modal
}
```

The back chevron over a bank flow is deliberately **not** in this list. It returns the user to the checkout with the session intact and the ability to pay, so reporting it as a termination would be false - and for a merchant who cancels the order on terminate, it would cancel live orders every time a customer backed out of one bank to try another.

#### GlomoTerminationPayload

**New in v5.**

```ts
interface GlomoTerminationPayload {
  source: TERMINATION_SOURCES;
}
```

An object rather than a bare enum, so a later field can be added without changing the signature or redefining what an existing value means.

#### SdkError

```ts
interface SdkError {
  type: "validation_error" | "device_forbidden";
  message: string;
  field?:
    | "publicKey"
    | "orderId"
    | "subscriptionId"
    | "baseCheckoutUrl"
    | "generatedCheckoutUrl"
    | "file.save";
}
```

`field: "file.save"` is new in v5 and is the one value that does not mean the integration is misconfigured. See [Document Downloads](#document-downloads).

## Asynchronous Payment Flows

The SDK fires `onUserJourneyCompleted` when an asynchronous payment flow completes. Use `payload.journeyType` to determine which flow triggered the callback:

```tsx
onUserJourneyCompleted={(payload) => {
  switch (payload.journeyType) {
    case ASYNC_PAYMENT_EVENTS.BANK_TRANSFER_SUBMITTED:
      /**
       * User submitted bank transfer details.
       * Relevant fields: orderId, senderAccountNumber
       * Note: fields may be absent if the checkout page omits them.
       */
      console.log("Transfer submitted for order:", payload.orderId);
      break;
    case ASYNC_PAYMENT_EVENTS.PAY_VIA_BANK_COMPLETED:
      /**
       * Pay via bank journey completed.
       * Relevant fields: orderId, status
       * The SDK deduplicates this event - fires only once per session.
       */
      console.log("Pay via bank status:", payload.status);
      break;
  }
}}
```

The checkout status transitions to `bank_transfer_submitted` or `pay_via_bank_completed` respectively. Use `getStatus()` for granular programmatic checks.

## Document Downloads

**New in v5.** Available on every checkout flow.

Some checkout flows offer a document for the customer to save. A page cannot write a file to the device from inside a WebView, so the SDK does it - when the two optional peer dependencies in [Optional: Document Downloads](#optional-document-downloads) are installed and the app has been rebuilt.

The capability is advertised to the checkout page only when both packages load successfully. When it is not advertised, the page keeps its own download behaviour and nothing breaks.

### What this changes about `onSdkError`

**Check this if your `onSdkError` handler aborts the checkout or shows a blocking error.**

In v4, `onSdkError` only ever meant the integration was misconfigured - a malformed `publicKey`, a missing `orderId` - so treating any error on that callback as fatal was a reasonable reading. In v5 it also fires once per mount, with `type: "validation_error"` and `field: "file.save"`, when the two document-download packages are not installed. Nothing is wrong in that case: the checkout works and the page keeps its own download path.

Filter on `field` rather than on `type`:

```tsx
onSdkError={(errors) => {
  const blocking = errors.filter((e) => e.field !== "file.save");
  if (blocking.length > 0) {
    // your existing handling
  }
}}
```

The report is deliberately not tied to whether a page actually requests a download: whether this build can write a file to the device is a property of the build, so you learn it once at integration rather than the first time a customer taps download in production.

A save that fails also reports through `onSdkError` with `field: "file.save"`, and never affects the payment session.

## Device Permissions

Some bank pages require camera access for authentication. The SDK handles permission prompts automatically on every checkout flow, but you must declare the permissions in your native project config.

File upload permissions are requested by `react-native-webview` itself and never reach the SDK, so they do not surface through `onUserRefusedDevicePermissions`.

### Android

Add to `android/app/src/main/AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.CAMERA" />
```

### iOS

Add to `ios/<YourApp>/Info.plist`:

```xml
<key>NSCameraUsageDescription</key>
<string>Camera access is required for identity verification during checkout</string>
```

### Behavior

- **Android**: SDK prompts the user via a native permission dialog. If denied, checkout closes and `onUserRefusedDevicePermissions` fires.
- **iOS**: SDK grants the WebView permission request; iOS shows its own system prompt. If the user denies at the system level, the WebView handles the denial and never reports it back, so `onUserRefusedDevicePermissions` does not fire on iOS.


LRS checkouts had no permission handling on either platform before v5, so a bank page requesting camera access got no prompt on Android and no grant on iOS. LRS now behaves identically to the standard checkout, and `onUserRefusedDevicePermissions` is forwarded to it.

## Platform Behavior

- **iOS**: Checkout opens as a `pageSheet` modal (swipe-down to dismiss). Dismissing fires `onPaymentTerminate` with `source: "modal_dismiss"`.
- **Android**: Checkout opens fullscreen. The hardware back button dismisses it and fires `onPaymentTerminate` with `source: "device_back_button"`.


## Device Security Compliance

The SDK uses `jail-monkey` to detect rooted (Android) or jailbroken (iOS) devices. As of v5 it is a required peer dependency and the gate **fails closed**: a device the SDK cannot verify is refused on a live key rather than assumed clean.

| Device | `test_` / `mock_` key | `live_` key |
|  --- | --- | --- |
| Clean | allowed | allowed |
| Emulator / simulator | allowed | allowed |
| Rooted / jailbroken | allowed | **blocked** |
| Unverifiable (`jail-monkey` missing) | allowed | **blocked** |


When blocked, `start()` returns `false` and `onSdkError` fires with `type: "device_forbidden"`. The message distinguishes a compromised device from an unverifiable one.

**Emulators are allowed on live keys, deliberately.** Android emulators ship AOSP test-keys and debuggable system properties, which trip two of the checks behind `isJailBroken()` - so the aggregate verdict reports every stock emulator as rooted. The SDK computes its own verdict from `jail-monkey`'s per-check breakdown and ignores those two, because merchants integration-test with live keys on emulators and an emulator is not the threat this gate exists for. Every strong root indicator (su binaries, Magisk, root-manager apps, remounted system paths) is still enforced.

The cost is custom-ROM detection on physical devices, the weakest signal in the set. On iOS, and on any build that exposes no breakdown, the SDK falls back to `isJailBroken()` unchanged.

## Mock Mode

The SDK infers mock mode from the `publicKey` prefix:

| Prefix | Mode | Environment |
|  --- | --- | --- |
| `live_` | Live | Production |
| `test_` | Mock | Test/sandbox |
| `mock_` | Mock | Test/sandbox |


Mock mode keys route to the sandbox backend. No real transactions are created.

In v4 a `mock_` key passed validation and then ran against live. If you have been using a `mock_` key and compensating for it, stop - v5 runs it in mock mode.

## Connection Handling

The SDK detects connection errors from the WebView and fires `onConnectionError`:

- **iOS**: NSURLErrorDomain codes (-1001 timeout, -1003 host not found, -1004 connection refused, -1009 offline)
- **Android**: ERR_EMPTY_RESPONSE, ERR_CONNECTION_REFUSED, ERR_NAME_NOT_RESOLVED, ERR_INTERNET_DISCONNECTED, ERR_CONNECTION_TIMED_OUT, ERR_NETWORK_CHANGED


The checkout modal is automatically dismissed on connection errors.

## Input Validation

The SDK validates inputs before starting the checkout:

| Field | Rule |
|  --- | --- |
| `publicKey` | Must start with `live_`, `mock_`, or `test_`. Min 6 characters. |
| `orderId` | Must start with `order_`. Min 7 characters. Required when `subscriptionId` is absent. |
| `subscriptionId` | Must start with `sub_`. Trimmed and checked for emptiness. Required when `orderId` is absent. |


Validation failures fire `onSdkError` with `type: "validation_error"` and the relevant `field` name. `start()` returns `false`.

Providing both `orderId` and `subscriptionId` (or neither) also fires `onSdkError`.

## useGlomoCheckout Hook (Deprecated)

> **Deprecated** - Legacy hook, will be removed in a future major version.


`useGlomoCheckout` is a legacy placeholder from the v1.x hook-based API. In v1.x, the equivalent hook (`useLrsCheckout`) provided reactive WebView state and rendering primitives that merchants used to build custom checkout UIs. In v3+, the inner checkout components are no longer exported, making this hook ineffective for custom rendering.

The hook's public return type (`UseGlomoCheckoutReturn`) exposes:

- `start()` - identical to `GlomoCheckoutRef.start()`. Returns `Promise<boolean>`.
- `getStatus()` - returns a point-in-time `CheckoutStatus` snapshot. **Not reactive** - does not trigger re-renders when status changes.


**Use the `GlomoCheckout` component with a ref instead:**

```tsx
import React, { useRef } from "react";
import { GlomoCheckout, type GlomoCheckoutRef } from "@glomopay/react-native-sdk";

const checkoutRef = useRef<GlomoCheckoutRef>(null);

// Start checkout
const started = await checkoutRef.current?.start();

// Check status (point-in-time, same as hook)
const status = checkoutRef.current?.getStatus();
```

The component-based API provides the same functionality with proper lifecycle management.

## Troubleshooting

### `start()` returns `false`

Check `onSdkError` for details. Common causes:

- Invalid `publicKey` format (must start with `live_`, `mock_`, or `test_`)
- Invalid `orderId` format (must start with `order_`)
- Invalid `subscriptionId` format (must start with `sub_`, non-empty after trimming)
- Both `orderId` and `subscriptionId` provided (or neither)
- Device is rooted or jailbroken, on a `live_` key
- Device security could not be verified on a `live_` key, because `jail-monkey` is missing or its native module is not linked. Install it and rebuild the app; the console warning names this case.


### Checkout opens but shows a blank screen

- Verify network connectivity
- Check that the `publicKey` and `orderId` are valid


### Document downloads do nothing

The capability is only advertised when both `react-native-blob-util` and `@react-native-documents/picker` load successfully, and the checkout page falls back to its own behaviour when it is absent. Check, in order:

- Both packages installed, and the app rebuilt afterwards - installing alone is not enough
- Metro cache reset (`npx react-native start --reset-cache`)
- Not running in Expo Go, which cannot load either package
- On React Native < 0.79, `@react-native-documents/picker` pinned to `10.1.x`


`start()` logs an ungated warning naming the package it could not load, and `onSdkError` fires once per mount with `field: "file.save"`.

### Camera permission denied

- Ensure `AndroidManifest.xml` and `Info.plist` include camera permission declarations
- `onUserRefusedDevicePermissions` fires when the user denies camera permission to your app, on Android only


### `onUserJourneyCompleted` not firing

- Ensure you are passing `onUserJourneyCompleted` as a prop to `GlomoCheckout`
- Verify the checkout flow involves an asynchronous payment (bank transfer or pay via bank). It never fires on an LRS order.


## Exports

```ts
// Components
export { GlomoCheckout } from "@glomopay/react-native-sdk";

// Hooks (deprecated - use GlomoCheckout component with ref instead)
export { useGlomoCheckout } from "@glomopay/react-native-sdk";

// Enums
export { ASYNC_PAYMENT_EVENTS, TERMINATION_SOURCES } from "@glomopay/react-native-sdk";

// Types
export type {
  GlomoCheckoutRef,
  GlomoCheckoutProps,
  GlomoCheckoutPayload,
  CheckoutStatus,
  GlomoUserJourneyCompletedPayload,
  GlomoTerminationPayload,
  UseGlomoCheckoutReturn, // deprecated
  SdkError,
} from "@glomopay/react-native-sdk";
```

## Security

The SDK loads checkout pages in an isolated WebView. No PAN, CVV, or sensitive payment data passes through the React Native bridge - all sensitive input is handled within the WebView's sandboxed context.

Device integrity checking is required via `jail-monkey` (see [Device Security Compliance](#device-security-compliance)).

To report a security vulnerability, email security@glomopay.com. Do not open a public GitHub issue for security reports.

## Related

- [Migration Guide (v1 to v5)](/platform/sdk/react-native-sdk/migration) - step-by-step upgrade instructions with before/after code
- [Changelog](/platform/sdk/react-native-sdk/changelog) - all release notes
- [React Native SDK v4 (Archived)](/platform/sdk/react-native-sdk/v4) - previous version documentation
- [React Native SDK v3 (Archived)](/platform/sdk/react-native-sdk/v3)
- [React Native SDK v1 (Archived)](/platform/sdk/react-native-sdk/v1)
- [Flutter SDK](/platform/sdk/flutter-sdk) - Glomo SDK for Flutter apps
- [Unified SDK (Web)](/platform/sdk/unified-sdk) - Glomo SDK for web apps
- [Checkout overview](/payin/checkout) - server-side checkout integration and signature verification