Reveal Card Details
Show a cardholder their own PAN, expiry, CVV and PIN in sealed native rows that never pass values to your app code.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Android SDK | Surfaces, isolation model and flows |
| SDK Setup | create(...), credential provider, screen protection |
| Card Details | Confirmation methods and action-token rules |
| Card Issuance | How the card id is produced |
Overview
WirexReveal opens a session for one card; each WirexRevealFieldView is a row you add to your layout and
the SDK draws the value into. The session calls the same endpoints documented in
Card Details:
| Field | Endpoint the SDK calls |
|---|---|
PAN, EXPIRY | POST /api/v1/cards/{cardId}/details |
CVV | POST /api/v1/cards/{cardId}/cvv |
PIN | POST /api/v1/cards/{cardId}/pin |
CARDHOLDER_NAME | None — drawn from the cardholderName you pass, never fetched |
Before the calls, the SDK asks your credential provider for an action token — once per scope per operation
(CARD_DATA, PIN). Return a GetCardDetails token — see
Credential Provider.
Virtual cards have no PIN.
POST /api/v1/cards/{cardId}/pinrejects a virtual retail card. OmitPIN
fromfieldsfor a virtual card.
Step 1: Create a Session and Add the Rows
class RevealScreen(private val activity: Activity, credentials: WirexCredentialProvider) {
private val scope = CoroutineScope(Dispatchers.Main)
private val reveal: WirexReveal = WirexReveal.create(credentials = credentials, testMode = true, chainId = "<chain id>")
?: error("this build has no endpoints for that environment")
fun show(container: LinearLayout, cardId: String) {
val fields = listOf(WirexRevealField.PAN, WirexRevealField.EXPIRY, WirexRevealField.CVV, WirexRevealField.PIN)
val session = reveal.session(cardId = cardId, fields = fields, cardholderName = "ALEX GREY")
val strings = WirexRevealStrings(fieldLabels = mapOf(WirexRevealField.PAN to "Card number"))
for (field in fields) {
val row = WirexRevealFieldView(activity)
row.bind(session, field, strings = strings)
container.addView(row, MATCH_PARENT, WRAP_CONTENT)
}
session.onChange = { snapshot ->
activity.runOnUiThread { render(snapshot) }
}
scope.launch {
try { session.load() } catch (_: WirexDeclined) { /* the user declined the step-up */ }
}
}
}| Parameter | Type | Description |
|---|---|---|
cardId | String | The issued card's id. A malformed id fails with reason INVALID_CARD_ID |
fields | List<WirexRevealField> | Rows to load: CARDHOLDER_NAME, PAN, EXPIRY, CVV, PIN. Drives which endpoints are called |
cardholderName | String | Drawn as-is in the CARDHOLDER_NAME row |
Trap:
onChangeis called on any thread. Hop to the main thread before touching views.
Trap: a row draws its value at its natural height. Use
WRAP_CONTENTorrow.naturalHeightPx(widthPx).
A box shorter than the content clips the value.
One session per open details view. The session holds the retrieved values natively; release it when the view
closes, and call row.release() to drop the row's observer.
Session State
| Property | Values |
|---|---|
snapshot.state | WirexRevealState.Idle, Loading, Ready, Failed(error) |
snapshot.outcome(field) | Per-field outcome |
snapshot.loaded / snapshot.failed | Fields in that outcome |
load()is asuspendfunction and idempotent. Request failures are reported in the snapshot; it
throws onlyWirexDeclined.- One failed field does not hide the others. The row shows only that it failed; log the failed fields with the
error code, because the row never says why. - Values are wiped 120 seconds after the first load;
session.clear()wipes them earlier.
Masking
PAN, CVV and PIN start masked.
| Member | Description |
|---|---|
reveal() | Unmasks the row. Call it right after load() when the user has just confirmed the step-up |
mask() | Masks the row again |
isMasked | Current state |
onRevealRefused | Fires when reveal() is refused because screen protection is not active on the window |
masksWhenDetached | A row re-masks when detached |
onCopy | Fires when the user copies a copyable field |
A row masked because the app left the foreground draws again on return while the session still holds the
value. Past the 120-second lifetime the row stays masked until load() runs again.
Sign Once
The user confirms a reveal before the details view opens. Mint the action token at that point, hand it to the
provider one-shot, and revoke it if the flow is abandoned — otherwise the SDK asks for a second confirmation
for the same action:
class ProductionCredentials(private val auth: AuthSession) : WirexCredentialProvider {
@Volatile private var offered: Pair<WirexActionScope, String>? = null
fun offerStepUpToken(scope: WirexActionScope, token: String) { offered = scope to token }
fun revokeOfferedStepUpToken() { offered = null }
override suspend fun accessToken(): String {
val current = auth.accessToken()
if (current != null && current.second - System.currentTimeMillis() > 30_000) return current.first
return auth.refreshAccessToken() ?: throw WirexError(WirexError.Code.UNAUTHORIZED, retryable = false)
}
override suspend fun actionToken(scope: WirexActionScope): String {
offered?.takeIf { it.first == scope }?.let { offered = null; return it.second }
return auth.signAction(scope)
}
override val userEmail: String? get() = null
override val userWallet: String? get() = auth.walletAddress
}Errors
| Symptom | Cause | Resolution |
|---|---|---|
Row will not unmask, onRevealRefused fires | FLAG_SECURE not active — the row is in a Dialog or PopupWindow | Pass the window with secureWindow(window) |
| Row blank | Box shorter than the content | WRAP_CONTENT or naturalHeightPx(widthPx) |
Failed with code UNAUTHORIZED | Stale bearer — the SDK never retries a 401 | Refresh inside accessToken() |
Failed with reason ACTION_TOKEN_REJECTED | The API rejected the action token — see the action_token errors in Card Details | Mint a fresh GetCardDetails token |
load() throws WirexDeclined | actionToken threw WirexDeclined | None — the user backed out |
PIN failed | Virtual card, or card not Active | Omit PIN for virtual cards |
Full error handling: Errors, Logging and Troubleshooting.
Updated about 3 hours ago

