Chromia Pay

React Native

Open the hosted checkout in an in-app browser, and ask your own backend what happened. No SDK, no WebView.

Use the hosted checkout. Open it in an in-app browser, and when the customer comes back, ask your own backend for the payment's status.

The embedded checkout does not apply here. embed.js builds an iframe and listens for postMessage; neither exists in React Native, and there is no port of it — see Why not a WebView.

Create the payment on your server

Exactly as in the hosted flow. The secret key must never reach the app, so the app calls an endpoint of yours and gets back two things:

Your backend
const payment = await cpay.payments.create({
  amount: "12.50",
  description: "Order #1041 — two plushies",
  metadata: { order_id: "1041" },
  successUrl: "https://shop.example.com/app/paid",
  idempotencyKey: "order-1041",
})

return Response.json({ id: payment.id, checkout_url: payment.checkout_url })

successUrl has to be https — a custom scheme like myapp://paid is refused when the payment is created. That is deliberate, and it is also what makes the optional App Link below possible.

Open it, then ask

app/checkout.tsx
import * as WebBrowser from 'expo-web-browser'

async function pay() {
  const { id, checkout_url } = await fetch('https://shop.example.com/api/checkout', {
    method: 'POST',
  }).then((r) => r.json())

  // Resolves when the customer dismisses the browser, however they dismiss it.
  await WebBrowser.openBrowserAsync(checkout_url)

  // Your own endpoint, which calls cpay.payments.get(id) with the secret key.
  const { status } = await fetch(`https://shop.example.com/api/payments/${id}`).then((r) => r.json())

  if (status === 'confirmed' || status === 'overpaid') showThanks()
  else showStillWaiting(id)
}

That is the whole integration. Note what it does not do: it never parses a return URL, so there are no deep links to register and nothing to configure per platform.

Do not read the query string with `new URL()`

If you do end up parsing a return URL, URL.searchParams is missing from React Native's URL polyfill and will throw at runtime rather than at build time. Use Linking.parse(url).queryParams.

Without Expo, react-native-inappbrowser-reborn behaves the same way. Linking.openURL(checkout_url) also works and sends the customer to Safari or Chrome proper — less tidy, and you lose the resolves-on-dismiss signal, so you would poll on app foreground instead.

What the customer sees

Send manually, not a wallet button. An in-app browser injects no wallet provider, so the page offers the deposit address, the exact amount, and the memo on the Chromia rail — updating live over its own stream the instant the transfer lands. On the EVM rails there is a single Open in a wallet app link that hands the address and amount straight to MetaMask, Trust or whatever else on the device has claimed the ethereum: scheme.

This is not a React Native limitation. Mobile Safari and Chrome inject no provider either — only desktop extensions and wallet apps' own browsers do — so every phone customer already takes this path.

Two rules

Fulfil from the webhook, never from the return. The customer can kill the browser between paying and being redirected, and the payment is no less real for it. The status check above is for deciding what screen to show, not for releasing goods. See Webhooks.

A dismissed browser means unknown, not cancelled. They may have sent the funds and swiped away while it was still confirming. Show a pending state and let the webhook settle it — telling someone their payment was cancelled while it is on its way to you is worse than saying nothing.

Optional: close the browser automatically

Register https://shop.example.com/app/paid as a Universal Link (iOS) / App Link (Android) and the system hands the redirect to your app, foregrounding it without the customer tapping Done. It needs an apple-app-site-association and an assetlinks.json served from that domain, plus ios.associatedDomains and android.intentFilters in your app config.

Treat it strictly as a nicety. It is fiddly to get right, iOS has historically been inconsistent about routing a redirect it was not given a user gesture for, and the flow above already works without it.

Why not a WebView

Two independent reasons, either one fatal.

embed.js cannot run: it is DOM code from top to bottom. And loading the page with ?embed=… directly, to skip the script, fails silently — inside a WebView window.parent is window, so the page posts its status messages to itself and your app hears nothing. You would ship a checkout that looks embedded and reports nothing.

Teaching the page about window.ReactNativeWebView.postMessage is a handful of lines, and still not worth it: a WebView has no wallet deep links and no password manager, Apple has opinions about payment flows inside them, and you would gain nothing over the in-app browser.

On this page