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 |
|---|---|
| Android 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 automated UI tests only,
dangerouslyDisableScreenProtection.
Step 1: Add the View and Start
class ThreeDSScreen(private val activity: Activity) {
private val threeDS = WirexThreeDS(testMode = true)
private val challengeView = WirexThreeDSView(activity)
private var session: WirexThreeDSSession? = null
fun mount(root: FrameLayout) = root.addView(challengeView, MATCH_PARENT, MATCH_PARENT)
fun start(threeDsUrl: String) {
val next = threeDS.session(threeDsUrl)
session = next
next.onChange = { snapshot -> handle(snapshot) }
challengeView.bind(next)
next.start(activity)
}
fun release() {
challengeView.unbind()
session?.destroy()
}
}threeDS.session(url)never throws. A refused URL returns a session already settled withORIGIN_REFUSED.onChangeis invoked immediately with the current snapshot when set.- The view stays invisible until the challenge is shown and applies
FLAG_SECUREwhile visible. Its close
control callssession.cancel(), which settles withUNKNOWN.
Trap: add and bind the view before
start(activity). A challenge can appear within one frame of the
page asking for it.
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 | ORIGIN_REFUSED, DECLINED, UNAVAILABLE |
The three endings are not interchangeable:
| Ending | Meaning | Handling |
|---|---|---|
outcome.error present | The challenge did not run | 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(
private val onError: (String) -> Unit,
private val onDismissed: () -> Unit,
private val onComplete: (WirexThreeDSStatus) -> Unit,
private val onChallengeVisible: (Boolean) -> Unit,
) {
private var settled = false
fun watch(session: WirexThreeDSSession) {
session.onChange = { snapshot ->
onChallengeVisible(snapshot.isChallengeVisible)
val outcome = snapshot.outcome
if (snapshot.state == WirexThreeDSState.SETTLED && outcome != null && !settled) {
settled = true
val error = outcome.error
when {
error != null -> onError(error.message ?: error.code.name)
outcome.status == WirexThreeDSStatus.UNKNOWN -> onDismissed()
else -> 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

