> ## 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"}
    @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
        }
      }
    }
    ```
  </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. Observe `cardScope.state` using `for await` in a `.task` modifier
    2. Read `state.isLoading` and `state.isValid` to control button state
    3. 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    |
  </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">
    ```swift theme={"dark"}
    @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
        }
      }
    }
    ```
  </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"}
    @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
        }
      }
    }
    ```
  </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">
    ```swift theme={"dark"}
    @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
        }
      }
    }
    ```

    <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="/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="/checkout/primer-checkout/guides-and-recipes/external-submit-button">
    Trigger payment submission from outside the checkout component
  </Card>
</CardGroup>
