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 |
|---|---|
| iOS 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 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
| 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 | 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 |
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 fromoutcome.status.
Updated about 3 hours ago

