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 |
|---|---|
| iOS SDK | Surfaces, isolation model and flows |
| SDK Setup | create(...), credential provider, action token scopes |
| 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 place in 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 |
.cardholderName | None — drawn from the cardholderName you pass |
Before the calls, the SDK asks your credential provider for an action token (.cardData or .pin). Return a
GetCardDetails token — see Credential Provider.
Virtual cards have no PIN.
POST /api/v1/cards/{cardId}/pinrejects a virtual retail card. Omit.pin
fromfieldsfor a virtual card.
Step 1: Create a Session
import UIKit
import WirexMobileSDK
final class RevealViewController: UIViewController {
private let credentials = AppCredentials()
private var session: WirexRevealSession?
override func viewDidLoad() {
super.viewDidLoad()
guard let reveal = WirexReveal.create(credentials: credentials, testMode: true, chainId: "<chain id>") else {
return
}
do {
let session = try reveal.session(cardId: cardId, fields: [.pan, .expiry, .cvv, .pin], cardholderName: "ALEX GREY")
self.session = session
mountRows(for: session)
Task { try await session.load() }
} catch {
print(error)
}
}
}| Parameter | Type | Description |
|---|---|---|
cardId | String | The issued card's id. A malformed id throws WirexError with reason invalidCardId |
fields | [WirexRevealField] | Rows to load: .cardholderName, .pan, .expiry, .cvv, .pin. Drives which endpoints are called |
cardholderName | String | Drawn as-is in the .cardholderName row |
One session per open details view. The session holds the retrieved values natively; release it when the view
closes.
Step 2: Place the Rows
let panRow = WirexRevealFieldView()
panRow.bind(session: session, field: .pan)
view.addSubview(panRow)
panRow.frame = CGRect(x: 16, y: 100, width: view.bounds.width - 32, height: panRow.naturalHeight)Trap: a row draws its value at its natural height. Size the host box to
naturalHeight, or let Auto
Layout useintrinsicContentSize. A box shorter than the content clips the value.
Step 3: Load and Observe
session.onChange = { snapshot in
switch snapshot.state {
case .idle, .loading, .ready: break
case .error(let error): print(error.code, error.retryable)
@unknown default: break
}
}
Task { try await session.load() }| Property | Values |
|---|---|
snapshot.state | .idle, .loading, .ready, .error(WirexError) |
snapshot.outcome(for:) | Per-field outcome |
snapshot.loaded / snapshot.failed | Fields in that outcome |
load()is idempotent and reports request failures through the snapshot. It throws only
WirexDeclined(the user backed out of the step-up) orCancellationError.- 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. session.clear()wipes loaded values.
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 |
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 session's 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:
final class ProductionCredentials: WirexCredentialProvider, @unchecked Sendable {
private let auth: AuthSession
private var offered: (scope: WirexActionScope, token: String)?
init(auth: AuthSession) { self.auth = auth }
func offerStepUpToken(_ scope: WirexActionScope, _ token: String) { offered = (scope, token) }
func revokeOfferedStepUpToken() { offered = nil }
func accessToken() async throws -> String {
if let current = await auth.accessToken(), current.expiresAt.timeIntervalSinceNow > 30 { return current.token }
if let refreshed = await auth.refreshAccessToken() { return refreshed }
throw WirexError(code: .unauthorized, retryable: false)
}
func actionToken(scope: WirexActionScope) async throws -> String {
if let offered, offered.scope == scope { self.offered = nil; return offered.token }
return try await auth.signAction(scope)
}
var userEmail: String? { nil }
var userWallet: String? { auth.walletAddress }
}Errors
| Symptom | Cause | Resolution |
|---|---|---|
| Row blank on the Simulator | Secure layer renders blank on the Simulator | Test on a device, or dangerouslyDisableScreenProtection in development only |
| Row blank on device | Box shorter than naturalHeight | Size the box to naturalHeight |
.error with code unauthorized | Stale bearer — the SDK never retries a 401 | Refresh inside accessToken() |
.error with reason actionTokenRejected | The API rejected the action token — see the action_token errors in Card Details | Mint a fresh GetCardDetails token |
load() throws WirexDeclined | actionToken(scope:) 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

