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 |
|---|---|
| Flutter 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.instance 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.instance.configure takes testMode, logging, pageLoadTimeout (Duration) and
dangerouslyDisableScreenProtection.
Step 1: Mount the View
import 'package:flutter/widgets.dart';
import 'package:wirex_mobile_sdk/wirex_mobile_sdk.dart';
class StepUpOverlay extends StatelessWidget {
const StepUpOverlay({required this.session, super.key});
final WirexThreeDSSession session;
@override
Widget build(BuildContext context) => Positioned.fill(
child: WirexThreeDSView(
session: session,
strings: const WirexThreeDSStrings(title: 'Verify your card', close: 'Close', loading: 'Loading'),
),
);
}Keep the view mounted full-size for the life of the session. It ignores pointers until a challenge is visible.
Step 2: Start the Session
Future<WirexThreeDSOutcome> runStepUp(String threeDsUrl, void Function(WirexThreeDSSession) mount) async {
await WirexThreeDS.instance.configure(testMode: true);
final WirexThreeDSSession session = await WirexThreeDS.instance.createSession(threeDsUrl);
mount(session);
await session.start();
final WirexThreeDSOutcome 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 | WirexThreeDSState.idle, loading, challenge, settled |
snapshot.isChallengeVisible | true while the issuer page is on screen — hide your own header |
outcome.status | WirexThreeDSStatus.completed, pending, failed, unknown |
outcome.error.code | WirexThreeDSErrorCode.originRefused, declined, unavailable |
The three endings are not interchangeable:
| Ending | Meaning | Handling |
|---|---|---|
outcome.error present | The challenge did 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 |
class StepUpWatcher {
StepUpWatcher({required this.onError, required this.onDismissed, required this.onComplete, required this.onChallengeVisible});
final void Function(String message) onError;
final void Function() onDismissed;
final void Function(WirexThreeDSStatus status) onComplete;
final void Function(bool visible) onChallengeVisible;
bool _settled = false;
void Function() watch(WirexThreeDSSession session) => session.onSnapshot((WirexThreeDSSnapshot snapshot) {
onChallengeVisible(snapshot.isChallengeVisible);
final WirexThreeDSOutcome? outcome = snapshot.outcome;
if (snapshot.state != WirexThreeDSState.settled || outcome == null || _settled) return;
_settled = true;
final WirexThreeDSError? error = outcome.error;
if (error != null) return onError(error.message ?? error.code.name);
if (outcome.status == WirexThreeDSStatus.unknown) return onDismissed();
onComplete(outcome.status);
});
}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

