> ## 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 custom success message

> Display a custom success message or modal after payment completion.

Display a personalized success message or modal to users after their payment completes.

## Recipe

> [tab] Web
>
> ```javascript theme={"dark"}
> checkout.addEventListener('primer:ready', (event) => {
>   const primer = event.detail;
>
>   primer.onPaymentSuccess = ({ payment, paymentMethodType }) => {
>     const message = `Payment of ${payment.amount} completed via ${paymentMethodType}`;
>     showSuccessModal(message);
>   };
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
> LaunchedEffect(state) {
>     when (val s = state) {
>         is PrimerCheckoutState.Success -> {
>             val payment = s.checkoutData.payment
>             Log.d("Checkout", "Payment completed: ${payment.id}")
>
>             navController.navigate("confirmation/${payment.id}") {
>                 popUpTo("checkout") { inclusive = true }
>             }
>         }
>         is PrimerCheckoutState.Failure -> {
>             val error = s.primerError
>             Log.e("Checkout", "Failed: ${error.description}")
>             Log.e("Checkout", "Diagnostics: ${error.diagnosticsId}")
>         }
>         else -> Unit
>     }
> }
>
> PrimerCheckoutSheet(checkout = checkout)
> ```

> [tab] iOS
>
> ```swift theme={"dark"}
> struct CheckoutView: View {
>   let clientToken: String
>   @State private var showSuccess = false
>
>   var body: some View {
>     ZStack {
>       PrimerCheckout(
>         clientToken: clientToken,
>         primerSettings: PrimerSettings(uiOptions: PrimerUIOptions(isSuccessScreenEnabled: false)),
>         onCompletion: { state in
>           if case .success = state {
>             withAnimation { showSuccess = true }
>           }
>         }
>       )
>
>       if showSuccess {
>         SuccessOverlay()
>           .transition(.opacity)
>       }
>     }
>   }
> }
> ```

## How it works

> [tab] Web
>
> 1. Listen for the `primer:ready` event to access the Primer SDK instance
> 2. Set the `onPaymentSuccess` callback
> 3. Access `payment` details and `paymentMethodType` from the callback parameters
> 4. Display your custom UI (modal, toast, inline message, etc.)

> [tab] Android
>
> 1. Observe `checkout.state` with `collectAsStateWithLifecycle()` and react in a `LaunchedEffect`
> 2. Handle `PrimerCheckoutState.Success` to access `PrimerCheckoutData` with payment details
> 3. Navigate to a confirmation screen or update your order UI
>
> | Property | Type | Description |
> | - | - | - |
> | `payment.id` | `String` | The Primer payment ID |
> | `payment.orderId` | `String?` | Your order ID |
>
> > [!NOTE]
> >
> > When using `PrimerCheckoutSheet`, the SDK shows a default success screen that auto-dismisses after 3 seconds. The `Success` state emits immediately -- you do not need to wait for the dismiss. Navigate as soon as you observe the state.

> [tab] iOS
>
> 1. Turn off the SDK success screen with `PrimerUIOptions(isSuccessScreenEnabled: false)`
>
> 2. Use the `onCompletion` callback on `PrimerCheckout`
>
> 3. Match on `.success` to detect payment completion (bind it with `case .success(let result)` to read `result.paymentId`)
>
> 4. Update your SwiftUI state to show a custom success view
>
> > [!NOTE]
> >
> > By default, `PrimerCheckout` shows its own success screen for 3 seconds and delivers `.success` to `onCompletion` when that screen closes. With `isSuccessScreenEnabled: false`, the SDK shows nothing and delivers `.success` as soon as the payment completes.

## Variations

### Custom success with payment details

> [tab] Web
>
> ```javascript theme={"dark"}
> primer.onPaymentSuccess = ({ payment }) => {
>   document.getElementById('success-container').innerHTML = `
>     <h2>Thank you for your order!</h2>
>     <p>Order ID: ${payment.orderId}</p>
>     <p>Amount: ${formatCurrency(payment.amount, payment.currencyCode)}</p>
>   `;
> };
> ```

> [tab] Android
>
> Display payment details from the `Success` event:
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
> LaunchedEffect(state) {
>     when (val s = state) {
>         is PrimerCheckoutState.Success -> {
>             val payment = s.checkoutData.payment
>             showSuccessScreen(
>                 orderId = payment.orderId,
>                 paymentId = payment.id,
>             )
>         }
>         is PrimerCheckoutState.Failure -> {
>             Log.e("Checkout", "Failed: ${s.primerError.diagnosticsId}")
>         }
>         else -> Unit
>     }
> }
>
> PrimerCheckoutSheet(checkout = checkout)
> ```

> [tab] iOS
>
> Display payment details from the success result:
>
> ```swift theme={"dark"}
> PrimerCheckout(
>   clientToken: clientToken,
>   primerSettings: PrimerSettings(uiOptions: PrimerUIOptions(isSuccessScreenEnabled: false)),
>   onCompletion: { state in
>     switch state {
>     case .success(let result):
>       showSuccessScreen(paymentId: result.paymentId)
>     case let .failure(error, _):
>       print("Failed [\(error.errorId)]: \(error.diagnosticsId)")
>     default:
>       break
>     }
>   }
> )
> ```

### Handle checkout dismissal

> [tab] Web
>
> ```javascript theme={"dark"}
> primer.onPaymentSuccess = () => {
>   const successEl = document.getElementById('success-message');
>   successEl.classList.add('visible');
>
>   // Hide checkout form
>   document.querySelector('primer-checkout').style.display = 'none';
> };
> ```

> [tab] Android
>
> Use the `onDismiss` callback to detect when the checkout sheet is closed:
>
> ```kotlin theme={"dark"}
> PrimerCheckoutSheet(
>     checkout = checkout,
>     onDismiss = {
>         navController.popBackStack()
>     },
> )
> ```
>
> > [!NOTE]
> >
> > `onDismiss` fires after the sheet closes. If you already navigated away when observing `PrimerCheckoutState.Success`, guard against double navigation.

> [tab] iOS
>
> Handle the `.dismissed` case to detect when the shopper closes the checkout. The payment method list shows a close button when `dismissalMechanism` includes `.closeButton`. Tapping it ends the checkout with `.dismissed`. When other methods are available, the close button on a payment method screen, such as the card form, returns the shopper to the list. In that case, `.dismissed` does not arrive. With a single method, that close button ends the checkout with `.dismissed`. If you present `PrimerCheckout` in your own sheet, a swipe-down does not reach `onCompletion`, so use your sheet's `onDismiss` for that.
>
> ```swift theme={"dark"}
> PrimerCheckout(
>   clientToken: clientToken,
>   primerSettings: PrimerSettings(
>     uiOptions: PrimerUIOptions(
>       dismissalMechanism: [.closeButton]
>     )
>   ),
>   onCompletion: { state in
>     switch state {
>     case .success:
>       withAnimation { showSuccess = true }
>     case .dismissed:
>       // The shopper closed the checkout
>       break
>     default:
>       break
>     }
>   }
> )
> ```

## See also

> [card] [Redirect after payment](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/redirect-after-payment.md)
>
> Navigate to a confirmation page after payment

> [card] [Error handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/error-handling.md)
>
> Handle payment failures and display error messages


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