Method states
A method the session offers but that cannot be used right now stays a PaymentItem with a non-nil disabled: DisabledState?. The state has a code, a displayable message, and five boolean behaviours that tell you how to render the method and whether it will come back. The vocabulary is the web SDK's DisabledState, so all three platforms react the same way.
guard let state = item.disabled else {
renderSelectable(item)
return
}
if state.isFiltered {
return // never in paymentMethods: you will not see it
} else if state.isTechnicallyDisabled {
hideOrGreyOut(state.message) // no UI can be mounted
} else if state.isRecoverable {
dim(state.message) // keep the element alive, it comes back
} else {
greyOut(state.message) // amount-driven or submitting, lifts by itself
}
The five behaviours
| Behaviour | Property | Meaning | Expected reaction |
|---|---|---|---|
| Filtered | isFiltered | The item is removed from paymentMethods, paymentTokens and savedSecondaryTokens altogether | Nothing to render. The method never existed for this session |
| Technically disabled | isTechnicallyDisabled | A hard runtime failure. paymentElement(for:) builds an element with no UI that reports .failed(methodDisabled) on submit() | Grey the row out with message, or hide it. Final for the session |
| Requires UI clear | requiresUIClear | Any mounted UI for the item should be torn down | Drop your PaymentElement and stop rendering its PaymentElementView |
| Recoverable | isRecoverable | Broken-for-now, not broken-for-good | Dim the row, keep the element alive. It is re-evaluated on the next recompute |
| Recomputable | isRecomputable | Re-evaluated whenever the amount left to pay changes | Nothing: the state lifts by itself when the split moves. Everything outside this group is final |
The codes
DisabledState.Code declares 11 codes. The web SDK has fourteen; the last three describe DOM situations and do not exist natively, so they are absent here as they are on Android.
| Code | Cause | Filtered | Technical | Clear UI | Recoverable | Recomputable | Expected action |
|---|---|---|---|---|---|---|---|
METHOD_UNSTABLE | The platform suspended the method for this session (configurations.stable == false) | ✓ | None: not listed | ||||
AMOUNT_CONDITION_UNREACHABLE | payment_min_amount exceeds the whole order: no split could ever enable it | ✓ | None: not listed | ||||
PLUGIN_INCOMPATIBLE | A plugin's ensureClientCompatibility rejected this client or session | ✓ | None: the method disappears from the list. Set at creation when a plugin refuses the device, e.g. Apple Pay where PKPaymentAuthorizationController.canMakePayments() is false | ||||
FATAL_ERROR_BEFORE_RENDER | A plugin failed before it could mount any UI | ✓ | None: not listed. Declared, and not set by the SDK today | ||||
TOKEN_INIT_FAILED | A saved gift card's balance could not be read from the issuer this session | ✓ | ✓ | Show the saved card as unavailable; offer to enter it again | |||
PARTNER_UI_FATAL_ERROR | A partner reported an unrecoverable failure while mounted | ✓ | ✓ | Grey out or hide the method with message; offer the others | |||
PARTNER_UI_RECOVERABLE_ERROR | A partner reported a failure it expects to come back from | ✓ | ✓ | Dim the method, keep its element alive; a later ready event from the partner clears it | |||
MIN_AMOUNT_NOT_MET | What is left for the primary to pay is under the method's minimum (a gift card took too much) | ✓ | ✓ | Grey out with message; removing a gift card lifts it | |||
MAX_AMOUNT_EXCEEDED | What is left to pay is over the method's maximum. Never applies to a secondary: a gift card may hold more than the amount owed | ✓ | ✓ | Grey out; applying a gift card lifts it | |||
PAYMENT_FULFILLED | Gift cards cover the whole amount: no primary is needed | ✓ | ✓ | Grey out every primary; show a Pay button that calls submitPayment() | |||
SUBMITTING | A payment is in flight: every otherwise enabled method is locked until submitPayment returns | Show a progress indicator; lifted whatever the outcome |
The amount-driven states carry their context: .minAmountNotMet(minAmount:restToPay:), .maxAmountExceeded(maxAmount:restToPay:), .amountConditionUnreachable(minAmount:maxAmount:orderAmount:). .partnerUIFatalError(reason:), .partnerUIRecoverableError(reason:) and .fatalErrorBeforeRender(reason:) carry the partner's reason.
Where states come from
| Moment | States set |
|---|---|
Session build (init, setSession) | METHOD_UNSTABLE and AMOUNT_CONDITION_UNREACHABLE from MethodAvailability.buildTimeDisabled, then the amount-driven ones; filtered states are dropped before you see the list |
Every change of remainingAmountToPay | MIN_AMOUNT_NOT_MET, MAX_AMOUNT_EXCEEDED, PAYMENT_FULFILLED recomputed. An item whose current state is not isRecomputable is left untouched |
A saved gift card's background /init | TOKEN_INIT_FAILED on that SavedSecondaryToken |
| Checkout creation, per installed plugin | PLUGIN_INCOMPATIBLE when the plugin's ensureClientCompatibility refuses this device or session |
A partner's plugin event (onPluginEvent) | initFailed or clientError → PARTNER_UI_FATAL_ERROR; partnerError → PARTNER_UI_RECOVERABLE_ERROR if recoverable, else PARTNER_UI_FATAL_ERROR; ready clears a recoverable state |
submitPayment entered and returned | SUBMITTING overlaid on every otherwise enabled item, then lifted. It is a published overlay only, so a real state underneath is never erased |
A partner failure moves the item into a PARTNER_UI_* state, which onPluginEvent announces; if it happens during a payment, the element's listener also gets a PaymentOutcome.failed (Events and monitoring). Only FATAL_ERROR_BEFORE_RENDER is declared and never set by the SDK today; handle it anyway, since the behaviour flags are what your rendering should branch on.
Every transition into a new state is reported to Purse's monitoring as HEADLESS_CHECKOUT_METHOD_DISABLED, at warning level for a technical failure (isTechnicallyDisabled, or FATAL_ERROR_BEFORE_RENDER) and at info level for a nominal one. Only the state's code is sent, never its message, which can quote amounts.
setPrimary(_:) and setPrimaryToken(_:) refuse a disabled item with PurseCheckoutError.methodDisabled (METHOD_DISABLED), whose disabled associated value is this state. Rendering the row from item.disabled avoids the error altogether.