# 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, Apple Pay, or Aeropay.

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.

Card-on-file also requires `methodConfig.card.showCOF` to resolve to `true` (the default when omitted). See [Cards](/guides/checkout/drop-in/ios/cards#card-display-properties).

`onGetShopper` and `merchantShopperId` identify the shopper for vault and card-on-file behaviour. Linking the Unity session to a Customer Profile is separate: set optional `customerProfileId` on the Sessions API request on your backend. See [Implementation — Customer Profile](/guides/checkout/drop-in/ios/implementation#customer-profile).

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 when `showCOF` is enabled: must be a non-empty string. See [Cards — card display properties](/guides/checkout/drop-in/ios/cards#card-display-properties). |
| `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`Date? | The shopper's date of birth. When shopper data is encoded for APIs, the SDK uses an ISO 8601 date-only string. |
| `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: {
    // 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., a UUID) that persists across the session. For registered users, use their customer/account ID from your system.

### onGetShippingAddress

This callback supplies a shipping address for billing prefill and for PayPal when `shippingPreference` is `.setProvidedAddress`. Card and PayPal transaction builders can consume it.

```swift
onGetShippingAddress: {
    ShippingAddress(
        // Populate from your checkout form / order record
        countryCode: "GB",
        postalCode: "NW1 6XE",
        address: "221B Baker Street",
        city: "London"
    )
}
```

For configuration details, see [Configuration — shipping address callback](/guides/checkout/drop-in/ios/configuration#shipping-address-callback) and [PayPal configuration](/guides/checkout/drop-in/ios/paypal).

### 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.
* **Apple Pay:** when the customer taps the Apple Pay button, before the payment sheet opens.
* **Aeropay:** when the shopper taps **Pay by bank**, before the Aeropay popup opens. Return `false` to prevent the popup from opening.


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`, `.paypal`, or `.aeropay`. |


#### Return value

Return `Bool` from the async closure:

* `true`: Continue with payment submission.
* `false`: Block payment submission without calling `onCancel` or `onError`.


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

#### Example implementation

Handle validation in `onBeforeSubmit` as follows:

```swift
onBeforeSubmit: { paymentMethod async 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 async in
    // Simple validation (still use async in — the callback type is async)
    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. |
| Apple Pay | After the customer authorises in the Apple Pay sheet, when Drop-in enters processing (not on button tap). |
| PayPal | Not called. Use `onBeforeSubmit` for pre-flight validation. For loading UI, show state after `onBeforeSubmit` returns `true` or when Drop-in enters processing during PayPal order creation. |
| Aeropay | After bank selection, when the Unity transaction is about to submit. Drop-in always proceeds after `onSubmit` (there's no merchant `onPreAuthorisation` gate). |


For card, Apple Pay, and Aeropay, `onSubmit` runs after `onBeforeSubmit` returns `true` (for Aeropay, after the popup flow reaches pre-authorisation).

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`, or `.aeropay`. Drop-in doesn't 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``.aeropay` |
| `paymentData`DropInPaymentData? | Typically `nil` for card, PayPal, Apple Pay, and Aeropay. Always verify transaction state on your backend using `systemTransactionId`. |


`DropInSubmitResult` type:

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

See `DropInPaymentData` in the SDK for all cases.

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 (e.g., `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 or refused | Prefer documented codes: `SDK1116` (generic card-payment fallback) or a preserved provider code on `error.errorCode`. Message substrings such as `"declined"` are optional UX polish only — not a reliable taxonomy. See [Error handling](/guides/checkout/drop-in/ios/error-handling). | Try a different card. |
| Insufficient funds / expired / CVV | Same as decline: use preserved `error.errorCode` when present; message heuristics (`"insufficient"`, `"expired"`, `"CVV"`) only as secondary copy. | Use a different card or correct the security code. |
| Update allow transaction failed | `error.errorCode == "SDK1113"` | Try again or contact support. |
| Authentication failed | `error.errorCode == "SDK1114"` | Try again or use different card. |
| Authorisation failed | `error.errorCode == "SDK1115"` (Drop-in card authorisation failed, not Aeropay `SDK0115`) | Try again or use different card. |
| Card payment failed (fallback) | `error.errorCode == "SDK1116"` when no more specific code was preserved | 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. |
| Aeropay create or render failed | `paymentMethod == .aeropay` with `SDK0113`, `SDK0115`, or `SDK1125` (see [Aeropay](/guides/checkout/drop-in/ios/aeropay) for the SDK1125 preserve nuance). Aeropay configuration or render failures during `create()` surface on `onError` (often `paymentMethod == .aeropay`), not as thrown errors from `await create()`. | Check session Aeropay credentials and `intent.aeropay`, or offer another method. |
| Invalid Aeropay consumer data at create | `paymentMethod == .aeropay` and `error.errorCode == "SDK1300"` | Correct non-empty `onGetShopper` values, or set `methodConfig.aeropay.userId` for returning shoppers. |
| Aeropay payment failed | `error.errorCode == "SDK1126"` (do not merge `SDK1300` with payment failure) | 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":
        // Generic card-payment failure fallback when no more specific code was preserved.
        // Declines may surface with a preserved underlying code or only a message — not only SDK1116 + substrings.
        userMessage = "Card payment failed. Please try another payment method."
    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 "SDK0113", "SDK0115", "SDK1125":
        userMessage = "Pay by Bank is unavailable. Please choose another payment method."
    case "SDK1300":
        userMessage = "Pay by Bank could not start. Check shopper email/phone or set methodConfig.aeropay.userId."
    case "SDK1126":
        userMessage = "Pay by Bank payment failed. Please try again or use a different payment method."
    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` Doesn't apply to Aeropay. |


#### 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 (e.g., 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. Aeropay doesn't use `onGetConsent`.

#### 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 login state (when no checkbox is shown):

```swift
onGetConsent: { paymentMethod in
    // Used when no consent checkbox is shown — grant or withhold store consent
    let isLoggedIn = checkUserLoginStatus()
    guard isLoggedIn else { return false }
    return paymentMethod == .card
}
```

### onCancel

This callback is triggered when the customer cancels a supported payment flow. For Apple Pay, that means dismissing the payment sheet. For Aeropay, that means closing the Aeropay popup. Use it to clear loading state, update UI, or track abandonment analytics.

Drop-in doesn't clear its internal processing flag on Apple Pay `onCancel`. Clear loading UI in this callback (PayPal cancellation clears processing but doesn't call `onCancel`). Aeropay unlocks the panel when the popup closes and then fires `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 (e.g., `.applePay` or `.aeropay`). |
| `data`Any? | Additional cancellation context. For Apple Pay this is typically a `BaseSdkException` (e.g., `ApplePaySessionCancelledException`). For Aeropay the payload is `nil`. |


This callback is wired for Apple Pay sheet cancellation and Aeropay popup close.

Configure `onCancel` on `DropInGlobalConfig`. Drop-in forwards it for Apple Pay and Aeropay. 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 and Aeropay cancellation through `onCancel`:

```swift
methodConfig: DropInMethodConfig(
    global: DropInGlobalConfig(
        onCancel: { paymentMethod, data in
            print("Payment cancelled: \(paymentMethod.rawValue)")
            if paymentMethod == .aeropay {
                // Shopper closed Pay by Bank — not an error
            }
            
            // 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 newTotal = baseAmount + method.amount
    
    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 with a non-empty `id` when card-on-file is enabled (`showCOF` defaults to `true`).
2. `onGetShippingAddress`: optional address resolution
  * Called when Drop-in needs a shipping address (e.g., billing prefill or PayPal `.setProvidedAddress`).
3. `onBeforeSubmit`: Validation phase
  * **Card:** card submit button tap.
  * **PayPal:** PayPal button tap.
  * **Apple Pay:** Apple Pay button tap (before the sheet opens).
  * **Aeropay:** **Pay by bank** tap (before the popup opens).
  * Return `true` to proceed, `false` to block submission without calling `onCancel` or `onError`.
4. `onSubmit`: Processing starts
  * **Card:** after `onBeforeSubmit` returns `true` and submission begins.
  * **Apple Pay:** after sheet authorisation, when Drop-in enters processing (not on button tap).
  * **PayPal:** not invoked. Show loading after `onBeforeSubmit` returns `true` or when Drop-in enters processing during PayPal order creation.
  * **Aeropay:** after bank selection, before Unity transaction submission. Drop-in always proceeds after this callback.
  * Show loading indicators and disable UI as needed.
5. `onSuccess` or `onError`: Payment completes
  * `onSuccess`: payment succeeded (verify on backend before fulfilling).
  * **PayPal:** `onSuccess` fires after the customer approves on PayPal.com, not when the button is first tapped.
  * `onError`: payment failed (show error message and offer alternatives).
6. `onCancel`: User cancels (optional)
  * **Apple Pay:** customer dismisses the payment sheet.
  * **Aeropay:** shopper closes the Aeropay popup (`data` is `nil`).
  * Clear loading UI. Drop-in doesn't reset processing for you on Apple Pay cancel.
  * **PayPal cancellation:** processing clears automatically; `onCancel` is not called.


For Aeropay-specific callback timing and configuration, see [Aeropay](/guides/checkout/drop-in/ios/aeropay).

## Complete integration example

If `create()` fails, do not assign `dropIn`, so you are not left on an empty `buildContent()`. The following example shows 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()
    private var isCreatingDropIn = false
    private var createFailed = false
    
    func loadDropIn() async {
        dropIn = nil
        isCreatingDropIn = true
        createFailed = false

        // 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: .purchase,
                    aeropay: .authorisation
                ),
                merchantTransactionId: UUID().uuidString,
                merchantTransactionDate: { Date() }
            ),
            merchantShopperId: "shopper-123",
            ownerId: "MERCHANT_GROUP_1", // Merchant group ID (ownerType is always "MerchantGroup")
            kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
            
            // Optional: shipping address for billing prefill / PayPal setProvidedAddress
            onGetShippingAddress: {
                ShippingAddress(
                    countryCode: "US",
                    postalCode: "10001",
                    address: "123 Main St",
                    city: "New York",
                    state: "NY"
                )
            },
            
            // Required: Get shopper information
            onGetShopper: {
                let customerId = getCurrentCustomerId()
                return TransactionShopper(
                    id: customerId ?? "guest-\(UUID().uuidString)"
                )
            },
            
            // Before payment submission
            onBeforeSubmit: { paymentMethod async 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
                if isCreatingDropIn {
                    createFailed = true
                }
                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 "SDK0113", "SDK0115", "SDK1125":
                    userMessage = "Pay by Bank is unavailable."
                case "SDK1300":
                    userMessage = "Pay by Bank could not start. Check shopper data or set methodConfig.aeropay.userId."
                case "SDK1126":
                    userMessage = "Pay by Bank payment failed."
                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 issues
                    if error.errorCode == "SDK1116" ||
                       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()
            isCreatingDropIn = false
            if !createFailed {
                dropIn = instance
            }
        } catch let error as BaseSdkException {
            isCreatingDropIn = false
            errorMessage = error.errorMessage
        } catch {
            isCreatingDropIn = false
            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 viewModel.errorMessage == nil {
                ProgressView("Loading checkout...")
            }

            if let errorMessage = viewModel.errorMessage {
                VStack(spacing: 16) {
                    Text("Unable to load checkout")
                        .font(.headline)
                    Text(errorMessage)
                        .font(.body)
                        .foregroundColor(.secondary)
                    Button("Retry") {
                        Task { await viewModel.loadDropIn() }
                    }
                }
                .padding()
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}
```