Recipe
- Web
- Android
- iOS
Copy
Ask AI
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';
}
});
Loading during initialization
Copy
Ask AI
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
Copy
Ask AI
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")
}
}
Custom loading screen
Copy
Ask AI
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
Copy
Ask AI
Button(action: { scope.submit() }) {
if isLoading {
ProgressView()
.tint(.white)
} else {
Text("Pay")
}
}
.frame(maxWidth: .infinity, minHeight: 48)
.buttonStyle(.borderedProminent)
.disabled(!isValid || isLoading)
How it works
- Web
- Android
- iOS
- Listen for the
primer:state-changeevent - Check the
isProcessingproperty from the event detail - Show your loading UI when
isProcessingistrue - Hide it when
isProcessingisfalse
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 |
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 |
Variations
Custom loading screen
- Web
- Android
- iOS
Copy
Ask AI
const overlay = document.getElementById('loading-overlay');
checkout.addEventListener('primer:state-change', (event) => {
overlay.classList.toggle('visible', event.detail.isProcessing);
});
Copy
Ask AI
.loading-overlay {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.loading-overlay.visible {
opacity: 1;
visibility: visible;
}
Override the
loading slot in PrimerCheckoutSheet to replace the default loading screen:Copy
Ask AI
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 = { },
)
Replace the default loading screen with a branded view:
Copy
Ask AI
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)
)
}
}
)
Inline loading overlay
- Web
- Android
- iOS
Copy
Ask AI
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;
});
});
For inline integrations, you manage the loading UI yourself. This example shows a semi-transparent overlay during form submission:
Copy
Ask AI
@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...")
}
}
}
}
}
}
}
}
}
Monitor the card form’s loading state to show an overlay during submission:
Copy
Ask AI
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
}
}
}
}
Show processing message
- Web
- Android
- iOS
Copy
Ask AI
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';
}
});
Copy
Ask AI
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...")
}
Track loading state through the async stream to update external UI:
Copy
Ask AI
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)
}
}
}
}
}
)
See also
Disable external buttons
Prevent double submission during payment processing
Events guide
Handle payment lifecycle events