> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native

> Open Dodo Payments hosted checkout from a React Native or Expo app in a system browser view, and get a typed checkout result back from one call.

<Info>
  This page covers the official Dodo Payments React Native checkout SDK, `@dodopayments/react-native-checkout`. It opens Dodo Payments hosted checkout in a native browser view and returns a typed result. An older package, `dodopayments-react-native-sdk` (unscoped), has a different API. This page documents only the scoped package.
</Info>

<CardGroup cols={2}>
  <Card title="Checkout Sessions API" icon="cart-shopping" href="/developer-resources/checkout-session">
    Create the `checkout_url` that this SDK opens, from your backend.
  </Card>

  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    See how this SDK fits into the full mobile payment flow.
  </Card>
</CardGroup>

The React Native SDK is a Turbo Module that wraps the native [iOS](/developer-resources/sdks/ios) and [Android](/developer-resources/sdks/android) checkout SDKs. It opens `SFSafariViewController` on iOS and a Custom Tab on Android. It holds no API key and has no checkout logic of its own, so it never calls the Dodo Payments API. Checkout runs in the browser view. The SDK presents and dismisses that view and reads the result from the return URL.

<Warning>
  This SDK supports the **New Architecture only**. It requires React Native 0.77 or later, iOS 16 or later, and Android `minSdk` 24. Your Android app must build with `compileSdk` 34 or later.
</Warning>

## Installation

<Steps>
  <Step title="Install the Package">
    <Tabs>
      <Tab title="Android">
        The package is autolinked and pulls `com.dodopayments.api:checkout-android` from Maven Central.

        ```sh theme={null}
        npm i @dodopayments/react-native-checkout
        ```

        The native dependency resolves automatically, so no other install step is needed.
      </Tab>

      <Tab title="iOS">
        ```sh theme={null}
        npm i @dodopayments/react-native-checkout
        cd ios && pod install
        ```

        The package bundles the Swift core, and CocoaPods installs it.
      </Tab>

      <Tab title="Expo">
        The SDK works in development builds only, not in Expo Go.

        ```sh theme={null}
        npm i @dodopayments/react-native-checkout
        ```

        Then configure the plugin in `app.json`, as shown in the next step.
      </Tab>
    </Tabs>

    [Appearance customization](#appearance-customization) requires version 1.2.0 or later.
  </Step>

  <Step title="Register a Callback URL Scheme">
    Register a URL scheme so that the operating system routes the checkout's return URL back to your app.

    <Tabs>
      <Tab title="Android (Gradle)">
        Set the scheme as a manifest placeholder in `android/app/build.gradle`:

        ```groovy android/app/build.gradle theme={null}
        android {
            defaultConfig {
                manifestPlaceholders["dodoCallbackScheme"] = "myapp"
            }
        }
        ```

        Replace `"myapp"` with your app's scheme.
      </Tab>

      <Tab title="iOS (Info.plist)">
        Add a URL type to `ios/YourApp/Info.plist`:

        ```xml Info.plist theme={null}
        <key>CFBundleURLTypes</key>
        <array>
          <dict>
            <key>CFBundleURLName</key>
            <string>myapp</string>
            <key>CFBundleURLSchemes</key>
            <array>
              <string>myapp</string>
            </array>
          </dict>
        </array>
        ```

        You can also add the URL type in Xcode under **Info → URL Types**.
      </Tab>

      <Tab title="Expo (both platforms)">
        The package's config plugin registers the scheme for both platforms during `prebuild`. Pass the scheme as the plugin's `scheme` option in `app.json`:

        ```json app.json theme={null}
        {
          "expo": {
            "scheme": "myapp",
            "plugins": [
              [
                "@dodopayments/react-native-checkout",
                { "scheme": "myappcheckout" }
              ]
            ]
          }
        }
        ```

        The plugin's `scheme` must match the scheme of the `returnUrl` you pass, for example `myappcheckout://return`. Use a scheme that differs from `expo.scheme`. Expo already registers `expo.scheme` on `MainActivity`, so reusing it can send the checkout return to the wrong activity on Android, and the plugin warns you. The plugin rejects `http`, `https`, and other system schemes.

        Rebuild the native project after you edit `app.json`:

        ```sh theme={null}
        npx expo prebuild --clean
        ```

        The plugin works with development builds only, not Expo Go. On iOS, you still need the `Linking` listener in [Forwarding the Return URL](#forwarding-the-return-url).

        If `android/app/build.gradle` has no `defaultConfig { }` block, the plugin stops `prebuild` with an error. In that case, add the placeholder yourself, as shown in the Android tab.
      </Tab>
    </Tabs>

    On every platform, set the same URL as the checkout session's `return_url` when your backend creates the session. The SDK matches the return URL on scheme, host, and path. The URL doesn't need to load a real page.
  </Step>
</Steps>

## Usage

Call `DodoCheckout.start` with the `checkout_url` from your backend:

```typescript theme={null}
import { Linking } from 'react-native';
import { DodoCheckout } from '@dodopayments/react-native-checkout';

// Required for iOS's return-URL handling.
Linking.addEventListener('url', ({ url }) => DodoCheckout.handleOpenURL(url));

// showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
const result = await DodoCheckout.start({
  checkoutUrl,                          // from your backend's checkout session
  returnUrl: 'myapp://checkout/return', // scheme must be registered (see Installation)
  onEvent: (e) => console.log(e.type),  // logging only
});

switch (result.status) {
  case 'succeeded': showSuccess(result.paymentId); break;
  case 'failed':    showFailure(); break;
  case 'cancelled': await reconcileAbandonedSession(); break; // outcome unknown, not a failure
  case 'pending':   await reconcileAbandonedSession(); break;
  case 'expired':   showExpired(); break;
}
```

`onEvent` receives events with a `type` of `checkout.opened`, `checkout.return_received`, or `checkout.closed`. Use them for logging only, never to decide the outcome.

## Forwarding the Return URL

iOS needs the `Linking` listener to handle the return URL, because `SFSafariViewController` can't catch its own return URL. On Android, `handleOpenURL` does nothing and resolves `false`, because the Android SDK catches its redirect natively. You can register the listener on both platforms.

```typescript theme={null}
import { Linking } from 'react-native';
import { DodoCheckout } from '@dodopayments/react-native-checkout';

Linking.addEventListener('url', ({ url }) => {
  DodoCheckout.handleOpenURL(url);
});
```

On iOS, `handleOpenURL` resolves `true` when the URL belongs to the checkout in progress, and `false` for any other URL.

## What the Result Means

The SDK builds the result from the query parameters on the return URL.

<Warning>
  `result.status` is a UI hint, not proof of payment. Confirm every payment from your backend, with the `payment.succeeded` or `subscription.active` webhook.
</Warning>

<ParamField body="status" type="CheckoutStatus" required>
  One of five values:

  * `succeeded`: the return URL has `status=succeeded` (one-time payment) or `status=active` (subscription).
  * `failed`: the payment was declined (`status=failed`).
  * `cancelled`: the customer closed the browser view before the return URL arrived. The SDK doesn't know the outcome, and the payment may have succeeded, so don't show a failure screen. Reconcile the [abandoned session](#abandoned-sessions) instead.
  * `pending`: the payment settles later (`status=processing` or any `requires_*` value), or the `status` parameter was missing or unrecognized. Reconcile it like `cancelled`.
  * `expired`: the checkout session expired (`status=expired`).
</ParamField>

<ParamField body="paymentId" type="string">
  The `payment_id` query parameter, when the return URL includes one. Show it in your UI, but don't use it to grant access. See [Verify the Payment](#verify-the-payment).
</ParamField>

<ParamField body="subscriptionId" type="string">
  The `subscription_id` query parameter. Set for subscription checkouts.
</ParamField>

<ParamField body="licenseKeys" type="string[]">
  The `license_key` query parameter. Set when the checkout includes license key products.
</ParamField>

<ParamField body="customerEmail" type="string">
  The `email` query parameter. Set when checkout captures an email address.
</ParamField>

<ParamField body="raw" type="Record<string, string>">
  Every query parameter from the return URL, verbatim.
</ParamField>

## Verify the Payment

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/developer-resources/webhooks">
    Dodo Payments calls your backend when a payment succeeds or a subscription activates.
  </Card>

  <Card title="Get Payment Detail" icon="magnifying-glass" href="/api-reference/payments/get-payments-1">
    Look up `paymentId` with your secret key to check its status.
  </Card>
</CardGroup>

Grant access only after one of these confirms the payment. Don't rely on `result.status` alone.

## Appearance Customization

To change the checkout browser's toolbar, buttons, and color scheme, pass `customization` to `start(...)`. Android Custom Tabs and iOS `SFSafariViewController` expose different native controls, so the options are grouped into an `android` object and an `ios` object. Each platform reads only its own object. Every field is optional. When you omit a field, the platform applies its own default.

<AccordionGroup>
  <Accordion title="Android — Custom Tab">
    <ParamField body="toolbarColor" type="string">
      Toolbar background color, as a hex string: `"#RRGGBB"` or `"#AARRGGBB"`.
    </ParamField>

    <ParamField body="navigationBarColor" type="string">
      Navigation bar color, as a hex string.
    </ParamField>

    <ParamField body="navigationBarDividerColor" type="string">
      Color of the divider above the navigation bar, as a hex string.
    </ParamField>

    <ParamField body="closeButtonStyle" type="'default' | 'back'">
      `default` shows the system "X" icon. `back` shows a back arrow that the SDK draws.
    </ParamField>

    <ParamField body="closeButtonPosition" type="'start' | 'end'">
      The side of the toolbar where the close button appears.
    </ParamField>

    <ParamField body="shareButtonEnabled" type="boolean">
      Shows the toolbar's share icon. `false` hides it.
    </ParamField>

    <ParamField body="showTitleEnabled" type="boolean">
      Shows the page title under the URL in the toolbar.
    </ParamField>

    <ParamField body="urlBarHidingEnabled" type="boolean">
      Hides the toolbar automatically as the page scrolls.
    </ParamField>

    <ParamField body="bookmarksButtonEnabled" type="boolean">
      Shows "Bookmark this page" in the overflow menu.
    </ParamField>

    <ParamField body="downloadsButtonEnabled" type="boolean">
      Shows "Download page" in the overflow menu.
    </ParamField>

    <ParamField body="colorScheme" type="'system' | 'light' | 'dark'">
      `light` or `dark` forces that appearance regardless of the device's system setting. `system` follows the system setting.
    </ParamField>
  </Accordion>

  <Accordion title="iOS — SFSafariViewController">
    <ParamField body="dismissButtonStyle" type="'done' | 'close' | 'cancel'">
      Style of the dismiss button. iOS decides whether it renders as a label or an icon.
    </ParamField>

    <ParamField body="presentationStyle" type="'pageSheet' | 'fullScreen'">
      `pageSheet` (the default) presents a card that the customer can swipe down to dismiss. `fullScreen` covers the whole screen.
    </ParamField>

    <ParamField body="barCollapsingEnabled" type="boolean">
      Lets the toolbar collapse as the page scrolls. It has a visible effect only when `presentationStyle` is `fullScreen`. With `pageSheet`, the bars stay pinned regardless of this setting.
    </ParamField>

    <ParamField body="colorScheme" type="'system' | 'light' | 'dark'">
      `light` or `dark` forces that appearance regardless of the device's system setting. `system` follows the system setting.
    </ParamField>
  </Accordion>
</AccordionGroup>

iOS has no toolbar color option, because the underlying `SFSafariViewController` tint properties are deprecated as of iOS 26.

```typescript theme={null}
const result = await DodoCheckout.start({
  checkoutUrl,
  returnUrl: 'myapp://checkout/return',
  customization: {
    android: { toolbarColor: '#6366F1', closeButtonStyle: 'back' },
    ios: { presentationStyle: 'fullScreen', colorScheme: 'dark' },
  },
});
```

## Errors

`start` rejects with a `CheckoutError` only for misuse or a platform failure. Read the reason from `error.code`. A customer who cancels, or a declined payment, is always a result, never a rejection.

* `INVALID_CHECKOUT_URL`: `checkoutUrl` isn't an `https` checkout session URL (path starting with `/session/`) on `checkout.dodopayments.com` or `test.checkout.dodopayments.com`.
* `INVALID_RETURN_URL`: `returnUrl` isn't an absolute URL with a scheme and a host.
* `ALREADY_IN_PROGRESS`: another checkout is running. Only one checkout can run at a time.
* `PLATFORM_ERROR`: an unexpected platform failure. The SDK also reports any unrecognized native error with this code.

## Abandoned Sessions

The native SDK records the checkout session when checkout starts, and clears the record only when checkout ends with `succeeded`, `failed`, or `expired`. The record stays when the app or the JavaScript bundle is killed during checkout, which loses the `start` promise, and after a `cancelled` or `pending` result. Check for it on the next mount and after every `cancelled` or `pending` result:

```typescript theme={null}
import { DodoCheckout } from '@dodopayments/react-native-checkout';

const abandoned = await DodoCheckout.getAbandonedSession();
if (abandoned) {
  // Ask your backend for the outcome of abandoned.sessionId, and show it.
  // Clear the record only after the outcome is final:
  await DodoCheckout.clearAbandonedSession();
}
```

`abandoned.sessionId` is the checkout session ID, which starts with `cks_`. `abandoned.createdAt` is the `Date` checkout started. Your backend can look up the session with [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts), which returns its `payment_id` and `payment_status`. Until the payment reaches a final status, treat it as pending, not failed.

## Related

<CardGroup cols={2}>
  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    The same contract for Android, iOS, and Flutter.
  </Card>

  <Card title="Expo Boilerplate" icon="layer-group" href="/developer-resources/expo-boilerplate">
    A complete Expo example with checkout integration.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.