> ## 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

<Tabs>
  <Tab title="Web">
    ```javascript theme={"dark"}
    const submitButton = document.getElementById('external-submit');

    checkout.addEventListener('primer:state-change', (event) => {
      submitButton.disabled = event.detail.isProcessing;
    });
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={"dark"}
    val controller = rememberCardFormController(checkout)
    val state by controller.state.collectAsStateWithLifecycle()

    MyPayButton(
        enabled = !state.isLoading,
        onClick = { controller.submit() },
    )
    ```

    <Info>
      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.
    </Info>
  </Tab>

  <Tab title="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.
  </Tab>
</Tabs>

## How it works

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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 |
  </Tab>
</Tabs>

## Variations

### Disable multiple buttons

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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 */ })
    })
    ```
  </Tab>
</Tabs>

### Add visual feedback

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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)
    })
    ```
  </Tab>
</Tabs>

### Disable navigation during payment

<Tabs>
  <Tab title="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>

  <Tab title="Android">
    ```kotlin theme={"dark"}
    val controller = rememberCardFormController(checkout)
    val state by controller.state.collectAsStateWithLifecycle()

    BackHandler(enabled = state.isLoading) {
        // Intercept back press during payment processing
    }
    ```

    <Info>
      `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.
    </Info>
  </Tab>

  <Tab title="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 }
      }
    }
    ```

    <Info>
      `.interactiveDismissDisabled()` prevents the sheet from being dismissed by swiping down during payment processing. This is the iOS equivalent of blocking navigation.
    </Info>
  </Tab>
</Tabs>

## See also

<CardGroup cols={2}>
  <Card title="Show loading indicator" icon="spinner" href="/docs/checkout/primer-checkout/guides-and-recipes/show-loading-indicator">
    Display a loading state during payment processing
  </Card>

  <Card title="External submit button" icon="hand-pointer" href="/docs/checkout/primer-checkout/guides-and-recipes/external-submit-button">
    Trigger payment submission from outside the checkout component
  </Card>
</CardGroup>
