# Events

Implement callbacks to customise your Drop-in payment flow.

## Overview

Drop-in provides merchant callbacks you configure on `CheckoutDropInConfig` (and `DropInMethodConfig` for method-specific hooks). Timing and availability differ by payment method — for example PayPal doesn't invoke `onSubmit`. For SDK-emitted analytics (distinct from these callbacks), see [Analytics](/guides/checkout/drop-in/ios/analytics).

You can use these callbacks to inject your own business logic and user experience customisations into the payment flow at critical moments.

Callbacks enable you to:

* Validate business rules before payments proceed.
* Display custom loading states and progress indicators.
* Show user-friendly error messages.
* Track analytics and monitor payment performance.
* Integrate with your own systems for fraud detection or customer management.
* Implement card-on-file functionality for returning customers.


Your callbacks receive normalised data regardless of whether the customer paid with cards, PayPal, or Apple Pay.

All events are optional, though `onSuccess` and `onError` are strongly recommended for production integrations to ensure proper payment handling and error recovery.

## Supported events

### onGetShopper

This callback provides shopper information for card-on-file functionality, allowing returning customers to use saved payment methods. Drop-in calls it at the start of `await dropIn.create()` (including when building component configuration) and again during payment flows when components need current shopper data.

You can use it to:

* Provide shopper ID for logged-in users.
* Generate anonymous shopper ID for guest checkout.
* Enable card-on-file for returning customers.
* Associate payment methods with customer accounts.


#### Event data

This callback receives no parameters and must return a `TransactionShopper?` object.

#### Return value

The `TransactionShopper` struct supports the following fields:

| Property  | Description   |
|  --- | --- |
| `id`String? | A unique shopper identifier. Use customer ID for logged-in users or generate a unique ID for guests. Required for card-on-file functionality. |
| `firstName`String? | The shopper's first name. |
| `lastName`String? | The shopper's last name. |
| `email`String? | The shopper's email address. |
| `phoneNumber`String? | The shopper's phone number. |
| `dateOfBirth`String? | The shopper's date of birth. |
| `houseNumberOrName`String? | The shopper's house number or name. |
| `street`String? | The shopper's street address. |
| `city`String? | The shopper's city. |
| `postalCode`String? | The shopper's postal code. |
| `countryCode`String? | The shopper's country code (ISO 3166-1 alpha-2). |
| `state`String? | The shopper's state or province. |
| `products`[String: String]? | Product-specific key-value data. |
| `orderDescription`String? | Order or basket description. |


#### Example implementation

Implement `onGetShopper` as follows:

```swift
onGetShopper: { async in
    // For logged-in users, return their customer ID
    if let customerId = getCurrentCustomerId() {
        return TransactionShopper(id: customerId)
    }
    
    // For guest users, generate or retrieve a session-based ID
    let guestId = getOrCreateGuestId()
    return TransactionShopper(id: guestId)
}
```

The shopper ID is used to store and retrieve saved payment methods. For guests, generate a unique ID (e.g., UUID) that persists across the session. For registered users, use their customer/account ID from your system.

### onBeforeSubmit

This callback runs immediately before a payment is submitted. The trigger depends on the payment method:

* **Card:** when the customer taps the card submit button (new card or saved card).
* **PayPal:** when the customer taps the PayPal button (`onClick`).
* **Apple Pay:** when the customer taps the Apple Pay button, before the payment sheet opens.


You can use it to:

* Perform final validation before payment submission.
* Check inventory availability.
* Verify customer account status.
* Display confirmation dialogs.
* Track analytics events.
* Perform fraud checks.


#### Event data

The callback receives the following parameter:

| Parameter | Description |
|  --- | --- |
| `paymentMethod`DropInPaymentMethod | The selected payment method: `.card`, `.applePay`, or `.paypal`. |


#### Return value

Return `Bool` from the async closure:

* `true`: Continue with payment submission.
* `false`: Cancel payment submission.


If you omit `onBeforeSubmit`, Drop-in proceeds without this validation step.

#### Example implementation

Handle validation in `onBeforeSubmit` as follows:

```swift
onBeforeSubmit: { paymentMethod in
    print("Payment method selected: \(paymentMethod.rawValue)")
    
    // Track analytics
    analytics.track("payment_initiated", properties: [
        "payment_method": paymentMethod.rawValue,
        "amount": 25.00
    ])
    
    // Show confirmation for large amounts
    if amount > 100 {
        let confirmed = await showConfirmationDialog(
            "Confirm payment of \(amount) USD?"
        )
        if !confirmed {
            return false
        }
    }
    
    // Check inventory availability
    let hasStock = await checkInventory()
    if !hasStock {
        showToast("Some items are no longer available")
        return false
    }
    
    return true // Proceed with payment
}
```

Simple validation example:

```swift
onBeforeSubmit: { paymentMethod in
    // Simple synchronous validation
    if !isValidOrder() {
        showToast("Please complete all required fields")
        return false // Payment won't proceed
    }
    
    return true // Payment will proceed
}
```

### onSubmit

This callback signals that Drop-in has entered a processing state for the payment. Use it to show loading indicators and disable UI elements.

Timing varies by method:

| Method | When `onSubmit` fires |
|  --- | --- |
| Card | After `onBeforeSubmit` returns `true` and card submission starts (`onStartSubmit`). |
| Apple Pay | After the customer authorises in the Apple Pay sheet, when pre-authorisation begins — not on button tap. |
| PayPal | Not called. Use `onBeforeSubmit` for pre-flight validation. Drop-in sets processing during order creation (`onPreAuthorisation`) after the button click succeeds. |


For Card and Apple Pay, `onSubmit` runs after `onBeforeSubmit` returns `true`.

You can use it to:

* Show loading spinners or progress indicators.
* Disable submit buttons to prevent double-submission.
* Display "Processing payment..." messages.
* Update UI to show payment is in progress.
* Track analytics for payment submission.


#### Event data

The callback receives the following parameter:

| Parameter | Description |
|  --- | --- |
| `paymentMethod`DropInPaymentMethod | The payment method being processed: `.card`, `.applePay`. Drop-in does not call `onSubmit` for PayPal — show loading after `onBeforeSubmit` returns `true` or when Drop-in enters processing during PayPal order creation. |


#### Example implementation

Show loading state when processing starts:

```swift
onSubmit: { paymentMethod in
    print("Processing payment with: \(paymentMethod.rawValue)")
    
    // Show loading overlay (synchronous UI updates are fine)
    showLoadingOverlay(true)
    
    // Disable submit button to prevent double-submission
    disableSubmitButton()
    
    // Track analytics
    analytics.track("payment_processing", properties: [
        "payment_method": paymentMethod.rawValue
    ])
}
```

Complete loading state management:

```swift
var loadingTimeout: Task<Void, Never>?

onSubmit: { paymentMethod in
    // Show loading state
    setLoadingState(true)
    
    // Set timeout in case payment takes too long (async work in Task)
    loadingTimeout = Task {
        try? await Task.sleep(nanoseconds: 10_000_000_000) // 10 seconds
        await MainActor.run {
            showWarning("Payment is taking longer than expected. Please wait...")
        }
    }
    
    // Track start time for performance monitoring
    paymentStartTime = Date()
}
```

### onSuccess

This callback is triggered after payment succeeds. It receives the final transaction result from the payment processing system.

You can use it to:

* Verify payment on your backend (required).
* Redirect to success page after verification.
* Display success messages.
* Track successful payment analytics.
* Update stock levels for purchased items.
* Send order confirmation emails to customers.
* Clear shopping cart.


#### Event data

The callback receives a `DropInSubmitResult` parameter (shown as `result` in examples):

| Property  | Description  |
|  --- | --- |
| `systemTransactionId`String | Unity's system transaction identifier. Use for backend verification. |
| `merchantTransactionId`String? | Your unique transaction identifier from `DropInTransactionData` (optional). |
| `paymentMethod`DropInPaymentMethod | The payment method used.Possible values:`.card``.applePay``.paypal` |
| `paymentData`DropInPaymentData? | `nil` for card, PayPal, and Apple Pay. Always verify transaction state on your backend using `systemTransactionId`. |


`DropInSubmitResult` type:

```swift
public enum DropInPaymentData: Codable, Sendable {
    case authorisation(AuthorisationPaymentData)
    case paze(PazeCheckoutDecoded)
}

public struct DropInSubmitResult: Codable {
    public let systemTransactionId: String
    public let merchantTransactionId: String?
    public let paymentMethod: DropInPaymentMethod
    public let paymentData: DropInPaymentData?
}
```

The `onSuccess` callback is a frontend event and can be manipulated by malicious users. Never fulfil orders based solely on this callback. Always verify payments on your backend using Unity webhooks or the [Get transaction details API](/apis/transaction/other/get-transaction-details) before fulfilling orders.

#### Example implementation

Verify the payment on your backend from `onSuccess`:

```swift
onSuccess: { result in
    print("Payment successful (frontend notification only)")
    print("System transaction ID: \(result.systemTransactionId)")
    print("Merchant transaction ID: \(result.merchantTransactionId ?? "N/A")")
    print("Payment method: \(result.paymentMethod.rawValue)")
    
    // Clear loading timeout if set
    loadingTimeout?.cancel()
    
    // Hide loading overlay
    setLoadingState(false)
    
    // Track analytics
    let processingTime = Date().timeIntervalSince(paymentStartTime)
    analytics.track("payment_success_frontend", properties: [
        "system_transaction_id": result.systemTransactionId,
        "merchant_transaction_id": result.merchantTransactionId ?? "N/A",
        "payment_method": result.paymentMethod.rawValue,
        "processing_time": processingTime
    ])
    
    // Show temporary success message
    showMessage("Payment received! Verifying...", type: .success)
    
    // CRITICAL: Verify payment on backend before fulfilling order
    Task {
        do {
            let verification = try await verifyPaymentOnBackend(
                systemTransactionId: result.systemTransactionId,
                merchantTransactionId: result.merchantTransactionId
            )
            
            if verification.success {
                // Payment verified on backend - safe to proceed
                print("Payment verified on backend: \(verification.orderId)")
                
                // Track backend verification success
                analytics.track("payment_verified", properties: [
                    "order_id": verification.orderId,
                    "system_transaction_id": result.systemTransactionId
                ])
                
                // Clear cart
                clearShoppingCart()
                
                // Navigate to success page
                navigateToOrderConfirmation(verification.orderId)
            } else {
                // Verification failed
                print("Backend verification failed: \(verification.error ?? "Unknown error")")
                
                analytics.track("payment_verification_failed", properties: [
                    "system_transaction_id": result.systemTransactionId,
                    "error": verification.error ?? "Unknown error"
                ])
                
                showError(
                    "Payment verification failed. Please contact support with " +
                    "transaction ID: \(result.merchantTransactionId ?? result.systemTransactionId)"
                )
            }
        } catch {
            // Network or server error during verification
            print("Failed to verify payment: \(error.localizedDescription)")
            
            analytics.track("payment_verification_error", properties: [
                "system_transaction_id": result.systemTransactionId,
                "error": error.localizedDescription
            ])
            
            // Show error but don't clear cart (webhook may still process it)
            showError(
                "Unable to verify payment. Your payment may still be processing. " +
                "Please check your email for confirmation or contact support with " +
                "transaction ID: \(result.merchantTransactionId ?? result.systemTransactionId)"
            )
        }
    }
}
```

### onError

This callback is triggered when an error occurs during the payment process. For a full error code reference and recovery patterns, see [Error handling](/guides/checkout/drop-in/ios/error-handling).

You can use it to:

* Display user-friendly error messages.
* Log errors for debugging and monitoring.
* Track failed payment analytics.
* Offer alternative payment methods.
* Provide retry options.
* Hide loading indicators.
* Re-enable form controls.


#### Event data

The callback receives the following parameters:

| Parameter | Description |
|  --- | --- |
| `paymentMethod`DropInPaymentMethod? | The payment method that encountered the error, or `nil` when the error is not tied to a specific method (for example `SDK1100` during `create()`). |
| `error`BaseSdkException | The error object containing details about what went wrong. |
| `error.errorMessage`String | A human-readable error message. |
| `error.errorCode`String | SDK error code in format `SDK####` (e.g., `SDK1114`, `SDK1116`). Card authorisation failures may also surface provider-specific codes via `CheckoutDropInFailedSubmitResultSdkException`. Use this for programmatic error handling. |


The SDK passes `BaseSdkException` to this callback.

#### Common error scenarios

The following table shows common error scenarios and how to detect and respond to them:

| Scenario | Detection approach | User action |
|  --- | --- | --- |
| Card declined | `error.errorMessage` contains "declined" or specific provider messages | Try a different card. |
| Insufficient funds | `error.errorMessage` contains "insufficient funds" | Use a different payment method. |
| Expired card | `error.errorMessage` contains "expired" | Use a different card. |
| Invalid CVV | `error.errorMessage` contains "CVV" or "security code" | Check security code. |
| Update allow transaction failed | `error.errorCode == "SDK1113"` | Try again or contact support. |
| Authentication failed | `error.errorCode == "SDK1114"` or `error.errorMessage` contains "Authentication failed" | Try again or use different card. |
| Authorisation failed | `error.errorCode == "SDK1115"` | Try again or use different card. |
| Card payment failed | `error.errorCode == "SDK1116"` | Check card details and try again. |
| PayPal payment failed | `error.errorCode == "SDK1117"` or `paymentMethod == .paypal` | Try again or use a different method. |
| Apple Pay payment failed | `error.errorCode == "SDK1119"` or `paymentMethod == .applePay` | Try again or use a different method. |
| Session expired | `error.errorMessage` contains "session" or "expired" | Create a new session and reinitialise Drop-in. |
| Network error | `error.errorCode == "SDK0500"` | Check connection and retry. |
| Configuration error | `error.errorCode` starts with `"SDK01"` or `"SDK02"` | Contact support. |


#### Example implementation

Handle payment errors programmatically as follows:

```swift
onError: { paymentMethod, error in
    print("Payment failed")
    print("Error code: \(error.errorCode)")
    print("Error message: \(error.errorMessage)")
    print("Payment method: \(paymentMethod?.rawValue ?? "initialisation")")
    
    // Clear loading timeout if set
    loadingTimeout?.cancel()
    
    // Hide loading overlay and re-enable submit button
    Task { @MainActor in
        setLoadingState(false)
        enableSubmitButton()
    }
    
    // Track error analytics
    analytics.track("payment_failed", properties: [
        "error_code": error.errorCode,
        "error_message": error.errorMessage,
        "payment_method": paymentMethod?.rawValue ?? "initialisation"
    ])
    
    // Log error for monitoring
    logErrorToMonitoring(
        category: "payment_error",
        code: error.errorCode,
        message: error.errorMessage,
        device: UIDevice.current.model
    )
    
    // Show user-friendly error message based on error code and message
    let userMessage: String
    switch error.errorCode {
    case "SDK0500":
        userMessage = "Network connection issue. Please check your internet connection and try again."
    case "SDK1113":
        userMessage = "Failed to update allow transaction. Please try again."
    case "SDK1114":
        userMessage = "Authentication failed. Please try again or use a different card."
    case "SDK1115":
        userMessage = "Authorisation failed. Please try again or use a different card."
    case "SDK1116":
        // Card payment failed - check message for specifics
        if error.errorMessage.localizedCaseInsensitiveContains("declined") {
            userMessage = "Your card was declined. Please try a different card or contact your bank for more information."
        } else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
            userMessage = "Insufficient funds. Please use a different payment method."
        } else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
            userMessage = "This card has expired. Please use a different card."
        } else if error.errorMessage.localizedCaseInsensitiveContains("cvv") || 
                  error.errorMessage.localizedCaseInsensitiveContains("security code") {
            userMessage = "Invalid security code. Please check the CVV on the back of your card and try again."
        } else {
            userMessage = "Card payment failed. Please check your details and try again."
        }
    case "SDK1117":
        userMessage = "PayPal payment failed. Please try again or use a different payment method."
    case "SDK1119":
        userMessage = "Apple Pay payment failed. Please try again or use a different payment method."
    case "SDK1122":
        userMessage = "Paze is unavailable. Please choose another payment method."
    case "SDK1123":
        userMessage = "Paze payment failed. Please try again or use a different payment method."
    case "SDK1124":
        userMessage = "Paze is only available for e-commerce checkout."
    case "SDK1209", "SDK1210", "SDK1211", "SDK1212":
        userMessage = "Invalid shopper identity: \(error.errorMessage)"
    case "SDK1213":
        userMessage = "Paze is only available for USD transactions."
    case "SDK0118":
        userMessage = "Paze client ID is missing from the session."
    case "SDK1202A":
        userMessage = "Paze merchant category code is missing from the session."
    default:
        if error.errorMessage.localizedCaseInsensitiveContains("timeout") {
            userMessage = "Request timed out. Please check your internet connection and try again."
        } else if error.errorMessage.localizedCaseInsensitiveContains("session") ||
                  error.errorMessage.localizedCaseInsensitiveContains("expired") {
            userMessage = "Your payment session has expired. Please reload checkout and try again."
        } else {
            userMessage = error.errorMessage
        }
    }
    
    showErrorMessage(userMessage)
}
```

Error handling with retry logic:

```swift
var retryCount = 0
let maxRetries = 3

onError: { paymentMethod, error in
    // Check if error is retryable (network issues, timeouts)
    let isNetworkError = error.errorCode == "SDK0500"
    let isTimeout = error.errorMessage.localizedCaseInsensitiveContains("timeout")
    let isRetryable = isNetworkError || isTimeout
    
    if isRetryable && retryCount < maxRetries {
        retryCount += 1
        
        print("Retryable error, attempt \(retryCount)/\(maxRetries)")
        
        showMessage(
            "Connection issue. Attempt \(retryCount)/\(maxRetries). Please try your payment again.",
            type: .warning
        )
    } else {
        retryCount = 0 // Reset retry count
        
        if isRetryable {
            showMessage(
                "Unable to process payment after multiple attempts. " +
                "Please check your connection and try again later.",
                type: .error
            )
        } else {
            showMessage(
                "Payment failed: \(error.errorMessage)",
                type: .error
            )
        }
        
        // Show alternative options for persistent failures
        showAlternativePaymentMethods()
    }
}
```

### onGetConsent

Configure this callback on `DropInGlobalConfig` to supply a card-on-file or wallet consent flag when Drop-in does not show a consent checkbox.

You can use it to:

* Grant or withhold consent to store payment methods for future use.
* Comply with regulations requiring explicit consent for storing payment data.
* Vary consent by payment method or user type (guest vs registered).


#### Event data

The callback receives the following parameter:

| Parameter | Description |
|  --- | --- |
| `paymentMethod`DropInPaymentMethod | The payment method being evaluated.Possible values:`.card``.applePay``.paypal` |


#### Return value

Return whether the customer consents to storing the payment method for future use:

* `true`: Consent granted.
* `false`: Consent not granted.


When Drop-in renders a visible consent checkbox (for example card consent when `storeCardConsent` is `Ask`, or PayPal consent when enabled), the checkbox value takes precedence over this callback. When no checkbox is shown, the return value is used as the consent flag.

For **new card** payments, `onGetConsent(.card)` is wired into the card submit flow. **Saved card (card-on-file) submit** does not call this callback. Apple Pay and PayPal use the same callback when no consent UI component is present.

#### Example implementation

Configure consent display through `DropInMethodConfig`:

```swift
methodConfig: DropInMethodConfig(
    global: DropInGlobalConfig(
        // Grant consent for card and PayPal when no checkbox is shown
        onGetConsent: { paymentMethod in
            return paymentMethod == .card || paymentMethod == .paypal
        }
    )
)
```

Dynamic consent based on user type:

```swift
onGetConsent: { paymentMethod in
    // Only show consent for logged-in users
    let isLoggedIn = checkUserLoginStatus()
    
    if !isLoggedIn {
        return false // Guest users can't save payment methods
    }
    
    // Show consent for cards only
    return paymentMethod == .card
}
```

### onCancel

This callback is triggered when the customer cancels an Apple Pay payment sheet. Use it to clear loading state, update UI, or track abandonment analytics.

Drop-in doesn't clear its internal processing flag on `onCancel` — clear loading UI in this callback (PayPal cancellation clears processing but doesn't call `onCancel`).

You can use it to:

* Track payment abandonment analytics.
* Show user-friendly cancellation message.
* Re-enable form fields or buttons.
* Clear loading indicators.
* Offer alternative payment methods.
* Send abandonment emails for cart recovery.


#### Event data

The callback receives the following parameters:

| Parameter | Description |
|  --- | --- |
| `paymentMethod`DropInPaymentMethod | The payment method that was cancelled. |
| `data`Any? | Additional cancellation context. For Apple Pay this is typically a `BaseSdkException` (for example `ApplePaySessionCancelledException`). |


This callback is currently wired for when a user cancels the Apple Pay payment sheet.

Configure `onCancel` on `DropInGlobalConfig`. Drop-in forwards it for Apple Pay only. PayPal cancellation clears Drop-in processing state but does not invoke `onCancel`. Card and 3DS cancellation don't forward to this callback.

#### Example implementation

Handle Apple Pay cancellation through `onCancel`:

```swift
methodConfig: DropInMethodConfig(
    global: DropInGlobalConfig(
        onCancel: { paymentMethod, data in
            print("Payment cancelled: \(paymentMethod.rawValue)")
            
            // Track analytics
            analytics.track("payment_cancelled", properties: [
                "method": paymentMethod.rawValue,
                "timestamp": Date().timeIntervalSince1970
            ])
            
            // Hide loading spinner and re-enable checkout button
            Task { @MainActor in
                setIsProcessing(false)
                setCheckoutButtonDisabled(false)
                
                // Show cancellation message
                showNotification(
                    type: .info,
                    message: "\(paymentMethod.rawValue.capitalized) payment was cancelled. " +
                             "Please try again or choose a different payment method."
                )
                
                // Reset payment form
                resetPaymentForm()
            }
        }
    )
)
```

## Method-specific events

### Apple Pay callbacks

In addition to the unified callbacks above, Apple Pay provides method-specific event callbacks that allow you to update transaction details dynamically during the Apple Pay payment sheet flow. These are configured through `DropInApplePayConfig` in your `methodConfig`.

#### onShippingContactSelected

Triggered when the user selects a shipping contact in the Apple Pay sheet.

**Callback signature:**

```swift
onShippingContactSelected: ((ApplePayContact?) -> ApplePayRequestUpdate?)?
```

**Example:**

```swift
methodConfig: DropInMethodConfig(
    applePay: DropInApplePayConfig(
        onShippingContactSelected: { contact in
            // Calculate shipping cost based on selected address
            let shippingCost = calculateShipping(for: contact)
            let newTotal = baseAmount + shippingCost
            
            return ApplePayRequestUpdate(
                totalPaymentItem: ApplePayPaymentSummaryItem(
                    amount: newTotal,
                    type: .final,
                    label: "Total"
                )
            )
        }
    )
)
```

#### onShippingMethodSelected

Triggered when the user selects a shipping method in the Apple Pay sheet.

**Callback signature:**

```swift
onShippingMethodSelected: ((ApplePayShippingMethod?) -> ApplePayRequestUpdate?)?
```

**Example:**

```swift
onShippingMethodSelected: { shippingMethod in
    // Update total based on selected shipping method
    guard let method = shippingMethod else { return nil }
    
    let shippingCost = Decimal(string: method.amount) ?? 0
    let newTotal = baseAmount + shippingCost
    
    return ApplePayRequestUpdate(
        totalPaymentItem: ApplePayPaymentSummaryItem(
            amount: newTotal,
            type: .final,
            label: "Total"
        )
    )
}
```

#### onPaymentMethodSelected

Triggered when the user selects or changes their payment method in the Apple Pay sheet.

**Callback signature:**

```swift
onPaymentMethodSelected: ((ApplePayPaymentMethod?) -> ApplePayRequestUpdate?)?
```

**Example:**

```swift
onPaymentMethodSelected: { paymentMethod in
    // Could adjust pricing based on payment method if needed
    return ApplePayRequestUpdate(
        totalPaymentItem: ApplePayPaymentSummaryItem(
            amount: finalAmount,
            type: .final,
            label: "Total"
        )
    )
}
```

#### onCouponCodeChanged

Triggered when the user enters or changes a coupon code in the Apple Pay sheet.

**Callback signature:**

```swift
onCouponCodeChanged: ((String) -> ApplePayRequestUpdate?)?
```

**Example:**

```swift
onCouponCodeChanged: { couponCode in
    // Validate coupon and calculate discount
    let discount = validateCoupon(couponCode)
    let discountedTotal = baseAmount - discount
    
    return ApplePayRequestUpdate(
        totalPaymentItem: ApplePayPaymentSummaryItem(
            amount: discountedTotal,
            type: .final,
            label: "Total"
        )
    )
}
```

## Callback execution order

Understanding the callback flow helps you implement proper payment handling:

1. `onGetShopper`: session setup
  * Called at the start of `await dropIn.create()`.
  * Return a shopper ID when card-on-file is enabled.
2. `onBeforeSubmit`: validation phase
  * **Card:** card submit button tap.
  * **PayPal:** PayPal button tap.
  * **Apple Pay:** Apple Pay button tap (before the sheet opens).
  * Return `true` to proceed, `false` to cancel.
3. `onSubmit`: processing starts
  * **Card:** after `onBeforeSubmit` returns `true` and submission begins.
  * **Apple Pay:** after sheet authorisation, when pre-authorisation starts (not on button tap).
  * **PayPal:** not invoked. Drop-in sets processing during order creation (`onPreAuthorisation`) after `onBeforeSubmit` returns `true`.
  * Show loading indicators and disable UI as needed.
4. `onSuccess` or `onError`: payment completes
  * `onSuccess`: payment succeeded (verify on backend before fulfilling).
  * **PayPal:** `onSuccess` fires after the customer approves on PayPal.com (`onApprove`), not when the button is first tapped.
  * `onError`: payment failed (show error message and offer alternatives).
5. `onCancel`: user cancels (optional)
  * **Apple Pay only:** customer dismisses the payment sheet.
  * Clear loading UI — Drop-in does not reset processing for you.
  * **PayPal cancellation:** processing clears automatically; `onCancel` is not called.


## Complete integration example

Here's a complete example showing all callbacks working together:

```swift
import SwiftUI
import PXPCheckoutSDK

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    
    private var loadingTimeout: Task<Void, Never>?
    private var retryCount = 0
    private let maxRetries = 3
    private var paymentStartTime = Date()
    
    func loadDropIn() async {
        // Get session from backend
        guard let sessionData = await fetchSessionFromBackend() else {
            errorMessage = "Failed to create session"
            return
        }
        
        // Initialise Drop-in with all callbacks
        let config = CheckoutDropInConfig(
            environment: .live,
            session: sessionData,
            transactionData: DropInTransactionData(
                amount: Decimal(string: "25.0") ?? 0,
                currency: "USD",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation,
                    paypal: .authorisation
                ),
                merchantTransactionId: UUID().uuidString,
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-123",
            ownerId: "MERCHANT-1",
            kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
            
            // Required: Get shopper information
            onGetShopper: { async in
                let customerId = getCurrentCustomerId()
                return TransactionShopper(
                    id: customerId ?? "guest-\(UUID().uuidString)"
                )
            },
            
            // Before payment submission
            onBeforeSubmit: { paymentMethod in
                print("Payment method selected: \(paymentMethod.rawValue)")
                
                // Track analytics
                analytics.track("payment_initiated", properties: [
                    "payment_method": paymentMethod.rawValue,
                    "amount": 25.00
                ])
                
                // Show confirmation for large amounts
                if 25.00 > 100 {
                    let confirmed = await showConfirmation(
                        "Confirm payment of 25.00 USD?"
                    )
                    if !confirmed {
                        return false
                    }
                }
                
                // Validate inventory
                let hasStock = await checkInventory()
                if !hasStock {
                    showToast("Some items are no longer available")
                    return false
                }
                
                return true // Proceed with payment
            },
            
            // Payment processing started
            onSubmit: { paymentMethod in
                print("Processing payment...")
                
                // Show loading state (wrap async UI work in Task)
                Task { @MainActor in
                    showLoadingOverlay(true)
                    disableSubmitButton()
                }
                
                // Set timeout warning
                self.loadingTimeout = Task {
                    try? await Task.sleep(nanoseconds: 10_000_000_000)
                    await MainActor.run {
                        showMessage("Payment is taking longer than expected...", type: .warning)
                    }
                }
                
                // Track processing start
                self.paymentStartTime = Date()
                
                analytics.track("payment_processing", properties: [
                    "payment_method": paymentMethod.rawValue
                ])
            },
            
            // Payment succeeded (frontend notification)
            onSuccess: { result in
                print("Payment successful (verifying on backend...)")
                
                // Clear timeout
                self.loadingTimeout?.cancel()
                
                // Calculate processing time
                let processingTime = Date().timeIntervalSince(self.paymentStartTime)
                print("Payment processed in \(processingTime)s")
                
                // Track frontend success
                analytics.track("payment_success_frontend", properties: [
                    "system_transaction_id": result.systemTransactionId,
                    "payment_method": result.paymentMethod.rawValue,
                    "processing_time": processingTime
                ])
                
                // Show verifying message
                showMessage("Payment received! Verifying...", type: .success)
                
                // CRITICAL: Verify on backend
                Task {
                    do {
                        let verified = try await verifyPaymentOnBackend(
                            systemTransactionId: result.systemTransactionId,
                            merchantTransactionId: result.merchantTransactionId
                        )
                        
                        if verified.success {
                            // Backend verification passed
                            analytics.track("payment_verified", properties: [
                                "order_id": verified.orderId
                            ])
                            
                            // Clear cart and redirect
                            clearCart()
                            navigateToSuccess(verified.orderId)
                        } else {
                            throw NSError(
                                domain: "PaymentError",
                                code: -1,
                                userInfo: [NSLocalizedDescriptionKey: verified.error ?? "Verification failed"]
                            )
                        }
                    } catch {
                        print("Verification error: \(error.localizedDescription)")
                        await MainActor.run {
                            showLoadingOverlay(false)
                            showError(
                                "Payment verification failed. Please contact support with " +
                                "transaction ID: \(result.merchantTransactionId ?? result.systemTransactionId)"
                            )
                        }
                    }
                }
            },
            
            // Payment failed
            onError: { paymentMethod, error in
                print("Payment failed: \(error.errorCode)")
                print("Error message: \(error.errorMessage)")
                
                // Clear timeout
                self.loadingTimeout?.cancel()
                
                // Hide loading state (wrap async UI work in Task)
                Task { @MainActor in
                    showLoadingOverlay(false)
                    enableSubmitButton()
                }
                
                // Track error
                analytics.track("payment_failed", properties: [
                    "error_code": error.errorCode,
                    "error_message": error.errorMessage,
                    "payment_method": paymentMethod?.rawValue ?? "initialisation"
                ])
                
                // Log for monitoring
                logError(error)
                
                // Handle retryable errors
                let isNetworkError = error.errorCode == "SDK0500"
                let isTimeout = error.errorMessage.localizedCaseInsensitiveContains("timeout")
                if (isNetworkError || isTimeout) && self.retryCount < self.maxRetries {
                    self.retryCount += 1
                    Task { @MainActor in
                        showMessage(
                            "Connection issue (attempt \(self.retryCount)/\(self.maxRetries)). Please try again.",
                            type: .warning
                        )
                    }
                    return
                }
                
                self.retryCount = 0 // Reset
                
                // Show user-friendly error
                let userMessage: String
                switch error.errorCode {
                case "SDK1113":
                    userMessage = "Failed to update allow transaction."
                case "SDK1114":
                    userMessage = "Authentication failed."
                case "SDK1115":
                    userMessage = "Authorisation failed."
                case "SDK1116":
                    userMessage = "Card payment failed."
                case "SDK1117":
                    userMessage = "PayPal payment failed."
                case "SDK1119":
                    userMessage = "Apple Pay payment failed."
                case "SDK1122":
                    userMessage = "Paze is unavailable."
                case "SDK1123":
                    userMessage = "Paze payment failed."
                case "SDK1124":
                    userMessage = "Paze is only available for e-commerce checkout."
                case "SDK1209", "SDK1210", "SDK1211", "SDK1212":
                    userMessage = "Invalid shopper identity: \(error.errorMessage)"
                case "SDK1213":
                    userMessage = "Paze is only available for USD transactions."
                case "SDK0118":
                    userMessage = "Paze client ID is missing from the session."
                case "SDK1202A":
                    userMessage = "Paze merchant category code is missing from the session."
                default:
                    if error.errorMessage.localizedCaseInsensitiveContains("declined") {
                        userMessage = "Card declined. Please try a different card."
                    } else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
                        userMessage = "Insufficient funds. Please use a different payment method."
                    } else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
                        userMessage = "This card has expired."
                    } else if error.errorMessage.localizedCaseInsensitiveContains("cvv") {
                        userMessage = "Invalid security code."
                    } else {
                        userMessage = "Payment failed. Please try again."
                    }
                }
                
                Task { @MainActor in
                    showErrorMessage(userMessage)
                    
                    // Offer alternatives for card and Paze issues
                    if error.errorCode == "SDK1116" ||
                       error.errorCode == "SDK0118" ||
                       error.errorCode == "SDK1122" ||
                       error.errorCode == "SDK1123" ||
                       error.errorCode == "SDK1213" ||
                       paymentMethod == .paze ||
                       error.errorMessage.localizedCaseInsensitiveContains("card") {
                        showAlternativePaymentMethods()
                    }
                }
            },
            
            // Method-specific configuration
            methodConfig: DropInMethodConfig(
                global: DropInGlobalConfig(
                    // Control consent checkbox
                    onGetConsent: { paymentMethod in
                        // Grant consent for cards and PayPal when no checkbox is shown
                        return paymentMethod == .card || paymentMethod == .paypal
                    },
                    
                    // Handle cancellation
                    onCancel: { paymentMethod, data in
                        print("Payment cancelled: \(paymentMethod.rawValue)")
                        
                        // Clear timeout
                        self.loadingTimeout?.cancel()
                        
                        // Hide loading (wrap async UI work in Task)
                        Task { @MainActor in
                            showLoadingOverlay(false)
                            enableSubmitButton()
                        }
                        
                        // Track cancellation
                        analytics.track("payment_cancelled", properties: [
                            "payment_method": paymentMethod.rawValue
                        ])
                        
                        Task { @MainActor in
                            showMessage("Payment was cancelled. Please try again.", type: .info)
                        }
                    }
                )
            )
        )
        
        do {
            // Create Drop-in instance
            let instance = try CheckoutDropIn(config: config)
            await instance.create()
            dropIn = instance
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

struct CheckoutView: View {
    @StateObject private var viewModel = CheckoutViewModel()
    
    var body: some View {
        Group {
            if let dropIn = viewModel.dropIn {
                dropIn.buildContent()
            } else if let errorMessage = viewModel.errorMessage {
                Text(errorMessage)
            } else {
                ProgressView("Loading checkout...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}
```