> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Disable buttons during payment

> Disable external buttons while payment is processing.

Prevent users from clicking buttons outside the checkout while payment is being processed.

## Recipe

> [tab] Web
>
> ```javascript theme={"dark"}
> const submitButton = document.getElementById('external-submit');
>
> checkout.addEventListener('primer:state-change', (event) => {
>   submitButton.disabled = event.detail.isProcessing;
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> val controller = rememberCardFormController(checkout)
> val state by controller.state.collectAsStateWithLifecycle()
>
> MyPayButton(
>     enabled = !state.isLoading,
>     onClick = { controller.submit() },
> )
> ```
>
> > [!NOTE]
> >
> > 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.

> [tab] iOS
>
> ```swift theme={"dark"}
> PrimerCardForm(submitButton: { session in
>   Button("Pay") { session.submit() }
>     .disabled(!session.state.isValid || session.state.isLoading)
> })
> ```
>
> Each `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

> [tab] Web
>
> 1. Get a reference to your external button(s)
> 2. Listen for the `primer:state-change` event
> 3. Set the button's `disabled` property based on `isProcessing`
>
> The `isProcessing` flag is unified across all payment methods -- when any payment is in progress, it returns `true`.

> [tab] Android
>
> 1. Create a `PrimerCardFormController` using `rememberCardFormController()`
> 2. Observe its `state` with `collectAsStateWithLifecycle()`
> 3. Use `state.isLoading` to 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 |

> [tab] iOS
>
> 1. Capture the `PrimerCardFormSession` passed into a `PrimerCardForm` slot closure
> 2. Read `session.state.isLoading` and `session.state.isValid` to control button state
> 3. 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

> [tab] Web
>
> ```javascript theme={"dark"}
> const buttons = document.querySelectorAll('.checkout-action');
>
> checkout.addEventListener('primer:state-change', (event) => {
>   buttons.forEach((button) => {
>     button.disabled = event.detail.isProcessing;
>   });
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> 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() })
> }
> ```

> [tab] iOS
>
> Replace the submit slot with a custom view, then disable your other action buttons from the same observed session:
>
> ```swift theme={"dark"}
> 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

> [tab] Web
>
> ```javascript theme={"dark"}
> 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);
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> 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")
>     }
> }
> ```

> [tab] iOS
>
> ```swift theme={"dark"}
> 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

> [tab] Web
>
> ```javascript theme={"dark"}
> 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 = '';
>     }
>   });
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> val controller = rememberCardFormController(checkout)
> val state by controller.state.collectAsStateWithLifecycle()
>
> BackHandler(enabled = state.isLoading) {
>     // Intercept back press during payment processing
> }
> ```
>
> > [!NOTE]
> >
> > `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.

> [tab] iOS
>
> Surface the session's loading state with the submit slot, then bind it to `.interactiveDismissDisabled()` on the enclosing sheet:
>
> ```swift theme={"dark"}
> @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 }
>   }
> }
> ```
>
> > [!NOTE]
> >
> > `.interactiveDismissDisabled()` stops the shopper from swiping away the sheet that holds `PrimerCardForm` while a card payment runs. This includes the time before your `onBeforePaymentCreate` handler decides. Primer's own sheets block swipe-to-dismiss while a payment is processing: the processing screen that `.primerCheckoutSession` presents, `PrimerCheckout` in a sheet, and `PrimerCheckoutPresenter`.

## See also

> [card] [Show loading indicator](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/show-loading-indicator.md)
>
> Display a loading state during payment processing

> [card] [External submit button](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/external-submit-button.md)
>
> Trigger payment submission from outside the checkout component


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.