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
Android 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 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 with ORIGIN_REFUSED.
  • onChange is invoked immediately with the current snapshot when set.
  • The view stays invisible until the challenge is shown and applies FLAG_SECURE while visible. Its close
    control calls session.cancel(), which settles with UNKNOWN.

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

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.codeORIGIN_REFUSED, DECLINED, UNAVAILABLE

The three endings are not interchangeable:

EndingMeaningHandling
outcome.error presentThe challenge did not runShow 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(
    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 from outcome.status.


Did this page help you?