Recipe
- Web
- Android
- iOS
Copy
Ask AI
const submitButton = document.getElementById('external-submit');
checkout.addEventListener('primer:state-change', (event) => {
submitButton.disabled = event.detail.isProcessing;
});
Copy
Ask AI
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.Copy
Ask AI
@State private var isLoading = false
@State private var isValid = false
var body: some View {
VStack {
// Card form fields...
Button("Pay") { /* submit */ }
.disabled(!isValid || isLoading)
}
.task {
for await state in cardScope.state {
isLoading = state.isLoading
isValid = state.isValid
}
}
}
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 |
- Observe
cardScope.stateusingfor awaitin a.taskmodifier - Read
state.isLoadingandstate.isValidto control button state - Use SwiftUI’s
.disabled()modifier to disable buttons during processing
| Property | Type | Description |
|---|---|---|
isLoading | Bool | true while a card payment is being processed |
isValid | Bool | true when all card fields pass validation |
Variations
Disable multiple buttons
- Web
- Android
- iOS
Copy
Ask AI
const buttons = document.querySelectorAll('.checkout-action');
checkout.addEventListener('primer:state-change', (event) => {
buttons.forEach((button) => {
button.disabled = event.detail.isProcessing;
});
});
Copy
Ask AI
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() })
}
Copy
Ask AI
@State private var isLoading = false
@State private var isValid = false
var body: some View {
VStack {
// Card form fields...
Button("Pay") { /* submit */ }
.disabled(!isValid || isLoading)
Button("Cancel") { /* cancel */ }
.disabled(isLoading)
Button("Edit Cart") { /* edit */ }
.disabled(isLoading)
}
.task {
for await state in cardScope.state {
isLoading = state.isLoading
isValid = state.isValid
}
}
}
Add visual feedback
- Web
- Android
- iOS
Copy
Ask AI
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);
});
Copy
Ask AI
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")
}
}
Copy
Ask AI
@State private var isLoading = false
var body: some View {
VStack {
// Card form fields...
Button(action: { /* submit */ }) {
if isLoading {
ProgressView()
.frame(width: 16, height: 16)
Text("Processing...")
} else {
Text("Pay Now")
}
}
.disabled(!isValid || isLoading)
}
.task {
for await state in cardScope.state {
isLoading = state.isLoading
}
}
}
Disable navigation during payment
- Web
- Android
- iOS
Copy
Ask AI
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 = '';
}
});
});
Copy
Ask AI
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.Copy
Ask AI
@State private var isLoading = false
var body: some View {
VStack {
// Card form fields...
}
.interactiveDismissDisabled(isLoading)
.task {
for await state in cardScope.state {
isLoading = state.isLoading
}
}
}
.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