Saved cards and accounts
paymentTokens carries the session's saved instruments: cards on file and PayPal accounts the shopper vaulted earlier. They pay like a primary method, and they are not in paymentMethods: a list built from that alone misses every one of them.
Setup
| Product | PurseHeadlessCheckout; PurseCheckoutPayPal to pay with a saved PayPal account |
| Client session | Created with a wallet session (wallet_session in the widget data) for the shopper. The server-side setup is the same as on the web: Saved payment methods |
| Dashboard | save_token_mode on the card method decides whether new cards can be saved |
| Project, keys | None |
Pay with a saved instrument
ForEach(checkout.paymentTokens, id: \.id) { token in
SavedRow(token: token, onSelect: { select(.paymentToken(token)) })
}
struct SavedTokenSection: View {
@ObservedObject var checkout: PurseHeadlessCheckout
let token: PaymentToken
/// checkout.paymentElement(for: .paymentToken(token), listener: listener, redirect: presenter)
@ObservedObject var element: PaymentElement
var body: some View {
VStack(spacing: 16) {
if element.hasUI { PaymentElementView(element) } // CVV-only field, when required
Button("Pay") { element.submit() }
.disabled(!element.isReady)
}
.task {
try? checkout.setPrimaryToken(token)
await element.prepare()
}
}
}
| Token | hasUI | Gate |
|---|---|---|
Saved card, requiresCVV == true | A CVV-only secure field | isPaymentFulfilled waits for a valid CVV, like a fresh card |
Saved card, paysWithoutCVV == true (network token, or the PSP does not ask) | Nothing | Payable at selection |
Saved PayPal account, isVaultedCard == false | Nothing | Payable at selection; submit() runs its own /init and captures without an approval step |
name, label, brand, maskedPAN, holderName and favorite are there to render the row; checkout.iconURL(for: .paymentToken(token)) gives the brand's badge as an SVG URL on Purse's CDN (schemeAssets(for:) takes a PrimaryMethod, not a token). 3-D Secure may still follow a saved card payment: handle returnedFromRedirect as for any card, which means passing a RedirectPresenter.
Manage the wallet
All async throws, called from the main actor like the rest of the checkout.
try await checkout.editToken(token, name: "work card")
try await checkout.setTokenFavorite(token) // toggles; reverted locally if the call fails
try await checkout.deleteToken(token)
try await checkout.deleteAllTokens()
paymentTokens and savedSecondaryTokens reflect the change without restarting the checkout; deleting the token that was primary clears the split. deleteAllTokens() removes every wallet token, saved gift cards included. Failures are PurseCheckoutError.walletOperationFailed:
| Code | Meaning |
|---|---|
MISSING_WALLET_SESSION | The client session carries no wallet session |
MISSING_TOKEN | The token is not in the wallet anymore |
INVALID_TOKEN_NAME | The label is not 1–25 alphanumeric or space characters |
TOKEN_NAME_EXISTS | Another saved instrument already uses that label |
TOKEN_NOT_REGISTERABLE, METHOD_NOT_REGISTERABLE | Saving is not allowed for this token or method |
editToken also has an overload for a SavedSecondaryToken. There is no deleteToken or setTokenFavorite overload for one: see Gift cards.
Save a new card
Saving a fresh card is decided on the card method before submit: Card, save the card. Saving a gift card: Gift cards, keep the gift card. Creating a PayPal token from the app is not available.
A saved card from a pci_proxy vault has no card plugin on iOS at all: paymentElement(for:) reports METHOD_NOT_IMPLEMENTED rather than rendering a CVV form. Cards vaulted by Purse are fully supported.
Troubleshooting
| Symptom | Cause |
|---|---|
| No saved cards appear | The client session was created without a wallet_session |
| The Pay button never enables on a saved card | requiresCVV is true and the CVV form is not valid yet, or prepare() was never called |
| The row renders with no label and no masked PAN | A vaulted card can arrive with neither. Fall back through name, maskedPAN, brand |
setTokenFavorite flips the star then flips it back | The wallet call failed; the optimistic update is reverted and the error rethrown |
| A saved PayPal account shows a CVV field | It should not: isVaultedCard is false for one, so its element has no UI. A CVV form means the token is a card |