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

> [tab] 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] 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] iOS
>
> ### Loading during initialization
>
> `PrimerCheckout` renders Primer's managed loading screen while it initializes. For a custom inline layout, observe the `PrimerCheckoutSession`'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("Loading payment methods...")
>           .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)
> })
> ```

## How it works

> [tab] 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] 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] iOS
>
> The SDK provides two loading states:
>
> | State Source | Property | When It's Active |
> | - | - | - |
> | `PrimerCheckoutSession` | `phase == .initializing` | SDK initializing, fetching configuration |
> | `PrimerCardFormSession.state` | `isLoading` | From submit until the payment result, including while `onBeforePaymentCreate` waits for your decision |

## Variations

### Custom loading screen

> [tab] 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] 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] 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)
>   }
> }
> ```

### Inline loading overlay

> [tab] 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] 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] iOS
>
> For inline integrations, once the payment starts, the SDK shows its own processing screen in a sheet above your layout. The sheet then shows the success or error screen. To hide the success and error screens, pass `PrimerSettings(uiOptions: PrimerUIOptions(isSuccessScreenEnabled: false, isErrorScreenEnabled: false))` as the `settings` of `PrimerCheckoutSession`. The processing screen always shows while a payment runs. The shopper cannot swipe that sheet away while the payment processes.
>
> `isLoading` on the `PrimerCardFormSession` turns `true` as soon as the shopper submits. It stays `true` while `onBeforePaymentCreate` waits for your decision. Use it to dim the form, disable your controls, or show a loading card until the SDK sheet appears, as this example does:
>
> ```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))
>       }
>     }
>   }
> }
> ```

### Show processing message

> [tab] 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] 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] iOS
>
> Observe the session `phase` and the card form session's `state.isLoading` to drive a status message. Observe the `PrimerCardFormSession` in its own view, because `PrimerCheckoutSession` does not publish card form changes:
>
> ```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 {
>       CardFormStatus(cardForm: cardForm)
>     }
>   }
> }
>
> struct CardFormStatus: View {
>   @ObservedObject var cardForm: PrimerCardFormSession
>
>   var body: some View {
>     if cardForm.state.isLoading {
>       Text("Processing payment...")
>     }
>   }
> }
> ```

## See also

> [card] [Disable external buttons](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/disable-buttons-during-payment.md)
>
> Prevent double submission during payment processing

> [card] [Events guide](https://primer.io/docs/checkout/primer-checkout/configuration/events.md)
>
> Handle payment lifecycle events


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