Skip to main content

Events and tokenization

The SDK delivers events through SecureFieldsDelegate as the user interacts with the card form. Events contain field metadata — never raw card values. When the form is complete, call submit() to tokenize and receive a vault_form_token.

Implement the delegate

Adopt SecureFieldsDelegate in your view controller. Four methods are required — secureFieldsDidTokenize, secureFieldsDidFail, secureFieldsBrandsDetected, and secureFieldsFormValidityChanged. The rest (secureFieldsBrandSelected, secureFieldsContentChanged, secureFieldsFocusChanged, secureFieldsScreenshotDetected, secureFieldsBinLookupFailed) have default no-op implementations — override only what you need.

extension CheckoutViewController: SecureFieldsDelegate {

func secureFieldsFormValidityChanged(_ isValid: Bool) {
payButton.isEnabled = isValid
}

func secureFieldsDidTokenize(_ result: TokenizationResult) {
// success path — see Handle the tokenization result below
}

func secureFieldsDidFail(_ error: SecureFieldsError) {
// error path — see Error reference below
}
}

Track form validity

secureFieldsFormValidityChanged(_:) fires whenever the aggregate form validity changes. Use it to enable or disable your submit button.

func secureFieldsFormValidityChanged(_ isValid: Bool) {
payButton.isEnabled = isValid

// Query individual field state without reading values:
let panValid = secureFields.isFieldValid(.pan)
let cvvValid = secureFields.isFieldValid(.cvv)
let expValid = secureFields.isFieldValid(.expDate)

// Character count for PAN (useful for progress indicators):
let digitCount = secureFields.panDigitCount

// PAN/CVV lengths currently expected, as driven by BIN lookup and brand selection:
let cvvLengths = secureFields.expectedLengths(for: .cvv) // e.g. [4] under AMEX
}

isFieldValid, isFieldFocused, hasFieldContent, panDigitCount, and expectedLengths(for:) never expose raw card data — only derived metadata.

Form validity covers the fields configured in SecureFieldsConfig.fields — by default the PAN, CVV, and expiry date; on a CVV-only form, the CVV alone. The cardholder name is optional at tokenization and excluded by default — pass requiresHolderName: true in SecureFieldsConfig to make it count toward secureFieldsFormValidityChanged and the submit() completeness check.

Track individual field state

secureFieldsFocusChanged(field:isFocused:) fires on focus and blur. secureFieldsContentChanged() fires on any keystroke in any field.

func secureFieldsFocusChanged(field: SecureField, isFocused: Bool) {
let borderView = containerView(for: field)
borderView.layer.borderColor = isFocused
? UIColor.systemBlue.cgColor
: UIColor.separator.cgColor
}

func secureFieldsContentChanged() {
// Fires on any keystroke in any field.
// Useful to update auxiliary UI without querying individual fields.
}

Handle brand detection

secureFieldsBrandsDetected(_:) fires when the BIN lookup returns results (≥8 digits typed) or when the card number drops below 8 digits (empty array).

func secureFieldsBrandsDetected(_ brands: [CardBrand]) {
// brands — e.g. [.visa] or [.carteBancaire, .visa] for co-branded cards,
// ordered by your config.brands preference
brandImageView.image = brands.first.map { brandImage($0) }
brandImageView.isHidden = brands.isEmpty
}

Brands are reported in the order of your configured brands list — your order expresses a commercial preference, and the first matching brand is pre-selected for co-branded cards.

Name the brand yourself

selectBrand(_:) names the card's brand from your own knowledge or UI — the counterpart of setBrandSelection on Android. On a CVV-only form it is how the SDK learns which CVV length to expect, since there is no PAN to look up: the field narrows to that brand's length (4 digits for Amex, 3 otherwise), expectedLengths(for: .cvv) reflects it, and a typed CVV of the wrong length turns invalid rather than being truncated. On a full form it acts like a tap on the brand selector chip and is refused for a brand the BIN lookup did not detect. selectedBrand reads the current choice.

// CVV-only form for a stored Amex card
secureFields.selectBrand(.amex)
secureFields.expectedLengths(for: .cvv) // [4]

Co-branded card selector

panContainer shows a brand picker automatically when two or more brands are detected for the same PAN. secureFieldsBrandSelected(_:) fires when the user makes a choice.

func secureFieldsBrandSelected(_ brand: CardBrand) {
// Fires when the user picks a brand from the in-PAN brand selector, or when you call selectBrand(_:).
cvvLabel.text = brand == .oney ? "Date of birth" : "CVV"
}

If a BIN lookup returns brands not in your configured brands list, they are filtered out. If no allowed brand is detected, the selector is not shown and secureFieldsBrandsDetected([]) fires.

CVV is kept — never truncated — when the expected length changes

If the user typed a 4-digit CVV under one brand and the detected/selected brand expects 3 digits (or vice versa), the typed value is kept and the field becomes invalid until the user corrects it. The SDK never silently truncates or clears a typed CVV on a brand change — matching the web SDK. The exception is switching to or from the Oney date-of-birth mode, which clears the field.

Handle BIN lookup failures

secureFieldsBinLookupFailed(_:) fires when a BIN lookup request fails (network error, HTTP error, or undecodable response). This is distinct from secureFieldsBrandsDetected([]), which means the server successfully answered "no brand". Brand state is left untouched and the lookup retries on the next PAN change — use this to degrade gracefully instead of leaving the user with a form that never detects anything:

func secureFieldsBinLookupFailed(_ error: SecureFieldsError) {
brandHintLabel.text = "Card network detection unavailable — check your connection."
}

Handle errors

secureFieldsDidFail(_:) fires when submit() encounters a problem:

func secureFieldsDidFail(_ error: SecureFieldsError) {
payButton.isEnabled = secureFields.isFieldValid(.pan)
&& secureFields.isFieldValid(.cvv)
&& secureFields.isFieldValid(.expDate)

switch error {
case .fieldsIncomplete:
showAlert("Please complete all card fields.")
case .networkError(let underlying):
showAlert("Network error: \(underlying.localizedDescription)")
case .apiError(let message, let statusCode):
showAlert("Payment error (\(statusCode)): \(message)")
case .invalidResponse:
showAlert("Unexpected response from the server.")
}
}

Handle screenshot detection

secureFieldsScreenshotDetected() fires immediately after the system saves a screenshot. The OS does not allow apps to prevent screenshots, but you can limit the exposure window:

func secureFieldsScreenshotDetected() {
secureFields.clearFields()
showAlert("Screenshot detected. For your security, please re-enter your card details.")
}

Submit the form

Call submit() when the user taps Pay. The SDK validates all fields internally and fires secureFieldsDidTokenize or secureFieldsDidFail on your delegate.

@objc func payTapped() {
payButton.isEnabled = false
loadingIndicator.startAnimating()
secureFields.submit()
}

submit() is a no-op and fires secureFieldsDidFail(.fieldsIncomplete) if any required field is invalid — it does not make a network request in that case. Disabling the Pay button on invalid state is belt-and-suspenders, not strictly required.

Submit with saveToken

Pass saveToken: true to persist the card for future payments:

secureFields.submit(saveToken: true)

The vault stores the card and returns the same vaultFormToken. Your backend can use the token for subsequent charges without re-entering the card.

Submit a CVV-only form

On a form configured with fields: .cvvOnly (see Customize), call submit() with no arguments. The request carries the CVV alone — no card block, no network — and selectedNetwork / saveToken are ignored with a console warning, since both describe a card block the request does not have.

secureFields.submit()

The result carries the vaultFormToken as usual, with bin, lastFourDigits and selectedNetwork set to nil.

Handle the tokenization result

secureFieldsDidTokenize(_:) delivers a TokenizationResult:

func secureFieldsDidTokenize(_ result: TokenizationResult) {
loadingIndicator.stopAnimating()

print("Token: \(result.vaultFormToken)")
print("BIN: \(result.bin ?? "—")") // first 8 digits — never the full PAN
print("Last four: \(result.lastFourDigits ?? "—")")
print("Brands: \(result.detectedBrands)")

// Send vaultFormToken to your backend — never send raw card data
sendTokenToBackend(result.vaultFormToken)
}
FieldDescription
vaultFormTokenOpaque server-side token — send to your backend
binFirst 8 digits of the PAN (safe to display). nil on a CVV-only form
lastFourDigitsLast 4 digits of the PAN (safe to display). nil on a CVV-only form
detectedBrandsCard networks detected by the BIN lookup. Empty on a CVV-only form
selectedNetworkThe network submitted as selected_network. nil on a CVV-only form
birthDateOney only — the date of birth entered, yyyy-MM-dd

bin and lastFourDigits are nil after a CVV-only tokenization: the vault stored a cryptogram against a card it already holds and echoes no card block.

Clear fields

Call clearFields() to wipe all card data from memory and reset the form:

@objc func clearTapped() {
secureFields.clearFields()
resultLabel.text = nil
payButton.isEnabled = false
}

clearFields() zeroes all internal field buffers, cancels any pending BIN lookup, resets the brand selector, and fires secureFieldsBrandsDetected([]) and secureFieldsFormValidityChanged(false) on your delegate.

To clear a single field — for a per-field erase button — use clearField(_:). It runs the same reformat/validate path as user typing and leaves the other fields untouched; clearing .pan also resets brand detection once the digit count drops below the BIN threshold:

secureFields.clearField(.cvv)

Delegate reference

MethodRequiredWhen it fires
secureFieldsDidTokenize(_:)Yessubmit() succeeds
secureFieldsDidFail(_:)Yessubmit() fails
secureFieldsFormValidityChanged(_:)YesAggregate form validity changes
secureFieldsBrandsDetected(_:)YesBIN lookup returns or clears results
secureFieldsBrandSelected(_:)NoUser picks a brand from the co-branded selector, or you call selectBrand(_:)
secureFieldsContentChanged()NoAny keystroke in any field
secureFieldsFocusChanged(field:isFocused:)NoA field gains or loses focus
secureFieldsScreenshotDetected()NoThe system saves a screenshot while the form is visible
secureFieldsBinLookupFailed(_:)NoA BIN lookup request fails (network/HTTP/decode error) — distinct from a "no brand" answer

Error reference

CaseTrigger
.fieldsIncompletesubmit() called while one or more required fields are invalid
.networkError(Error)URLSession transport failure (no network, timeout, cancelled)
.apiError(message:statusCode:)Non-2xx HTTP response from the vault API
.invalidResponseResponse could not be decoded