Skip to main content

Card (Purse vault)

The card method renders the iOS Secure Fields form: card number, expiry, CVV and holder. Card data is tokenized by the Purse vault and never reaches your code.

Setup​

ProductPurseHeadlessCheckout, nothing else
DashboardA card method whose vault is Purse (vault.vendor = purse) activated on the entity, with any acquiring partner
InstallNothing. CardModule is built into PluginRegistry
ProjectA RedirectPresenter if you want 3-D Secure to open. The SDK never presents one you did not pass
KeysNone in the app. The vault tenant travels in the client session
Cards outside the Purse vault

A card method hosted by a PSP's own form (Adyen, Stripe, Checkout.com, a pci_proxy vault…) has no native form and is not rendered by this SDK. Configure the method in redirection mode to offer it through the redirection guide.

Render and pay​

A PaymentElement is a plain object, not a SwiftUI view: build it once when the shopper selects the method and keep it. Rebuilding it on every body evaluation restarts the form and loses what was typed.

@MainActor
final class Elements {
private var cache: [String: PaymentElement] = [:]

/// The generation changes when `setSession` replaces the item; the key follows it.
func element(
for item: PaymentItem,
in checkout: PurseHeadlessCheckout,
listener: any PaymentElementListener,
redirect: (any RedirectPresenter)?,
appReturnURL: URL? = nil // required for Klarna, ignored by every other method
) -> PaymentElement {
let key = "\(item.id):\(checkout.itemGenerations[item.id] ?? 0)"
if let existing = cache[key] { return existing }
let element = checkout.paymentElement(
for: item, listener: listener, redirect: redirect, appReturnURL: appReturnURL
)
cache[key] = element
return element
}
}
struct CardSection: View {
@ObservedObject var checkout: PurseHeadlessCheckout
let method: PrimaryMethod
@ObservedObject var element: PaymentElement

var body: some View {
VStack(spacing: 16) {
PaymentElementView(element)
Button("Pay") { element.submit() }
.disabled(!element.isReady)
}
.task {
try? checkout.setPrimary(method)
await element.prepare()
}
}
}

An empty or invalid form is never payable: element.isReady and isPaymentFulfilled stay false until every field validates. submit() tokenizes the card, then runs /init and /validate, then presents 3-D Secure through the RedirectPresenter you passed. It is synchronous and returns a Task you may await; calling it again while one is in flight returns the same task rather than starting a second payment.

checkout.schemeAssets(for: method) gives the accepted schemes as (url, label) pairs, to show above the form. The URLs are SVGs on Purse's CDN: loading and rendering them is yours, the SDK ships no images.

Outcomes​

OutcomeWhen
completed(ValidationResult)/validate answered without a redirect. result.status is the API's verdict
returnedFromRedirect(url:)The shopper came back from 3-D Secure. Confirm the payment status on your backend
canceledThe 3-D Secure page was dismissed
failed(PurseError)Tokenization refused, /init or /validate failed. error.code says which

With no redirect: presenter, a /validate that asks for 3-D Secure reports completed instead: the SDK takes it that you will follow the redirect yourself, and checkout.pendingRedirect(for:) hands you the action to follow.

Save the card​

When the method allows it (save_token_mode PREFER or PREFER_NO in the dashboard), canBeRegistered is true and the shopper can keep the card for the next session:

if method.canBeRegistered {
try checkout.register(method, registered: true, name: "holiday card")
}
let decision = checkout.registrationOf(method) // readable before and after submit

register throws PurseCheckoutError.walletOperationFailed with METHOD_NOT_REGISTERABLE when the method cannot save cards, or INVALID_TOKEN_NAME / TOKEN_NAME_EXISTS on a bad label (1–25 alphanumeric or space characters, unique across saved tokens). Saving needs a wallet session in the client session: see Saved cards and accounts.

Customize the form​

Locale, texts and theme come from the PluginOptions you pass to PurseHeadlessCheckout(widgetData:…, options:), with a PurseTheme built from native UIColor, UIFont and CGFloat, not CSS strings. See Customization.

Troubleshooting​

SymptomCause
The Pay button never enablesA field is invalid. The form shows which; element.isReady mirrors it
The form loses its input as the screen redrawsA new PaymentElement is built on every body evaluation. Cache it on the item's "{id}:{generation}" key
The form has no height in a VStackPaymentElementView sizes itself from the partner view; do not give it a .frame(height:) unless you want to override that
failed with API_ERROR, message creditcard init not successful/init was refused for this item. Check the method's configuration on the entity
The form does not initialize at allThe session carries no vault tenant: the card method is not vaulted by Purse
3-D Secure never opens, the outcome is completedNo redirect: presenter was passed to paymentElement(for:)
A 3-D Secure page refuses to renderSome issuers block embedded web views. The default presenter uses ASWebAuthenticationSession for a GET precisely to avoid this; check you have not forced prefersInAppWebView