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:

GuideWhy
iOS SDKSurfaces, isolation model and flows
SDK Setupcreate(...), credential provider, action token scopes
Card DetailsConfirmation methods and action-token rules
Card IssuanceHow 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:

FieldEndpoint the SDK calls
.pan, .expiryPOST /api/v1/cards/{cardId}/details
.cvvPOST /api/v1/cards/{cardId}/cvv
.pinPOST /api/v1/cards/{cardId}/pin
.cardholderNameNone — 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}/pin rejects a virtual retail card. Omit .pin
from fields for 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)
        }
    }
}
ParameterTypeDescription
cardIdStringThe 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
cardholderNameStringDrawn 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 use intrinsicContentSize. 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() }
PropertyValues
snapshot.state.idle, .loading, .ready, .error(WirexError)
snapshot.outcome(for:)Per-field outcome
snapshot.loaded / snapshot.failedFields 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) or CancellationError.
  • 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.

MemberDescription
reveal()Unmasks the row. Call it right after load() when the user has just confirmed the step-up
mask()Masks the row again
isMaskedCurrent state
masksWhenDetachedA row re-masks when detached
onCopyFires 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

SymptomCauseResolution
Row blank on the SimulatorSecure layer renders blank on the SimulatorTest on a device, or dangerouslyDisableScreenProtection in development only
Row blank on deviceBox shorter than naturalHeightSize the box to naturalHeight
.error with code unauthorizedStale bearer — the SDK never retries a 401Refresh inside accessToken()
.error with reason actionTokenRejectedThe API rejected the action token — see the action_token errors in Card DetailsMint a fresh GetCardDetails token
load() throws WirexDeclinedactionToken(scope:) threw WirexDeclinedNone — the user backed out
.pin failedVirtual card, or card not ActiveOmit .pin for virtual cards

Full error handling: Errors, Logging and Troubleshooting.


Did this page help you?