Recipe
- Web
- Android
- iOS
const submitButton = document.getElementById('external-submit');
checkout.addEventListener('primer:state-change', (event) => {
submitButton.disabled = event.detail.isProcessing;
});
val controller = rememberCardFormController(checkout)
val state by controller.state.collectAsStateWithLifecycle()
MyPayButton(
enabled = !state.isLoading,
onClick = { controller.submit() },
)
Android’s
isLoading only covers card form submission. There is no unified checkout-level processing state across all payment methods. For PrimerCheckoutSheet, swipe dismissal can be controlled via PrimerSettings.uiOptions.dismissalMechanism, but the back button is always active.PrimerCardForm(submitButton: { session in
Button("Pay") { session.submit() }
.disabled(!session.state.isValid || session.state.isLoading)
})
PrimerCardForm slot receives the active PrimerCardFormSession. Read session.state.isLoading and session.state.isValid directly to drive button state — the session republishes its state on every change.How it works
- Web
- Android
- iOS
- Get a reference to your external button(s)
- Listen for the
primer:state-changeevent - Set the button’s
disabledproperty based onisProcessing
isProcessing flag is unified across all payment methods — when any payment is in progress, it returns true.- Create a
PrimerCardFormControllerusingrememberCardFormController() - Observe its
statewithcollectAsStateWithLifecycle() - Use
state.isLoadingto disable external buttons during card payment processing
| Scope | Property | Coverage |
|---|---|---|
| Card form | cardFormController.state.isLoading | Card payments only |
| Checkout session | PrimerCheckoutState.Loading / .Ready | Initialization only, not payment processing |
- Capture the
PrimerCardFormSessionpassed into aPrimerCardFormslot closure - Read
session.state.isLoadingandsession.state.isValidto control button state - Use SwiftUI’s
.disabled()modifier to disable buttons during processing
| Property | Type | Description |
|---|---|---|
session.state.isLoading | Bool | true while a card payment is being submitted |
session.state.isValid | Bool | true when all required card fields pass validation |
Variations
Disable multiple buttons
- Web
- Android
- iOS
const buttons = document.querySelectorAll('.checkout-action');
checkout.addEventListener('primer:state-change', (event) => {
buttons.forEach((button) => {
button.disabled = event.detail.isProcessing;
});
});
val controller = rememberCardFormController(checkout)
val state by controller.state.collectAsStateWithLifecycle()
val isProcessing = state.isLoading
Column {
MyPayButton(enabled = !isProcessing, onClick = { controller.submit() })
MyCancelButton(enabled = !isProcessing, onClick = { onCancel() })
MyEditCartButton(enabled = !isProcessing, onClick = { onEditCart() })
}
Replace the submit slot with a custom view, then disable your other action buttons from the same observed session:
struct CheckoutActions: View {
@ObservedObject var session: PrimerCardFormSession
let onCancel: () -> Void
let onEditCart: () -> Void
var body: some View {
let isProcessing = session.state.isLoading
VStack {
Button("Pay") { session.submit() }
.disabled(!session.state.isValid || isProcessing)
Button("Cancel", action: onCancel)
.disabled(isProcessing)
Button("Edit Cart", action: onEditCart)
.disabled(isProcessing)
}
}
}
PrimerCardForm(submitButton: { session in
CheckoutActions(session: session, onCancel: { /* cancel */ }, onEditCart: { /* edit */ })
})
Add visual feedback
- Web
- Android
- iOS
const submitButton = document.getElementById('external-submit');
checkout.addEventListener('primer:state-change', (event) => {
const { isProcessing } = event.detail;
submitButton.disabled = isProcessing;
submitButton.textContent = isProcessing ? 'Processing...' : 'Pay Now';
submitButton.classList.toggle('loading', isProcessing);
});
val controller = rememberCardFormController(checkout)
val state by controller.state.collectAsStateWithLifecycle()
Button(
onClick = { controller.submit() },
enabled = !state.isLoading,
) {
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(8.dp))
Text("Processing...")
} else {
Text("Pay Now")
}
}
PrimerCardForm(submitButton: { session in
Button(action: { session.submit() }) {
if session.state.isLoading {
ProgressView()
.frame(width: 16, height: 16)
Text("Processing...")
} else {
Text("Pay Now")
}
}
.disabled(!session.state.isValid || session.state.isLoading)
})
Disable navigation during payment
- Web
- Android
- iOS
checkout.addEventListener('primer:state-change', (event) => {
const { isProcessing } = event.detail;
document.querySelectorAll('a').forEach((link) => {
if (isProcessing) {
link.dataset.originalHref = link.href;
link.removeAttribute('href');
link.style.pointerEvents = 'none';
} else if (link.dataset.originalHref) {
link.href = link.dataset.originalHref;
link.style.pointerEvents = '';
}
});
});
val controller = rememberCardFormController(checkout)
val state by controller.state.collectAsStateWithLifecycle()
BackHandler(enabled = state.isLoading) {
// Intercept back press during payment processing
}
BackHandler only intercepts the system back gesture. For PrimerCheckoutSheet, swipe-to-dismiss is configured separately via PrimerSettings.uiOptions.dismissalMechanism. The SDK does not block the back button during processing.Surface the session’s loading state with the submit slot, then bind it to
.interactiveDismissDisabled() on the enclosing sheet:@StateObject private var checkoutSession: PrimerCheckoutSession
@State private var isLoading = false
init(clientToken: String) {
_checkoutSession = StateObject(
wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
)
}
var body: some View {
ScrollView {
PrimerCardForm(submitButton: { cardSession in
SubmitButton(session: cardSession, isLoading: $isLoading)
})
}
.interactiveDismissDisabled(isLoading)
.primerCheckoutSession(checkoutSession)
}
struct SubmitButton: View {
@ObservedObject var session: PrimerCardFormSession
@Binding var isLoading: Bool
var body: some View {
Button("Pay") { session.submit() }
.disabled(!session.state.isValid || session.state.isLoading)
.onChange(of: session.state.isLoading) { isLoading = $0 }
}
}
.interactiveDismissDisabled() prevents the sheet from being dismissed by swiping down during payment processing. This is the iOS equivalent of blocking navigation.See also
Show loading indicator
Display a loading state during payment processing
External submit button
Trigger payment submission from outside the checkout component