# @rnw-community/react-native-payments — full documentation Full concatenation of every doc in this package, in doc-map order, per https://llmstxt.org/. Every relative link below is rebased to be relative to this package root (e.g. docs/api/foo.md), regardless of which file it originally appeared in, since that's this bundle's own frame of reference. Use llms.txt for the linked index instead if you can fetch files individually. --- # FILE: readme.md # ReactNative Payments [![npm version](https://badge.fury.io/js/%40rnw-community%2Freact-native-payments.svg)](https://badge.fury.io/js/%40rnw-community%2Freact-native-payments) [![coverage](https://img.shields.io/codecov/c/github/rnw-community/rnw-community?flag=react-native-payments&label=coverage)](https://app.codecov.io/gh/rnw-community/rnw-community) [![npm downloads](https://img.shields.io/npm/dm/%40rnw-community%2Freact-native-payments.svg)](https://www.npmjs.com/package/%40rnw-community%2Freact-native-payments) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) > Accept Payments with Apple Pay and Android Pay using the Payment Request API. TurboModule-based implementation of the [W3C Payment Request API](https://www.w3.org/TR/payment-request/) (08 September 2022) for React Native — full TypeScript, a unified iOS/Android API, and an Expo config plugin. A rewrite of [naoufal/react-native-payments](https://github.com/naoufal/react-native-payments); see [Migrating from upstream](docs/guides/migrate-from-upstream.md) if you are porting an existing integration. ## For AI agents Start with [llms.txt](llms.txt) for a curated, agent-oriented index of this package's docs and [AGENTS.md](AGENTS.md) for architecture and contributor conventions. ## Install ```bash npm install @rnw-community/react-native-payments ``` Autolinking picks up the TurboModule on both architectures — no manual `react-native link` step. Complete the one-time platform setup before writing any code: [iOS](docs/getting-started/quickstart-ios.md) · [Android](docs/getting-started/quickstart-android.md) · [Expo](docs/getting-started/quickstart-expo.md). ## Quickstart ```ts import { PaymentComplete, PaymentRequest, createApplePayMethodData, createGooglePayMethodData, } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'US', currencyCode: 'USD' }), createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'id' }, }), ]; const paymentRequest = new PaymentRequest(methodData, { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, }); if (await paymentRequest.canMakePayment()) { const paymentResponse = await paymentRequest.show(); const isConfirmed = await sendToYourBackend(paymentResponse.details); // your own gateway call await paymentResponse.complete(isConfirmed ? PaymentComplete.SUCCESS : PaymentComplete.FAIL); } ``` [`createApplePayMethodData`](docs/api/create-apple-pay-method-data.md) and [`createGooglePayMethodData`](docs/api/create-google-pay-method-data.md) fill in the defaults: Visa and Mastercard on both platforms, plus the Google Pay `TEST` environment — pass `environment: EnvironmentEnum.PRODUCTION` for live payments. Each one returns the plain `methodData` entry, so you can still write that object by hand — see [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md) and [api/android-payment-method-data.md](docs/api/android-payment-method-data.md) for the raw shape. `sendToYourBackend` is where your payment service provider comes in — the [gateway recipes](docs/guides/gateways/index.md) give the exact method data, token and server call for Stripe, Adyen, Braintree and Checkout.com. Only call `complete(PaymentComplete.SUCCESS)` once your backend has actually confirmed the charge. A `PaymentRequest` is single-use — build a new one per payment attempt rather than reusing a settled request; see [docs/architecture.md](docs/architecture.md). [`createPaymentRequestFactory`](docs/api/create-payment-request-factory.md) builds a fresh request per attempt from shared configuration. For shipping/coupon change events and payment details modifiers, see the [doc map](#doc-map) below. ### Screenshots Recording is deferred, not dropped — capture needs the on-device Maestro fleet, tracked in [docs/roadmap.md](docs/roadmap.md#docs). Once captured, an Apple Pay and a Google Pay sheet GIF replace this placeholder. ## Doc map - **Getting started** — [Install](docs/getting-started/install.md) · [iOS](docs/getting-started/quickstart-ios.md) · [Android](docs/getting-started/quickstart-android.md) · [Expo](docs/getting-started/quickstart-expo.md) - **Platforms** — [iOS](docs/platforms/ios.md) · [Android](docs/platforms/android.md) · [Web](docs/platforms/web.md) · [Expo](docs/platforms/expo.md) - **API reference** — [index](docs/api/index.md) (`PaymentRequest`, `PaymentResponse`, every exported type/enum) - **Guides** — [Payment change events](docs/guides/change-events.md) · [Payment details modifiers](docs/guides/modifiers.md) · [Error handling](docs/guides/errors.md) · [Retrying a payment](docs/guides/retry.md) · [Unit testing](docs/guides/testing.md) · [Setup doctor](docs/guides/setup-doctor.md) · [Troubleshooting](docs/guides/troubleshooting.md) - **Gateway recipes** — [Pick your gateway](docs/guides/gateways/index.md): [Stripe](docs/guides/gateways/stripe.md) · [Adyen](docs/guides/gateways/adyen.md) · [Braintree](docs/guides/gateways/braintree.md) · [Checkout.com](docs/guides/gateways/checkout-com.md) · [Direct tokenization](docs/guides/gateways/direct.md) - **Architecture** — [The JS↔native contract, single-use requests, event lifecycle](docs/architecture.md) - **Roadmap** — [Open work and the W3C compliance checklist](docs/roadmap.md) ## Migrating - [From `v2` to `v3`](docs/guides/migrate-from-v2.md) — the native module interface change and the single-use request behavior change. - [From upstream `react-native-payments`](docs/guides/migrate-from-upstream.md) — the full API mapping and a worked before/after example. ## W3C compliance This package implements the [W3C Payment Request API](https://www.w3.org/TR/payment-request/) — change events, `PaymentDetailsModifier`, `hasEnrolledInstrument()`, `retry()`, `toJSON()` and the event-handler attributes are all implemented, with a small set of documented platform deviations (Android delivers only the shipping change events, iOS ignores `shippingOption.selected`, `PaymentRequest` is single-use). See the full [W3C compliance checklist](docs/roadmap.md#w3c-compliance-checklist) and each platform's known deviations in [docs/platforms/](docs/platforms/). ## Architecture & contributing See [AGENTS.md](AGENTS.md) for the source layout, TurboModule/Expo-plugin architecture, and coverage. For end-to-end verification, see [react-native-payments-example/e2e/readme.md](../react-native-payments-example/e2e/readme.md). ## License This library is licensed under The [MIT License](LICENSE.md). --- # FILE: docs/getting-started/install.md # Install Install the package with your package manager, e.g.: ```bash npm install @rnw-community/react-native-payments ``` Autolinking picks up the TurboModule on both architectures — no manual `react-native link` step. Before writing any code, complete the one-time platform setup for every platform you target: - [iOS quickstart](docs/getting-started/quickstart-ios.md) — Apple developer account, merchant ID, `pod install`. - [Android quickstart](docs/getting-started/quickstart-android.md) — Google developer account, `play-services-wallet` dependency, test-card allowlist. - [Expo quickstart](docs/getting-started/quickstart-expo.md) — the `app.plugin` entry plus `expo prebuild --clean`. In a bare React Native app, declare the merchant identifier(s), card networks and Google Pay environment once in a [`payments.config.ts`](docs/api/payments-config.md) and let the [setup doctor](docs/guides/setup-doctor.md) write the iOS entitlement and the Android manifest meta-data: ```bash npx rnw-payments doctor --fix ``` Then ship a payment sheet by following the [readme quickstart](readme.md#quickstart) or the full [`PaymentRequest` API reference](docs/api/payment-request.md). --- # FILE: docs/getting-started/quickstart-ios.md # iOS quickstart The fastest path to an Apple Pay sheet. See [Platforms — iOS](docs/platforms/ios.md) for the full setup story (capabilities, deviations, native code snippets). 1. Create an [Apple developer account](https://developer.apple.com/programs/enroll/) and a merchant ID. 2. Follow Apple's [Apple Pay configuration guide](https://developer.apple.com/library/archive/ApplePay_Guide/Configuration.html) to enable the capability and register the merchant ID. 3. Declare the merchant ID in a [`payments.config.ts`](docs/api/payments-config.md) and run `npx rnw-payments doctor --fix` to write it into the app's entitlements — see [Setup doctor](docs/guides/setup-doctor.md). 4. Run `pod install` — the pod links `PassKit` itself, no `AppDelegate` import is needed (see [Platforms — iOS](docs/platforms/ios.md#native-setup)). 5. Build the Apple Pay method data with `createApplePayMethodData`, construct a `PaymentRequest` and call `show()`: ```ts import { PaymentRequest, createApplePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'US', currencyCode: 'USD' }), ]; const paymentResponse = await new PaymentRequest(methodData, { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, }).show(); ``` 5. Pick your gateway: the [gateway recipes](docs/guides/gateways/index.md) show how each payment service provider issues the Apple Pay processing certificate for your merchant ID and how to forward `applePayToken` to it. `supportedNetworks` defaults to Visa and Mastercard — see [api/create-apple-pay-method-data.md](docs/api/create-apple-pay-method-data.md). To write the method data by hand instead, pass `{ supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier, supportedNetworks, countryCode, currencyCode } }` — see [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md). The [readme quickstart](readme.md#quickstart) shows the full flow, including `complete()`. `merchantIdentifier` passed to `methodData.data` must exactly match the merchant ID declared in the app's `com.apple.developer.in-app-payments` entitlement, or the sheet fails with a merchant/entitlement error — see [Troubleshooting](docs/guides/troubleshooting.md). --- # FILE: docs/getting-started/quickstart-android.md # Android quickstart The fastest path to a Google Pay sheet. See [Platforms — Android](docs/platforms/android.md) for the full setup story (capabilities, deviations, dependency version). 1. Create a [Google developer account](https://support.google.com/googleplay/android-developer/answer/6112435?hl=en). 2. Follow Google's [Google Pay API for Android setup guide](https://developers.google.com/pay/api/android/guides/setup). 3. Depend on `com.google.android.gms:play-services-wallet:20.0.0` or newer — see [Platforms — Android](docs/platforms/android.md#native-setup) for the Gradle snippet. 4. Add your test Google account to the [Google Pay API Test Cards Allowlist](https://groups.google.com/g/googlepay-test-mode-stub-data?pli=1). 5. Build the Google Pay method data with `createGooglePayMethodData`, construct a `PaymentRequest` and call `show()`: ```ts import { PaymentRequest, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'id' }, }), ]; const paymentResponse = await new PaymentRequest(methodData, { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, }).show(); ``` 6. Pick your gateway: replace the `example` gateway, which only returns a placeholder token, with your payment service provider's `gatewayConfig` from the [gateway recipes](docs/guides/gateways/index.md). `environment` defaults to `EnvironmentEnum.TEST` and `supportedNetworks` to Visa and Mastercard. Pass `environment: EnvironmentEnum.PRODUCTION` for live payments — see [api/create-google-pay-method-data.md](docs/api/create-google-pay-method-data.md). To write the method data by hand instead, pass `{ supportedMethods: PaymentMethodNameEnum.AndroidPay, data: { supportedNetworks, environment, countryCode, currencyCode, gatewayConfig } }` — see [api/android-payment-method-data.md](docs/api/android-payment-method-data.md). The [readme quickstart](readme.md#quickstart) shows the full flow, including `complete()`. `canMakePayment()` and `hasEnrolledInstrument()` on Android check Google Pay readiness against the same `environment` set on `methodData.data` that `show()` uses. See [Platforms — Android](docs/platforms/android.md). --- # FILE: docs/getting-started/quickstart-expo.md # Expo quickstart This package links native code (PassKit on iOS, the Google Pay API on Android), so it cannot run inside **Expo Go**. It requires an Expo [custom build](https://docs.expo.dev/custom-builds/get-started/) (a.k.a. development build / `expo-dev-client`). See [Platforms — Expo](docs/platforms/expo.md) for the full plugin options reference. 1. Add the `@rnw-community/react-native-payments` plugin to your `app.config.js`: ```js export default { plugins: [ ...[ '@rnw-community/react-native-payments/app.plugin', { merchantIdentifier: 'merchant.react-native-payments', }, ], ], }; ``` 2. Prebuild your project: ```bash npx expo prebuild --clean ``` Building the package before prebuild is required for local/monorepo consumers: `expo prebuild` resolves `@rnw-community/react-native-payments/app.plugin` through the package's `exports` map, which only points at `dist` — run `pnpm build` (or your workspace's build step) for this package before `expo prebuild` if you are linking it locally rather than installing it from npm. See [Platforms — Expo](docs/platforms/expo.md#plugin-options-reference) for every plugin option (`merchantIdentifier`, `supportedNetworks`, `googlePayEnvironment`). To keep the native setup and the runtime method data in sync, list the plugin without options and declare them once in a `payments.config.ts` at the app root instead — see [api/payments-config.md](docs/api/payments-config.md): ```js export default { plugins: ['@rnw-community/react-native-payments/app.plugin'], }; ``` 3. Pick your gateway: the [gateway recipes](docs/guides/gateways/index.md) give each payment service provider's Google Pay `gatewayConfig`, its Apple Pay certificate setup for the `merchantIdentifier` above, and the server call that charges the token. --- # FILE: docs/platforms/ios.md # iOS (Apple Pay) Setup, capabilities, and how this package's `PaymentRequest` maps onto PassKit. ## Setup - Apple Pay [overview](https://developer.apple.com/apple-pay/planning/). - Create an [Apple developer account](https://developer.apple.com/programs/enroll/). - Follow [this guide](https://developer.apple.com/library/archive/ApplePay_Guide/Configuration.html) to set up Apple Pay in your application. - [Payment token reference](https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference?language=objc). ### Native setup No `AppDelegate` changes are needed. The pod links `PassKit` and `Contacts` itself (`s.frameworks` in `react-native-payments.podspec`), so `pod install` — or `expo prebuild` — is the whole native step. ## Capabilities - `merchantCapabilities` (`IosPKMerchantCapability`, see [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md)) defaults to 3-D Secure, debit and credit when omitted. - `supportedNetworks` accepts every `SupportedNetworkEnum` member, but Apple Pay introduced some of them after the oldest supported iOS version: `girocard` needs iOS 14, `mir` needs iOS 14.5, `dankort` needs iOS 15.1 and `bancontact` needs iOS 16 — each is rejected as an invalid supported network below its minimum iOS version. See [api/supported-network-enum.md](docs/api/supported-network-enum.md). - `shippingType` (`Shipping` / `Delivery` / `Pickup`) forwards to `PKShippingType` — `Pickup` maps to `PKShippingTypeStorePickup`. See [Known deviations](#known-deviations). - `couponCode` prefills the coupon code field of the sheet, but the field itself is only rendered when a `couponcodechange` listener is registered before `show()`, and only on iOS 15+. See [guides/change-events.md](docs/guides/change-events.md). - `canMakePayment()` maps to PassKit's `canMakePaymentsUsingNetworks:`, restricting the check to the request's `supportedNetworks`. - `retry()` reuses the same `PKPaymentErrorDomain` field-error constructors as [Sheet errors](docs/guides/change-events.md#sheet-errors) to fail the pending authorization and let the user correct and resubmit. See [guides/retry.md](docs/guides/retry.md). ## Known deviations - **`PaymentShippingOption.selected` is ignored.** PassKit has no preselection support and always shows its shipping-method picker with the first option of the array highlighted. Put the option you want preselected first in `shippingOptions` instead of relying on `selected`. - **`shippingType: 'pickup'` maps to `PKShippingTypeStorePickup`.** PassKit also has `PKShippingTypeServicePickup`, which has no W3C equivalent and is not exposed by this library. - **`retry()` supports at most one in-sheet correction pass.** This package's `PaymentRequest` is single-use (see [architecture.md](docs/architecture.md)) and its native bridge resolves the `show()` promise exactly once per authorization, so there is no channel left to deliver a second submission to JavaScript. If the user corrects the fields and resubmits, this package fails and dismisses the sheet automatically instead of silently hanging — see [guides/retry.md](docs/guides/retry.md). - **`hasEnrolledInstrument()`** maps to PassKit's `canMakePaymentsUsingNetworks:`, the same capability check as `canMakePayment()` restricted to `supportedNetworks`. --- # FILE: docs/platforms/android.md # Android (Google Pay) Setup, capabilities, and how this package's `PaymentRequest` maps onto the Google Pay API. ## Setup - Create a [Google developer account](https://support.google.com/googleplay/android-developer/answer/6112435?hl=en). - Follow [this guide](https://developers.google.com/pay/api/android/guides/setup) to set up the Google Pay API in your application. - [Google payments tutorial](https://developers.google.com/pay/api/android/guides/tutorial). - [Google brand guidelines](https://developers.google.com/pay/api/android/guides/brand-guidelines). - Your Google account used for testing must be added to the [Google Pay API Test Cards Allowlist](https://groups.google.com/g/googlepay-test-mode-stub-data?pli=1). ### Native setup This package's own `android/build.gradle` depends on `com.google.android.gms:play-services-wallet:20.0.0` — the first release with the payment data callbacks that deliver in-sheet change events — match or exceed that in your application if you pin the wallet dependency yourself: ```groovy dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation 'com.google.android.gms:play-services-wallet:20.0.0' } ``` ## Capabilities - `environment` (`EnvironmentEnum`) selects the Google Pay environment for the payment; see [api/environment-enum.md](docs/api/environment-enum.md). - `totalPriceStatus` describes how the total price will change: `'FINAL'` (default) or `'ESTIMATED'`. A zero total amount (`'0.00'`) is valid per the W3C spec. `'NOT_CURRENTLY_KNOWN'` is still accepted by the type for backwards compatibility, but Google's current `TransactionInfo` reference only lists `'ESTIMATED'` and `'FINAL'` — for a charge whose amount is not known at checkout, use a [merchant-initiated transaction](#merchant-initiated-transactions) instead. See [TransactionInfo](https://developers.google.com/pay/api/android/reference/request-objects#TransactionInfo). - `checkoutOption` selects the payment sheet submit button: `'DEFAULT'`, `'COMPLETE_IMMEDIATE_PURCHASE'` ("Pay now") or `'CONTINUE_TO_REVIEW'` ("Review Order"). Google Pay only allows `'COMPLETE_IMMEDIATE_PURCHASE'` together with the `'FINAL'` `totalPriceStatus`, so the constructor throws on any other combination. - `transactionId` correlates the payment attempt in Google Pay transaction events. - `allowedAuthMethods` (`AndroidAllowedAuthMethodsEnum`) defaults to both `PAN_ONLY` and `CRYPTOGRAM_3DS` when omitted. See [api/android-payment-method-data.md](docs/api/android-payment-method-data.md). - `canMakePayment()` calls Google Pay's `isReadyToPay` against the `environment` set in `methodData.data`, the same environment `show()` opens the sheet in. - `hasEnrolledInstrument()` calls Google Pay's `isReadyToPay` with `existingPaymentMethodRequired: true`. See [Known deviations](#known-deviations). - `shippingaddresschange` / `shippingoptionchange` fire while the Google Pay sheet is open and their `updateWith` answers update the total and the display items (regular `transactionInfo` requests only — a merchant-initiated request gets no price update), the shipping options and the error in place, through Google Pay's [dynamic price updates](https://developers.google.com/pay/api/android/guides/tutorial#enable-callbacks): `show()` declares the `callbackIntents` of the shipping listeners registered before it, and the library manifest declares the `PaymentsCallbacksService` Google Pay binds to (`BIND_PAYMENTS_CALLBACK_SERVICE` permission, `PAYMENT_DATA_CALLBACKS` action) — no app-side manifest or Expo plugin change is needed. Both require `requestShipping`, and a regular `transactionInfo` request also needs `countryCode`. See [guides/change-events.md](docs/guides/change-events.md#android). ### Merchant-initiated transactions For pre-orders, bookings, subscriptions and automatic reloads, set exactly one of `deferredTransactionInfo`, `recurringTransactionInfo` or `automaticReloadTransactionInfo` on the Android `methodData.data`. The package sends it in place of `transactionInfo`, with `details.total` as the `immediateTotalPrice` due today (`'0.00'` allowed) and `currencyCode`, `countryCode` (required) and `transactionId` taken from the method data. It cannot be combined with `checkoutOption` or `totalPriceStatus`. See [api/android-merchant-initiated-transaction-info.md](docs/api/android-merchant-initiated-transaction-info.md). ## Known deviations - **`canMakePayment()`/`hasEnrolledInstrument()`/`show()` resolve gracefully without Google Play Services.** `GoogleApiAvailability.isGooglePlayServicesAvailable()` is checked before the chained `isReadyToPay()`/`loadPaymentData()` call that actually reaches into Play services — `Wallet.getPaymentsClient(...)` itself never fails on its own. On a device where Play Services is missing or invalid, Play Services would otherwise turn that chained call into a blocking, unrecoverable system dialog over the whole screen instead of failing the promise; the guard reports AndroidPay as unsupported instead, so `canMakePayment()`/`hasEnrolledInstrument()` resolve `false` and `show()` rejects like any other unsupported configuration. - **`paymentmethodchange` and `couponcodechange` never fire.** Google Pay's Android `callbackIntents` only cover `SHIPPING_ADDRESS`, `SHIPPING_OPTION` and `PAYMENT_AUTHORIZATION` — there is no payment-method intent and the `OFFER` (promo code) intent exists on the web API only — so these listeners can be registered but stay silent. - **A change event has 25 seconds, not 30.** Google Pay fails the sheet with `CALLBACK_TIMED_OUT` after 30 seconds, so the module answers a pending event with the unchanged details after 25 seconds on its own. JS timers keep running behind the sheet through a headless JS task (`RNPaymentsChangeEvents`) the package registers itself. Shipping options render from `label`/`detail` only — Google Pay has no price column. See [guides/change-events.md](docs/guides/change-events.md#android). - **`onPaymentAuthorized` is answered with `SUCCESS` immediately.** Google Pay requires the `PAYMENT_AUTHORIZATION` intent alongside the shipping intents; the payment still settles `show()` through the activity result, so `complete()` keeps its no-op behaviour below. - **`complete()` and `abort()` have no effect** — an artifact of the Google Pay activity-result flow, which has no in-sheet dismiss/complete call to make. - **`retry()` is a documented no-op.** It resolves without any visual effect, consistent with the `complete()`/ `abort()` no-op boundary above — Google Pay's sheet is a separate activity with no retry or dismiss call; its only in-sheet update path is the shipping change-event callbacks above, which `retry()` cannot reach. See [guides/retry.md](docs/guides/retry.md). - **`methodData.data.shippingType` is a no-op.** Google Pay has no `PKShippingType`-equivalent concept; the value is validated but not forwarded to native. - **`hasEnrolledInstrument()` is an optimistic signal, not a guarantee.** `isReadyToPay` with `existingPaymentMethodRequired: true` is the closest reachable equivalent of "an instrument is enrolled" the API exposes; Google documents this as best-effort and it can still resolve `true` without a fully usable card in some configurations. - **The address of a change event is redacted** by Google Pay to `locality`, `administrativeArea`, `postalCode` and `countryCode` (`IntermediateAddress`); the full address arrives with the payment response. --- # FILE: docs/platforms/web.md # Web (react-native-web) `payment-request.web.ts` / `payment-response.web.ts` are one-line passthroughs — `PaymentRequest` and `PaymentResponse` resolve to `window.PaymentRequest` / `window.PaymentResponse` (or `null` when `window` is not defined, e.g. during SSR), typed as `WebPaymentRequestConstructor` / `WebPaymentResponseConstructor` — aliases for the browser's own `typeof window.PaymentRequest` / `typeof window.PaymentResponse` from `lib.dom`. On web there is no TurboModule, no native class and none of this package's own logic in the loop: you get the browser's implementation of the W3C Payment Request API, unmodified. > **Type visibility caveat:** a bundler that platform-resolves `.web.ts` files (Metro building for the web > target, `react-native-web` webpack configs) swaps in this passthrough at *runtime* regardless of what > TypeScript shows you. `src/index.ts` re-exports the platform-agnostic `PaymentRequest` / `PaymentResponse` > specifiers without a build-time branch, so a plain `tsc`/IDE setup resolves > `import { PaymentRequest } from '@rnw-community/react-native-payments'` to the native class documented in > [api/payment-request.md](docs/api/payment-request.md) on every platform, web included. Add > `"moduleSuffixes": [".web", ".native", ""]` (or an order matching your own bundler's platform resolution) to > your app's `tsconfig.json` if you need the IDE/`tsc` to show the true DOM types for a web build. Apple Pay through this browser passthrough is **not** the native iOS integration documented in [platforms/ios.md](docs/platforms/ios.md) — calling `show()` in Safari drives Safari's own Apple Pay JS flow, which fires a `merchantvalidation` event that your own server must answer by completing Apple's merchant validation session round-trip (TLS, your merchant certificate, Apple's validation URL) before the sheet can display line items. There is no `merchantIdentifier` Expo plugin config, no PassKit entitlement and no `merchantCapabilities` on this path — see [Apple Pay on the Web](https://developer.apple.com/documentation/apple_pay_on_the_web). Browser support is inconsistent: Chrome, Edge and Safari (with the merchant-validation caveat above) implement the Payment Request API; Firefox removed its implementation. Check [caniuse](https://caniuse.com/payment-request) before shipping a web checkout on top of it, and always guard for `PaymentRequest`/`PaymentResponse` being **nullish** (a truthiness or `== null` check, never `=== null`) — the passthrough returns `null` outside a `window` (SSR), while an unsupported browser has no `window.PaymentRequest` at all, so the export resolves to `undefined` there. ## Known deviations from the native classes None of the native-only behavior documented for the `PaymentRequest`/`PaymentResponse` classes applies to the browser's own implementation: - **`couponCode`** — populated only by the iOS 15+ PassKit `couponcodechange` flow; the browser's `PaymentRequest` has no `couponCode` property. - **Normalized `AbortError`** — dismissing the sheet on web throws the browser's own native `DOMException`, not this package's [`PaymentsErrorEnum`](docs/api/payments-error-enum.md)-driven `DOMException`; `isNativeUserCancellation` never runs on web. - **Single-use request semantics** — the native class tracks `state: 'created' | 'interactive' | 'closed'` itself and rejects a reused, settled request with its own `InvalidStateError`. The browser enforces single-use per the W3C spec independently, through its own internal slots, not this package's state machine. See [architecture.md](docs/architecture.md). - **Listener auto-cleanup** — the request-scoped subscription bookkeeping and automatic teardown on `show()`/`abort()` described in [guides/change-events.md](docs/guides/change-events.md) is this package's `NativeEventEmitter` plumbing over the TurboModule. The browser's `PaymentRequest` follows plain DOM `addEventListener`/`removeEventListener` semantics with no auto-cleanup — remove your own listeners when you are done with them. ## Usage Detailed guide can be found at: - [developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API/Using_the_Payment_Request_API) as the API is fully compliant. - [Google Web Payments guide](https://web.dev/payments/). --- # FILE: docs/platforms/expo.md # Expo This package links native code (PassKit on iOS, the Google Pay API on Android), so it cannot run inside **Expo Go**. It requires an Expo [custom build](https://docs.expo.dev/custom-builds/get-started/) (a.k.a. development build / `expo-dev-client`) — add the `@rnw-community/react-native-payments` plugin into your `app.config.js`. See [getting-started/quickstart-expo.md](docs/getting-started/quickstart-expo.md) for the minimal setup steps. `merchantIdentifier` accepts either a single identifier or an array of identifiers. Pass an array when your app resolves the Apple Pay merchant per country/environment at runtime — every identifier is then declared in the `com.apple.developer.in-app-payments` entitlement. Empty identifiers are ignored; if no non-empty identifier remains, prebuild fails with an error: ```js { "merchantIdentifier": ["merchant.react-native-payments.fr", "merchant.react-native-payments.mg"] } ``` ## Plugin options reference The options are a [`PaymentsConfig`](docs/api/payments-config.md). When the plugin is listed without options, it reads them from `payments.config.ts`, `payments.config.js` or `payments.config.json` at the project root, the same file the app passes to `resolvePaymentsConfig` for its method data. Every value is validated by `resolvePaymentsConfig`, so prebuild fails with the same message the app would throw at runtime. | Option | Type | Default | What it mutates | | ---------------------- | ------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `merchantIdentifier` | `string \| string[]` | _(required)_ | iOS entitlements plist: appends every non-empty identifier to `com.apple.developer.in-app-payments`, de-duplicated. Throws at prebuild time if no non-empty identifier is provided. | | `supportedNetworks` | `SupportedNetworkEnum[]` | every `SupportedNetworkEnum` value | Android `AndroidManifest.xml`: writes the comma-joined list as the `com.rnw-community.react-native-payments.supported-networks` meta-data value on the main application. Throws if given an empty array or a value outside `SupportedNetworkEnum`. | | `googlePayEnvironment` | `EnvironmentEnum` | `EnvironmentEnum.PRODUCTION` | Android `AndroidManifest.xml`: writes the `com.google.android.gms.wallet.api.environment` meta-data value on the main application. Throws if given a value outside `EnvironmentEnum`. | `withGooglePay` also always writes `com.google.android.gms.wallet.api.enabled=true` (no option needed) so the Google Pay API is enabled for the app. `SupportedNetworkEnum` and `EnvironmentEnum` are both exported from the package root — see [api/supported-network-enum.md](docs/api/supported-network-enum.md) and [api/environment-enum.md](docs/api/environment-enum.md). Building the package before prebuild is required for local/monorepo consumers: `expo prebuild` resolves `@rnw-community/react-native-payments/app.plugin` through the package's `exports` map, which only points at `dist` — run `pnpm build` (or your workspace's build step) for this package before `expo prebuild` if you are linking it locally rather than installing it from npm. ## Example You can find a working example in the `App` component of the [react-native-payments-example](../react-native-payments-example/readme.md) package, running through its `apps/expo` target. --- # FILE: docs/api/index.md # API reference One entry per group of public exports from [`src/index.ts`](src/index.ts). Grouped files follow the same pairing the package already uses for tightly-coupled siblings (a data shape and its platform-specific `*DataInterface`, an event and its payload/listener types). ## Core classes - [payment-request.md](docs/api/payment-request.md) — `PaymentRequest` - [create-payment-request-factory.md](docs/api/create-payment-request-factory.md) — `createPaymentRequestFactory`, `PaymentRequestFactoryConfig`, `PaymentRequestFactory` - [payment-response.md](docs/api/payment-response.md) — `PaymentResponse` - [ios-payment-response.md](docs/api/ios-payment-response.md) — `IosPaymentResponse` - [ios-pk-token.md](docs/api/ios-pk-token.md) — `IosPKToken` - [android-payment-response.md](docs/api/android-payment-response.md) — `AndroidPaymentResponse` - [android-payment-method-token.md](docs/api/android-payment-method-token.md) — `AndroidPaymentMethodToken` ## Change events - [payment-request-update-event.md](docs/api/payment-request-update-event.md) — `PaymentRequestUpdateEvent`, `PaymentMethodChangeEvent` - [events-types.md](docs/api/events-types.md) — `PaymentRequestEventType`, `PaymentRequestEventListener`, `PaymentMethodChangeEventListener`, `PaymentRequestEventPayloadInterface` ## Enums - [payment-method-name-enum.md](docs/api/payment-method-name-enum.md) — `PaymentMethodNameEnum` - [environment-enum.md](docs/api/environment-enum.md) — `EnvironmentEnum` - [payment-complete-enum.md](docs/api/payment-complete-enum.md) — `PaymentComplete` - [supported-network-enum.md](docs/api/supported-network-enum.md) — `SupportedNetworkEnum` - [payments-error-enum.md](docs/api/payments-error-enum.md) — `PaymentsErrorEnum` - [payment-address-contact-field-enums.md](docs/api/payment-address-contact-field-enums.md) — `PaymentAddressFieldEnum`, `PaymentContactFieldEnum` - [payment-update-error-type-enum.md](docs/api/payment-update-error-type-enum.md) — `PaymentUpdateErrorTypeEnum` - [payment-shipping-type-enum.md](docs/api/payment-shipping-type-enum.md) — `PaymentShippingTypeEnum` ## Errors - [constructor-error.md](docs/api/constructor-error.md) — `ConstructorError` - [dom-exception.md](docs/api/dom-exception.md) — `DOMException` - [payments-error.md](docs/api/payments-error.md) — `PaymentsError` ## Payment details shapes - [payment-details-init.md](docs/api/payment-details-init.md) — `PaymentDetailsInit` - [payment-details-update.md](docs/api/payment-details-update.md) — `PaymentDetailsUpdate`, `PaymentDetailsUpdateError` - [payment-details-modifier.md](docs/api/payment-details-modifier.md) — `PaymentDetailsModifier` - [payment-item-shipping-option.md](docs/api/payment-item-shipping-option.md) — `PaymentItem`, `PaymentShippingOption` - [payment-validation-errors.md](docs/api/payment-validation-errors.md) — `PaymentValidationErrors` - [payment-response-json.md](docs/api/payment-response-json.md) — `PaymentResponseJsonInterface` - [payment-response-address.md](docs/api/payment-response-address.md) — `PaymentResponseAddressInterface` - [payment-method-data.md](docs/api/payment-method-data.md) — `PaymentMethodData` ## Platform method data - [create-apple-pay-method-data.md](docs/api/create-apple-pay-method-data.md) — `createApplePayMethodData`, `ApplePayMethodDataOptions` - [create-google-pay-method-data.md](docs/api/create-google-pay-method-data.md) — `createGooglePayMethodData`, `GooglePayMethodDataOptions` - [android-payment-method-data.md](docs/api/android-payment-method-data.md) — `AndroidPaymentMethodDataInterface`, `AndroidPaymentMethodDataDataInterface` - [android-allowed-auth-methods-enum.md](docs/api/android-allowed-auth-methods-enum.md) — `AndroidAllowedAuthMethodsEnum` - [android-merchant-initiated-transaction-info.md](docs/api/android-merchant-initiated-transaction-info.md) — `AndroidDeferredTransactionInfo`, `AndroidRecurringTransactionInfo`, `AndroidAutomaticReloadTransactionInfo`, `AndroidRecurrencePeriodItem`, `AndroidIntroductoryPeriodInfo`, `AndroidDisplayItem` - [ios-payment-method-data.md](docs/api/ios-payment-method-data.md) — `IosPaymentMethodDataInterface`, `IosPaymentMethodDataDataInterface` - [ios-pk-merchant-capability.md](docs/api/ios-pk-merchant-capability.md) — `IosPKMerchantCapability` ## Payments config - [payments-config.md](docs/api/payments-config.md) — `PaymentsConfig`, `ResolvedPaymentsConfig`, `resolvePaymentsConfig` --- # FILE: docs/api/android-allowed-auth-methods-enum.md # `AndroidAllowedAuthMethodsEnum` ## What & why Restricts `methodData.data.allowedAuthMethods` on the Android entry to the auth methods Google Pay should accept. Reach for it only when you need to narrow acceptance below the default. ## How | Member | Meaning | | --- | --- | | `PAN_ONLY` | Accept cards without requiring 3-D Secure cryptogram data. | | `CRYPTOGRAM_3DS` | Accept cards tokenized with a 3-D Secure cryptogram. | Both members are the default when `allowedAuthMethods` is omitted from [`AndroidPaymentMethodDataDataInterface`](docs/api/android-payment-method-data.md). ## Example ```ts import { AndroidAllowedAuthMethodsEnum } from '@rnw-community/react-native-payments'; const allowedAuthMethods = [AndroidAllowedAuthMethodsEnum.PAN_ONLY]; ``` ## Pitfalls None — narrowing this list only restricts which cards Google Pay offers; it does not change any other validation. ## References - [Google Pay API for Android — CardParameters](https://developers.google.com/pay/api/android/reference/request-objects#CardParameters) - [api/android-payment-method-data.md](docs/api/android-payment-method-data.md) --- # FILE: docs/api/android-merchant-initiated-transaction-info.md # `AndroidDeferredTransactionInfo` / `AndroidRecurringTransactionInfo` / `AndroidAutomaticReloadTransactionInfo` Also covers the nested `AndroidRecurrencePeriodItem`, `AndroidIntroductoryPeriodInfo` and `AndroidDisplayItem` shapes. ## What & why Google Pay's merchant-initiated transaction (MIT) objects describe a payment the merchant charges later instead of a regular checkout: a pre-order or a booking (`deferredTransactionInfo`), a subscription (`recurringTransactionInfo`), or a stored-value automatic reload (`automaticReloadTransactionInfo`). Reach for them when the amount charged at checkout differs from what the user is agreeing to be charged in the future — for example a `'0.00'` hotel reservation that is charged on the arrival date. ## How Set exactly one of the three objects on the Android entry of `methodData.data`. The package sends it to Google Pay in place of the regular `transactionInfo` and fills the fields it already owns: | `PaymentDataRequest` field | Filled from | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `currencyCode` | `methodData.data.currencyCode` | | `countryCode` | `methodData.data.countryCode` — required by Google Pay for every MIT object | | `transactionId` | `methodData.data.transactionId`, when set | | `immediateTotalPrice` | `details.total.amount.value` after [modifiers](docs/guides/modifiers.md) are resolved — the amount due today, `'0.00'` allowed | Everything else comes from the object you supply: | Type | Fields | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AndroidDeferredTransactionInfo` | `billingDateTime` (RFC 3339, in the future), `label`, `priceStatus` + `price`, `displayItems?` | | `AndroidRecurringTransactionInfo` | `recurrenceItems` (non-empty `AndroidRecurrencePeriodItem[]`), `introductoryPeriodInfo?` (`AndroidIntroductoryPeriodInfo`) | | `AndroidAutomaticReloadTransactionInfo` | `label`, `minimumBalanceAmount`, `reloadAmount` | | `AndroidRecurrencePeriodItem` | `label`, `priceStatus` + `price`, `recurrencePeriod` (`'DAY'`/`'WEEK'`/`'MONTH'`/`'YEAR'`), `recurrencePeriodCount`, `billingInitialDateTime?`, `billingFinalDateTime?`, `displayItems?` | | `AndroidIntroductoryPeriodInfo` | `introductoryPeriodEndDateTime`, `label`, `totalPrice`, `introductoryPeriodStartDateTime?`, `displayItems?` | | `AndroidDisplayItem` | `label`, `price`, `type` (`'DISCOUNT'`/`'LINE_ITEM'`/`'SHIPPING_OPTION'`/`'SUBTOTAL'`/`'TAX'`), `status?` | The three MIT objects also accept `tokenUpdateUrl?`, `managementUrl?`, `billingAgreement?` and `immediateDisplayItems?` (`AndroidDisplayItem[]`). `priceStatus` is `'FINAL'` or `'ESTIMATED'` together with a `price`, or `'NOT_CURRENTLY_KNOWN'` without one — the type rejects any other pairing. | Platform | Behavior | | -------- | ----------------------------------------------------------------- | | Android | Serialized into the `PaymentDataRequest` as documented by Google. | | iOS | Not applicable — the fields live on the Android method data only. | ## Example ```ts import { EnvironmentEnum, PaymentMethodNameEnum, PaymentRequest, SupportedNetworkEnum, } from '@rnw-community/react-native-payments'; const request = new PaymentRequest( [ { supportedMethods: PaymentMethodNameEnum.AndroidPay, data: { supportedNetworks: [SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard], environment: EnvironmentEnum.TEST, countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }, deferredTransactionInfo: { billingDateTime: '2026-12-01T08:00:00Z', label: 'Hotel Room Reservation', priceStatus: 'FINAL', price: '200.00', }, }, }, ], { total: { label: 'Due today', amount: { currency: 'USD', value: '0.00' } } } ); ``` ## Pitfalls - The constructor throws a `ConstructorError` when more than one MIT object is set, when `countryCode` is missing, or when an MIT object is combined with `checkoutOption` or `totalPriceStatus` — those two only exist on the regular `transactionInfo` that the MIT object replaces. - Google Pay documents MIT objects for merchant-initiated transactions only and does not support dynamic price updates for them. Your gateway must also support MIT tokens — confirm with it before going live. - `tokenUpdateUrl` and `managementUrl` must be HTTPS endpoints with no trailing slash, query parameters or fragment. ## References - [Google Pay Android `PaymentDataRequest`](https://developers.google.com/pay/api/android/reference/request-objects#PaymentDataRequest) - [Google Pay `DeferredTransactionInfo`](https://developers.google.com/pay/api/android/reference/request-objects#DeferredTransactionInfo), [`RecurringTransactionInfo`](https://developers.google.com/pay/api/android/reference/request-objects#RecurringTransactionInfo), [`AutomaticReloadTransactionInfo`](https://developers.google.com/pay/api/android/reference/request-objects#AutomaticReloadTransactionInfo) - [Google Pay merchant-initiated transactions guide](https://developers.google.com/pay/api/web/guides/resources/merchant-initiated-transactions) - [api/android-payment-method-data.md](docs/api/android-payment-method-data.md) - [platforms/android.md](docs/platforms/android.md) --- # FILE: docs/api/android-payment-method-data.md # `AndroidPaymentMethodDataInterface` / `AndroidPaymentMethodDataDataInterface` ## What & why The typed shape of the Android entry of `methodData`. Reach for these when building the `AndroidPay` entry of your `methodData` array. ## How | Type | Notes | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AndroidPaymentMethodDataInterface` | `supportedMethods: PaymentMethodNameEnum.AndroidPay` paired with an `AndroidPaymentMethodDataDataInterface` `data`. | | `AndroidPaymentMethodDataDataInterface` | `supportedNetworks`, `environment`, `countryCode?`, `currencyCode`, exactly one of `gatewayConfig` (`{ gateway, gatewayMerchantId?, ...gateway-specific keys }`) or `directConfig` (`{ protocolVersion, publicKey }`) — the type forbids supplying both, `allowedAuthMethods?` ([`AndroidAllowedAuthMethodsEnum`](docs/api/android-allowed-auth-methods-enum.md)), `totalPriceStatus?`, `checkoutOption?`, `transactionId?`, at most one of `deferredTransactionInfo?` / `recurringTransactionInfo?` / `automaticReloadTransactionInfo?` ([merchant-initiated transactions](docs/api/android-merchant-initiated-transaction-info.md)) — see [platforms/android.md](docs/platforms/android.md) for every field. | `requestBillingAddress`, `requestPayerEmail`, `requestPayerName`, `requestPayerPhone` and `requestShipping` are shared with [`IosPaymentMethodDataDataInterface`](docs/api/ios-payment-method-data.md) (both extend the package's common `GenericPaymentMethodDataDataInterface`): each is an optional boolean that, when `true`, adds the matching field to the resulting `PaymentResponse` — see [api/payment-response.md](docs/api/payment-response.md). [`createGooglePayMethodData`](docs/api/create-google-pay-method-data.md) builds this entry with defaults filled in. ## Example ```ts const androidMethod: AndroidPaymentMethodDataInterface = { supportedMethods: PaymentMethodNameEnum.AndroidPay, data: { supportedNetworks: [SupportedNetworkEnum.Visa], environment: EnvironmentEnum.TEST, countryCode: 'DE', currencyCode: 'EUR', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }, }, }; ``` ## Pitfalls - `checkoutOption: 'COMPLETE_IMMEDIATE_PURCHASE'` is only allowed together with `totalPriceStatus: 'FINAL'` — the constructor throws on any other combination. See [platforms/android.md](docs/platforms/android.md). - A merchant-initiated transaction object replaces `transactionInfo`, so it cannot be combined with `checkoutOption` or `totalPriceStatus`, and it requires `countryCode` — the constructor throws otherwise. See [api/android-merchant-initiated-transaction-info.md](docs/api/android-merchant-initiated-transaction-info.md). - `countryCode` is optional on this interface (unlike the required `countryCode` on the iOS side) — omitting it is valid TypeScript, but Google Pay's own requirements for your merchant configuration may still need it set. - `gatewayConfig` and `directConfig` are mutually exclusive at the type level (`{ directConfig; gatewayConfig?: never } | { directConfig?: never; gatewayConfig }`) — providing both, or neither, is a type error. - `gatewayConfig` is passed verbatim as the Google Pay `tokenizationSpecification.parameters`, so gateway-specific keys (`stripe:publishableKey`, `braintree:clientKey`, …) go straight into it. `gatewayMerchantId` is optional because not every gateway takes it — Google's [processor table](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) lists none for Stripe or Braintree. See [guides/gateways/](docs/guides/gateways/index.md) for each PSP's exact parameters. ## References - [platforms/android.md](docs/platforms/android.md) - [api/payment-method-name-enum.md](docs/api/payment-method-name-enum.md) - [api/android-allowed-auth-methods-enum.md](docs/api/android-allowed-auth-methods-enum.md) - [api/android-merchant-initiated-transaction-info.md](docs/api/android-merchant-initiated-transaction-info.md) --- # FILE: docs/api/android-payment-method-token.md # `AndroidPaymentMethodToken` ## What & why The Google Pay payment token exposed as `paymentResponse.details.androidPayToken` on an [`AndroidPaymentResponse`](docs/api/android-payment-response.md). Reach for it to read the tokenized card data to send to your payment gateway. ## How | Member | Type | Notes | | --- | --- | --- | | `cardInfo.cardNetwork` | `string` | The card network of the tokenized card. | | `cardInfo.cardDetails` | `string` | The last four digits or similar display detail, as returned by Google Pay. | | `intermediateSigningKey` | `{ signatures: string[]; signedKey: AndroidSignedKey }` | The intermediate signing key used to verify the token signature. | | `protocolVersion` | `string` | The protocol version of the signed message (e.g. `ECv2`); `''` for a gateway-issued token. | | `signature` | `string` | The signature over `signedMessage`. | | `signedMessage` | `{ encryptedMessage: string; ephemeralPublicKey: string; tag: string }` | The encrypted message envelope — see [Google Pay payment data cryptography](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#signed-message). | | `rawToken` | `string` | The untouched `tokenizationData.token` string as returned by Google Pay — the value to send to your gateway. | ## Example ```ts import { AndroidPaymentResponse } from '@rnw-community/react-native-payments'; const response = await paymentRequest.show(); if (response instanceof AndroidPaymentResponse) { const token = response.details.androidPayToken; token.cardInfo.cardNetwork; } ``` ## Pitfalls - `protocolVersion`, `signature`, `signedMessage` and `intermediateSigningKey` are only filled in when the token is Google's signed-message envelope: always for `directConfig` (`DIRECT`) tokenization, and for `gatewayConfig` (`PAYMENT_GATEWAY`) gateways that forward Google's encrypted payload. A gateway that issues its own token instead (Stripe returns a Stripe Token JSON, Braintree a JSON carrying a nonce) leaves them at their empty defaults — send `rawToken` to the gateway and parse it as that gateway documents. You chose the tokenization type yourself through `gatewayConfig` or `directConfig`, so branch on that rather than on empty fields. See [guides/gateways/](docs/guides/gateways/index.md). - `AndroidPaymentMethodToken` is a plain TypeScript interface with no runtime validation of its own — constructing an object that merely matches its shape never throws. `PaymentsError` is thrown by [`AndroidPaymentResponse`](docs/api/android-payment-response.md), not by this token type: when the native JSON payload is malformed or incomplete, when the token is missing, when a `DIRECT` token is not a complete signed-message envelope, or when an envelope's `signedMessage` / `signedKey` is not valid JSON. A non-envelope `PAYMENT_GATEWAY` token is not an error — see [guides/errors.md](docs/guides/errors.md). ## References - [Google Pay API for Android — response objects](https://developers.google.com/pay/api/android/reference/response-objects) - [Google Pay payment method token structure](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#payment-method-token-structure) and [intermediate signing key](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#intermediate-signing-key) — the raw token this type is parsed from - [api/android-payment-response.md](docs/api/android-payment-response.md) --- # FILE: docs/api/android-payment-response.md # `AndroidPaymentResponse` ## What & why The `PaymentResponse` subclass `show()` resolves with on Android. Reach for it when you need to branch on the platform response type or read the Google Pay token via `details.androidPayToken` — see [api/android-payment-method-token.md](docs/api/android-payment-method-token.md). ## How | Member | Signature | Notes | | --- | --- | --- | | `AndroidPaymentResponse` | `class extends PaymentResponse` | Parsed from the Google Pay JSON payload. Consumers do not construct it directly — it comes back from `show()`. | | `AndroidPaymentResponse.details.androidPayToken` | `AndroidPaymentMethodToken` | The Google Pay token exposed on the response — see [api/android-payment-method-token.md](docs/api/android-payment-method-token.md). | ## Example ```ts import { AndroidPaymentResponse } from '@rnw-community/react-native-payments'; const response = await paymentRequest.show(); if (response instanceof AndroidPaymentResponse) { response.details.androidPayToken.cardInfo.cardNetwork; } ``` ## Pitfalls - A native payment response payload that fails to parse (malformed or incomplete JSON from Google Pay, including direct construction of `AndroidPaymentResponse` with malformed tokenization data) throws `PaymentsError` — see [guides/errors.md](docs/guides/errors.md). A missing token, or a `DIRECT` token that is not Google's signed-message envelope, throws `PaymentsError` naming the tokenization type. A `PAYMENT_GATEWAY` token the gateway issued in its own format is kept as `androidPayToken.rawToken` instead — see [api/android-payment-method-token.md](docs/api/android-payment-method-token.md). ## References - [api/payment-response.md](docs/api/payment-response.md) - [api/android-payment-method-token.md](docs/api/android-payment-method-token.md) - [Google Pay API for Android — response objects](https://developers.google.com/pay/api/android/reference/response-objects) - [Google Pay `PaymentData`](https://developers.google.com/pay/api/android/reference/response-objects#PaymentData) — the payload `AndroidPaymentResponse` parses ([Android SDK `PaymentData`](https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentData)) - [Google Pay `PaymentMethodData`](https://developers.google.com/pay/api/android/reference/response-objects#PaymentMethodData) and [`PaymentMethodTokenizationData`](https://developers.google.com/pay/api/android/reference/response-objects#PaymentMethodTokenizationData) --- # FILE: docs/api/constructor-error.md # `ConstructorError` ## What & why A native `TypeError` thrown from `new PaymentRequest(...)` when the input dictionaries fail W3C validation. Reach for `instanceof TypeError` to catch it, matching the spec's WebIDL / `check and canonicalize (total) amount` algorithm, which itself throws `TypeError`. ## How | Check | Trigger | | --- | --- | | `instanceof TypeError` | Always true — `ConstructorError` is a `TypeError` subclass. | | `name` | `'TypeError'` | | Thrown from | Missing/invalid payment methods, total, display items or shipping options. | ## Example ```ts try { new PaymentRequest([], paymentDetails); } catch (error) { if (error instanceof TypeError) { // invalid constructor input } } ``` ## Pitfalls Distinct from `DOMException NotSupportedError`, which is also thrown at construction time but only when the input is otherwise valid and no platform-matching payment method exists — see [architecture.md](docs/architecture.md). ## References - [guides/errors.md](docs/guides/errors.md) --- # FILE: docs/api/create-apple-pay-method-data.md # `createApplePayMethodData` / `ApplePayMethodDataOptions` ## What & why Builds the Apple Pay entry of the `methodData` array from the few fields that are specific to your merchant. Reach for it instead of hand-writing `{ supportedMethods: PaymentMethodNameEnum.ApplePay, data: { … } }` when the defaults below match your integration. ## How ```ts createApplePayMethodData(options: ApplePayMethodDataOptions): IosPaymentMethodDataInterface ``` `ApplePayMethodDataOptions` is [`IosPaymentMethodDataDataInterface`](docs/api/ios-payment-method-data.md) with `supportedNetworks` made optional. Every option is copied into `data` unchanged. Only the defaults below are filled in: | Option | Required | Default when omitted | | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `merchantIdentifier` | yes | — | | `countryCode` | yes | — | | `currencyCode` | yes | — | | `supportedNetworks` | no | `[SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard]` | | `merchantCapabilities` | no | Left unset — the request then uses `3DS`, `Debit` and `Credit`, see [ios-pk-merchant-capability.md](docs/api/ios-pk-merchant-capability.md) | The function is pure: it has no native call, no platform check, and every result has its own `supportedNetworks` array. ## Example ```ts import { createApplePayMethodData } from '@rnw-community/react-native-payments'; const applePayMethodData = createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app.namespace', countryCode: 'US', currencyCode: 'USD', }); ``` ## References - [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md) — the raw shape this function returns - [api/create-google-pay-method-data.md](docs/api/create-google-pay-method-data.md) - [platforms/ios.md](docs/platforms/ios.md) - [Apple `PKPaymentRequest`](https://developer.apple.com/documentation/passkit/pkpaymentrequest) --- # FILE: docs/api/create-google-pay-method-data.md # `createGooglePayMethodData` / `GooglePayMethodDataOptions` ## What & why Builds the Google Pay entry of the `methodData` array from the few fields that are specific to your merchant. Reach for it instead of hand-writing `{ supportedMethods: PaymentMethodNameEnum.AndroidPay, data: { … } }` when the defaults below match your integration. ## How ```ts createGooglePayMethodData(options: GooglePayMethodDataOptions): AndroidPaymentMethodDataInterface ``` `GooglePayMethodDataOptions` is [`AndroidPaymentMethodDataDataInterface`](docs/api/android-payment-method-data.md) with `environment` and `supportedNetworks` made optional. Every option is copied into `data` unchanged. Only the defaults below are filled in: | Option | Required | Default when omitted | | --------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `currencyCode` | yes | — | | `gatewayConfig` or `directConfig` | exactly one | — | | `countryCode` | no | — (Google Pay requires it for EEA merchants) | | `environment` | no | `EnvironmentEnum.TEST` | | `supportedNetworks` | no | `[SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard]` | | `allowedAuthMethods` | no | Left unset — the request then sends `PAN_ONLY` and `CRYPTOGRAM_3DS`, see [android-allowed-auth-methods-enum.md](docs/api/android-allowed-auth-methods-enum.md) | The function is pure: it has no native call, no platform check, and every result has its own `supportedNetworks` array. ## Example ```ts import { EnvironmentEnum, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const googlePayMethodData = createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }, environment: __DEV__ ? EnvironmentEnum.TEST : EnvironmentEnum.PRODUCTION, }); ``` ## Pitfalls - `environment` defaults to `EnvironmentEnum.TEST`, which only returns test card data. Pass `EnvironmentEnum.PRODUCTION` for live payments. ## References - [api/android-payment-method-data.md](docs/api/android-payment-method-data.md) — the raw shape this function returns - [api/create-apple-pay-method-data.md](docs/api/create-apple-pay-method-data.md) - [api/environment-enum.md](docs/api/environment-enum.md) - [platforms/android.md](docs/platforms/android.md) - [Google Pay `PaymentDataRequest`](https://developers.google.com/pay/api/android/reference/request-objects#PaymentDataRequest) --- # FILE: docs/api/create-payment-request-factory.md # `createPaymentRequestFactory` / `PaymentRequestFactoryConfig` / `PaymentRequestFactory` ## What & why A `PaymentRequest` is single-use (see [architecture.md](docs/architecture.md)), so every checkout attempt — including a retry after a failed or cancelled payment — needs a new instance. `createPaymentRequestFactory` captures the configuration that stays the same across attempts once, and returns a function that builds a fresh request from the details of each checkout. ## How ```ts createPaymentRequestFactory(config: PaymentRequestFactoryConfig): PaymentRequestFactory type PaymentRequestFactory = (details: PaymentDetailsInit) => PaymentRequest; ``` | `PaymentRequestFactoryConfig` member | Required | Notes | | ------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------- | | `methodData` | yes | Passed to every request. Each request gets its own copy of the array. | | `details` | no | Base `PaymentDetailsInit` without `total` and `id` (for example shared `shippingOptions` or `modifiers`). | Merge semantics: every call builds `new PaymentRequest([...methodData], { ...config.details, ...details })`. - The merge is shallow. A key in the per-checkout `details` replaces the base value as a whole, so arrays are replaced, not concatenated. - A key set to `undefined` in the per-checkout `details` clears the base value. - `total` and `id` only come from the per-checkout `details`. When `id` is omitted, every request gets a new generated one. - The constructor runs on every call, so invalid details throw `ConstructorError` from the factory call, exactly like `new PaymentRequest(...)`. Event listeners belong to one request. Register them on each request the factory returns. The factory is a plain function with no React dependency. In a component, create it once (module scope or `useMemo`) and call it inside the pay handler. ## Example ```ts import { PaymentComplete, createApplePayMethodData, createGooglePayMethodData, createPaymentRequestFactory, } from '@rnw-community/react-native-payments'; const createPaymentRequest = createPaymentRequestFactory({ methodData: [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app.namespace', countryCode: 'US', currencyCode: 'USD', }), createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'id' }, }), ], }); const pay = async (value: string): Promise => { const paymentResponse = await createPaymentRequest({ total: { label: 'Total', amount: { currency: 'USD', value } }, }).show(); await paymentResponse.complete( (await sendToYourBackend(paymentResponse.details)) ? PaymentComplete.SUCCESS : PaymentComplete.FAIL ); }; ``` ## Pitfalls - On react-native-web, `PaymentRequest` resolves to the browser's `window.PaymentRequest`, and the factory builds that one. It is `null` during SSR. See [platforms/web.md](docs/platforms/web.md). ## References - [api/payment-request.md](docs/api/payment-request.md) - [api/payment-details-init.md](docs/api/payment-details-init.md) - [guides/retry.md](docs/guides/retry.md#starting-a-new-attempt) - [W3C `PaymentRequest` constructor](https://www.w3.org/TR/payment-request/#constructor) --- # FILE: docs/api/dom-exception.md # `DOMException` ## What & why The spec-mandated runtime error for `created`/`interactive`/`closed` state violations and abort/not-supported conditions. Reach for `instanceof DOMException` plus `error.name` to branch on the W3C error name. ## How | Check | Trigger | | --- | --- | | `instanceof DOMException` | Always true for spec-mandated runtime states. | | `error.name` | One of `AbortError`, `InvalidStateError`, `NotAllowedError`, `NotSupportedError`, `SecurityError` — see [api/payments-error-enum.md](docs/api/payments-error-enum.md). | See [guides/errors.md](docs/guides/errors.md) for the full table of which public API failure produces which `DOMException` name. ## Example ```ts import { DOMException } from '@rnw-community/react-native-payments'; try { await paymentRequest.show(); } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { // user cancelled } } ``` ## Pitfalls `SecurityError` is defined but not currently reachable from this implementation — no permission-policy check exists in React Native. ## References - [guides/errors.md](docs/guides/errors.md) --- # FILE: docs/api/environment-enum.md # `EnvironmentEnum` ## What & why Selects the Google Pay environment for a payment, and the `googlePayEnvironment` Expo plugin option. Reach for it when setting `methodData.data.environment` on the Android entry, or when configuring the Expo plugin. ## How | Member | Runtime value | Meaning | | --- | --- | --- | | `TEST` | `'TEST'` | Google Pay's test environment — used internally by `canMakePayment()` regardless of the configured value. | | `PRODUCTION` | `'PRODUCTION'` | Google Pay's production environment — the default for the Expo plugin's `googlePayEnvironment` option. | ## Example ```ts import { EnvironmentEnum, PaymentMethodNameEnum } from '@rnw-community/react-native-payments'; const methodData = [ { supportedMethods: PaymentMethodNameEnum.AndroidPay, data: { environment: EnvironmentEnum.TEST, supportedNetworks: [], countryCode: 'DE', currencyCode: 'EUR' }, }, ]; ``` ## References - [platforms/android.md](docs/platforms/android.md) - [platforms/expo.md](docs/platforms/expo.md) --- # FILE: docs/api/events-types.md # Event support types ## What & why The small supporting types for the change-event system: the event-name union, the listener signatures, and the raw native payload. Reach for these when typing a standalone listener function or when inspecting the payload before it reaches a listener. ## How | Type | Shape | Notes | | --- | --- | --- | | `PaymentRequestEventType` | `'shippingaddresschange' \| 'shippingoptionchange' \| 'paymentmethodchange' \| 'couponcodechange'` | Accepted by `addEventListener`/`removeEventListener`. | | `PaymentRequestEventListener` | `(event: PaymentRequestUpdateEvent) => Promise \| void` | For `shippingaddresschange`, `shippingoptionchange` and `couponcodechange`. Both sync and async listeners are accepted — see [guides/change-events.md](docs/guides/change-events.md). | | `PaymentMethodChangeEventListener` | `(event: PaymentMethodChangeEvent) => Promise \| void` | For `paymentmethodchange`. | | `PaymentRequestEventPayloadInterface` | `{ requestId: string; eventId?: number; … }` | The raw native payload carried by a change event, before it is applied to the request and dispatched to listeners. `requestId` always identifies the request; `eventId` identifies the native completion handler and is optional — the rest is event-type specific. | ## Example ```ts const eventType: PaymentRequestEventType = 'shippingoptionchange'; paymentRequest.addEventListener(eventType, event => event.updateWith({})); const onShippingOptionChange: PaymentRequestEventListener = event => { event.updateWith({}); }; const onPaymentMethodChange: PaymentMethodChangeEventListener = event => { event.updateWith({}); }; const payload: PaymentRequestEventPayloadInterface = { requestId: paymentRequest.id, eventId: 1, shippingOption: 'express', }; ``` ## Pitfalls - `eventId` on `PaymentRequestEventPayloadInterface` is optional — guard with `isDefined`/`?.` before forwarding it to a native completion call instead of assuming it is always a `number`. - A listener may return either synchronously or a `Promise` — `updateWith` does not have to be called before the listener function returns. See [guides/change-events.md](docs/guides/change-events.md). ## References - [guides/change-events.md](docs/guides/change-events.md) - [api/payment-request-update-event.md](docs/api/payment-request-update-event.md) --- # FILE: docs/api/ios-payment-method-data.md # `IosPaymentMethodDataInterface` / `IosPaymentMethodDataDataInterface` ## What & why The typed shape of the Apple Pay entry of `methodData`. Reach for these when building the `ApplePay` entry of your `methodData` array. ## How | Type | Notes | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `IosPaymentMethodDataInterface` | `supportedMethods: PaymentMethodNameEnum.ApplePay` paired with an `IosPaymentMethodDataDataInterface` `data`. | | `IosPaymentMethodDataDataInterface` | `merchantIdentifier`, `supportedNetworks`, `countryCode`, `currencyCode`, plus the iOS-only options in [platforms/ios.md](docs/platforms/ios.md) (`merchantCapabilities?` — [`IosPKMerchantCapability`](docs/api/ios-pk-merchant-capability.md), `shippingType`, `couponCode`, `applicationData`) and the cross-platform request flags below. | `requestBillingAddress`, `requestPayerEmail`, `requestPayerName`, `requestPayerPhone` and `requestShipping` are shared with [`AndroidPaymentMethodDataDataInterface`](docs/api/android-payment-method-data.md) (both extend the package's common `GenericPaymentMethodDataDataInterface`): each is an optional boolean that, when `true`, adds the matching field to the resulting `PaymentResponse` — see [api/payment-response.md](docs/api/payment-response.md). [`createApplePayMethodData`](docs/api/create-apple-pay-method-data.md) builds this entry with defaults filled in. ## Example ```ts const iosMethod: IosPaymentMethodDataInterface = { supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier: 'merchant.com.your-app.namespace', countryCode: 'US', currencyCode: 'USD', supportedNetworks: [SupportedNetworkEnum.Visa], merchantCapabilities: [ IosPKMerchantCapability.PKMerchantCapability3DS, IosPKMerchantCapability.PKMerchantCapabilityDebit, ], }, }; ``` ## Pitfalls `applicationData` is not transmitted to Apple but is included in the decrypted payment token payload as a SHA-256 hash, under the token's own `applicationData` key (per Apple's [Payment Token Format Reference](https://developer.apple.com/documentation/passkit/payment-token-format-reference)) — use it to prevent replay attacks by associating a payment with a specific transaction, not to pass data your backend needs verbatim. ## References - [platforms/ios.md](docs/platforms/ios.md) - [api/payment-method-name-enum.md](docs/api/payment-method-name-enum.md) - [api/ios-pk-merchant-capability.md](docs/api/ios-pk-merchant-capability.md) --- # FILE: docs/api/ios-payment-response.md # `IosPaymentResponse` ## What & why The `PaymentResponse` subclass `show()` resolves with on iOS. Reach for it when you need to branch on the platform response type or read the Apple Pay token via `details.applePayToken` — see [api/ios-pk-token.md](docs/api/ios-pk-token.md). ## How | Member | Signature | Notes | | --- | --- | --- | | `IosPaymentResponse` | `class extends PaymentResponse` | Parsed from the PassKit payment token. Consumers do not construct it directly — it comes back from `show()`. | | `IosPaymentResponse.details.applePayToken` | `IosPKToken` | The Apple Pay token exposed on the response, carrying the PassKit payment data — see [api/ios-pk-token.md](docs/api/ios-pk-token.md). | ## Example ```ts import { IosPaymentResponse } from '@rnw-community/react-native-payments'; const response = await paymentRequest.show(); if (response instanceof IosPaymentResponse) { const token = response.details.applePayToken; token.transactionIdentifier; } ``` ## Pitfalls - A native payment response payload that fails to parse (malformed or incomplete JSON from PassKit, including direct construction of `IosPaymentResponse` with malformed tokenization data) throws `PaymentsError` — see [guides/errors.md](docs/guides/errors.md). ## References - [api/payment-response.md](docs/api/payment-response.md) - [api/ios-pk-token.md](docs/api/ios-pk-token.md) - [Apple Pay payment token reference](https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference?language=objc) - [Apple `PKPayment`](https://developer.apple.com/documentation/passkit/pkpayment) — the payload `IosPaymentResponse` parses, with its [`PKContact`](https://developer.apple.com/documentation/passkit/pkcontact?language=objc) and [`PKShippingMethod`](https://developer.apple.com/documentation/passkit/pkshippingmethod?language=objc) --- # FILE: docs/api/ios-pk-merchant-capability.md # `IosPKMerchantCapability` ## What & why Populates the optional `merchantCapabilities` of the Apple Pay `methodData.data`, declaring which payment processing capabilities the merchant supports. Reach for it only when the default set does not match your merchant configuration. ## How | Member | Meaning | Accepted by this package's native bridge? | | --- | --- | --- | | `PKMerchantCapability3DS` | Supports 3-D Secure. | Yes | | `PKMerchantCapabilityCredit` | Supports credit cards. | Yes | | `PKMerchantCapabilityDebit` | Supports debit cards. | Yes | | `PKMerchantCapabilityEMV` | Supports the EMV payment protocol — per Apple's guidance, only relevant for China UnionPay transactions; use `PKMerchantCapability3DS` for other networks. | Yes | | `PKMerchantCapabilityInstantFundsOut` | Supports Instant Funds Out **disbursements** (`PKDisbursementRequest`), not ordinary purchase payments. | **No — see Pitfalls.** | `merchantCapabilities` defaults to `PKMerchantCapability3DS`, `PKMerchantCapabilityDebit` and `PKMerchantCapabilityCredit` when omitted from [`IosPaymentMethodDataDataInterface`](docs/api/ios-payment-method-data.md). ## Example ```ts import { IosPKMerchantCapability } from '@rnw-community/react-native-payments'; const data = { merchantIdentifier: 'merchant.com.your-app.namespace', merchantCapabilities: [ IosPKMerchantCapability.PKMerchantCapability3DS, IosPKMerchantCapability.PKMerchantCapabilityDebit, ], }; ``` ## Pitfalls - **`PKMerchantCapabilityInstantFundsOut` is rejected by this package's native iOS bridge.** `merchantCapabilityFromString:` in `ios/Payments.mm` only maps `PKMerchantCapability3DS`, `PKMerchantCapabilityEMV`, `PKMerchantCapabilityCredit` and `PKMerchantCapabilityDebit` — passing `PKMerchantCapabilityInstantFundsOut` fails the native `merchantCapabilityFromString:` lookup and rejects the whole `show()` call with `invalid_merchant_capability`. This member exists on the TypeScript enum but is not currently usable through this package. - PassKit also rejects the request if none of the (accepted) declared capabilities match an available payment network. ## References - [Apple `PKMerchantCapability`](https://developer.apple.com/documentation/passkit/pkmerchantcapability?language=objc) - [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md) --- # FILE: docs/api/ios-pk-token.md # `IosPKToken` ## What & why The Apple Pay token exposed as `paymentResponse.details.applePayToken` on an [`IosPaymentResponse`](docs/api/ios-payment-response.md), carrying the PassKit payment data. Reach for it to read the tokenized card data to send to your payment gateway. ## How | Member | Type | Notes | | --- | --- | --- | | `paymentData` | `IosPaymentData` | The still-**encrypted** payment token envelope (`data`, `header`, `signature`, `version`) — send it to your e-commerce backend, where it is decrypted with your payment processing certificate and submitted to your payment processor. | | `paymentMethod.displayName` / `.network` / `.type` | `string` / `string` / `IosPKPaymentMethodType` | Describes the card used for the payment. | | `transactionIdentifier` | `string` | Correlates the payment attempt with Apple's servers. | ## Example ```ts import { IosPaymentResponse } from '@rnw-community/react-native-payments'; const response = await paymentRequest.show(); if (response instanceof IosPaymentResponse) { const token = response.details.applePayToken; token.transactionIdentifier; } ``` ## Pitfalls - `IosPKToken` is a plain TypeScript interface with no runtime validation of its own — constructing an object that merely matches its shape never throws. `PaymentsError` is thrown by [`IosPaymentResponse`](docs/api/ios-payment-response.md) when it parses a malformed or incomplete native JSON payload (including direct construction of `IosPaymentResponse` with malformed tokenization data), not by this token type — see [guides/errors.md](docs/guides/errors.md). - `paymentData` is never decrypted by this package — decrypt it on your backend with your payment processing certificate before submitting it to your processor. ## References - [Apple Pay payment token reference](https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference?language=objc) - [api/ios-payment-response.md](docs/api/ios-payment-response.md) --- # FILE: docs/api/payment-address-contact-field-enums.md # `PaymentAddressFieldEnum` / `PaymentContactFieldEnum` ## What & why The field-key enums used by field-level sheet errors and `PaymentResponse.retry()`'s `errorFields`. Reach for these when you need to point a validation error at a specific row of the sheet instead of showing a generic banner. ## How | Type | Maps onto | Members | | --- | --- | --- | | `PaymentAddressFieldEnum` | PassKit `CNPostalAddress` keys | `AddressLine` (street), `City`, `Country` (ISO code), `DependentLocality` (sub locality), `PostalCode`, `Region` (state), `SubAdministrativeArea` | | `PaymentContactFieldEnum` | PassKit `PKContactField` | `Email`, `Name`, `Phone`, `PostalAddress` | ## Example ```ts import { PaymentAddressFieldEnum, PaymentContactFieldEnum } from '@rnw-community/react-native-payments'; await paymentResponse.retry({ payer: { [PaymentContactFieldEnum.Email]: 'Please provide a valid email' }, shippingAddress: { [PaymentAddressFieldEnum.PostalCode]: 'We do not ship to this postal code' }, }); ``` ## Pitfalls An unknown field, an empty message, or a coupon error below iOS 15 is dropped and the sheet is answered with the updated details only. On Android the errors passed to `PaymentResponse.retry()` are ignored because Google Pay has no in-sheet retry, while a field error answered to a shipping change event reaches Google Pay — a `shippingAddressField` error as `SHIPPING_ADDRESS_INVALID`, any other as `OTHER_ERROR`. ## References - [guides/change-events.md](docs/guides/change-events.md#sheet-errors) - [guides/retry.md](docs/guides/retry.md) --- # FILE: docs/api/payment-complete-enum.md # `PaymentComplete` ## What & why The outcome passed to `PaymentResponse.complete()` to close the sheet. Reach for it right after your backend has confirmed (or rejected) the charge. ## How | Member | Runtime value | Meaning | | --- | --- | --- | | `SUCCESS` | `'success'` | The payment was confirmed by your backend. | | `FAIL` | `'fail'` | The payment failed or your backend rejected it. | | `UNKNOWN` | `'unknown'` | The outcome could not be determined. | ## Example ```ts import { PaymentComplete } from '@rnw-community/react-native-payments'; paymentResponse.complete(PaymentComplete.SUCCESS); // OR PaymentComplete.FAIL ``` ## Pitfalls - Only call `complete(PaymentComplete.SUCCESS)` once your backend has actually confirmed the charge — completing with `SUCCESS` before that point tells the sheet (and the user) the payment went through even if it didn't. - Has no effect on Android — see [platforms/android.md](docs/platforms/android.md). ## References - [api/payment-response.md](docs/api/payment-response.md) --- # FILE: docs/api/payment-details-init.md # `PaymentDetailsInit` ## What & why The second constructor argument to `PaymentRequest`. Reach for it to type the `paymentDetails` object you build before constructing a request. ## How | Member | Required | Notes | | --- | --- | --- | | `total` | yes | The `PaymentItem` shown as the sheet's total — see [api/payment-item-shipping-option.md](docs/api/payment-item-shipping-option.md). | | `displayItems` | no | Line items shown above the total. | | `shippingOptions` | no | Offered shipping options — see [api/payment-item-shipping-option.md](docs/api/payment-item-shipping-option.md). | | `modifiers` | no | Per-method total/display-item overrides — see [guides/modifiers.md](docs/guides/modifiers.md). | | `id` | no | Generated with `uuid.v4()` when omitted. | ## Example ```ts const paymentDetails: PaymentDetailsInit = { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, displayItems: [{ label: 'Item', amount: { currency: 'USD', value: '10.00' } }], }; ``` ## Pitfalls The total, the display items and the shipping options all have to carry a valid decimal monetary value, or the constructor throws `ConstructorError` — see [api/constructor-error.md](docs/api/constructor-error.md). ## References - [W3C `PaymentDetailsInit`](https://www.w3.org/TR/payment-request/#dom-paymentdetailsinit) - [api/payment-request.md](docs/api/payment-request.md) --- # FILE: docs/api/payment-details-modifier.md # `PaymentDetailsModifier` ## What & why A per-`supportedMethods` override for `total` and `displayItems`. Reach for it when a payment method needs a different price than the top-level `total` (e.g. an Apple Pay discount). ## How | Member | Notes | | --- | --- | | `supportedMethods` | Matched against the platform's active payment method — see [api/payment-method-name-enum.md](docs/api/payment-method-name-enum.md). | | `total` | Overrides the top-level `total` when matched. | | `additionalDisplayItems` | Appended to `displayItems` when matched. | | `data` | Not validated by `validateModifiers()` (which only checks `supportedMethods`, `total` and `additionalDisplayItems`) and not forwarded to native — the bridge has no per-method extension point for it. | ## Example ```ts const paymentDetails = { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, modifiers: [ { supportedMethods: PaymentMethodNameEnum.ApplePay, total: { label: 'Total with Apple Pay discount', amount: { currency: 'USD', value: '9.00' } }, additionalDisplayItems: [{ label: 'Apple Pay discount', amount: { currency: 'USD', value: '-1.00' } }], }, ], }; ``` ## Pitfalls A modifier for the other platform's method is ignored. The same resolution re-runs on every `updateWith()` call, so a listener can ship an updated `modifiers` array together with the rest of the update. ## References - [W3C `PaymentDetailsModifier`](https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier) - [guides/modifiers.md](docs/guides/modifiers.md) --- # FILE: docs/api/payment-details-update.md # `PaymentDetailsUpdate` / `PaymentDetailsUpdateError` ## What & why The dictionary answered from `updateWith`, and the type of its optional `error` member. Reach for these when typing a change-event listener's response. ## How | Type | Shape | Notes | | --- | --- | --- | | `PaymentDetailsUpdate` | `{ total?, displayItems?, shippingOptions?, modifiers?: PaymentDetailsModifier[], error?: PaymentDetailsUpdateError }` | Every member is optional — only the provided members replace the current details. `modifiers` is inherited from `PaymentDetailsBase` and is re-resolved against the platform's active payment method on every `updateWith()` call — see [guides/modifiers.md](docs/guides/modifiers.md). | | `PaymentDetailsUpdateError` | `string \| { type: PaymentUpdateErrorTypeEnum; … }` | A plain string, or a field-level error — see [api/payment-update-error-type-enum.md](docs/api/payment-update-error-type-enum.md). | ## Example ```ts const fieldError: PaymentDetailsUpdateError = { type: PaymentUpdateErrorTypeEnum.CouponCode, message: 'SALE10 expired last week', expired: true, }; event.updateWith({ total: { label: 'Total', amount: { currency: 'USD', value: '25.00' } }, error: fieldError, }); ``` ## Pitfalls Updated details go through the same validation as the ones passed to the constructor — a malformed amount is reported to the console and never reaches the sheet. See [guides/change-events.md](docs/guides/change-events.md). ## References - [W3C `PaymentDetailsUpdate`](https://www.w3.org/TR/payment-request/#dom-paymentdetailsupdate) - [guides/change-events.md](docs/guides/change-events.md#sheet-errors) - [guides/modifiers.md](docs/guides/modifiers.md) --- # FILE: docs/api/payment-item-shipping-option.md # `PaymentItem` / `PaymentShippingOption` ## What & why The line-item shapes used throughout `PaymentDetailsInit` and change-event updates: `PaymentItem` for `total` and `displayItems`, `PaymentShippingOption` for `shippingOptions`. Reach for these when building or updating any priced row of the sheet. ## How | Type | Member | Notes | | --- | --- | --- | | `PaymentItem` | `label`, `amount`, `pending?` | `pending: true` renders `PKPaymentSummaryItemTypePending` on iOS instead of the amount and a `PENDING` display item on Android when change-event callbacks are active. | | `PaymentShippingOption` | `id`, `label`, `amount`, `detail?`, `selected?` | `id`/`label`/`amount` are required — iOS renders the row from the label and amount and reports the selection back by id. `selected` is **ignored on iOS** (see Pitfalls); on Android the last `selected` option becomes Google Pay's `defaultSelectedOptionId` and the amount is not rendered. | ## Example ```ts const paymentDetails = { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, displayItems: [{ label: 'Shipping', amount: { currency: 'USD', value: '0.00' }, pending: true }], }; const shippingOptions = [ { id: 'express', label: 'Express', detail: 'Next business day', amount: { currency: 'USD', value: '5.00' } }, { id: 'ground', label: 'Ground', detail: '3-5 business days', amount: { currency: 'USD', value: '0.00' } }, ]; ``` ## Pitfalls - `PaymentShippingOption.selected` is part of the W3C dictionary but is **silently ignored on iOS**: PassKit has no preselection support and always shows its shipping-method picker with the first option of the array highlighted. Put the option you want preselected first in `shippingOptions` instead. - `amount.currency` on a shipping option is ignored because the sheet is already bound to the `currencyCode` of the method data. ## References - [W3C `PaymentItem`](https://www.w3.org/TR/payment-request/#dom-paymentitem) - [guides/change-events.md](docs/guides/change-events.md) --- # FILE: docs/api/payment-method-data.md # `PaymentMethodData` ## What & why The generic W3C union type backing one entry of the `methodData` array passed to `new PaymentRequest(...)`. Reach for it only when writing platform-agnostic helper code; concrete integrations use the platform-specific `IosPaymentMethodDataInterface` / `AndroidPaymentMethodDataInterface` instead. ## How `PaymentMethodData` is the union of `IosPaymentMethodDataInterface` and `AndroidPaymentMethodDataInterface`, discriminated by `supportedMethods` (`PaymentMethodNameEnum`). ## Example ```ts const methodData: PaymentMethodData[] = [ { supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: [SupportedNetworkEnum.Visa], countryCode: 'US', currencyCode: 'USD', }, }, ]; ``` ## Pitfalls Prefer the platform-specific interfaces directly — see [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md) and [api/android-payment-method-data.md](docs/api/android-payment-method-data.md) — for full field-level typing instead of this union. ## References - [W3C `PaymentMethodData`](https://www.w3.org/TR/payment-request/#dom-paymentmethoddata) - [api/payment-request.md](docs/api/payment-request.md) --- # FILE: docs/api/payment-method-name-enum.md # `PaymentMethodNameEnum` ## What & why Discriminates which platform a `methodData` entry targets. Reach for it whenever you build the `methodData` array passed to `new PaymentRequest(...)`. ## How | Member | Runtime value | Pairs with | | --- | --- | --- | | `ApplePay` | `'apple-pay'` | [`IosPaymentMethodDataInterface`](docs/api/ios-payment-method-data.md) | | `AndroidPay` | `'android-pay'` | [`AndroidPaymentMethodDataInterface`](docs/api/android-payment-method-data.md) | ## Example ```ts import { PaymentMethodNameEnum, SupportedNetworkEnum } from '@rnw-community/react-native-payments'; const methodData = [ { supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: [SupportedNetworkEnum.Visa], countryCode: 'US', currencyCode: 'USD', }, }, ]; ``` ## Pitfalls A `PaymentRequest` constructed with no platform-matching payment method throws `DOMException NotSupportedError` at construction time — see [architecture.md](docs/architecture.md). ## References - [api/payment-request.md](docs/api/payment-request.md) --- # FILE: docs/api/payment-request-update-event.md # `PaymentRequestUpdateEvent` / `PaymentMethodChangeEvent` ## What & why The event object handed to a change-event listener; `PaymentMethodChangeEvent` is the subtype delivered for `paymentmethodchange`, extending the base with the selected method. Reach for these when you register `addEventListener` and need to answer with updated details. The full narrative (dispatch order, timeout, `isAnswered`, sheet errors) lives in [guides/change-events.md](docs/guides/change-events.md); this page is the per-class signature reference. ## How | Member | Signature | Notes | | --- | --- | --- | | `updateWith(detailsOrPromise)` | `(details: PaymentDetailsUpdate \| Promise) => void` | Answers the event. Throws `InvalidStateError` if called twice. | | `isAnswered` | `boolean` | `true` once `updateWith` was called for the event. | | `PaymentMethodChangeEvent.methodDetails` | `Record \| null` | The selected payment method's details, only on `paymentmethodchange`. `null` (not `undefined`) when the native layer omits the field. | ## Example ```ts paymentRequest.addEventListener('paymentmethodchange', event => { if (event.methodDetails?.['network'] === 'Amex') { event.updateWith({ error: 'Amex is not supported for this order' }); } }); ``` ## Pitfalls - Calling `updateWith` twice, or once the event was already answered or the request is no longer showing, throws `DOMException InvalidStateError`. - A listener that throws, rejects, sends invalid details, never calls `updateWith`, or leaves its promise pending for more than 30 seconds is logged and answered with the unchanged details — see [guides/change-events.md](docs/guides/change-events.md). ## References - [W3C `PaymentRequestUpdateEvent`](https://www.w3.org/TR/payment-request/#dom-paymentrequestupdateevent) - [W3C `PaymentMethodChangeEvent`](https://www.w3.org/TR/payment-request/#dom-paymentmethodchangeevent) - [guides/change-events.md](docs/guides/change-events.md) --- # FILE: docs/api/payment-request.md # `PaymentRequest` ## What & why The entry point of the library: constructs a W3C-shaped payment request, checks device capability, and drives the native Apple Pay / Google Pay sheet through one TurboModule. Reach for it whenever you need to accept a payment — everything else in this package (events, responses, errors) hangs off an instance of this class. ## How ```ts import { PaymentRequest } from '@rnw-community/react-native-payments'; ``` | Member | Signature | Notes | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | constructor | `new PaymentRequest(methodData, details)` | Validates per W3C spec, then serializes platform-specific JSON for native. Throws `ConstructorError` on invalid input, `DOMException NotSupportedError` when no platform-matching method is found — see [architecture.md](docs/architecture.md). | | `canMakePayment()` | `(): Promise` | iOS: PassKit `canMakePaymentsUsingNetworks:` restricted to `supportedNetworks`. Android: Google Pay `isReadyToPay` against `EnvironmentEnum.TEST` always — see [platforms/android.md](docs/platforms/android.md). Rejects `InvalidStateError` when not `created`. | | `hasEnrolledInstrument()` | `(): Promise` | Android: `isReadyToPay` with `existingPaymentMethodRequired: true`, optimistic — see [platforms/android.md](docs/platforms/android.md). Rejects `InvalidStateError` when not `created`. | | `show()` | `(): Promise` | Presents the sheet; single-use — see [architecture.md](docs/architecture.md). Rejects `AbortError` on user cancellation, `InvalidStateError` when not `created`. | | `abort()` | `(): Promise` | Dismisses an interactive sheet. No effect on Android (Google Pay activity has no in-sheet dismiss). Rejects `InvalidStateError` when not `interactive`. | | `addEventListener` / `removeEventListener` | `(type, listener) => void` | See [guides/change-events.md](docs/guides/change-events.md). | | `on*` attributes | `onshippingaddresschange`, `onshippingoptionchange`, `onpaymentmethodchange`, `oncouponcodechange` | See [guides/change-events.md](docs/guides/change-events.md#event-handler-attributes). | | `shippingAddress`, `shippingOption`, `couponCode`, `updating`, `id` | properties | Mirror the latest change-event selection — see [guides/change-events.md](docs/guides/change-events.md#changed-values-on-the-request). | A `PaymentRequest` is single-use — [`createPaymentRequestFactory`](docs/api/create-payment-request-factory.md) builds a fresh one per checkout attempt from shared configuration. `methodData` is an array of `IosPaymentMethodDataInterface` / `AndroidPaymentMethodDataInterface` entries — see [api/ios-payment-method-data.md](docs/api/ios-payment-method-data.md) and [api/android-payment-method-data.md](docs/api/android-payment-method-data.md). `details` is a `PaymentDetailsInit` — see [api/payment-details-init.md](docs/api/payment-details-init.md). ## Example ```ts import { PaymentComplete, PaymentMethodNameEnum, PaymentRequest, SupportedNetworkEnum, } from '@rnw-community/react-native-payments'; const methodData = [ { supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: [SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard], countryCode: 'US', currencyCode: 'USD', }, }, ]; const paymentDetails = { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } } }; const paymentRequest = new PaymentRequest(methodData, paymentDetails); if (await paymentRequest.canMakePayment()) { const paymentResponse = await paymentRequest.show(); const isConfirmed = await sendToYourBackend(paymentResponse.details); await paymentResponse.complete(isConfirmed ? PaymentComplete.SUCCESS : PaymentComplete.FAIL); } ``` ## Pitfalls - **A `PaymentRequest` is single-use.** As soon as `show()` settles — resolved, rejected or aborted — the request moves to the `closed` state, its change-event listeners are released and every further `show()` rejects with `InvalidStateError`. Build a new `PaymentRequest` to retry a payment. See [architecture.md](docs/architecture.md). - Only call `complete(PaymentComplete.SUCCESS)` once your backend has actually confirmed the charge — completing with `SUCCESS` before that point tells the sheet (and the user) the payment went through even if it didn't. - On web, `PaymentRequest` resolves to the browser's own implementation — see [platforms/web.md](docs/platforms/web.md). ## References - [W3C `PaymentRequest`](https://www.w3.org/TR/payment-request/#paymentrequest-interface) - [architecture.md](docs/architecture.md) - [guides/errors.md](docs/guides/errors.md) --- # FILE: docs/api/payment-response-address.md # `PaymentResponseAddressInterface` ## What & why The shape of `PaymentResponse.details.billingAddress` and `PaymentResponse.details.shippingAddress`, and of `PaymentRequest.shippingAddress` while a change event is pending. Reach for it when reading a user's address off a response or an in-progress shipping-address change. ## How `details.billingAddress` and `details.shippingAddress` are always objects on the two concrete response classes (`IosPaymentResponse`, `AndroidPaymentResponse`) — never absent — but every field is an empty string unless the matching request flag was set: | Platform | Populated when | | --- | --- | | iOS `billingAddress` | `requestBillingAddress` is `true`. | | iOS `shippingAddress` | `requestShipping` is `true`. | | Android `billingAddress` | `requestBillingAddress`, `requestPayerName` **or** `requestPayerPhone` is `true` — Google Pay returns the billing address whenever any of the three is requested, not only `requestBillingAddress`. | | Android `shippingAddress` | `requestShipping` is `true`. | `PaymentRequest.shippingAddress` (the in-progress change-event value, not the settled response) is `Maybe` — `null` until the first `shippingaddresschange` event arrives. ## Example ```ts const response = await paymentRequest.show(); response.details.billingAddress; // PaymentResponseAddressInterface, empty strings if not requested ``` ## Pitfalls - Reading `response.billingAddress` directly (instead of `response.details.billingAddress`) reads `undefined` — see [api/payment-response.md](docs/api/payment-response.md). - On Android, `billingAddress` is populated by `requestPayerName`/`requestPayerPhone` alone, even without `requestBillingAddress` — do not use its presence to infer that billing address was explicitly requested. - On iOS the shipping address of a change event is **redacted** by PassKit: only `address2` (city), `address3` (state), `postalCode` and `countryCode` are filled, while the street and the payer name, email and phone stay empty until the payment is authorized — quote shipping from the postal code and the country, never from the street. See [guides/change-events.md](docs/guides/change-events.md#changed-values-on-the-request). ## References - [api/payment-response.md](docs/api/payment-response.md) - [guides/change-events.md](docs/guides/change-events.md) --- # FILE: docs/api/payment-response-json.md # `PaymentResponseJsonInterface` ## What & why The return type of `PaymentResponse.toJSON()`, and what `JSON.stringify(paymentResponse)` produces. Reach for it when you need the spec-shaped serialization of a response, e.g. to log or transmit it. ## How | Member | Notes | | --- | --- | | `requestId`, `methodName`, `details` | Always present. | | `shippingAddress`, `payerName`, `payerEmail`, `payerPhone` | Read from `details`; default to `null` when not requested. | | `shippingOption` | Mirrors `PaymentRequest.shippingOption` at the moment `show()` resolved; `null` when shipping options were never offered. | ## Example ```ts import type { PaymentResponseJsonInterface } from '@rnw-community/react-native-payments'; const paymentResponse = await paymentRequest.show(); const json: PaymentResponseJsonInterface = paymentResponse.toJSON(); // { requestId, methodName, details, shippingAddress, shippingOption, payerName, payerEmail, payerPhone } json.requestId; json.shippingAddress; // PaymentResponseAddressInterface | null const serialized = JSON.stringify(paymentResponse); // same shape, via JSON.stringify's toJSON() hook ``` ## Pitfalls None — a pure serialization of `PaymentResponse`, always safe to call once `show()` has resolved. ## References - [api/payment-response.md](docs/api/payment-response.md) --- # FILE: docs/api/payment-response.md # `PaymentResponse` ## What & why The result of a settled `show()` call — carries the payment method's token/details and the methods to close out the sheet. Reach for it right after `await paymentRequest.show()` to send data to your backend and finish the transaction. ## How | Member | Signature | Notes | | --- | --- | --- | | `requestId`, `methodName`, `shippingOption` | readonly properties | Mirror the constructor arguments — `shippingOption` is the selected option's id at the moment `show()` resolved, or `null`. | | `details` | `PaymentResponseDetailsInterface` | Everything platform/payment-method specific lives here — see below. | | `complete(result)` | `(result: PaymentComplete): Promise` | Dismisses the sheet with the given outcome. No effect on Android. See [platforms/android.md](docs/platforms/android.md). | | `retry(errorFields?)` | `(errorFields?: PaymentValidationErrors): Promise` | Asks the user to correct fields instead of completing — see [guides/retry.md](docs/guides/retry.md). | | `toJSON()` | `(): PaymentResponseJsonInterface` | Spec-shaped serialization — see [api/payment-response-json.md](docs/api/payment-response-json.md). | `billingAddress`, `shippingAddress`, `payerEmail`, `payerName`, `payerPhone`, `androidPayToken` and `applePayToken` are **not** direct properties of `PaymentResponse` — they live on `paymentResponse.details` (`PaymentResponseDetailsInterface`): | `details` member | Type | Notes | | --- | --- | --- | | `billingAddress?`, `shippingAddress?` | `PaymentResponseAddressInterface` | Optional at the type level, but both concrete response classes (`IosPaymentResponse`, `AndroidPaymentResponse`) always assign a full object — never absent — with every field an empty string unless the matching `request*` flag was set (also true for `requestPayerName`/`requestPayerPhone` on Android's `billingAddress`). See [api/payment-response-address.md](docs/api/payment-response-address.md). | | `payerEmail?`, `payerName?`, `payerPhone?` | `string` | Always present with an empty-string default on iOS. On Android, `payerEmail` reflects the native payload directly (can be `undefined` if `requestPayerEmail` was never set) and `payerName`/`payerPhone` are only present at all when a shipping or billing address was returned. | | `androidPayToken` | `AndroidPaymentMethodToken` | Populated on `AndroidPaymentResponse`, an empty placeholder token otherwise — see [api/android-payment-response.md](docs/api/android-payment-response.md). | | `applePayToken` | `IosPKToken` | Populated on `IosPaymentResponse`, an empty placeholder token otherwise — see [api/ios-payment-response.md](docs/api/ios-payment-response.md). | ## Example ```ts const paymentResponse = await paymentRequest.show(); paymentResponse.details.billingAddress; paymentResponse.details.payerEmail; const json = paymentResponse.toJSON(); // { requestId, methodName, details, shippingAddress, shippingOption, payerName, payerEmail, payerPhone } await paymentResponse.complete(PaymentComplete.SUCCESS); ``` ## Pitfalls - `PaymentResponse.complete()` **after** `retry()` throws `InvalidStateError` instead of reaching native — `complete()` unconditionally dismisses the sheet, which would silently cancel the correction opportunity `retry()` just opened. See [guides/retry.md](docs/guides/retry.md). - `shippingAddress`, `payerName`, `payerEmail` and `payerPhone` on `toJSON()`'s output default to `null` when not requested; `shippingOption` mirrors `PaymentRequest.shippingOption` at the moment `show()` resolved and is `null` when shipping options were never offered. - Reading `paymentResponse.billingAddress` (or any of the other `details` members) directly on the response instead of through `.details` is `undefined` at the type level — `PaymentResponse` only declares `requestId`, `methodName`, `details` and `shippingOption`. ## References - [W3C `PaymentResponse`](https://www.w3.org/TR/payment-request/#paymentresponse-interface) - [guides/retry.md](docs/guides/retry.md) - [guides/errors.md](docs/guides/errors.md) --- # FILE: docs/api/payment-shipping-type-enum.md # `PaymentShippingTypeEnum` ## What & why Populates the optional `shippingType` of `methodData.data`, mapping to the W3C `PaymentOptions.shippingType` concept. Reach for it when you need PassKit to label the shipping picker as delivery vs. pickup. ## How | Member | iOS `PKShippingType` | Android | | --- | --- | --- | | `Shipping` | `PKShippingTypeShipping` | No-op | | `Delivery` | `PKShippingTypeDelivery` | No-op | | `Pickup` | `PKShippingTypeStorePickup` | No-op | ## Example ```ts import { PaymentShippingTypeEnum } from '@rnw-community/react-native-payments'; const data = { merchantIdentifier: 'merchant.com.your-app.namespace', shippingType: PaymentShippingTypeEnum.Delivery, }; ``` ## Pitfalls - No-op on Android, which has no equivalent concept — validated but not forwarded to native. - PassKit also has `PKShippingTypeServicePickup`, which has no W3C equivalent and is not exposed by this library. ## References - [`PaymentOptions.shippingType`](https://www.w3.org/TR/payment-request/#dom-paymentoptions-shippingtype) - [platforms/ios.md](docs/platforms/ios.md) --- # FILE: docs/api/payment-update-error-type-enum.md # `PaymentUpdateErrorTypeEnum` ## What & why The discriminator for a field-level `PaymentDetailsUpdateError` answered from `updateWith`. Reach for it when building a field-level sheet error instead of a plain string. ## How | Member | Additional member | iOS `PKPaymentErrorDomain` error | | --- | --- | --- | | `ShippingAddressField` | `key: PaymentAddressFieldEnum` | `paymentShippingAddressInvalidErrorWithKey:` | | `ContactField` | `field: PaymentContactFieldEnum` | `paymentContactInvalidErrorWithContactField:` | | `CouponCode` | `expired?: boolean` | `paymentCouponCodeInvalidError` / `paymentCouponCodeExpiredError` (iOS 15+) | ## Example ```ts import { PaymentAddressFieldEnum, PaymentUpdateErrorTypeEnum } from '@rnw-community/react-native-payments'; event.updateWith({ error: { type: PaymentUpdateErrorTypeEnum.ShippingAddressField, key: PaymentAddressFieldEnum.PostalCode, message: 'We do not ship to this postal code', }, }); ``` ## Pitfalls `shippingoptionchange` has no error slot in PassKit, so an error answered there is ignored. ## References - [guides/change-events.md](docs/guides/change-events.md#sheet-errors) - [api/payment-address-contact-field-enums.md](docs/api/payment-address-contact-field-enums.md) --- # FILE: docs/api/payment-validation-errors.md # `PaymentValidationErrors` ## What & why The type of `PaymentResponse.retry()`'s `errorFields` argument. Reach for it when telling `retry()` which fields to highlight in the sheet. ## How | Member | Notes | | --- | --- | | `error?` | Optional generic error message. | | `payer?` | Keyed by `PaymentContactFieldEnum` — see [api/payment-address-contact-field-enums.md](docs/api/payment-address-contact-field-enums.md). | | `shippingAddress?` | Keyed by `PaymentAddressFieldEnum`. | Uses the same keys as the [Sheet errors](docs/guides/change-events.md#sheet-errors) field-level `PaymentDetailsUpdateError`. ## Example ```ts const errorFields: PaymentValidationErrors = { payer: { [PaymentContactFieldEnum.Email]: 'Please provide a valid email' }, }; await paymentResponse.retry(errorFields); ``` ## Pitfalls An omitted `payer`/`shippingAddress` still fails the attempt but nothing is highlighted in the sheet. See [guides/retry.md](docs/guides/retry.md). ## References - [guides/retry.md](docs/guides/retry.md) --- # FILE: docs/api/payments-config.md # `PaymentsConfig` / `ResolvedPaymentsConfig` / `resolvePaymentsConfig` ## What & why One typed config for the facts the native setup and the JS method data must agree on: the Apple Pay merchant identifier(s), the card networks and the Google Pay environment. The Expo config plugin and `rnw-payments doctor` write it into the native projects, and `resolvePaymentsConfig` hands the same values to the method data at runtime, so changing a merchant identifier in one place updates the entitlement and the request together. ## How Put it in `payments.config.ts`, `payments.config.js` or `payments.config.json` at the app root (looked up in that order; a `.ts` file needs Node 22.18+ to be loaded by the Expo plugin and the doctor): | Field | Type | Default | | ---------------------- | ------------------------------------------------ | ---------------------------------- | | `merchantIdentifier` | `string \| string[]` | _(required)_ | | `supportedNetworks` | `SupportedNetworkEnum[]` or their string values | every `SupportedNetworkEnum` value | | `googlePayEnvironment` | `EnvironmentEnum` or its string value | `EnvironmentEnum.PRODUCTION` | ```ts resolvePaymentsConfig(config: PaymentsConfig): ResolvedPaymentsConfig ``` `resolvePaymentsConfig` drops empty and duplicated merchant identifiers, applies the defaults and throws the same error in every consumer (prebuild, doctor, app) when no merchant identifier is left, the network list is empty, or a network or environment is unknown. `ResolvedPaymentsConfig` carries `merchantIdentifiers` (non-empty tuple), `supportedNetworks` and `googlePayEnvironment`. The Expo plugin uses its inline options when given and falls back to the config file otherwise. ## Example ```ts // payments.config.ts import type { PaymentsConfig } from '@rnw-community/react-native-payments'; export default { merchantIdentifier: 'merchant.com.your-app', supportedNetworks: ['visa', 'masterCard'], googlePayEnvironment: 'TEST', } satisfies PaymentsConfig; ``` ```ts import { createApplePayMethodData, createGooglePayMethodData, resolvePaymentsConfig } from '@rnw-community/react-native-payments'; import paymentsConfig from './payments.config'; const { merchantIdentifiers: [merchantIdentifier], supportedNetworks, googlePayEnvironment, } = resolvePaymentsConfig(paymentsConfig); const applePay = createApplePayMethodData({ merchantIdentifier, supportedNetworks, countryCode: 'US', currencyCode: 'USD' }); const googlePay = createGooglePayMethodData({ environment: googlePayEnvironment, supportedNetworks, currencyCode: 'USD', gatewayConfig: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }, }); ``` ## Pitfalls - Import only types from the package in `payments.config.ts`: the Expo plugin and the doctor load the file in Node, where the package root (which imports `react-native`) cannot run. Use the string values (`'visa'`, `'TEST'`) instead of the enums there. ## References - [platforms/expo.md](docs/platforms/expo.md) — what each field writes into the native projects - [create-apple-pay-method-data.md](docs/api/create-apple-pay-method-data.md), [create-google-pay-method-data.md](docs/api/create-google-pay-method-data.md) - [Apple — Setting up Apple Pay](https://developer.apple.com/documentation/passkit/setting-up-apple-pay) --- # FILE: docs/api/payments-error-enum.md # `PaymentsErrorEnum` ## What & why The W3C `DOMException` **names** this library throws — `PaymentsErrorEnum.AbortError` etc. are the exact strings assigned to `error.name`, not the human-readable message. Reach for it when constructing or comparing against a `DOMException`'s `name`, e.g. in a custom native module shim or a test fixture. ## How | Member | `error.name` value | Meaning | | --- | --- | --- | | `AbortError` | `'AbortError'` | User or code aborted the request. | | `InvalidStateError` | `'InvalidStateError'` | Method called while the request/response is in the wrong state. | | `NotAllowedError` | `'NotAllowedError'` | Not currently reachable from this implementation. | | `NotSupportedError` | `'NotSupportedError'` | No platform-matching payment handler. | | `SecurityError` | `'SecurityError'` | Defined but not currently reachable — no permission-policy check exists in React Native. | Every `DOMException` sets `this.name` to the `PaymentsErrorEnum` member it was constructed with, so `error.name` (not `error.message`) is the stable way to branch on the failure — `error.message` is a separate, human-readable string (e.g. `"The operation was aborted."`) formatted independently of this enum. Native user cancellation (the person dismissing the payment sheet on either platform) is normalized to an `AbortError` `DOMException`, matching the W3C behaviour. ## Example ```ts paymentRequest.show().catch((error: Error) => { if (error.name === 'AbortError') { // the user dismissed the sheet } }); ``` ## Pitfalls Branch on `error.name`, not `error.message` — `PaymentsErrorEnum` members are names, and the message text is an implementation detail that can change without notice. ## References - [guides/errors.md](docs/guides/errors.md) - [api/dom-exception.md](docs/api/dom-exception.md) --- # FILE: docs/api/payments-error.md # `PaymentsError` ## What & why A plain domain error for failures the W3C spec does not name — the catch-all for native bridge and payload-parsing failures. Reach for `instanceof PaymentsError` combined with excluding `DOMException` to catch only this shape (see Pitfalls — neither check alone is unambiguous). ## How | Trigger | | --- | | `show()` rejecting with a non-`Error` reason from the native module bridge (an `Error` reason is propagated as-is instead — see Pitfalls). | | Every `abort()` rejection from the native module bridge, regardless of the rejection reason's type. | | `PaymentResponse.retry()` rejecting for any reason — caught and re-thrown as `new PaymentsError('Failed retrying PaymentRequest')`, regardless of the underlying rejection reason. | | A native payment response payload that fails to parse (malformed or incomplete JSON, including direct construction of `AndroidPaymentResponse`/`IosPaymentResponse` with malformed tokenization data). | ## Example ```ts import { DOMException, PaymentsError } from '@rnw-community/react-native-payments'; try { await paymentRequest.abort(); } catch (error) { if (error instanceof PaymentsError && !(error instanceof DOMException)) { // this package's own catch-all — not a raw native rejection, not a DOMException } } ``` ## Pitfalls - **`instanceof PaymentsError` alone also matches `DOMException`** — `DOMException extends PaymentsError` in this package's error hierarchy, so a bare `instanceof PaymentsError` check catches every spec-mapped abort/invalid-state/not-supported error too, not just the failures documented above. - **`error.name === 'Error'` alone also matches a raw propagated native rejection** — when `show()`'s native bridge rejects with a reason that is already an `Error` instance, that `Error` is propagated **unchanged** (not wrapped in `PaymentsError`), and an ordinary `Error` also has `name === 'Error'` by default without being `instanceof PaymentsError` at all. Neither check alone is unambiguous; use `error instanceof PaymentsError && !(error instanceof DOMException)` together, as in the example. - Not spec-mandated — do not branch on `error.name` the way you would for `DOMException`; `PaymentsError` inherits the default `Error.prototype.name` (`'Error'`) instead of a stable W3C name. ## References - [guides/errors.md](docs/guides/errors.md) --- # FILE: docs/api/supported-network-enum.md # `SupportedNetworkEnum` ## What & why The card networks a `methodData` entry accepts. Reach for it when populating `methodData.data.supportedNetworks` on either platform. ## How Accepts the standard networks (`Visa`, `Mastercard`, `Amex`, …) plus several Apple Pay introduced after the oldest supported iOS version, rejected below their minimum: | Member | Minimum iOS version | | --- | --- | | `Girocard` | iOS 14 | | `Mir` | iOS 14.5 — **deprecated**, see Pitfalls | | `Dankort` | iOS 15.1 | | `Bancontact` | iOS 16 | On Android only the networks Google Pay accepts are sent: `Amex`, `Discover`, `Interac`, `Jcb`, `Mastercard` and `Visa`, plus `Electron`, `Maestro` and `Elo` for Brazilian combo cards. Other members are left out of the Google Pay request, so one cross-platform `supportedNetworks` list works on both platforms. If a Google Pay `methodData` entry contains none of those networks, the constructor throws `ConstructorError`. ## Example ```ts import { SupportedNetworkEnum } from '@rnw-community/react-native-payments'; const supportedNetworks = [SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard]; ``` ## Pitfalls `SupportedNetworkEnum.Mir` is **deprecated**. Apple delisted the network over the sanctions against the issuing banks, so it resolves on iOS 14.5+ and keeps an existing integration building, but no Mir card can be provisioned into Apple Pay anymore. It is kept functional instead of being removed so upgrading does not break a build; do not add it to a new integration. ## References - [platforms/ios.md](docs/platforms/ios.md) --- # FILE: docs/guides/change-events.md # Payment change events While the payment sheet is open the user can change the shipping address, the shipping option, the payment card or a coupon code. `PaymentRequest` models these as W3C change events: register listeners **before** calling `show()` and answer each event with `PaymentRequestUpdateEvent.updateWith()`. > **Platform support.** On iOS the events are delivered by PassKit: the payment sheet waits for the answer of a > listener and is completed with the unchanged details whenever there is no listener for the event type, the > listener fails or the sheet is torn down, so it can never hang. On Android `shippingaddresschange` and > `shippingoptionchange` are delivered by Google Pay's payment data callbacks (`callbackIntents`) with the same > answer guarantees — see [Android](#android) below; `paymentmethodchange` and `couponcodechange` never fire there > because Google Pay has no callback intent for them. On web > the browser's own `PaymentRequest` is used, so change events there follow the browser implementation. A request > without listeners shows the same sheet with the same summary items as before — PassKit now asks the app on > every change and is answered immediately with no change, which is a main thread round trip and no longer a > purely local update. The end-to-end verification on devices is tracked in > [#393](https://github.com/rnw-community/rnw-community/issues/393). > > iOS only shows the shipping method picker and the coupon code field (iOS 15+) when a `shippingoptionchange` / > `couponcodechange` listener is registered before `show()` — `details.shippingOptions` are passed to PassKit in > that case. Android follows the same opt-in: the Google Pay request only declares a callback intent, and only > shows the shipping option picker, for the shipping listeners registered before `show()`. ## Android `show()` adds Google Pay `callbackIntents` to the `PaymentDataRequest` for the listeners registered at that moment, provided `requestShipping` is set in the Android method data (Google Pay only allows the shipping intents together with `shippingAddressRequired`): | Registered listener | Request additions | | --- | --- | | `shippingaddresschange` | `SHIPPING_ADDRESS` intent | | `shippingoptionchange` with non-empty `details.shippingOptions` | `SHIPPING_OPTION` intent, `shippingOptionRequired`, `shippingOptionParameters` | | either of the above | the mandatory `PAYMENT_AUTHORIZATION` intent, `transactionInfo.displayItems` from `details.displayItems` | Google Pay requires every callback `newTransactionInfo` to repeat the `countryCode` of the original request, so a request with regular `transactionInfo` and no `countryCode` in its Android method data declares no callback intents at all — set `countryCode` to receive Android change events. A merchant-initiated request (`deferredTransactionInfo`, `recurringTransactionInfo` or `automaticReloadTransactionInfo`) already requires `countryCode`. The package ships a `PaymentsCallbacksService` (`BasePaymentDataCallbacksService`) in its library manifest, merged into the app by autolinking and by Expo prebuild alike, so there is nothing to declare by hand. Google Pay calls it with `onPaymentDataChanged` when the sheet opens and on every address or option change. The opening `INITIALIZE` callback is delivered as `shippingaddresschange` for the default address when that listener is registered, and as `shippingoptionchange` when only the option listener is — so do not expect an initial address event without a `shippingaddresschange` listener. The module emits the event, and the `updateWith` answer becomes a `PaymentDataRequestUpdate`: | `updateWith` member | `PaymentDataRequestUpdate` | | --- | --- | | `total` | `newTransactionInfo.totalPrice` / `totalPriceLabel` (currency, country and price status of the request kept) — regular `transactionInfo` requests only | | `displayItems` | `newTransactionInfo.displayItems` (`LINE_ITEM`, `PENDING` for a `pending` item) — regular `transactionInfo` requests only | | `shippingOptions` (address change only) | `newShippingOptionParameters` — `label`, `detail` as `description`, the last `selected` option (else the option the sheet shows, else the first) as `defaultSelectedOptionId` | | `error` | `error` with the event's `intent` — see [Sheet errors](#sheet-errors) | A merchant-initiated request carries no `transactionInfo` and Google Pay does not support dynamic price updates for it, so `total` and `displayItems` of an answer are not sent there — only the shipping options and the error reach the sheet. Like on iOS, an option change answers totals only, so the picker never snaps back to another option. Google Pay renders a shipping option from its `label` and `description` only — put the price in one of them. The address of a change event is redacted by Google Pay to `locality`, `administrativeArea`, `postalCode` and `countryCode`. `onPaymentAuthorized` is answered with `SUCCESS` right away, so the payment still resolves `show()` as before. React Native pauses JS timers while the app's activity sits behind another activity, so while a callback-enabled sheet is open the module keeps a headless JS task (`RNPaymentsChangeEvents`, registered by `show()` itself) running, which keeps `setTimeout`-based listeners and the change-event timeout working exactly like on iOS. Google Pay fails the sheet with `CALLBACK_TIMED_OUT` when a callback is not answered within 30 seconds, so the module also answers a still pending event with the unchanged details after 25 seconds on its own and rejects the late JS answer with `no_completion` — on Android the effective listener budget is therefore 25 seconds, not 30. ## `PaymentRequest.addEventListener(type, listener)` Registers the listener for one of `shippingaddresschange`, `shippingoptionchange`, `paymentmethodchange` or `couponcodechange` (the last one is a PassKit extension, not part of the W3C specification). Several listeners can be registered for the same event type — they run in registration order and the same function is never registered twice. Dispatch stops at the first listener that answers with `updateWith`, exactly like the stop immediate propagation flag of the W3C algorithm. One native subscription is kept per event type no matter how many listeners are added and removed, and events are scoped to the request they belong to, so concurrent `PaymentRequest` instances never see each other's events. Listeners are released when `show()` settles and when `abort()` resolves; registering on a closed request does nothing because a request is single-use — create a new `PaymentRequest` to show the sheet again. ```ts paymentRequest.addEventListener('shippingaddresschange', event => { event.updateWith({ total: { label: 'Total', amount: { currency: 'USD', value: '25.00' } }, displayItems: [{ label: 'Shipping', amount: { currency: 'USD', value: '5.00' } }], }); }); ``` ## `PaymentRequest.removeEventListener(type, listener)` Removes the passed listener from the event type, matching the `EventTarget` signature. The native subscription is released once the last listener of the type is gone, and native is told about the remaining event types right away — also while the payment sheet is open. ```ts paymentRequest.removeEventListener('shippingaddresschange', onShippingAddressChange); ``` ## Event-handler attributes `onshippingaddresschange`, `onshippingoptionchange`, `onpaymentmethodchange`, `oncouponcodechange` are thin property alternatives to `addEventListener`/`removeEventListener`, one per event type, matching the `EventTarget` IDL attribute semantics: assigning a function **replaces** the previously assigned attribute handler (an implicit `removeEventListener` of the old one followed by `addEventListener` of the new one); assigning `null` clears it without registering a new listener; reading the property returns the currently assigned handler, or `null` when none was set. The attribute handler is otherwise an ordinary listener — it coexists with every listener registered through `addEventListener` for the same type and runs in the order it was (re-)registered. ```ts paymentRequest.onshippingaddresschange = event => { event.updateWith({ total: { label: 'Total', amount: { currency: 'USD', value: '25.00' } }, displayItems: [{ label: 'Shipping', amount: { currency: 'USD', value: '5.00' } }], }); }; paymentRequest.onshippingaddresschange = null; // clears it ``` ## `PaymentRequestUpdateEvent.updateWith(detailsOrPromise)` Answers the event with updated `PaymentDetailsUpdate` — `total`, `displayItems`, `shippingOptions` and `error` are all optional and only the provided members replace the current details. It accepts a promise, so a listener can await a server call before answering: ```ts paymentRequest.addEventListener('shippingoptionchange', async event => { const quote = await fetch(`https://example.com/quote?option=${paymentRequest.shippingOption}`).then(response => response.json() ); event.updateWith({ total: { label: 'Total', amount: { currency: 'USD', value: quote.total } }, shippingOptions: [{ id: 'express', label: 'Express', amount: { currency: 'USD', value: quote.shipping } }], }); }); ``` Every `PaymentShippingOption` needs an `id`, a `label` and an `amount`, because iOS renders the row from the label and the amount and reports the selection back by the id. `detail` is optional and is shown by Apple Pay as the secondary line of the row (`PKShippingMethod.detail`); `amount.currency` is ignored because the sheet is already bound to the `currencyCode` of the method data. The initial `details.shippingOptions` and the ones answered with `updateWith` go through the same conversion, so the same option always renders the same row. > `selected` is part of the W3C dictionary but is **silently ignored on iOS**: PassKit has no preselection > support and always shows its shipping-method picker with the first option of the array highlighted. Put the > option you want preselected first in `shippingOptions` instead of relying on `selected`. ```ts const shippingOptions = [ { id: 'express', label: 'Express', detail: 'Next business day', amount: { currency: 'USD', value: '5.00' } }, { id: 'ground', label: 'Ground', detail: '3-5 business days', amount: { currency: 'USD', value: '0.00' } }, ]; ``` Calling `updateWith` twice, or calling it once the event was already answered or the request is no longer showing, throws a `DOMException` with `InvalidStateError`. A listener that throws, rejects, sends invalid details, never calls `updateWith` or leaves its promise pending for more than 30 seconds is logged and answered with the unchanged details, so the payment sheet never stalls. Updated details go through the same validation as the ones passed to the constructor — the total, the display items and the shipping options all have to carry a valid decimal monetary value, and a shipping option also has to carry an id and a label — so a malformed amount is reported to the console and never reaches the sheet. ## Sheet errors `error` is either a plain string or a field-level error that Apple Pay renders inline, next to the offending row of the sheet, instead of as a generic banner. A string keeps the previous behaviour: an unserviceable shipping address for `shippingaddresschange`, an invalid coupon code for `couponcodechange` (iOS 15+) and a generic payment error everywhere else. `shippingoptionchange` has no error slot in PassKit, so an error answered there is ignored. `error` has three possible shapes, all discriminated by `type`: a `shippingAddressField` or `contactField` error carries the offending field (`key`/`field`) and the message shown to the user; a `couponCode` error carries no field — instead an optional `expired` flag and the message: ```ts import { PaymentAddressFieldEnum, PaymentContactFieldEnum, PaymentUpdateErrorTypeEnum, } from '@rnw-community/react-native-payments'; paymentRequest.addEventListener('shippingaddresschange', event => { event.updateWith({ error: { type: PaymentUpdateErrorTypeEnum.ShippingAddressField, key: PaymentAddressFieldEnum.PostalCode, message: 'We do not ship to this postal code', }, }); }); paymentRequest.addEventListener('couponcodechange', event => { event.updateWith({ error: { type: PaymentUpdateErrorTypeEnum.CouponCode, expired: true, message: 'SALE10 expired last week' }, }); }); ``` | `error.type` | Additional member | iOS `PKPaymentErrorDomain` error | | --- | --- | --- | | `shippingAddressField` | `key: PaymentAddressFieldEnum` | `paymentShippingAddressInvalidErrorWithKey:` | | `contactField` | `field: PaymentContactFieldEnum` | `paymentContactInvalidErrorWithContactField:` | | `couponCode` | `expired?: boolean` | `paymentCouponCodeInvalidError` / `paymentCouponCodeExpiredError` (iOS 15+) | `PaymentAddressFieldEnum` maps onto the `CNPostalAddress` keys PassKit accepts: `addressLine` (street), `city`, `country` (ISO country code), `dependentLocality` (sub-locality), `postalCode`, `region` (state) and `subAdministrativeArea`. `PaymentContactFieldEnum` maps onto `PKContactField`: `email`, `name`, `phone` and `postalAddress`. An unknown field, an empty message or a coupon error below iOS 15 is dropped and the sheet is answered with the updated details only. On Android the error is rendered by Google Pay as a `PaymentDataError` whose `intent` is the one of the answered event: a string becomes `SHIPPING_ADDRESS_UNSERVICEABLE` for `shippingaddresschange` and `SHIPPING_OPTION_INVALID` for `shippingoptionchange` (unlike iOS, an option error is shown), a `shippingAddressField` error becomes `SHIPPING_ADDRESS_INVALID` and every other field error `OTHER_ERROR`, with the message shown in the sheet. ## `PaymentRequestUpdateEvent.isAnswered` `true` once `updateWith` was called for the event. A listener built from several helpers can check it before answering a second time: ```ts paymentRequest.addEventListener('shippingoptionchange', event => { applyExpressSurcharge(event); if (!event.isAnswered) { event.updateWith({ total: { label: 'Total', amount: { currency: 'USD', value: '25.00' } } }); } }); ``` ## `PaymentMethodChangeEvent` The event delivered for `paymentmethodchange` extends `PaymentRequestUpdateEvent` with the selected method: ```ts paymentRequest.addEventListener('paymentmethodchange', event => { if (event.methodDetails?.['network'] === 'Amex') { event.updateWith({ error: 'Amex is not supported for this order' }); } }); ``` ## Changed values on the request Before a listener runs, the changed value is stored on the request: `paymentRequest.shippingAddress` (`PaymentResponseAddressInterface`), `paymentRequest.shippingOption` (the selected `PaymentShippingOption` id) and `paymentRequest.couponCode`. On iOS the shipping address of a change event is **redacted** by PassKit: only `address2` (city), `address3` (state), `postalCode` and `countryCode` are filled, while the street and the payer name, email and phone stay empty until the payment is authorized — quote shipping from the postal code and the country, never from the street. `paymentRequest.updating` is `true` while an event is being processed; a change event that arrives during that window is answered with the unchanged details and is not dispatched to the listeners, but its selection is still stored on the request, so these values always describe what the sheet shows right now. ## References - [`PaymentRequestUpdateEvent`](https://www.w3.org/TR/payment-request/#dom-paymentrequestupdateevent) - [`PaymentMethodChangeEvent`](https://www.w3.org/TR/payment-request/#dom-paymentmethodchangeevent) - [api/payment-request-update-event.md](docs/api/payment-request-update-event.md) - [api/events-types.md](docs/api/events-types.md) - [Google Pay dynamic price updates](https://developers.google.com/pay/api/android/guides/tutorial#enable-callbacks) - [Google Pay `PaymentDataRequestUpdate`](https://developers.google.com/pay/api/android/reference/request-objects#PaymentDataRequestUpdate) --- # FILE: docs/guides/modifiers.md # Payment details modifiers `details.modifiers` accepts an array of [`PaymentDetailsModifier`](https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier), one entry per `supportedMethods`. The library picks the entry whose `supportedMethods` matches the platform's active payment method (`PaymentMethodNameEnum.ApplePay` on iOS, `PaymentMethodNameEnum.AndroidPay` on Android) and applies it before serializing details to native: `modifier.total` overrides the top-level `total` and `modifier.additionalDisplayItems` is appended to `displayItems`. A modifier for the other platform's method is ignored. The same resolution runs again on every `updateWith()` call, so a listener can ship an updated `modifiers` array together with the rest of the update. `modifier.data` is not validated (`validateModifiers()` only checks `supportedMethods`, `total` and `additionalDisplayItems`) and is not forwarded to native — the bridge has no per-method extension point for it. ```ts const paymentDetails = { total: { label: 'Total', amount: { currency: 'USD', value: '10.00' } }, modifiers: [ { supportedMethods: PaymentMethodNameEnum.ApplePay, total: { label: 'Total with Apple Pay discount', amount: { currency: 'USD', value: '9.00' } }, additionalDisplayItems: [{ label: 'Apple Pay discount', amount: { currency: 'USD', value: '-1.00' } }], }, ], }; ``` ## References - [`PaymentDetailsModifier`](https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier) - [api/payment-details-modifier.md](docs/api/payment-details-modifier.md) --- # FILE: docs/guides/errors.md # Error handling Every throw/reject in this package maps to one of three W3C-facing error shapes, or a plain `Error` for the one non-spec build/config failure (the native module not being linked at all — see the table below): - **`ConstructorError`** — a native `TypeError` (`instanceof TypeError`, `name === 'TypeError'`) for `new PaymentRequest(...)` validation failures: missing/invalid payment methods, total, display items or shipping options. This matches the W3C algorithm, which validates the constructor's dictionaries via WebIDL and `check and canonicalize (total) amount`, both of which throw `TypeError`. - **`DOMException`** (`instanceof DOMException`, `error.name` is the W3C name) — for the spec-mandated runtime states: `AbortError`, `InvalidStateError`, `NotAllowedError`, `NotSupportedError`. `SecurityError` is defined but not currently reachable from this implementation (no permission-policy check exists in React Native). - **`PaymentsError`** — a plain domain error for failures the W3C spec does not name: `show()` rejecting with a non-`Error` reason from the native module bridge (an `Error` reason is propagated **as-is** instead — see Pitfalls), every `abort()` rejection from the native module bridge regardless of the rejection reason's type, and a native payment response payload that fails to parse (malformed or syntactically valid but incomplete JSON from the platform SDK, including direct construction of `AndroidPaymentResponse`/`IosPaymentResponse` with malformed tokenization data). ```ts import { DOMException, PaymentsError } from '@rnw-community/react-native-payments'; try { await paymentRequest.show(); } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { // user cancelled } else if (error instanceof PaymentsError && !(error instanceof DOMException)) { // this package's own catch-all — see Pitfalls below } } ``` | Public API failure | Spec-mandated error | Implemented as | | --- | --- | --- | | `new PaymentRequest()` with no/invalid payment methods | `TypeError` | `ConstructorError` (`instanceof TypeError`) | | `new PaymentRequest()` with missing/invalid/negative total | `TypeError` | `ConstructorError` | | `new PaymentRequest()` with invalid display items | `TypeError` | `ConstructorError` | | `new PaymentRequest()` with invalid shipping options | `TypeError` | `ConstructorError` | | `new PaymentRequest()` with no platform-matching payment method | `NotSupportedError` | `DOMException` (thrown at construction, see [architecture.md](docs/architecture.md)) | | `canMakePayment()` when not `created` | `InvalidStateError` | `DOMException` | | `show()` when not `created` | `InvalidStateError` | `DOMException` | | `show()` after the user cancels the native sheet | `AbortError` | `DOMException` | | `abort()` when not `interactive` | `InvalidStateError` | `DOMException` | | `abort()` resolves a pending `show()` | `AbortError` | `DOMException` | | `PaymentRequestUpdateEvent.updateWith()` called twice for one event | `InvalidStateError` | `DOMException` | | `PaymentResponse.complete()` / `retry()` called after `complete()` | `InvalidStateError` | `DOMException` | | `PaymentResponse.complete()` called after `retry()` | `InvalidStateError` | `DOMException` (see [retry.md](docs/guides/retry.md)) | | `PaymentResponse.retry()` called a second time on the same response | `InvalidStateError` | `DOMException` (see [retry.md](docs/guides/retry.md)) | | `PaymentResponse.retry()` on a native binary built before this method existed | `NotSupportedError` | `DOMException` | | Native module bridge rejects `show()` with a non-`Error` reason | _(not specified)_ | `PaymentsError` | | Native module bridge rejects `abort()` (any reason) | _(not specified)_ | `PaymentsError` | | Native module bridge rejects `retry()` (any reason) | _(not specified)_ | `PaymentsError` | | Native payment response payload is malformed or incomplete JSON (incl. direct `AndroidPaymentResponse`/`IosPaymentResponse` construction) | _(not specified)_ | `PaymentsError` | | An `updateWith()` listener answers with an invalid total/items/options | _(not specified — spec treats this as no update)_ | Logged via `console.warn`, change event answered with unchanged details | | Native module is not linked (`Payments` bridge missing) | _(not specified — build/config error)_ | `Error` | ## Pitfalls **Neither `error.name === 'Error'` nor `instanceof PaymentsError` alone uniquely identifies `PaymentsError`.** `DOMException extends PaymentsError` (see [api/payments-error.md](docs/api/payments-error.md)), so `instanceof PaymentsError` also matches every `DOMException`. Conversely, `show()`'s non-`Error`-reason path propagates an already-`Error` native rejection **unchanged**, and that propagated `Error` also has `name === 'Error'` by default without being `instanceof PaymentsError` at all. Use `error instanceof PaymentsError && !(error instanceof DOMException)` to catch only this package's own catch-all, as distinct from a raw `Error` propagated from the native bridge. ## References - [api/constructor-error.md](docs/api/constructor-error.md) - [api/dom-exception.md](docs/api/dom-exception.md) - [api/payments-error.md](docs/api/payments-error.md) - [api/payments-error-enum.md](docs/api/payments-error-enum.md) --- # FILE: docs/guides/retry.md # Retrying the payment `PaymentResponse.retry(errorFields?)` asks the user to correct one or more invalid fields instead of completing the payment. Call it **instead of** `complete()`, before the sheet has been dismissed: ```ts import { PaymentAddressFieldEnum, PaymentComplete, PaymentContactFieldEnum } from '@rnw-community/react-native-payments'; const validation = await validatePaymentOnBackend(paymentResponse); if (!validation.ok) { await paymentResponse.retry({ error: 'We could not process your payment', payer: { [PaymentContactFieldEnum.Email]: 'Please provide a valid email' }, shippingAddress: { [PaymentAddressFieldEnum.PostalCode]: 'We do not ship to this postal code' }, }); } else { await paymentResponse.complete(PaymentComplete.SUCCESS); } ``` `retry()` throws a `DOMException` with `InvalidStateError` if `complete()` was already called on this response, or if `retry()` was already called once — this package supports **at most one** `retry()` call per `PaymentResponse` (see [Known deviations](#known-deviations)). `errorFields` is optional; an omitted `payer`/`shippingAddress` still fails the attempt but nothing is highlighted in the sheet. Calling `complete()` **after** `retry()` also throws `InvalidStateError` instead of reaching native — `complete()` unconditionally dismisses the sheet, which would silently cancel the correction opportunity `retry()` just opened. > **iOS**: `retry()` reuses the same `PKPaymentErrorDomain` field-error constructors as > [Sheet errors](docs/guides/change-events.md#sheet-errors) — `payer` keys are `PaymentContactFieldEnum`, `shippingAddress` > keys are `PaymentAddressFieldEnum` — to fail the pending authorization with those errors instead of dismissing > the sheet, so PassKit highlights the offending rows and lets the user correct and resubmit. This package > cannot route a second submission back to `show()`'s already-settled promise (see > [PaymentRequest is single-use](docs/architecture.md)), so if the user resubmits, the sheet is failed and > dismissed automatically; `retry()` itself only resolves once the errors have been handed to the sheet, not once > the user has finished correcting them. **Android**: `retry()` is a documented no-op — it resolves like the > spec's return type without re-displaying the Google Pay sheet, matching the existing `complete()`/`abort()` > no-op boundary on Android. ## Starting a new attempt `retry()` only corrects fields inside the sheet that is still open. Once a request has settled — the user cancelled, `complete(PaymentComplete.FAIL)` was called, or `show()` rejected — it is `closed` for good, and the next attempt needs a new `PaymentRequest`. [`createPaymentRequestFactory`](docs/api/create-payment-request-factory.md) keeps the shared configuration in one place, so each attempt only passes its own details: ```ts import { PaymentComplete, createPaymentRequestFactory } from '@rnw-community/react-native-payments'; import type { PaymentItem } from '@rnw-community/react-native-payments'; const createPaymentRequest = createPaymentRequestFactory({ methodData }); const pay = async (total: PaymentItem): Promise => { const paymentResponse = await createPaymentRequest({ total }).show(); const isConfirmed = await sendToYourBackend(paymentResponse.details); await paymentResponse.complete(isConfirmed ? PaymentComplete.SUCCESS : PaymentComplete.FAIL); return isConfirmed; }; ``` Calling `pay(total)` again after it returns `false` or rejects builds a new request. There is no state to reset, and listeners are registered on the request that `createPaymentRequest` returns. ## Known deviations - **`PaymentResponse.retry()` supports at most one in-sheet correction pass, and only on iOS.** The spec algorithm re-presents the sheet and lets the user submit a corrected response an arbitrary number of times, handing back the same `PaymentResponse` updated in place. This package's `PaymentRequest` is single-use (see [architecture.md](docs/architecture.md)) and its native bridge resolves the `show()` promise exactly once per authorization, so there is no channel left to deliver a second submission to JavaScript. `retry()` therefore only feeds `errorFields` into the still-open native sheet through the existing `PKPaymentErrorDomain` field-error path (see [Sheet errors](docs/guides/change-events.md#sheet-errors)) for the _current_ pending authorization, then resolves — it does not wait for, or expose, whatever the user does next. If PassKit fires a second authorization after that (the user corrected the fields and resubmitted), this package fails and dismisses it automatically instead of silently hanging, and that resubmission is lost — a real re-presentation would need a new native show-path (a second, JS-observable authorization channel) that is out of scope here. On Android, `retry()` is a documented no-op: it resolves without any visual effect, consistent with `complete()` and `abort()`'s existing no-op boundary on Android (Google Pay's sheet is a separate activity with no in-sheet update mechanism at all). ## References - [api/payment-response.md](docs/api/payment-response.md) - [api/payment-validation-errors.md](docs/api/payment-validation-errors.md) - [api/create-payment-request-factory.md](docs/api/create-payment-request-factory.md) --- # FILE: docs/guides/testing.md # Unit testing Without a linked native binary, `NativePayments` (`src/class/native-payments/native-payments.ts`) falls back to a `Proxy` that throws `The package 'react-native-payments' doesn't seem to be linked` for any required method call (`show`, `abort`, `complete`, `canMakePayment`, `hasEnrolledInstrument`) — see [architecture.md](docs/architecture.md). Mock `react-native`'s `NativeModules.Payments` so the module resolves to your own fakes instead of falling through to that proxy: ```ts import { jest } from '@jest/globals'; jest.mock('react-native', () => ({ NativeModules: { Payments: { show: jest.fn(() => Promise.resolve(JSON.stringify({ token: {} }))), abort: jest.fn(() => Promise.resolve()), canMakePayments: jest.fn(() => Promise.resolve(true)), hasEnrolledInstrument: jest.fn(() => Promise.resolve(true)), complete: jest.fn(() => Promise.resolve()), retry: jest.fn(() => Promise.resolve()), setActiveEvents: jest.fn(() => Promise.resolve()), updatePaymentDetails: jest.fn(() => Promise.resolve()), addListener: jest.fn(), removeListeners: jest.fn(), }, }, Platform: { OS: 'ios', select: (specifics: { default: string }) => specifics.default }, TurboModuleRegistry: { get: () => null }, NativeEventEmitter: class { addListener() { return { remove: () => undefined }; } removeAllListeners() { return undefined; } }, })); ``` This works regardless of architecture: `NativePayments` only reaches for the TurboModule path (`TurboModuleRegistry.get('Payments')`, plural) when `global.__turboModuleProxy` is set, which a Jest run normally never sets, so it resolves `NativeModules.Payments` instead — mock that key directly rather than `TurboModuleRegistry.getEnforcing`, which this package never calls. All ten `Spec` members from `NativePayments.ts` are stubbed, so `retry()` and event registration do not silently no-op in a test that exercises them. The `NativeEventEmitter` stub is required as soon as `addListener`/`removeListeners` are present: `getNativePaymentsEventEmitter()` (`src/util/get-native-payments-event-emitter/`) constructs a real `new NativeEventEmitter(...)` the moment those two methods are defined, and a fully-replaced `react-native` module has no real `NativeEventEmitter` export to construct — omitting the stub throws `NativeEventEmitter is not a constructor` from `PaymentRequest.addEventListener(...)`. If Jest fails with `The package 'react-native-payments' doesn't seem to be linked`, this mock is the fix — see [#227](https://github.com/rnw-community/rnw-community/issues/227) and [troubleshooting.md](docs/guides/troubleshooting.md). `show`'s fake resolves to `{ token: {} }` rather than `'{}'` because `PaymentRequest.show()` parses the resolved string straight into `IosPaymentResponse`/`AndroidPaymentResponse` — `'{}'` has no `token` (iOS) or `paymentMethodData` (Android) key and rejects with `PaymentsError: Failed parsing PaymentRequest details`. Replace the fake per test with a payload shaped like `IosPKPayment` (iOS) or `AndroidPaymentData` (Android) once the assertions need real token/address fields. ## Example apps - **Expo** — the `App` component of the [react-native-payments-example](../react-native-payments-example/readme.md) package, running through its `apps/expo` target. - **Bare React Native CLI** — the same `App` component, running through the [react-native-payments-example](../react-native-payments-example/readme.md) package's `apps/bare` target. ## End-to-end verification Unit tests cover the JS layer at 100%; on-device verification of the event API (sheet opens, shipping/coupon change round-trip, async `updateWith` completion) runs locally through the Maestro flow suite in [react-native-payments-example/e2e/readme.md](../react-native-payments-example/e2e/readme.md), but is not yet wired into CI — tracked in [#395](https://github.com/rnw-community/rnw-community/issues/395). --- # FILE: docs/guides/setup-doctor.md # Setup doctor `rnw-payments doctor` checks a bare React Native app's native projects against its [`payments.config`](docs/api/payments-config.md) and `--fix` patches them. It applies the Expo config plugin's own mutations to the files on disk, so a bare app and an `expo prebuild` app end up with the same native setup. ```bash npx rnw-payments doctor # report, exit code 1 when a file does not match npx rnw-payments doctor --fix # report, then write the files ``` Run it from the app root, next to `payments.config.ts|js|json` and the `ios/` / `android/` folders (a missing platform folder is skipped). Exit code `0` means every file matches, so it can gate a CI pipeline. | Platform | File | What it must contain | | -------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | iOS | the entitlements file of `CODE_SIGN_ENTITLEMENTS` | every merchant identifier in `com.apple.developer.in-app-payments` | | Android | `android/app/src/main/AndroidManifest.xml` | the `com.google.android.gms.wallet.api.enabled`, `…wallet.api.environment` and `com.rnw-community.react-native-payments.supported-networks` meta-data | Every mismatch prints the file and the lines to remove (`-`) and add (`+`): ```text android/app/src/main/AndroidManifest.xml + ``` `--fix` behaves like `expo prebuild`: when the iOS target has no entitlements file yet, it creates `ios//.entitlements` and points `CODE_SIGN_ENTITLEMENTS` at it, and it rewrites a mismatching `AndroidManifest.xml` in Expo's formatting. Review the diff and commit it. The merchant ID still has to be registered for your team in the Apple developer portal (see [Platforms — iOS](docs/platforms/ios.md)). Loading a `payments.config.ts` needs Node 22.18+ (built-in type stripping); use `.js` or `.json` on older Node. --- # FILE: docs/guides/troubleshooting.md # Troubleshooting Every entry below traces back to a real report against this package or its native tooling. **`pod install` / Gradle fails right after adding the package.** Autolinking (RN 0.60+) should pick this module up without a manual link step. A stale `Podfile.lock`/`Pods` directory or an old Node version behind the CocoaPods autolinking script are the most common causes of a cryptic `Podfile` parse error — matching the Node version pinned in [`.nvmrc`](.nvmrc) resolved it in [#163](https://github.com/rnw-community/rnw-community/issues/163). Delete `ios/Pods`, `ios/Podfile.lock` and re-run `pod install` before assuming the package itself is at fault. **`UnsupportedTypeAnnotationParserError: TypeScript type annotation 'TSObjectKeyword' is unsupported in NativeModule specs` during Gradle/Pod codegen, or Gradle's `PaymentsModule is not abstract and does not override abstract method show(...)`.** Both are react-native-codegen/TurboModule spec mismatches between the installed package version and the installed `react-native` version — see [#238](https://github.com/rnw-community/rnw-community/issues/238) and [#174](https://github.com/rnw-community/rnw-community/issues/174). Upgrade `@rnw-community/react-native-payments` and `react-native` together rather than pinning one against a much newer or older other; a leftover generated `PaymentsSpec.java`/`Payments.mm` codegen artifact from before the bump is a common secondary cause — see the Metro vs native rebuild entry below. **Apple Pay sheet never appears, or fails with a merchant/entitlement error.** The `merchantIdentifier` passed to `methodData.data` must exactly match a merchant ID declared in the app's `com.apple.developer.in-app-payments` entitlement. For a bare RN app, add it in Xcode's Signing & Capabilities; for Expo, use the `merchantIdentifier` config plugin option — single string or array — documented in [platforms/expo.md](docs/platforms/expo.md) and re-run `expo prebuild --clean` after changing it. If the sheet still won't open on a physical device while running from Metro in `DEV`, first check `merchantCapabilities`/`supportedNetworks` are set explicitly on `methodData.data` — a worked configuration that resolved this for other users is in [#234](https://github.com/rnw-community/rnw-community/issues/234). **Apple Pay works in the simulator but the token is unusable, or the reverse.** The simulator renders the real PassKit sheet UI, but `paymentResponse.details.applePayToken` from it is not valid production payment data — Apple Pay must be verified end-to-end on a physical device signed into an Apple ID with at least one provisioned card before shipping. Conversely, a device-only failure usually means the merchant ID/capabilities/entitlement above, not the JS integration. **Metro reload doesn't pick up a fix, or the app red-screens / white-screens after bumping `react-native`.** A JS-only Metro reload never re-runs native linking or codegen — after bumping `react-native`, this package's major version (see [migrate-from-v2.md](docs/guides/migrate-from-v2.md)), or Xcode/Gradle toolchain versions, do a full native rebuild rather than a Fast Refresh: delete `ios/Pods`, `ios/build`, `android/build`, `android/.cxx`, then `pod install` and rebuild from Xcode/Gradle. A patch-version-only `react-native` bump that broke both platforms with no JS-visible error, as reported in [#185](https://github.com/rnw-community/rnw-community/issues/185), is exactly this class of stale-native-artifact failure, not a JS regression. **New Architecture / bridgeless.** The package ships one TurboModule `Spec` consumed identically on the old architecture, the New Architecture and bridgeless — `getNativePaymentsEventEmitter()` resolves through the same module handle on all three, so switching `newArchEnabled` does not by itself require touching this package's integration (see [architecture.md](docs/architecture.md)). This `null`-when-unimplemented guard only covers the *optional* change-event contract (`setActiveEvents`/`updatePaymentDetails`/`addListener`/`removeListeners`) degrading to the v2 no-events flow — it does **not** cover the required `show(requestId, methodData, details)` call: a `v3` JS bundle still needs a rebuilt `v3` native binary, exactly as [migrate-from-v2.md](docs/guides/migrate-from-v2.md) describes, regardless of architecture. A build error mentioning `TSObjectKeyword` or an abstract-method mismatch when New Architecture is enabled is the codegen version-skew issue above, not a New Architecture incompatibility. **Jest: `The package 'react-native-payments' doesn't seem to be linked` (originally reported as `TurboModuleRegistry.getEnforcing(...): 'Payments' could not be found`).** Covered in [testing.md](docs/guides/testing.md) — mock `NativeModules.Payments` so the module resolves to a fake instead of falling through to the linking-error proxy ([#227](https://github.com/rnw-community/rnw-community/issues/227)). --- # FILE: docs/guides/gateways/index.md # Gateway recipes Copy-paste integrations for the payment service providers (PSPs) that charge the Apple Pay and Google Pay tokens this package returns. Each recipe goes from the sheet to a charged token: the method data the PSP expects, the token to read off the `PaymentResponse`, the server call that charges it, and a sandbox checklist. | PSP | Google Pay tokenization | Apple Pay token handoff | Recipe | | --- | --- | --- | --- | | Stripe | `PAYMENT_GATEWAY`, `gateway: 'stripe'` | Stripe-issued certificate, `paymentData` exchanged for a Stripe token | [stripe.md](docs/guides/gateways/stripe.md) | | Adyen | `PAYMENT_GATEWAY`, `gateway: 'adyen'` | Your own certificate, base64 `paymentData` in `/payments` | [adyen.md](docs/guides/gateways/adyen.md) | | Braintree | `PAYMENT_GATEWAY`, `gateway: 'braintree'` | Braintree-issued certificate, `paymentData` exchanged for a nonce | [braintree.md](docs/guides/gateways/braintree.md) | | Checkout.com | `PAYMENT_GATEWAY`, `gateway: 'checkoutltd'` | Checkout.com-issued certificate, `paymentData` exchanged for a token | [checkout-com.md](docs/guides/gateways/checkout-com.md) | | None (self-decrypt) | `DIRECT`, your own public key | Your own certificate, decrypted on your server | [direct.md](docs/guides/gateways/direct.md) | ## How to read a recipe Every PSP-specific value in a recipe sits in a table with the public doc page it was checked against and the date of that check. PSPs rename parameters and bump API versions, so re-check the linked page when a recipe's verification date is old. Where a PSP documents a step only through its own native SDK, the recipe says so and names the SDK source file the shape was taken from. ## The flow every recipe shares 1. Build `methodData` with [`createApplePayMethodData`](docs/api/create-apple-pay-method-data.md) and [`createGooglePayMethodData`](docs/api/create-google-pay-method-data.md), passing the PSP values from the recipe. `createGooglePayMethodData` defaults to `EnvironmentEnum.TEST`, which is what every sandbox checklist below expects. 2. `show()` the sheet and read the token off the response: - **iOS** — `response.details.applePayToken` ([`IosPKToken`](docs/api/ios-pk-token.md)): `paymentData` is the still-encrypted Apple Pay payment token, already parsed into `{ data, header, signature, version }`; `paymentMethod` and `transactionIdentifier` describe the card and the transaction. - **Android** — `response.details.androidPayToken.rawToken` ([`AndroidPaymentMethodToken`](docs/api/android-payment-method-token.md)): the untouched `tokenizationData.token` string Google Pay returned. Forward this string — whether it is Google's encrypted envelope or a token the gateway issued depends on the gateway. 3. Send the token to your backend over your own API. Never call a PSP endpoint that needs a secret key from the app. 4. Your backend charges the token through the PSP, and the app calls `complete()` with the outcome. ```ts import { AndroidPaymentResponse, IosPaymentResponse, PaymentComplete, PaymentRequest, } from '@rnw-community/react-native-payments'; const response = await new PaymentRequest(methodData, details).show(); if (response instanceof IosPaymentResponse) { const { paymentData, paymentMethod, transactionIdentifier } = response.details.applePayToken; await yourBackend.charge({ wallet: 'apple-pay', paymentData: JSON.stringify(paymentData), paymentMethod, transactionIdentifier, }); } else if (response instanceof AndroidPaymentResponse) { await yourBackend.charge({ wallet: 'google-pay', token: response.details.androidPayToken.rawToken }); } await response.complete(PaymentComplete.SUCCESS); ``` `yourBackend.charge` stands for your own API call; each recipe lists what its payload carries and what the backend does with it. Call `complete(PaymentComplete.FAIL)` instead when the backend reports a declined or failed charge — see [guides/errors.md](docs/guides/errors.md). ## Pitfalls - `JSON.stringify(paymentData)` re-serializes the parsed Apple Pay payload; the PSP parses it as JSON, it never needs the original bytes. Each recipe says whether the PSP wants that JSON as-is or base64-encoded. - On the iOS simulator PassKit returns an empty `paymentData`, so every field of `applePayToken.paymentData` is an empty string. Exercise a recipe's iOS path on a device. - The Google Pay `example` gateway in the quickstarts is accepted only in `TEST` and returns a token no PSP can charge (`rawToken` is `examplePaymentMethodToken`). Swap in the recipe's gateway before testing against the PSP. ## References - [Google Pay API — gateway tokenization and participating processors](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) - [Google Pay API — `PaymentMethodTokenizationData`](https://developers.google.com/pay/api/android/reference/response-objects#PaymentMethodTokenizationData) - [Apple Pay — payment token format reference](https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference) - [platforms/android.md](docs/platforms/android.md), [platforms/ios.md](docs/platforms/ios.md) --- # FILE: docs/guides/gateways/stripe.md # Stripe Charge Apple Pay and Google Pay through [Stripe](https://docs.stripe.com/) without Stripe's native SDKs. Both wallets end in the same server step: turn the wallet token into a Stripe `tok_…` token, attach it to a PaymentMethod, and confirm a PaymentIntent with it. > Stripe documents both wallets primarily through its own SDKs. The Google Pay parameters come from Google's > processor table, and the Apple Pay exchange step mirrors what Stripe's iOS SDK sends — both are marked below. > If you would rather stay on Stripe's documented path, use > [`@stripe/stripe-react-native`](https://docs.stripe.com/apple-pay?platform=react-native) instead of this package. ## Values | Value | Where | Source | Verified | | --- | --- | --- | --- | | `gateway: 'stripe'` | `gatewayConfig` | [Google Pay tutorial — Stripe row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | `'stripe:version': '2018-10-31'` | `gatewayConfig` | [Google Pay tutorial — Stripe row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | `'stripe:publishableKey': 'pk_test_…'` | `gatewayConfig` | [Google Pay tutorial — Stripe row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | no `gatewayMerchantId` | `gatewayConfig` | [Google Pay tutorial — Stripe row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) lists only the three keys above | 2026-10-16 | | Merchant ID registered with a Stripe-issued CSR | `merchantIdentifier` | [Stripe — Apple Pay, create a certificate](https://docs.stripe.com/apple-pay?platform=ios#csr) | 2026-10-16 | | `POST /v1/payment_methods` with `type=card`, `card[token]=tok_…` | backend | [Stripe API — create a PaymentMethod (`card.token`)](https://docs.stripe.com/api/payment_methods/create) | 2026-10-16 | | `POST /v1/payment_intents` with `payment_method`, `confirm=true` | backend | [Stripe API — create a PaymentIntent](https://docs.stripe.com/api/payment_intents/create) | 2026-10-16 | | `POST /v1/tokens` with `pk_token`, `pk_token_instrument_name`, `pk_token_payment_network`, `pk_token_transaction_id` | backend (Apple Pay) | Not in Stripe's API reference — mirrors [stripe-ios `STPAPIClient+ApplePay.swift`](https://github.com/stripe/stripe-ios/blob/master/StripePayments/StripePayments/Source/API%20Bindings/STPAPIClient%2BApplePay.swift) | 2026-10-16 | For Stripe Connect, Stripe's Android SDK sends `stripe:publishableKey` as `pk_…/acct_…` (publishable key, slash, connected account id) — see [stripe-android `GooglePayConfig.kt`](https://github.com/stripe/stripe-android/blob/master/payments-core/src/main/java/com/stripe/android/GooglePayConfig.kt). ## 1. Account setup - **Apple Pay** — register a merchant ID in the Apple Developer portal, then in the Stripe Dashboard under [iOS certificates](https://dashboard.stripe.com/settings/ios_certificates) choose **Add new application**, download Stripe's CSR, create the Apple Pay Payment Processing Certificate from it and upload the `.cer` back to Stripe. Stripe can only decrypt `paymentData` encrypted for that certificate, so a CSR you generated yourself does not work. Add the merchant ID to the app's entitlement — see [quickstart-ios.md](docs/getting-started/quickstart-ios.md). - **Google Pay** — nothing to configure on Stripe for `TEST`. To go live, request production access in the Google Pay & Wallet Console with the **Gateway** integration type. ## 2. Method data ```ts import { createApplePayMethodData, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'US', currencyCode: 'USD', }), createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'stripe', 'stripe:version': '2018-10-31', 'stripe:publishableKey': 'pk_test_your_publishable_key', }, }), ]; ``` ## 3. Read the token - **Android** — `response.details.androidPayToken.rawToken` is a Stripe Token object serialized as JSON (`{ "id": "tok_…", "object": "token", "card": { "tokenization_method": "android_pay", … } }`). It is not Google's encrypted envelope, so the envelope fields of `androidPayToken` stay empty. Send `rawToken` to your backend, or parse it and send only `id`. - **iOS** — send `JSON.stringify(applePayToken.paymentData)`, `applePayToken.paymentMethod.displayName`, `applePayToken.paymentMethod.network` and `applePayToken.transactionIdentifier`. The [shared flow](docs/guides/gateways/index.md#the-flow-every-recipe-shares) shows the client code for both. ## 4. Backend 1. **Google Pay** — parse the forwarded `rawToken` and take its `id` (`tok_…`). 2. **Apple Pay** — exchange the payment token for a Stripe token: `POST https://api.stripe.com/v1/tokens` (form-encoded) with `pk_token` = the `paymentData` JSON string, `pk_token_instrument_name` = `displayName`, `pk_token_payment_network` = `network`, `pk_token_transaction_id` = `transactionIdentifier`. The response is a Token object; take its `id`. This is the request stripe-ios sends with the publishable key; it is not part of Stripe's public API reference, so validate it in test mode before relying on it. 3. Create a PaymentMethod from the token: `POST /v1/payment_methods` with `type=card` and `card[token]=tok_…`. 4. Create and confirm a PaymentIntent: `POST /v1/payment_intents` with `amount`, `currency`, `payment_method=pm_…` and `confirm=true`, authenticated with your secret key. 5. Return the PaymentIntent `status` to the app; `succeeded` maps to `PaymentComplete.SUCCESS`. A `requires_action` status needs 3-D Secure, which this package cannot present — treat it as a failure or hand off to Stripe's SDK. ## 5. Sandbox checklist - [ ] Google Pay: `environment` left at `TEST`, `stripe:publishableKey` is a `pk_test_…` key, and the test runs on a physical Android device with a card in Google Wallet (or a member of [Google's test card suite](https://developers.google.com/pay/api/android/guides/resources/test-card-suite)). - [ ] Google Pay: `rawToken` parses to an object whose `id` starts with `tok_`, and the PaymentIntent shows up in the Stripe Dashboard in test mode. - [ ] Apple Pay: the merchant ID's processing certificate was created from the Stripe CSR, and the test runs on a device with a **real** card in Wallet plus your test API keys — Stripe recognizes test mode and returns a test card token (see [Stripe — testing Apple Pay](https://docs.stripe.com/apple-pay?platform=ios#testing)). - [ ] Backend only: [Stripe test tokens](https://docs.stripe.com/testing) such as `tok_visa` exercise steps 3–4 without a device. ## Sources - [Google Pay API — tutorial, tokenization and processor table](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) - [Stripe — Apple Pay (iOS)](https://docs.stripe.com/apple-pay?platform=ios) - [Stripe — Google Pay (Android)](https://docs.stripe.com/google-pay?platform=android) - [Stripe API — PaymentMethods](https://docs.stripe.com/api/payment_methods/create), [PaymentIntents](https://docs.stripe.com/api/payment_intents/create), [Tokens](https://docs.stripe.com/api/tokens) - [stripe-ios — Apple Pay token request](https://github.com/stripe/stripe-ios/blob/master/StripePayments/StripePayments/Source/API%20Bindings/STPAPIClient%2BApplePay.swift) --- # FILE: docs/guides/gateways/adyen.md # Adyen Charge Apple Pay and Google Pay through [Adyen](https://docs.adyen.com/)'s API-only integration. Adyen takes both wallet tokens directly in one Checkout API `/payments` request, so there is no separate token-exchange step. ## Values | Value | Where | Source | Verified | | --- | --- | --- | --- | | `gateway: 'adyen'` | `gatewayConfig` | [Google Pay tutorial — Adyen row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization), [Adyen — Google Pay](https://docs.adyen.com/payment-methods/google-pay/) | 2026-10-16 | | `gatewayMerchantId`: your Adyen merchant account name (or company account name) | `gatewayConfig` | [Adyen — Google Pay](https://docs.adyen.com/payment-methods/google-pay/) | 2026-10-16 | | Merchant ID with your **own** Apple Pay certificate, CSR downloaded from the Customer Area | `merchantIdentifier` | [Adyen — Apple Pay certificate (iOS)](https://docs.adyen.com/payment-methods/apple-pay/apple-pay-certificate/ios) | 2026-10-16 | | `POST https://checkout-test.adyen.com/v72/payments`, `X-API-Key` header | backend | [Adyen Checkout API reference](https://docs.adyen.com/api-explorer/Checkout/latest/overview) | 2026-10-16 | | `paymentMethod: { type: 'googlepay', googlePayToken }` | backend | [Adyen — Google Pay API-only](https://docs.adyen.com/payment-methods/google-pay/api-only/) | 2026-10-16 | | `paymentMethod: { type: 'applepay', applePayToken }`, `applePayToken` = base64 of the stringified `paymentData` | backend | [Adyen — Apple Pay API-only](https://docs.adyen.com/payment-methods/apple-pay/api-only/) | 2026-10-16 | The Google Pay API-only page requires Checkout API v67 or later; v72 is the latest version listed in the API reference on the verification date. ## 1. Account setup - **Apple Pay** — mobile apps must use their own Apple Pay certificate; Adyen's shared certificate is web-only. In the Customer Area go to **Developers › API credentials ›** your web service user **› Wallet payment methods › Apple Pay › Add › Use your own certificate**, enter the merchant ID, download the CSR, create the Apple Pay Payment Processing Certificate from it in the Apple Developer portal and upload it back. Adyen asks for separate merchant IDs for test and live (it suggests `merchant.com.adyen..test`). In-app payments need no merchant session validation. Live Apple Pay payments need the **API Clientside Encryption Payments** role on the API credential. - **Google Pay** — nothing extra for `TEST`. Before going live, complete Google's production access request with the **Gateway** integration type. ## 2. Method data ```ts import { createApplePayMethodData, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.adyen.YourMerchantAccount.test', countryCode: 'NL', currencyCode: 'EUR', }), createGooglePayMethodData({ countryCode: 'NL', currencyCode: 'EUR', gatewayConfig: { gateway: 'adyen', gatewayMerchantId: 'YourMerchantAccount' }, }), ]; ``` ## 3. Read the token - **Android** — `response.details.androidPayToken.rawToken`, the Google Pay encrypted envelope string. Forward it unchanged; it becomes `googlePayToken`. - **iOS** — `JSON.stringify(response.details.applePayToken.paymentData)`. Base64-encode it (on the device or on your backend); the result becomes `applePayToken`. The [shared flow](docs/guides/gateways/index.md#the-flow-every-recipe-shares) shows the client code for both. ## 4. Backend 1. `POST https://checkout-test.adyen.com/v72/payments` with the `X-API-Key` header and: - `merchantAccount`, `reference`, `amount: { currency, value }` (minor units, so `'10.00'` EUR is `1000`); - Google Pay: `paymentMethod: { type: 'googlepay', googlePayToken: }`; - Apple Pay: `paymentMethod: { type: 'applepay', applePayToken: }`. 2. `resultCode: 'Authorised'` maps to `PaymentComplete.SUCCESS`. A response with an `action` (`redirect` or `threeDS2`) needs 3-D Secure handling, which this package cannot present — include `browserInfo` and `returnUrl` only if you handle that action yourself, otherwise treat it as a failure. 3. Live payments use your account's live prefix: `https://{PREFIX}-checkout-live.adyenpayments.com/checkout/v72/payments`. ## 5. Sandbox checklist - [ ] Google Pay: `environment` left at `TEST` and `gateway: 'adyen'` (not `example`) so the token can be authorised. Join [Google's test card suite](https://developers.google.com/pay/api/android/guides/resources/test-card-suite) — its cards map to Adyen test cards — or add a real card, which is mapped to an Adyen test card of the same brand and never charged. - [ ] Google Pay: 3-D Secure 2 test flows need American Express or Discover cards. - [ ] Apple Pay: test merchant ID with its own Adyen-uploaded certificate, and an [App Store Connect sandbox tester](https://developer.apple.com/apple-pay/sandbox-testing/) signed in on the device with an Apple test card in Wallet. - [ ] The payment appears in the Customer Area under **Payments › Payment list**. ## Sources - [Google Pay API — tutorial, tokenization and processor table](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) - [Adyen — Google Pay](https://docs.adyen.com/payment-methods/google-pay/), [Google Pay API-only](https://docs.adyen.com/payment-methods/google-pay/api-only/) - [Adyen — Apple Pay API-only](https://docs.adyen.com/payment-methods/apple-pay/api-only/), [Apple Pay certificate](https://docs.adyen.com/payment-methods/apple-pay/apple-pay-certificate/), [iOS certificate steps](https://docs.adyen.com/payment-methods/apple-pay/apple-pay-certificate/ios) - [Adyen Checkout API reference](https://docs.adyen.com/api-explorer/Checkout/latest/overview) - [Apple Pay sandbox testing](https://developer.apple.com/apple-pay/sandbox-testing/) --- # FILE: docs/guides/gateways/braintree.md # Braintree Charge Apple Pay and Google Pay through [Braintree](https://developer.paypal.com/braintree/docs/) without Braintree's native SDKs. Both wallets end in a Braintree payment method nonce that your server charges with `Transaction: Sale`. > Braintree's guides cover both wallets through its own SDKs. The Google Pay parameters come from Google's processor > table, the token shape and the Apple Pay exchange from Braintree's public SDK sources and GraphQL schema — each > marked below. If you would rather stay on Braintree's documented path, use its native SDKs instead of this package. ## Values | Value | Where | Source | Verified | | --- | --- | --- | --- | | `gateway: 'braintree'` | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | `'braintree:apiVersion': 'v1'` | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | `'braintree:sdkVersion'` | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) fills it from `braintree.client.VERSION`; Braintree documents no value for SDK-less integrations | 2026-10-16 | | `'braintree:merchantId'`: your Braintree merchant ID | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | `'braintree:clientKey'`: your Braintree tokenization key | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | no `gatewayMerchantId` | `gatewayConfig` | [Google Pay tutorial — Braintree row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) | 2026-10-16 | | Google Pay token: `{ "androidPayCards": [{ "nonce": … }] }` | Android response | [braintree-web `google-payment.js` parser](https://braintree.github.io/braintree-web/current/google-payment_google-payment.js.html) | 2026-10-16 | | Merchant ID with a Braintree-issued CSR | `merchantIdentifier` | [Braintree — Apple Pay configuration (iOS)](https://developer.paypal.com/braintree/docs/guides/apple-pay/configuration/ios/v6) | 2026-10-16 | | GraphQL `tokenizeApplePayCard` with `applePaymentToken: { paymentData (base64), paymentInstrumentName, paymentNetwork, transactionIdentifier }` | backend (Apple Pay) | [Braintree public GraphQL schema](https://github.com/braintree/graphql-api/blob/master/schema.graphql) | 2026-10-16 | | `Transaction: Sale` with the nonce as `payment_method_nonce` | backend | [Braintree — Google Pay server-side](https://developer.paypal.com/braintree/docs/guides/google-pay/server-side) | 2026-10-16 | ## 1. Account setup - **Google Pay** — enable Google Pay in the Braintree Control Panel; it is required in sandbox too. For production you also complete Google's own go-live process. - **Apple Pay** — in the Control Panel (gear icon › **Processing**) open the Apple Pay **Options**, add an iOS certificate and download Braintree's CSR, create the Apple Pay Payment Processing Certificate from it in the Apple Developer portal, and upload that certificate back under **Apple Merchant Certificates (iOS)**. Braintree requires its own CSR — do not generate one yourself. Add the merchant ID to the app's entitlement — see [quickstart-ios.md](docs/getting-started/quickstart-ios.md). ## 2. Method data ```ts import { createApplePayMethodData, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'US', currencyCode: 'USD', }), createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', gatewayConfig: { gateway: 'braintree', 'braintree:apiVersion': 'v1', 'braintree:sdkVersion': 'your-braintree-sdk-version', 'braintree:merchantId': 'your_braintree_merchant_id', 'braintree:clientKey': 'sandbox_your_tokenization_key', }, }), ]; ``` ## 3. Read the token - **Android** — `response.details.androidPayToken.rawToken` is Braintree's JSON, not Google's encrypted envelope, so the envelope fields of `androidPayToken` stay empty. The nonce is `JSON.parse(rawToken).androidPayCards[0].nonce`. Send the nonce (or the whole `rawToken`) to your backend. - **iOS** — send `JSON.stringify(applePayToken.paymentData)`, `applePayToken.paymentMethod.displayName`, `applePayToken.paymentMethod.network` and `applePayToken.transactionIdentifier`. The [shared flow](docs/guides/gateways/index.md#the-flow-every-recipe-shares) shows the client code for both. ## 4. Backend 1. **Apple Pay** — exchange the payment token for a nonce with the GraphQL `tokenizeApplePayCard` mutation, passing `applePaymentToken.paymentData` = base64 of the `paymentData` JSON, `paymentInstrumentName` = `displayName`, `paymentNetwork` = `network`, `transactionIdentifier`. The returned single-use payment method id is the nonce. The mutation is defined in Braintree's public schema but not described in its guides, so validate it in sandbox before relying on it. 2. Charge the nonce with `Transaction: Sale` (`amount` plus `payment_method_nonce`) through a Braintree server SDK, or with the GraphQL `chargePaymentMethod` mutation. 3. A successful sale maps to `PaymentComplete.SUCCESS`. ## 5. Sandbox checklist - [ ] Google Pay is enabled in the sandbox Control Panel and `braintree:clientKey` is a sandbox tokenization key. - [ ] Google Pay: `environment` left at `TEST`, and `JSON.parse(rawToken).androidPayCards[0].nonce` is present. - [ ] Apple Pay: the processing certificate was created from the Braintree sandbox CSR, and an [App Store Connect sandbox tester](https://developer.apple.com/apple-pay/sandbox-testing/) is signed in on the device with an Apple test card in Wallet. - [ ] Backend only: the [sandbox test nonces](https://developer.paypal.com/braintree/docs/reference/general/testing) `fake-android-pay-visa-nonce` and `fake-apple-pay-visa-nonce` exercise step 2 without a device. ## Sources - [Google Pay API — tutorial, tokenization and processor table](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) - [Braintree — Google Pay configuration](https://developer.paypal.com/braintree/docs/guides/google-pay/configuration), [server-side](https://developer.paypal.com/braintree/docs/guides/google-pay/server-side) - [Braintree — Apple Pay configuration (iOS)](https://developer.paypal.com/braintree/docs/guides/apple-pay/configuration/ios/v6) - [Braintree — testing reference](https://developer.paypal.com/braintree/docs/reference/general/testing) - [Braintree GraphQL schema](https://github.com/braintree/graphql-api/blob/master/schema.graphql) - [braintree-web — Google Pay token parser](https://braintree.github.io/braintree-web/current/google-payment_google-payment.js.html) --- # FILE: docs/guides/gateways/checkout-com.md # Checkout.com Charge Apple Pay and Google Pay through [Checkout.com](https://www.checkout.com/docs)'s Unified Payments API. Both wallets go through the same two calls: exchange the wallet token for a Checkout.com `tok_…` token with `POST /tokens`, then pay with it through `POST /payments`. ## Values | Value | Where | Source | Verified | | --- | --- | --- | --- | | `gateway: 'checkoutltd'` | `gatewayConfig` | [Google Pay tutorial — Checkout.com row](https://developers.google.com/pay/api/android/guides/tutorial#tokenization), [Checkout.com — Google Pay](https://www.checkout.com/docs/payments/add-payment-methods/google-pay/unified-payments-api) | 2026-10-16 | | `gatewayMerchantId`: your Checkout.com **public** key | `gatewayConfig` | [Checkout.com — Google Pay](https://www.checkout.com/docs/payments/add-payment-methods/google-pay/unified-payments-api) | 2026-10-16 | | Merchant ID with a Checkout.com-issued CSR (`POST /applepay/signing-requests`) | `merchantIdentifier` | [Checkout.com — Apple Pay](https://www.checkout.com/docs/payments/add-payment-methods/apple-pay/unified-payments-api) | 2026-10-16 | | `https://{prefix}.api.sandbox.checkout.com` (live: `https://{prefix}.api.checkout.com`) | backend | [Checkout.com — API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) | 2026-10-16 | | `POST /tokens` with `type: 'googlepay'`, `token_data: { protocolVersion, signature, signedMessage }`, authenticated with the public key | backend | [Checkout.com — Google Pay](https://www.checkout.com/docs/payments/add-payment-methods/google-pay/unified-payments-api) | 2026-10-16 | | `POST /tokens` with `type: 'applepay'`, `token_data: { version, data, signature, header: { ephemeralPublicKey, publicKeyHash, transactionId } }` | backend | [Checkout.com — Apple Pay](https://www.checkout.com/docs/payments/add-payment-methods/apple-pay/unified-payments-api) | 2026-10-16 | | `POST /payments` with `source: { type: 'token', token: 'tok_…' }`, `Authorization: Bearer sk_sbox_…` | backend | [Checkout.com — Apple Pay](https://www.checkout.com/docs/payments/add-payment-methods/apple-pay/unified-payments-api), [API keys](https://www.checkout.com/docs/developer-resources/api/manage-api-keys/api-keys) | 2026-10-16 | `{prefix}` is the first eight characters of your `client_id` without the `cli_` prefix, and differs between sandbox and production. Requests to a base URL without it are rejected with `403` and `error_type: base_url_invalid`. ## 1. Account setup - **Apple Pay** — create the CSR through Checkout.com: `POST /applepay/signing-requests` with `{ "protocol_version": "ec_v1" }`, save the response `content` as a `.csr` file (it expires after 24 hours), create the Apple Pay Payment Processing Certificate from it in the Apple Developer portal, then upload the downloaded certificate, base64-encoded, with `POST /applepay/certificates`. Add the merchant ID to the app's entitlement — see [quickstart-ios.md](docs/getting-started/quickstart-ios.md). - **Google Pay** — nothing to configure for `TEST`; Checkout.com validates the Google merchant ID only in production. ## 2. Method data ```ts import { createApplePayMethodData, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'GB', currencyCode: 'GBP', }), createGooglePayMethodData({ countryCode: 'GB', currencyCode: 'GBP', gatewayConfig: { gateway: 'checkoutltd', gatewayMerchantId: 'pk_sbox_your_public_key' }, }), ]; ``` ## 3. Read the token - **Android** — `response.details.androidPayToken.rawToken`, the Google Pay encrypted envelope string. Forward it unchanged: `signedMessage` must reach Checkout.com as the exact string Google produced, so build `token_data` on the backend from `JSON.parse(rawToken)` rather than from the parsed `androidPayToken` fields. - **iOS** — `JSON.stringify(response.details.applePayToken.paymentData)`; its `version`, `data`, `signature` and `header` are the `token_data` fields as-is. The [shared flow](docs/guides/gateways/index.md#the-flow-every-recipe-shares) shows the client code for both. ## 4. Backend 1. Exchange the wallet token. Checkout.com documents the Google Pay call as authenticated with the same public key as `gatewayMerchantId`; its Apple Pay page does not name the key, so confirm it in sandbox: - Google Pay: `POST /tokens` with `{ type: 'googlepay', token_data: { protocolVersion, signature, signedMessage } }`, copying the three fields from `JSON.parse(rawToken)`. Checkout.com's example shows only these three fields (with an `ECv1` token); if Google returns an `ECv2` token, its `intermediateSigningKey` is not part of the documented request — validate the call in sandbox. - Apple Pay: `POST /tokens` with `{ type: 'applepay', token_data: { version, data, signature, header } }`, where `header` carries `ephemeralPublicKey`, `publicKeyHash` and `transactionId`. 2. Pay with the returned `token` (`tok_…`): `POST /payments` with `{ source: { type: 'token', token }, amount, currency }` (`amount` in minor units), authenticated with your secret key. 3. An approved payment maps to `PaymentComplete.SUCCESS`. ## 5. Sandbox checklist - [ ] All calls go to `https://{prefix}.api.sandbox.checkout.com` with sandbox keys (`pk_sbox_…`, `sk_sbox_…`). - [ ] Google Pay: `environment` left at `TEST` and `gateway: 'checkoutltd'` (not `example`). In sandbox Google substitutes a test card, so no real card is charged. - [ ] Apple Pay: the certificate was issued from a sandbox signing request and uploaded to the sandbox account, and an [App Store Connect sandbox tester](https://developer.apple.com/apple-pay/sandbox-testing/) is signed in on the device with an Apple test card in Wallet. - [ ] `POST /tokens` returns a `tok_…` token for both wallets before you wire up `POST /payments`. ## Sources - [Google Pay API — tutorial, tokenization and processor table](https://developers.google.com/pay/api/android/guides/tutorial#tokenization) - [Checkout.com — Google Pay](https://www.checkout.com/docs/payments/add-payment-methods/google-pay/unified-payments-api), [Google Pay API-only](https://www.checkout.com/docs/payments/add-payment-methods/google-pay/api-only) - [Checkout.com — Apple Pay](https://www.checkout.com/docs/payments/add-payment-methods/apple-pay/unified-payments-api) - [Checkout.com — API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints), [API keys](https://www.checkout.com/docs/developer-resources/api/manage-api-keys/api-keys) --- # FILE: docs/guides/gateways/direct.md # Direct tokenization (self-decrypt) Decrypt the Apple Pay and Google Pay payloads on your own servers and send the card data to your processor yourself. Only for merchants with their own PCI DSS scope — every PSP recipe in this folder avoids it. ## Values | Value | Where | Source | Verified | | --- | --- | --- | --- | | `directConfig: { protocolVersion: 'ECv2', publicKey }` (Google Pay `DIRECT` tokenization) | `directConfig` | [Google Pay — `DIRECT` parameters](https://developers.google.com/pay/api/android/reference/request-objects#direct) | 2026-10-16 | | `publicKey`: base64 elliptic-curve public key, uncompressed point format, registered in the Google Pay & Wallet Console, rotated yearly | `directConfig` | [Google Pay — payment data cryptography](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography) | 2026-10-16 | | Eligibility: PCI DSS certification by a Qualified Security Assessor; not available to third-party gateways and processors | Google Pay | [Google Pay — `DIRECT` parameters](https://developers.google.com/pay/api/android/reference/request-objects#direct) | 2026-10-16 | | `PaymentMethodTokenRecipient` (Tink), `recipientId("merchant:")`, `INSTANCE_TEST` / `INSTANCE_PRODUCTION` signing keys | backend | [Google Pay — payment data cryptography](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography) | 2026-10-16 | | Apple Pay `paymentData`: `version` `EC_v1` or `RSA_v1`, `header.ephemeralPublicKey` (EC) / `wrappedKey` (RSA), `publicKeyHash`, `transactionId` | backend | [Apple — payment token format reference](https://developer.apple.com/documentation/passkit/payment-token-format-reference) | 2026-10-16 | ## 1. Account setup - **Google Pay** — generate an EC key pair, register the public key in the [Google Pay & Wallet Console](https://pay.google.com/business/console) under **Google Pay API › Direct integration**, and keep the private key on your server. - **Apple Pay** — create the Apple Pay Payment Processing Certificate from a CSR you generate yourself, and keep its private key on your server. Add the merchant ID to the app's entitlement — see [quickstart-ios.md](docs/getting-started/quickstart-ios.md). ## 2. Method data ```ts import { createApplePayMethodData, createGooglePayMethodData } from '@rnw-community/react-native-payments'; const methodData = [ createApplePayMethodData({ merchantIdentifier: 'merchant.com.your-app', countryCode: 'US', currencyCode: 'USD', }), createGooglePayMethodData({ countryCode: 'US', currencyCode: 'USD', directConfig: { protocolVersion: 'ECv2', publicKey: 'your-base64-uncompressed-ec-public-key' }, }), ]; ``` ## 3. Read the token - **Android** — `response.details.androidPayToken` is Google's ECv2 envelope already split into `protocolVersion`, `signature`, `intermediateSigningKey` and `signedMessage`. Forward `rawToken` — the decryption libraries verify signatures over the original strings. - **iOS** — forward `JSON.stringify(response.details.applePayToken.paymentData)`. ## 4. Backend 1. **Google Pay** — unseal `rawToken` with Tink's `PaymentMethodTokenRecipient` (`protocolVersion("ECv2")`, your `merchant:` recipient id, your private key, Google's `INSTANCE_TEST` signing keys in `TEST`). The decrypted message carries the card or network token and, for tokenized cards, a cryptogram. 2. **Apple Pay** — follow Apple's steps: verify the signature and its certificate chain to Apple Root CA - G3, reject a signing time more than five minutes from the transaction time, select your certificate by `publicKeyHash`, restore the symmetric key, decrypt `data` (AES-256-GCM for `EC_v1`), reject a `transactionId` you already processed, and check `currencyCode`, `transactionAmount` and `applicationData` against your order. 3. Send the decrypted credentials to your processor, and map its result to `PaymentComplete`. ## 5. Sandbox checklist - [ ] Google Pay: `environment` left at `TEST`, and the recipient uses Google's test signing keys (`INSTANCE_TEST`). Google's [test card suite](https://developers.google.com/pay/api/android/guides/resources/test-card-suite) returns placeholder card data — use it to exercise decryption, not to charge. - [ ] Apple Pay: an [App Store Connect sandbox tester](https://developer.apple.com/apple-pay/sandbox-testing/) with an Apple test card, and a payload that decrypts with your processing certificate's private key. - [ ] Your servers that touch decrypted data are in your PCI DSS scope before going live. ## Sources - [Google Pay API — request objects, `DIRECT`](https://developers.google.com/pay/api/android/reference/request-objects#direct) - [Google Pay API — payment data cryptography](https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography) - [Apple — payment token format reference](https://developer.apple.com/documentation/passkit/payment-token-format-reference) --- # FILE: docs/guides/migrate-from-v2.md # Migrating from v2 > **The native module interface changed** (`show()` now carries the request id, and the change-event methods are > part of the TurboModule spec). Rebuild the native app when upgrading — a JavaScript-only update (e.g. > CodePush/OTA) shipped on top of a v2 binary will fail to open the payment sheet. The `v2.x` line shipped no change-event API: the sheet only ever showed the `PaymentDetailsInit` given to the constructor, and neither `addEventListener` nor `removeEventListener` existed. Adopting the event API in [change-events.md](docs/guides/change-events.md) is purely additive — `PaymentRequest`, `canMakePayment()`, `show()`, `abort()` and `PaymentComplete` keep their v2 signatures, and a consumer who never calls `addEventListener` sees the same sheet as before (aside from the iOS round trip described in [change-events.md](docs/guides/change-events.md)). > **Behavior change:** in `v2.x` a settled `PaymentRequest` could call `show()` again to reopen the sheet. From > `v3` a `PaymentRequest` is single-use — once `show()` settles or `abort()` resolves the request is `closed`, > `addEventListener` becomes a no-op and every further `show()` rejects with `InvalidStateError`. Construct a new > `PaymentRequest` per payment attempt instead of reusing one across retries. See > [architecture.md](docs/architecture.md) for why. ## References - [architecture.md](docs/architecture.md) - [api/payment-request.md](docs/api/payment-request.md) --- # FILE: docs/guides/migrate-from-upstream.md # Migrating from `react-native-payments` (upstream) This package started as a rewrite of [naoufal/react-native-payments](https://github.com/naoufal/react-native-payments), the original — now unmaintained — library. The [readme](readme.md) summarizes the rewrite; this page maps the concrete API surface so an existing integration can be ported. ## API mapping | Upstream (`react-native-payments`) | This package (`@rnw-community/react-native-payments`) | Notes | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `npm install react-native-payments` + `react-native link` | `npm install @rnw-community/react-native-payments` | Autolinked TurboModule — no manual linking step. See [getting-started/install.md](docs/getting-started/install.md). | | `global.PaymentRequest = require('react-native-payments').PaymentRequest;` | `import { PaymentRequest } from '@rnw-community/react-native-payments';` | No global polyfill; import the class where you use it. | | `supportedMethods: ['apple-pay']` / `['android-pay']` (string) | `supportedMethods: PaymentMethodNameEnum.ApplePay` / `PaymentMethodNameEnum.AndroidPay` (enum) | Same runtime values, typed. | | `new PaymentRequest(methodData, details, options)` — 3rd arg `options.requestPayerName` etc. | `new PaymentRequest(methodData, details)` — payer/shipping flags live on each entry's `methodData.data` | No top-level `options` object. See [api/payment-request.md](docs/api/payment-request.md). | | `paymentRequest.show()`, reusable after a rejection | `paymentRequest.show()` — **single-use**, `closed` once it settles | See [architecture.md](docs/architecture.md) and [migrate-from-v2.md](docs/guides/migrate-from-v2.md). | | `paymentRequest.abort()` | `paymentRequest.abort()` | Same name, TurboModule-backed, spec-mapped `DOMException` on misuse. | | `addEventListener('shippingaddresschange' \| 'shippingoptionchange', e => e.updateWith(...))` | Same two, plus `paymentmethodchange` and the PassKit-only `couponcodechange` | Request-scoped native events, `isAnswered`, field-level errors. See [change-events.md](docs/guides/change-events.md). | | `paymentResponse.details.paymentData` / `transactionIdentifier` (iOS) | `paymentResponse.details.applePayToken` (`IosPKToken`) | One typed token object instead of loose fields. | | `paymentResponse.details.getPaymentToken()` (Android, async) / `.paymentToken` (gateway) | `paymentResponse.details.androidPayToken` (`AndroidPaymentMethodToken`) | Synchronous typed field; no async indirection, no built-in gateway token. | | `paymentResponse.complete('success' \| 'fail' \| 'unknown')` (string) | `paymentResponse.complete(PaymentComplete.SUCCESS \| PaymentComplete.FAIL \| PaymentComplete.UNKNOWN)` (enum) | Same three outcomes, typed — see [api/payment-complete-enum.md](docs/api/payment-complete-enum.md). | | Built-in Stripe / Braintree add-on packages | Removed — bring your own gateway via `gatewayConfig` (Android) or the raw token (iOS) | Stripe/Braintree already ship their own maintained RN SDKs. | | Untyped JS, legacy bridge module | Full TypeScript, TurboModule (New Architecture-ready) | See [architecture.md](docs/architecture.md). | | Ad-hoc thrown errors, no stable identity | Spec-mapped `ConstructorError` / `DOMException` / `PaymentsError` | See [errors.md](docs/guides/errors.md). | ## Worked example Before (upstream, from the original readme's Apple Pay quickstart): ```js // index.ios.js global.PaymentRequest = require('react-native-payments').PaymentRequest; const METHOD_DATA = [ { supportedMethods: ['apple-pay'], data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: ['visa', 'mastercard', 'amex'], countryCode: 'US', currencyCode: 'USD', }, }, ]; const DETAILS = { id: 'basic-example', displayItems: [{ label: 'Movie Ticket', amount: { currency: 'USD', value: '15.00' } }], total: { label: 'Merchant Name', amount: { currency: 'USD', value: '15.00' } }, }; const paymentRequest = new PaymentRequest(METHOD_DATA, DETAILS); paymentRequest.show().then(paymentResponse => { const { transactionIdentifier, paymentData } = paymentResponse.details; return fetch('...', { method: 'POST', body: { transactionIdentifier, paymentData } }) .then(res => res.json()) .then(successHandler) .catch(errorHandler) .then(() => paymentResponse.complete('success')); }); ``` After (this package): ```ts import { PaymentComplete, PaymentMethodNameEnum, PaymentRequest, SupportedNetworkEnum, } from '@rnw-community/react-native-payments'; const methodData = [ { supportedMethods: PaymentMethodNameEnum.ApplePay, data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: [SupportedNetworkEnum.Visa, SupportedNetworkEnum.Mastercard, SupportedNetworkEnum.Amex], countryCode: 'US', currencyCode: 'USD', }, }, ]; const paymentDetails = { id: 'basic-example', displayItems: [{ label: 'Movie Ticket', amount: { currency: 'USD', value: '15.00' } }], total: { label: 'Merchant Name', amount: { currency: 'USD', value: '15.00' } }, }; const paymentRequest = new PaymentRequest(methodData, paymentDetails); const paymentResponse = await paymentRequest.show(); const applePayToken = paymentResponse.details.applePayToken; // typed IosPKToken let responseBody: unknown; let backendAccepted = false; let backendError: unknown; try { const result = await fetch('...', { method: 'POST', body: JSON.stringify(applePayToken) }); if (!result.ok) { throw new Error(`Backend rejected the payment: ${result.status}`); } responseBody = await result.json(); backendAccepted = true; } catch (error) { backendError = error; } // complete() is called exactly once, before either handler, so a throw from successHandler/errorHandler can // never prevent it from running or trigger a second call. await paymentResponse.complete(backendAccepted ? PaymentComplete.SUCCESS : PaymentComplete.FAIL); if (backendAccepted) { successHandler(responseBody); } else { errorHandler(backendError); } ``` The differences that matter beyond syntax: no `global.PaymentRequest` polyfill, enums instead of string literals, one typed `applePayToken` instead of loose `transactionIdentifier`/`paymentData` fields, and — most importantly — this `paymentRequest` cannot `show()` again. A retry constructs a new `PaymentRequest` from the same `methodData` / `paymentDetails`, matching [migrate-from-v2.md](docs/guides/migrate-from-v2.md) above. --- # FILE: docs/architecture.md # How it works `PaymentRequest` is a thin JS state machine (`created` → `interactive` → `closed`) sitting on top of one TurboModule, `Payments` (`NativePayments.ts` → `Payments.mm` on iOS, `PaymentsModule.kt` on Android). Method calls (`show`, `abort`, `canMakePayment`) go JS → native directly through the module. Change events flow the other way — native → JS — through a `NativeEventEmitter` built over the same module handle, scoped to the request that is currently on screen: ```text JS Native (PassKit / Google Pay) ┌────────────────────────┐ show(requestId, …) ┌───────────────────────────────┐ │ PaymentRequest │ ───────────────────▶ │ Payments TurboModule │ │ created -> interactive │ │ (Payments.mm / …Module.kt) │ └────────────┬────────────┘ └───────────────┬───────────────┘ │ addEventListener(type) │ │ ── setActiveEvents(requestId, types) ──────────────▶│ │ │ │◀─ shippingaddresschange {requestId, eventId, …} ────┤ (request-scoped emit) ▼ ChangeEventDispatcher.dispatch(listeners) │ listener runs, calls updateWith(details) — or times out (changeEventTimeoutMs) ▼ updatePaymentDetails(update, displayItems, shippingOptions) ───────▶ resolves the *pending* update = { requestId, eventId, eventName, total, error } completion for that eventId │ ▼ show() resolves/rejects ── closeRequest() ──▶ state: closed (single-use — see below) ``` ## Why events are request-scoped The native module is a singleton — exactly one sheet can be interactive at a time — but a JS app can construct several `PaymentRequest` instances in a session (retries, different carts). `show()` passes the request's own `id`, native adopts it as the `activeRequestId`, and every event and completion carries `requestId` / `eventId` so a second request can never intercept or answer the first one's events. The full contract — every native method, payload shape and teardown path — is documented in [get-native-payments-event-emitter.md](src/util/get-native-payments-event-emitter/get-native-payments-event-emitter.md); the JS-side dispatch/timeout/answer lifecycle for one event is in [change-event-dispatcher.md](src/class/change-event-dispatcher/change-event-dispatcher.md). ## Why a request is single-use The W3C spec already moves a settled `PaymentRequest` to a `closed` state; this package treats that state as terminal instead of reusable. Reusing a request would mean keeping its native event subscriptions alive with no terminal path left to release them, since `show()`/`abort()` are what tear them down. Making `closed` permanent gives every request exactly one teardown path, guarantees against a leaked native listener, and keeps the request-scoping guarantee above simple — a `requestId` is only ever active once. See [guides/migrate-from-v2.md](docs/guides/migrate-from-v2.md) for the concrete behavior change, and the platform pages ([platforms/ios.md](docs/platforms/ios.md), [platforms/android.md](docs/platforms/android.md), [platforms/web.md](docs/platforms/web.md)) for how this differs from the spec. ## `NotSupportedError` thrown at construction, not at `show()` The spec rejects `show()`'s promise with `NotSupportedError` when no payment handler is available; this implementation instead throws synchronously from the `PaymentRequest` constructor as soon as it fails to find a platform-matching payment method, since the native bridge needs to know the target platform's method data up front to serialize the request. The error name matches the spec; only the algorithm step it fires from differs. See [guides/errors.md](docs/guides/errors.md). For the full class/file layout and the native-side invariants that keep this contract intact across old architecture, New Architecture and bridgeless, see [AGENTS.md](AGENTS.md). --- # FILE: docs/roadmap.md # Roadmap Open work for this package, as tracked GitHub issues, plus the W3C compliance checklist. The [landing readme](readme.md) links here instead of carrying its own TODO ledger. ## Open work - [ ] Payment sheet GIFs for iOS and Android — [#469](https://github.com/rnw-community/rnw-community/issues/469), capture procedure below. - [x] Shipping change events on Android through Google Pay callback intents — [#438](https://github.com/rnw-community/rnw-community/issues/438); coupons stay iOS-only, see below. - Rewrite iOS to Swift — **deferred indefinitely**, decision recorded on [#442](https://github.com/rnw-community/rnw-community/issues/442). - [x] Rewrite Android to Kotlin — done in [#664](https://github.com/rnw-community/rnw-community/pull/664), tracked in [#442](https://github.com/rnw-community/rnw-community/issues/442). - [x] Drop the `AppDelegate.h` PassKit import requirement — the pod links `PassKit` itself, tracked in [#442](https://github.com/rnw-community/rnw-community/issues/442). ## Docs ### Payment sheet GIFs — [#469](https://github.com/rnw-community/rnw-community/issues/469) Android capture is automated: dispatch the `docs-media.yml` workflow. It builds the bare example APK, runs [`e2e/recording/android_sheet_capture.yaml`](../react-native-payments-example/e2e/readme.md#layout) on the self-hosted Android emulator fleet, and uploads `google-pay-sheet.mp4` plus an ffmpeg-converted GIF as the `docs-media-payments` artifact. Remaining before the GIFs replace the [Screenshots](readme.md#screenshots) placeholder: - **Android**: the CI emulator has no Google account, so Google Pay answers `show()` with its "Google Pay requires a Google account to be set up on this device" dialog instead of the payment sheet. A recording of the real sheet needs an emulator image signed into a test Google account. - **iOS**: the Apple Pay loop needs the macOS Maestro fleet; add an iOS job to `docs-media.yml` once that fleet is available. ## Native ### Android shipping change events — [#438](https://github.com/rnw-community/rnw-community/issues/438) Done: `shippingaddresschange` / `shippingoptionchange` are delivered on Android through Google Pay's dynamic price updates (`play-services-wallet` 20.0.0+): `show()` declares `callbackIntents` for the registered shipping listeners, the library manifest declares `PaymentsCallbacksService`, and `updateWith` answers become `PaymentDataRequestUpdate`s — see [guides/change-events.md](docs/guides/change-events.md#android). Outcome of the remaining spikes: - **Coupons stay iOS-only.** Google Pay's Android `callbackIntents` are limited to `PAYMENT_AUTHORIZATION`, `SHIPPING_ADDRESS` and `SHIPPING_OPTION`; the `OFFER` intent (promo codes) exists on the web API only. - **`paymentmethodchange` stays iOS-only.** There is no payment-method callback intent on Android. - **In-sheet `complete()` / `retry()` remain future work.** Google Pay requires `PAYMENT_AUTHORIZATION` alongside the shipping intents; it is answered `SUCCESS` immediately today. Routing it to `PaymentResponse.complete()` / `retry()` would give Android an in-sheet authorization result, but changes when `show()` resolves. - **On-device verification** with a signed-in Google account and a TEST-environment card is pending the Android Maestro suite ([#393](https://github.com/rnw-community/rnw-community/issues/393)). ### Native modernization — [#442](https://github.com/rnw-community/rnw-community/issues/442) Spike findings (decision recorded on the issue): - **iOS → Swift: deferred indefinitely.** `Payments.mm`/`Payments.h` (1079 + 24 lines, ObjC++) implement a hardened PassKit delegate lifecycle — per-event pending-completion tracking with superseded-event detection, a teardown state machine invoked from five call sites, iOS 15+ conditional coupon-code support, and ~30 delegate callbacks/helpers. Codegen (React Native 0.86.2, the version this monorepo is pinned to) has no Swift TurboModule support — it only emits Objective-C++ glue — so a Swift rewrite would still sit behind an Objective-C++ spec surface, adding a bridging layer without removing the interop this module already has, while asking to re-verify ~1100 lines of delegate state whose regressions are silent runtime sheet hangs, not compile errors. Effort: L. Risk: high. Revisit only as part of an Expo Modules API spike (see #445), not as a standalone plain-Swift TurboModule rewrite. - [x] **Android → Kotlin: done.** The former `PaymentsModule.java` (284 lines) was a single activity-result round trip, not a delegate protocol — `abort`/`complete`/`setActiveEvents`/`updatePaymentDetails` were no-ops that resolved immediately, no coupon codes, no in-sheet change events, no teardown state machine (the change events arrived later with #438). It is now `PaymentsModule.kt`, `PaymentsPackage.kt` and the two `android/src/*arch/PaymentsSpec.kt` variants, behaviour-identical (same method names, promise codes and messages); the newarch spec extends the codegen-generated Java `NativePaymentsSpec` directly, with no bridging shim. The library applies the `kotlin-android` plugin, resolving `kotlinVersion` from the consuming app's root project (React Native 0.86 apps already define it). - **`AppDelegate.h` PassKit import: dropped.** `react-native-payments.podspec` declares `s.frameworks = "PassKit", "Contacts"`, linking the framework at the Pod target level, and the bare example's `AppDelegate.swift` (RN 0.86 community template) builds and passes the iOS Maestro suite without `import PassKit`. An `AppDelegate` import only matters when the app's own delegate code references PassKit symbols. ## W3C compliance checklist - [x] [PaymentRequestUpdateEvent](https://www.w3.org/TR/payment-request/#dom-paymentrequestupdateevent) — JavaScript layer, iOS PassKit delivery and Android Google Pay callback delivery (shipping events) implemented (see [guides/change-events.md](docs/guides/change-events.md)); on-device verification is tracked in [#393](https://github.com/rnw-community/rnw-community/issues/393) - [x] [PaymentMethodChangeEvent](https://www.w3.org/TR/payment-request/#dom-paymentmethodchangeevent) — JavaScript layer and iOS PassKit delivery implemented; Android stays silent because Google Pay has no payment-method callback intent; on-device verification is tracked in [#393](https://github.com/rnw-community/rnw-community/issues/393) - [x] Implement [PaymentDetailsModifier](https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier) — see [guides/modifiers.md](docs/guides/modifiers.md) - [x] Improve and unify errors according to the spec — see [guides/errors.md](docs/guides/errors.md) - [x] Implement [`hasEnrolledInstrument()`](https://www.w3.org/TR/payment-request/#hasenrolledinstrument-method) — see [api/payment-request.md](docs/api/payment-request.md); Android answers via Google Pay's `existingPaymentMethodRequired`, see [platforms/android.md](docs/platforms/android.md) - [x] Implement the event-handler attributes (`onshippingaddresschange`, `onshippingoptionchange`, `onpaymentmethodchange`, `oncouponcodechange`) — see [guides/change-events.md](docs/guides/change-events.md#event-handler-attributes) - [x] Implement [`PaymentOptions.shippingType`](https://www.w3.org/TR/payment-request/#dom-paymentoptions-shippingtype) as `methodData.data.shippingType` — see [platforms/ios.md](docs/platforms/ios.md); no-op on Android, see [platforms/android.md](docs/platforms/android.md) - [x] Implement `PaymentResponse` `retry()` method — best-effort subset on iOS, documented no-op on Android; see [guides/retry.md](docs/guides/retry.md) - [x] Implement `PaymentResponse` `toJSON()` method — see [api/payment-response.md](docs/api/payment-response.md) ## Related - Epic: [#372](https://github.com/rnw-community/rnw-community/issues/372) — react-native-payments revival - Docs Oasis: [#467](https://github.com/rnw-community/rnw-community/issues/467) — documentation platform umbrella