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

# Show loading indicator

> Display a loading state while payment is processing.

Show a loading overlay or spinner while the payment is being processed to provide feedback to users.

## Recipe

<Tabs>
  <Tab title="Web">
    ```javascript theme={"dark"}
    checkout.addEventListener('primer:state-change', (event) => {
      const { isProcessing } = event.detail;

      if (isProcessing) {
        document.getElementById('loading-overlay').style.display = 'block';
      } else {
        document.getElementById('loading-overlay').style.display = 'none';
      }
    });
    ```
  </Tab>

  <Tab title="Android">
    ### Loading during initialization

    ```kotlin theme={"dark"}
    val checkout = rememberPrimerCheckoutController(clientToken)
    val state by checkout.state.collectAsStateWithLifecycle()

    when (state) {
        is PrimerCheckoutState.Loading -> {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center,
            ) {
                CircularProgressIndicator()
            }
        }
        is PrimerCheckoutState.Ready -> {
            PrimerCheckoutSheet(checkout = checkout)
        }
    }
    ```

    ### Loading during form submission

    ```kotlin theme={"dark"}
    val cardFormController = rememberCardFormController(checkout)
    val formState by cardFormController.state.collectAsStateWithLifecycle()

    Button(
        onClick = { cardFormController.submit() },
        enabled = formState.isFormValid && !formState.isLoading,
        modifier = Modifier.fillMaxWidth(),
    ) {
        if (formState.isLoading) {
            CircularProgressIndicator(
                modifier = Modifier.size(20.dp),
                color = Color.White,
                strokeWidth = 2.dp,
            )
        } else {
            Text("Pay")
        }
    }
    ```
  </Tab>

  <Tab title="iOS">
    ### Custom loading screen

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        checkoutScope.loadingScreen = {
          AnyView(
            VStack(spacing: 16) {
              ProgressView()
                .scaleEffect(1.5)
              Text("Processing your payment...")
                .font(.subheadline)
                .foregroundColor(.secondary)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
          )
        }
      }
    )
    ```

    ### Loading during form submission

    ```swift theme={"dark"}
    Button(action: { scope.submit() }) {
      if isLoading {
        ProgressView()
          .tint(.white)
      } else {
        Text("Pay")
      }
    }
    .frame(maxWidth: .infinity, minHeight: 48)
    .buttonStyle(.borderedProminent)
    .disabled(!isValid || isLoading)
    ```
  </Tab>
</Tabs>

## How it works

<Tabs>
  <Tab title="Web">
    1. Listen for the `primer:state-change` event
    2. Check the `isProcessing` property from the event detail
    3. Show your loading UI when `isProcessing` is `true`
    4. Hide it when `isProcessing` is `false`
  </Tab>

  <Tab title="Android">
    The SDK provides two loading states:

    | State Source                     | Property                      | When It's True                           |
    | -------------------------------- | ----------------------------- | ---------------------------------------- |
    | `PrimerCheckoutController.state` | `PrimerCheckoutState.Loading` | SDK initializing, fetching configuration |
    | `PrimerCardFormController.state` | `isLoading`                   | Payment is being processed after submit  |
  </Tab>

  <Tab title="iOS">
    The SDK provides two loading states:

    | State Source                | Property        | When It's Active                         |
    | --------------------------- | --------------- | ---------------------------------------- |
    | `PrimerCheckoutScope`       | `loadingScreen` | SDK initializing, fetching configuration |
    | `PrimerCardFormScope.state` | `isLoading`     | Payment is being processed after submit  |
  </Tab>
</Tabs>

## Variations

### Custom loading screen

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

    checkout.addEventListener('primer:state-change', (event) => {
      overlay.classList.toggle('visible', event.detail.isProcessing);
    });
    ```

    ```css theme={"dark"}
    .loading-overlay {
      opacity: 0;
      visibility: hidden;
      transition: opacity 0.2s, visibility 0.2s;
    }

    .loading-overlay.visible {
      opacity: 1;
      visibility: visible;
    }
    ```
  </Tab>

  <Tab title="Android">
    Override the `loading` slot in `PrimerCheckoutSheet` to replace the default loading screen:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        loading = {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(48.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                CircularProgressIndicator(
                    color = MaterialTheme.colorScheme.primary,
                )
                Spacer(Modifier.height(16.dp))
                Text(
                    text = "Preparing your checkout...",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                )
            }
        },
        onEvent = { event -> },
        onDismiss = { },
    )
    ```
  </Tab>

  <Tab title="iOS">
    Replace the default loading screen with a branded view:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        checkoutScope.loadingScreen = {
          AnyView(
            VStack(spacing: 16) {
              ProgressView()
                .controlSize(.large)
                .tint(.accentColor)
              Text("Preparing your checkout...")
                .font(.body)
                .foregroundColor(.secondary)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.ultraThinMaterial)
          )
        }
      }
    )
    ```
  </Tab>
</Tabs>

### Inline loading overlay

<Tabs>
  <Tab title="Web">
    ```javascript theme={"dark"}
    checkout.addEventListener('primer:state-change', (event) => {
      const { isProcessing } = event.detail;

      // Disable all inputs on the page during processing
      document.querySelectorAll('input, button, select').forEach((el) => {
        el.disabled = isProcessing;
      });
    });
    ```
  </Tab>

  <Tab title="Android">
    For inline integrations, you manage the loading UI yourself. This example shows a semi-transparent overlay during form submission:

    ```kotlin theme={"dark"}
    @Composable
    fun InlineCheckoutWithLoading(clientToken: String) {
        val checkout = rememberPrimerCheckoutController(clientToken)
        val state by checkout.state.collectAsStateWithLifecycle()

        when (state) {
            is PrimerCheckoutState.Loading -> {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center,
                ) {
                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        CircularProgressIndicator()
                        Spacer(Modifier.height(12.dp))
                        Text("Loading payment methods...")
                    }
                }
            }
            is PrimerCheckoutState.Ready -> {
                PrimerCheckoutHost(
                    checkout = checkout,
                    onEvent = { event ->
                        when (event) {
                            is PrimerCheckoutEvent.Success -> { }
                            is PrimerCheckoutEvent.Failure -> { }
                        }
                    },
                ) {
                    val cardFormController = rememberCardFormController(checkout)
                    val formState by cardFormController.state.collectAsStateWithLifecycle()

                    Box {
                        Column(Modifier.padding(16.dp)) {
                            PrimerCardForm(
                                controller = cardFormController,
                                submitButton = {
                                    Button(
                                        onClick = { cardFormController.submit() },
                                        enabled = formState.isFormValid && !formState.isLoading,
                                        modifier = Modifier.fillMaxWidth(),
                                    ) {
                                        if (formState.isLoading) {
                                            CircularProgressIndicator(
                                                modifier = Modifier.size(20.dp),
                                                color = Color.White,
                                                strokeWidth = 2.dp,
                                            )
                                        } else {
                                            Text("Pay")
                                        }
                                    }
                                },
                            )
                        }

                        if (formState.isLoading) {
                            Box(
                                modifier = Modifier
                                    .matchParentSize()
                                    .background(Color.Black.copy(alpha = 0.3f)),
                                contentAlignment = Alignment.Center,
                            ) {
                                Card(
                                    modifier = Modifier.padding(32.dp),
                                ) {
                                    Column(
                                        modifier = Modifier.padding(24.dp),
                                        horizontalAlignment = Alignment.CenterHorizontally,
                                    ) {
                                        CircularProgressIndicator()
                                        Spacer(Modifier.height(12.dp))
                                        Text("Processing payment...")
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    ```
  </Tab>

  <Tab title="iOS">
    Monitor the card form's loading state to show an overlay during submission:

    ```swift theme={"dark"}
    struct CheckoutWithLoading: View {
      let scope: any PrimerCardFormScope
      @State private var isValid = false
      @State private var isLoading = false

      var body: some View {
        ZStack {
          VStack(spacing: 16) {
            scope.PrimerCardNumberField(label: "Card number", styling: nil)
            HStack(spacing: 12) {
              scope.PrimerExpiryDateField(label: "Expiry", styling: nil)
              scope.PrimerCvvField(label: "CVV", styling: nil)
            }

            Button(action: { scope.submit() }) {
              Text("Pay")
                .frame(maxWidth: .infinity, minHeight: 48)
            }
            .buttonStyle(.borderedProminent)
            .disabled(!isValid || isLoading)
          }
          .padding()

          if isLoading {
            Color.black.opacity(0.3)
              .ignoresSafeArea()
            VStack(spacing: 12) {
              ProgressView()
              Text("Processing payment...")
                .font(.subheadline)
            }
            .padding(24)
            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
          }
        }
        .task {
          for await state in scope.state {
            isValid = state.isValid
            isLoading = state.isLoading
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Show processing message

<Tabs>
  <Tab title="Web">
    ```javascript theme={"dark"}
    checkout.addEventListener('primer:state-change', (event) => {
      const statusEl = document.getElementById('status-message');

      if (event.detail.isProcessing) {
        statusEl.textContent = 'Processing your payment...';
        statusEl.className = 'status processing';
      } else if (event.detail.isSuccessful) {
        statusEl.textContent = 'Payment successful!';
        statusEl.className = 'status success';
      } else {
        statusEl.textContent = '';
        statusEl.className = 'status';
      }
    });
    ```
  </Tab>

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

    when {
        state is PrimerCheckoutState.Loading -> Text("Loading payment methods...")
        formState.isLoading -> Text("Processing payment...")
    }
    ```
  </Tab>

  <Tab title="iOS">
    Track loading state through the async stream to update external UI:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        if let cardScope: PrimerCardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self) {
          Task {
            for await state in cardScope.state {
              if state.isLoading {
                // Update external UI (e.g., disable navigation)
              }
            }
          }
        }
      }
    )
    ```
  </Tab>
</Tabs>

## See also

<CardGroup cols={2}>
  <Card title="Disable external buttons" icon="lock" href="/checkout/primer-checkout/guides-and-recipes/disable-buttons-during-payment">
    Prevent double submission during payment processing
  </Card>

  <Card title="Events guide" icon="bolt" href="/checkout/primer-checkout/configuration/events">
    Handle payment lifecycle events
  </Card>
</CardGroup>
