> ## 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">
    ### Loading during initialization

    `PrimerCheckout` renders Primer's managed loading screen while the session initializes. For a custom inline layout, observe the session's `phase` and show your own loading UI until it becomes `.ready`:

    ```swift theme={"dark"}
    @StateObject private var session: PrimerCheckoutSession

    var body: some View {
      ScrollView {
        if session.phase == .initializing {
          VStack(spacing: 16) {
            ProgressView()
              .scaleEffect(1.5)
            Text("Processing your payment...")
              .font(.subheadline)
              .foregroundColor(.secondary)
          }
          .frame(maxWidth: .infinity, maxHeight: .infinity)
        } else {
          PrimerCardForm()
        }
      }
      .primerCheckoutSession(session) { state in
        handle(state)
      }
    }
    ```

    ### Loading during form submission

    Drive the submit button from the `PrimerCardFormSession` injected into the slot, using `state.isLoading` and `state.isValid`:

    ```swift theme={"dark"}
    PrimerCardForm(submitButton: { session in
      Button(action: { session.submit() }) {
        if session.state.isLoading {
          ProgressView()
            .tint(.white)
        } else {
          Text("Pay")
        }
      }
      .frame(maxWidth: .infinity, minHeight: 48)
      .buttonStyle(.borderedProminent)
      .disabled(!session.state.isValid || session.state.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                         |
    | ----------------------------- | ------------------------ | ---------------------------------------- |
    | `PrimerCheckoutSession`       | `phase == .initializing` | SDK initializing, fetching configuration |
    | `PrimerCardFormSession.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,
                )
            }
        },
        onDismiss = { },
    )
    ```
  </Tab>

  <Tab title="iOS">
    The managed `PrimerCheckout` renders its own loading screen. To show a branded loading view, compose your UI inline with a `PrimerCheckoutSession` and render your view while `phase` is `.initializing`:

    ```swift theme={"dark"}
    @StateObject private var session: PrimerCheckoutSession

    var body: some View {
      ScrollView {
        if session.phase == .initializing {
          VStack(spacing: 16) {
            ProgressView()
              .controlSize(.large)
              .tint(.accentColor)
            Text("Preparing your checkout...")
              .font(.body)
              .foregroundColor(.secondary)
          }
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .background(.ultraThinMaterial)
        } else {
          PrimerCardForm()
        }
      }
      .primerCheckoutSession(session) { state in
        handle(state)
      }
    }
    ```
  </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()

        LaunchedEffect(state) {
            when (state) {
                is PrimerCheckoutState.Success -> { }
                is PrimerCheckoutState.Failure -> { }
                else -> Unit
            }
        }

        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,
                ) {
                    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">
    For inline integrations, you manage the loading UI yourself. This example overlays a semi-transparent loading card during form submission, reading `isLoading` from the `PrimerCardFormSession`:

    ```swift theme={"dark"}
    struct InlineCheckoutWithLoading: View {
      @StateObject private var session: PrimerCheckoutSession

      init(clientToken: String) {
        _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
      }

      var body: some View {
        ScrollView {
          PrimerCardForm(
            cardDetails: { session in
              VStack(spacing: 16) {
                CardFormDefaults.cardNumber(session)
                HStack(spacing: 12) {
                  CardFormDefaults.expiryDate(session)
                  CardFormDefaults.cvv(session)
                }
              }
            },
            submitButton: { session in
              SubmitWithOverlay(session: session)
            }
          )
          .padding()
        }
        .primerCheckoutSession(session) { state in
          // Handle the terminal PrimerCheckoutState.
        }
      }
    }

    struct SubmitWithOverlay: View {
      @ObservedObject var session: PrimerCardFormSession

      var body: some View {
        ZStack {
          Button(action: { session.submit() }) {
            Text("Pay")
              .frame(maxWidth: .infinity, minHeight: 48)
          }
          .buttonStyle(.borderedProminent)
          .disabled(!session.state.isValid || session.state.isLoading)

          if session.state.isLoading {
            Color.black.opacity(0.3)
              .ignoresSafeArea()
            VStack(spacing: 12) {
              ProgressView()
              Text("Processing payment...")
                .font(.subheadline)
            }
            .padding(24)
            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
          }
        }
      }
    }
    ```
  </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">
    Observe the session `phase` and the card form session's `state.isLoading` to drive a status message:

    ```swift theme={"dark"}
    struct StatusMessage: View {
      @ObservedObject var session: PrimerCheckoutSession

      var body: some View {
        if session.phase == .initializing {
          Text("Loading payment methods...")
        } else if let cardForm = session.cardForm, cardForm.state.isLoading {
          Text("Processing payment...")
        }
      }
    }
    ```
  </Tab>
</Tabs>

## See also

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

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