Recipe
- Web
- Android
- iOS
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
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
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")
}
}
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:@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 thePrimerCardFormSession injected into the slot, using state.isLoading and state.isValid: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
- 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 |
|---|---|---|
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
- Web
- Android
- iOS
const overlay = document.getElementById('loading-overlay');
checkout.addEventListener('primer:state-change', (event) => {
overlay.classList.toggle('visible', event.detail.isProcessing);
});
.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: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 = { },
)
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:@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
- Web
- Android
- iOS
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:
@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...")
}
}
}
}
}
}
}
}
}
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: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
- Web
- Android
- iOS
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';
}
});
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...")
}
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: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
Disable external buttons
Prevent double submission during payment processing
Events guide
Handle payment lifecycle events