SDK Setup

Requirements, installation, Expo configuration, environments and the credential provider for the React Native SDK.

Before You Start

Read the following guides before proceeding:

GuideWhy
React Native SDKSurfaces, isolation model and flows
AuthenticationThe user token getAccessToken returns
Api BasicsX-Chain-Id and the identity headers the SDK sends

Requirements

RequirementValue
React Native0.74 or later, New Architecture enabled. The modules are TurboModules; the old architecture is not supported
React / NodeReact 18 or later, Node 18 or later
iOS15 or later, Xcode 26.5 or later
Android6.0 (API 23) or later, mavenCentral() among the repositories
ExpoA development build. Expo Go cannot load native modules

Install

npm install @wirexapp/[email protected]
cd ios && pod install

The package installs the native SDKs at its own version: the WirexMobileSDK pod from CocoaPods trunk and
com.wirexapp.baas:mobile-sdk from Maven Central. React Native templates already list mavenCentral().

Expo

Add the config plugin to app.json or app.config.ts:

{ "expo": { "plugins": ["@wirexapp/mobile-sdk"] } }

The plugin raises the iOS and Android minimum versions where they are lower than the SDK's, and adds the
Android <queries> intents Google Pay needs on Android 11 and later.

Bare React Native

Without Expo, add the Google Pay intents to AndroidManifest.xml yourself:

<queries>
  <intent><action android:name="org.chromium.intent.action.PAY" /></intent>
  <intent><action android:name="org.chromium.intent.action.IS_READY_TO_PAY" /></intent>
  <intent><action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS" /></intent>
</queries>

Without them, the Google Pay button reports unavailable on Android 11 and later.


Integration Values

ValueWhat it isWhere it goes
testModefalse — production, true — the Wirex test environment. Used by the card surfacesWirexReveal.configure, WirexCollect.configure, WirexThreeDS.configure
environmentThe same choice by name: 'production' or 'test'. Used by the walletWirexWalletConfig.environment
chainIdThe chain your integration operates on, sent as X-Chain-Id on every authenticated call. Required for Reveal and CollectchainId in configure(...)
clientIdOptional, wallet only. Selects your branding on the Wirex-hosted checkout pages. Omit it unless Wirex assigned one. It is not a credentialWirexWalletConfig.clientId

The native SDK holds the environment hosts. No API takes a URL. An unknown environment name is an error —
the SDK never falls back to another environment.


Credential Provider

The SDK never issues credentials. You implement one WirexCredentialProvider; the SDK calls it when it
needs a token, so tokens are never pushed in advance.

import { WirexDeclined, type WirexActionScope, type WirexCredentialProvider } from '@wirexapp/mobile-sdk';

declare function fetchAccessToken(): Promise<string>;
declare function fetchActionToken(scope: WirexActionScope): Promise<string>;
declare function userConfirmedStepUp(): Promise<boolean>;

export const credentials: WirexCredentialProvider = {
  getAccessToken: () => fetchAccessToken(),
  async getActionToken(scope) {
    if (!(await userConfirmedStepUp())) throw new WirexDeclined();
    return fetchActionToken(scope);
  },
  userEmail: undefined,
  userWallet: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18',
};
MemberReturnsUsed by
getAccessToken()The user token from User Token Issuance (Login as User), sent as Authorization: BearerReveal, Collect
getActionToken(scope)An action_token for scope: 'cardData' or 'pin'Reveal
userEmailSent as X-User-EmailReveal, Collect
userWalletSent as X-User-WalletReveal, Collect
  • Exactly one identity. Set userEmail or userWallet, never both. Both set is a 400 from the API.
  • The SDK sends the bearer as-is and never retries a 401. Check the token's expiry inside
    getAccessToken and refresh before returning. A stale token surfaces as unauthorized.
  • Both action scopes are answered with a GetCardDetails action token. Mint it through
    POST /api/v1/confirmation/signature/verify or the SMS OTP flow in
    Card Details. The token is valid for 5 minutes.
  • Throw new WirexDeclined() from getActionToken when the user backs out. The SDK stops the operation and
    reports it as declined, not as an error.
  • Collect needs no action token. WirexCollectCredentialProvider has only getAccessToken and the identity;
    a full WirexCredentialProvider satisfies it.

Configure Once Per Process

import { WirexCollect, WirexReveal, WirexThreeDS } from '@wirexapp/mobile-sdk';

let configured: Promise<void> | undefined;

export const configureSdk = (testMode: boolean, dev: boolean): Promise<void> => {
  configured ??= (async () => {
    const logging = dev ? ('debug' as const) : ('warn' as const);
    const shared = { testMode, chainId: '<chain id>', credentials, logging };
    await Promise.all([
      WirexReveal.configure(shared),
      WirexCollect.configure(shared),
      WirexThreeDS.configure({ testMode, logging }),
    ]);
  })().catch((error: unknown) => { configured = undefined; throw error; });
  return configured;
};
  • Cache the configure promise and clear the cache on failure, so a later attempt retries.
  • Derive testMode from your runtime environment when your app switches backends at runtime.

Identity Changes

The identity headers are read once at configure time and then held natively. Push the new identity
whenever the signed-in user or wallet changes:

WirexReveal.setIdentity(userEmail, userWallet);
WirexCollect.setIdentity(userEmail, userWallet);

Call it only on a real change — a store that updates often otherwise turns into bridge traffic.


Screen Protection

Revealed values and the card form are drawn in a capture-proof layer: FLAG_SECURE on Android, a secure
layer on iOS. The iOS Simulator renders that layer blank.

Important: dangerouslyDisableScreenProtection: true on configure(...) — and on
WirexThreeDS.configure(...) for the challenge — turns this off for simulator development and automated
UI tests. It defeats PCI screen-capture protection. Never ship it enabled.


Quickstart

One credential provider, both card surfaces configured once, a top-up card collected and an issued card
revealed. Each surface is covered in detail on its own page.

import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Button, View } from 'react-native';
import {
  WirexCollect, WirexCollectFormView, WirexDeclined, WirexReveal, WirexRevealFieldView,
  type WirexCollectFormHandle, type WirexCollectState, type WirexCredentialProvider,
  type WirexRevealSession,
} from '@wirexapp/mobile-sdk';

declare const auth: {
  freshAccessToken(): Promise<string>;
  confirmStepUp(): Promise<boolean>;
  mintActionToken(): Promise<string>;   // GetCardDetails, via POST /api/v1/confirmation/signature/verify
  walletAddress(): string;
};
declare const backend: { topUp(externalCardId: string): Promise<void> };

const credentials: WirexCredentialProvider = {
  getAccessToken: () => auth.freshAccessToken(),
  async getActionToken() {
    if (!(await auth.confirmStepUp())) throw new WirexDeclined();
    return auth.mintActionToken();
  },
  get userEmail() { return undefined; },
  get userWallet() { return auth.walletAddress(); },
};

let configured: Promise<void> | undefined;
const configureSdk = (): Promise<void> => {
  configured ??= (async () => {
    const shared = { testMode: true, chainId: '<chain id>', credentials };
    await Promise.all([WirexReveal.configure(shared), WirexCollect.configure(shared)]);
  })().catch((error: unknown) => { configured = undefined; throw error; });
  return configured;
};

export function AddCard() {
  const form = useRef<WirexCollectFormHandle | null>(null);
  const [state, setState] = useState<WirexCollectState | null>(null);
  useEffect(() => { void configureSdk(); }, []);

  const submit = async () => {
    if (!form.current || !state?.isComplete) return;
    const token = await form.current.tokenizeTopUp({
      billingDetails: { line1: '1 High Street', zipCode: 'N1 9GU', city: 'London', country: 'GB' },
    });
    await backend.topUp(token.cardId);   // estimate + execute with external_card_id
  };

  return (
    <View>
      <WirexCollectFormView profile="topup" onReady={(h) => { form.current = h; }} onChange={setState} style={{ width: '100%' }} />
      <Button title="Add card" disabled={!state?.isComplete} onPress={() => void submit()} />
    </View>
  );
}

export function CardNumber({ cardId }: { cardId: string }) {
  const [session, setSession] = useState<WirexRevealSession | null>(null);
  const [height, setHeight] = useState<number | undefined>();

  useEffect(() => {
    let current: WirexRevealSession | null = null;
    void configureSdk()
      .then(() => WirexReveal.createSession({ cardId, fields: ['pan'], cardholderName: 'ALEX GREY' }))
      .then((s) => { current = s; setSession(s); void s.load(); });
    return () => { void current?.destroy(); };
  }, [cardId]);

  if (!session) return null;
  return <WirexRevealFieldView session={session} field="pan" onNaturalHeight={setHeight} style={{ height }} />;
}

Did this page help you?