Skip to main content

Sessions

Follow the session state​

A client session can be initialized many times but pays once. sessionState follows it; the expiry watchdog runs on a server-corrected clock.

val state by checkout.sessionState.collectAsState()
when (state) {
SessionState.READY -> Unit
SessionState.SUBMITTING -> Spinner() // every method reads DisabledState.Submitting meanwhile
SessionState.SUBMITTED -> Unit // submitted: your backend confirms the outcome
SessionState.ERROR -> NewSessionOrRetry() // spent after a decline; retryable only if the API never answered
SessionState.EXPIRED, SessionState.TERMINATED -> NewSession()
}

ERROR follows a declined payment as well as a failure. After a decline the session is spent like after SUBMITTED; only an ERROR where the API never answered about the payment can be submitted again. A second submit while one is in flight, or after the API answered about the payment, is refused with SubmitBlocked (SUBMIT_BLOCKED) and never reaches the PSP.

Refresh the session​

The cart changed, or the session expired: mint a new client session on your backend and hand it over without rebuilding the checkout.

checkout.setSession(newWidgetData) // suspend; throws SessionExpired if the new one is already expired

What survives: a primary method whose configuration is unchanged between the two sessions keeps its collected data, its form, its save decision and its place as the split's primary. Everything else is rebuilt: applied gift cards are dropped, and itemGenerations bumps a counter for the items that did not survive, so rememberPaymentElement recreates only those. Observe paymentSession for the swap.

Survive process death​

Android may kill the app while the shopper is in a bank app, PayPal or a 3-D Secure page. Snapshot the checkout into your saved instance state and rebuild it from there; hold the checkout itself in a ViewModel so a rotation never touches it.

class CheckoutViewModel(private val handle: SavedStateHandle) : ViewModel() {
val checkout: PurseHeadlessCheckout =
handle.get<CheckoutSavedState>(KEY)?.let { PurseHeadlessCheckout.restore(it, hooks, options) }
?: PurseHeadlessCheckout.init(widgetData, lockedEnvironment = PurseEnvironment.PRODUCTION, hooks, options)

init {
handle.setSavedStateProvider(KEY) { bundleOf(KEY to checkout.saveState()) } // CheckoutSavedState is Parcelable
}

override fun onCleared() = checkout.teardown()
}
RestoredNot restored
The client session, the locked environment, disabledMethodsA card form's input, never saved by design
The primary selection, the applied gift cards and their sliceshooks and options: they hold code, pass the same ones to restore
Save decisions (register, registerSecondary)Anything the session no longer offers, or a slice it cannot honour (skipped with a warning)
What the native steps collected

If a submit had already left the device, the restored sessionState is SUBMITTED and submitPayment refuses, so the shopper cannot pay twice: get the outcome from your backend, through webhooks or Get a client session. The return from the redirect Activity after the process was killed reaches the recreated payment element as PaymentOutcome.ReturnedFromRedirect, which leads to the same backend check.

Plain JSON too

CheckoutSavedState.toJson() / fromJson() serve a store of your own; submitted and savedAt let you decide whether a snapshot is worth restoring. A snapshot from a newer SDK version is refused with IllegalArgumentException.

Tear down​

override fun onCleared() = checkout.teardown() // in the ViewModel, not on rotation

Releases partner clients, the expiry watchdog and background work, and flushes pending monitoring events; idempotent. isTornDown turns true, and every later call that reaches the network, hands out a host or replaces the session throws CheckoutTornDown (CHECKOUT_TORN_DOWN). Call it when the checkout screen is dismissed for good.