3DS Challenge

Present the issuer 3DS challenge returned by a top-up execute in a native view and resolve the flow on its outcome.

Before You Start

Read the following guides before proceeding:

GuideWhy
React Native SDKSurfaces, isolation model and flows
SDK SetupEnvironments and screen protection
Top-Up TransferThe execute call that returns three_ds_state

Overview

POST /api/v1/cards/topup/execute returns three_ds_state when the issuer requires a challenge. Your backend
passes three_ds_state.url to the app; WirexThreeDS renders the challenge and reports the outcome.

3-D Secure takes no credentials. The SDK makes no API call of its own — it only renders the URL you hand
it. WirexThreeDS.configure takes testMode, logging, requestSeconds, resourceSeconds and
dangerouslyDisableScreenProtection.


Step 1: Mount the View

import * as React from 'react';
import { StyleSheet } from 'react-native';
import { WirexThreeDSView, type WirexThreeDSSession } from '@wirexapp/mobile-sdk';

export const StepUpView = ({ session }: { session: WirexThreeDSSession }) => (
  <WirexThreeDSView
    session={session}
    strings={{ title: 'Verify your card', close: 'Close', loading: 'Loading' }}
    style={StyleSheet.absoluteFill}
  />
);

Keep the view mounted full-size for the life of the session.


Step 2: Start the Session

import { WirexThreeDS, type WirexThreeDSOutcome, type WirexThreeDSSession } from '@wirexapp/mobile-sdk';

export async function runStepUp(
  threeDsUrl: string,
  mount: (s: WirexThreeDSSession) => void,
): Promise<WirexThreeDSOutcome> {
  await WirexThreeDS.configure({ testMode: true });
  const session = await WirexThreeDS.createSession(threeDsUrl);
  mount(session);
  await session.start();
  const outcome = await session.settled();
  await session.destroy();
  return outcome;
}

Trap: call start() only after the view is mounted. A challenge can appear within one frame of the
page asking for it.

session.cancel() settles the session with 'unknown'.


Step 3: Resolve the Outcome

PropertyValues
snapshot.state'idle', 'loading', 'challenge', 'settled'
snapshot.isChallengeVisibletrue while the issuer page is on screen — hide your own header
outcome.status'completed', 'pending', 'failed', 'unknown'
outcome.error.code'originRefused', 'declined', 'unavailable'

The three endings are not interchangeable:

EndingMeaningHandling
outcome.error presentThe challenge could not run. A refused URL returns already settled with originRefusedShow the error; reconcile the top-up status
'unknown' without errorThe user closed the challenge. The payment can still completeReconcile the top-up status
'completed', 'pending', 'failed'The challenge result'pending' is not an error — the payment is not settled yet
import type { WirexThreeDSOutcome, WirexThreeDSSession, WirexThreeDSStatus } from '@wirexapp/mobile-sdk';

type StepUpHandlers = {
  onError(message: string): void;
  onDismissed(): void;
  onComplete(status: WirexThreeDSStatus): void;
  onChallengeVisible(visible: boolean): void;
};

export function watchStepUp(session: WirexThreeDSSession, h: StepUpHandlers): () => void {
  let settled = false;
  const settle = (outcome: WirexThreeDSOutcome) => {
    if (settled) return;
    settled = true;
    if (outcome.error) return h.onError(outcome.error.message ?? outcome.error.code);
    if (outcome.status === 'unknown') return h.onDismissed();
    h.onComplete(outcome.status);
  };
  return session.onSnapshot((snapshot) => {
    h.onChallengeVisible(snapshot.isChallengeVisible);
    if (snapshot.state === 'settled' && snapshot.outcome) settle(snapshot.outcome);
  });
}

Important: the 3DS outcome is not the payment result. Settle the top-up from its status in the
Top-Up Transfer flow, not from outcome.status.


Did this page help you?