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:
| Guide | Why |
|---|---|
| React Native SDK | Surfaces, isolation model and flows |
| SDK Setup | Environments and screen protection |
| Top-Up Transfer | The 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
| Property | Values |
|---|---|
snapshot.state | 'idle', 'loading', 'challenge', 'settled' |
snapshot.isChallengeVisible | true 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:
| Ending | Meaning | Handling |
|---|---|---|
outcome.error present | The challenge could not run. A refused URL returns already settled with originRefused | Show the error; reconcile the top-up status |
'unknown' without error | The user closed the challenge. The payment can still complete | Reconcile 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 fromoutcome.status.
Updated about 3 hours ago

