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
iOS 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 in a WirexThreeDSView 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 is created with testMode and, for Simulator development only,
dangerouslyDisableScreenProtection.


Step 1: Mount the View and Start

import UIKit
import WirexMobileSDK

final class ThreeDSViewController: UIViewController {
    private let threeDS = WirexThreeDS(testMode: true)
    private let challengeView = WirexThreeDSView()
    private var session: WirexThreeDSSession?

    func startStepUp(threeDsUrl: String) {
        view.addSubview(challengeView)
        challengeView.frame = view.bounds
        let session = threeDS.session(orchestratorUrl: threeDsUrl)
        self.session = session
        session.onChange = { [weak self] snapshot in self?.handle(snapshot) }
        challengeView.bind(session)
        session.start()
    }

    func cancel() {
        session?.cancel()
        challengeView.unbind()
    }
}

Trap: add and bind the view before start(). A challenge can appear within one frame of the page
asking for it.

Keep the view full-size for the life of the session. cancel() settles the session with .unknown.


Step 2: 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.codeoriginRefused, 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 result.pending is not an error — the payment is not settled yet
final class StepUpWatcher {
    private var settled = false

    func watch(_ session: WirexThreeDSSession,
               onError: @escaping (String) -> Void,
               onDismissed: @escaping () -> Void,
               onComplete: @escaping (WirexThreeDSStatus) -> Void,
               onChallengeVisible: @escaping (Bool) -> Void) {
        session.onChange = { [weak self] snapshot in
            guard let self else { return }
            onChallengeVisible(snapshot.isChallengeVisible)
            guard snapshot.state == .settled, let outcome = snapshot.outcome, !self.settled else { return }
            self.settled = true
            if let error = outcome.error { onError(error.message ?? error.code.rawValue); return }
            if outcome.status == .unknown { onDismissed(); return }
            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?