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) 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
}
isFieldValid, isFieldFocused, hasFieldContent, and panDigitCount never expose raw card data — only derived metadata.
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 [.visa, .carteBancaire] for co-branded cards
brandImageView.image = brands.first.map { brandImage($0) }
brandImageView.isHidden = brands.isEmpty
}
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.
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.
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.
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)
}
| Field | Description |
|---|---|
vaultFormToken | Opaque server-side token — send to your backend |
bin | First 8 digits of the PAN (safe to display) |
lastFourDigits | Last 4 digits of the PAN (safe to display) |
detectedBrands | Card networks detected by the BIN lookup |
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.
Delegate reference
| Method | Required | When it fires |
|---|---|---|
secureFieldsDidTokenize(_:) | Yes | submit() succeeds |
secureFieldsDidFail(_:) | Yes | submit() fails |
secureFieldsFormValidityChanged(_:) | Yes | Aggregate form validity changes |
secureFieldsBrandsDetected(_:) | Yes | BIN lookup returns or clears results |
secureFieldsBrandSelected(_:) | No | User picks a brand from the co-branded selector |
secureFieldsContentChanged() | No | Any keystroke in any field |
secureFieldsFocusChanged(field:isFocused:) | No | A field gains or loses focus |
secureFieldsScreenshotDetected() | No | The system saves a screenshot while the form is visible |
Error reference
| Case | Trigger |
|---|---|
.fieldsIncomplete | submit() 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 |
.invalidResponse | Response could not be decoded |