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
Flutter 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.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

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

The three endings are not interchangeable:

EndingMeaningHandling
outcome.error presentThe challenge did 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, failedThe challenge resultpending 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 from outcome.status.


Did this page help you?