Skip to content

3DS

Integrate 3D Secure (3DS) into your Apple Pay checkout for iOS applications.

Overview

By implementing 3DS authentication into your Apple Pay payment flow on iOS, you benefit from:

  • Additional security: 3DS adds multiple layers of authentication and risk assessment.
  • Liability shift: Successful 3DS authentication typically shifts fraud liability from merchant to card issuer.
  • Higher success rate: Banks are more likely to approve 3DS-authenticated transactions.
  • Apple Pay integration: 3DS works seamlessly with Apple Pay's built-in security features.

However, the 3DS payment flow may be longer than the non-3DS one due to the additional authentication steps. With Apple Pay on iOS, 3DS often runs in the background during the Apple Pay authorisation process.

Payment flow

The Apple Pay 3D Secure flow on iOS is made up of seven key steps.

Step 1: Apple Pay authorisation

The customer authorises payment using Touch ID, Face ID, or passcode in the Apple Pay sheet. The onPreAuthorisation callback in ApplePayButtonComponent is invoked with the Apple Pay payment token.

Step 2: 3DS evaluation

The SDK evaluates whether 3DS authentication is required based on factors like transaction amount, risk assessment, merchant configuration, or regulatory requirements.

Step 3: Payment token processing

The Apple Pay payment token is processed and prepared for 3DS authentication. The token contains encrypted payment data that will be used in the 3DS flow.

Step 4: Pre-authorisation validation

This is where you can provide additional transaction data for 3DS processing. The onPreAuthorisation callback allows you to specify risk screening data, address verification, and 3DS configuration.

Step 5: 3DS authentication

The 3DS server evaluates the transaction risk using both Apple Pay data and additional merchant data:

  • Frictionless flow: If the transaction is low-risk (enhanced by Apple Pay's built-in security), authentication completes automatically.
  • Challenge flow: If additional verification is needed, the customer may complete a 3DS challenge, though this is less common with Apple Pay due to its inherent security.

Step 6: Transaction authorisation

The SDK sends the authorisation request to the payment gateway, including both the Apple Pay token and 3DS authentication data. This combines Apple Pay's security with 3DS protection.

Step 7: Authorisation result

You receive the final authorisation response from the payment gateway. The transaction is either approved or declined, with both Apple Pay and 3DS authentication confirmation.

Implementation

Before you start

To use 3D Secure with Apple Pay in your iOS application, you first need to:

  • Enable 3DS in the Unity Portal:
    • Go to Merchant setup > Merchant groups.
    • Select a merchant group.
    • Click the Services tab.
    • Click Edit in the Card service row.
    • Click Configure modules and enable ThreeD secure service.
  • Configure Apple Pay:
    • Set up your Apple Pay merchant identifier.
    • Add Apple Pay capability to your iOS app.
    • Configure your Apple Developer account with merchant IDs.
  • Get 3DS credentials from your payment processor:
    • acquirerProfileId: Your acquirer profile identifier.
    • providerId: Your 3DS provider identifier.
    • Test credentials for the sandbox environment.

Step 1: Configure your SDK

Set up your CheckoutConfig to include both Apple Pay and 3DS-friendly data.

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: SessionConfig(
        sessionId: "your-session-id",
        allowedFundingTypes: AllowedFundingTypes(
            wallets: WalletConfig(
                applePay: ApplePayConfig(
                    merchantId: "merchant.com.yourcompany"
                )
            )
        )
    ),
    transactionData: TransactionData(
        amount: 99.99,
        currency: "USD",
        entryType: .ecom,
        intent: .sale,
        merchantTransactionId: "order-123",
        merchantTransactionDate: Date(),
        // Include 3DS-friendly shopper data
        shopper: Shopper(
            email: "customer@example.com",
            firstName: "John",
            lastName: "Doe"
        )
    )
)

Step 2: Implement Apple Pay callbacks

Configure the Apple Pay component with 3DS capabilities.

let applePayConfig = ApplePayButtonComponentConfig()

// Basic payment configuration
applePayConfig.currencyCode = "USD"
applePayConfig.countryCode = "US"

// Supported payment networks
applePayConfig.supportedNetworks = [.visa, .masterCard, .amex, .discover]

// Merchant capabilities - IMPORTANT: Include .threeDSecure
applePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Contact field requirements for 3DS
applePayConfig.requiredBillingContactFields = [.postalAddress, .name, .phoneNumber, .emailAddress]
applePayConfig.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]

// Payment items
applePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 99.99,
    label: "Total",
    type: .final
)

applePayConfig.paymentItems = [
    ApplePayPaymentSummaryItem(amount: 89.99, label: "Product", type: .final),
    ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
]

// Button appearance
applePayConfig.buttonType = .buy
applePayConfig.buttonStyle = .black
applePayConfig.buttonRadius = 8.0

// REQUIRED: Provide 3DS configuration and additional transaction data
applePayConfig.onPreAuthorisation = {
    let logMessage = "onPreAuthorisation: Called before authorisation to update transaction details"
    print(logMessage)
    
    return ApplePayTransactionInitData(
        // 3DS configuration
        threeDSecureData: ThreeDSecureData(
            threeDSecureVersion: "2.1.0",
            electronicCommerceIndicator: .eci5, // Authenticated
            cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
            directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
            threeDSecureTransactionStatus: "Y"
        ),
        
        // Identity verification
        identityVerification: IdentityVerification(
            nameVerification: true
        ),
        
        // Address verification (helps with 3DS risk assessment)
        addressVerification: AddressVerification(
            countryCode: "US",
            houseNumberOrName: "123 Main St",
            postalCode: "10001"
        ),
        
        // Risk screening data for fraud detection
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            deviceSessionId: generateDeviceSessionId(),
            items: [
                Item(
                    name: "Product Name",
                    price: 89.99,
                    quantity: 1,
                    category: "Electronics"
                )
            ],
            fulfillments: [
                Fulfillment(
                    type: "SHIPPING",
                    address: Address(
                        countryCode: "US",
                        postalCode: "10001",
                        city: "New York",
                        state: "NY"
                    )
                )
            ]
        )
    )
}

// REQUIRED: Handle the final transaction result
applePayConfig.onPostAuthorisation = { result in
    if let authorizedResult = result as? AuthorisedSubmitResult {
        print("Payment successful with Apple Pay + 3DS!")
        print("Transaction ID: \(authorizedResult.provider.code)")
        
        // Check if 3DS was used
        if let threeDSData = authorizedResult.threeDSData {
            print("3DS authentication confirmed")
            print("ECI: \(threeDSData.electronicCommerceIndicator?.rawValue ?? "N/A")")
            
            // Check liability shift
            if threeDSData.electronicCommerceIndicator == .eci5 || 
               threeDSData.electronicCommerceIndicator == .eci6 {
                print("Liability shift achieved - protected from chargebacks")
            }
        }
        
        // Navigate to success screen
        DispatchQueue.main.async {
            // Present success view controller or update UI
            self.showPaymentSuccess(transactionId: authorizedResult.provider.code)
        }
        
    } else if let failedResult = result as? FailedSubmitResult {
        print("Payment failed: \(failedResult.errorReason)")
        
        DispatchQueue.main.async {
            self.showError("Payment failed. Please try again.")
        }
    }
}

// OPTIONAL: Handle Apple Pay errors
applePayConfig.onError = { error in
    print("Apple Pay error: \(error.localizedDescription)")
    
    DispatchQueue.main.async {
        // Handle specific 3DS + Apple Pay errors
        if error.localizedDescription.contains("3DS") {
            self.showError("Payment verification failed. Please try again.")
        } else if error.localizedDescription.contains("Apple Pay") {
            self.showError("Apple Pay is not available. Please try a different payment method.")
        } else {
            self.showError("Payment failed. Please try again.")
        }
    }
}

// OPTIONAL: Handle cancellation
applePayConfig.onCancel = { error in
    print("Apple Pay cancelled by user")
    // User closed Apple Pay sheet
}

Step 3: Handle common scenarios

Conditional 3DS based on amount

Use 3DS only for transactions above a certain amount.

applePayConfig.onPreAuthorisation = {
    let transactionAmount = checkoutConfig.transactionData.amount
    
    // Only use 3DS for amounts over $100
    if transactionAmount > 100 {
        return ApplePayTransactionInitData(
            threeDSecureData: ThreeDSecureData(
                threeDSecureVersion: "2.1.0",
                electronicCommerceIndicator: .eci5,
                cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
                directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
                threeDSecureTransactionStatus: "Y"
            ),
            addressVerification: AddressVerification(
                countryCode: "US",
                houseNumberOrName: "123 Main St",
                postalCode: "10001"
            ),
            riskScreeningData: RiskScreeningData(
                performRiskScreening: true,
                deviceSessionId: generateDeviceSessionId()
            )
        )
    }
    
    // For smaller amounts, just provide basic data
    return ApplePayTransactionInitData(
        addressVerification: AddressVerification(
            countryCode: "US",
            houseNumberOrName: "123 Main St",
            postalCode: "10001"
        )
    )
}

Different transaction types

Handle different types of Apple Pay transactions with appropriate 3DS settings.

func get3DSConfig(for transactionType: TransactionType) -> ThreeDSecureData {
    let baseVersion = "2.1.0"
    let dsTransactionId = "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260"
    
    switch transactionType {
    case .payment:
        return ThreeDSecureData(
            threeDSecureVersion: baseVersion,
            electronicCommerceIndicator: .eci5, // Authenticated
            cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
            directoryServerTransactionId: dsTransactionId,
            threeDSecureTransactionStatus: "Y"
        )
    case .recurring:
        return ThreeDSecureData(
            threeDSecureVersion: baseVersion,
            electronicCommerceIndicator: .eci6, // Recurring authenticated
            cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
            directoryServerTransactionId: dsTransactionId,
            threeDSecureTransactionStatus: "Y"
        )
    case .addCard:
        return ThreeDSecureData(
            threeDSecureVersion: baseVersion,
            electronicCommerceIndicator: .eci1, // Card verification
            cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
            directoryServerTransactionId: dsTransactionId,
            threeDSecureTransactionStatus: "Y"
        )
    }
}

enum TransactionType {
    case payment, recurring, addCard
}

applePayConfig.onPreAuthorisation = {
    return ApplePayTransactionInitData(
        threeDSecureData: get3DSConfig(for: .payment),
        addressVerification: AddressVerification(
            countryCode: "US",
            houseNumberOrName: "123 Main St",
            postalCode: "10001"
        ),
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            deviceSessionId: generateDeviceSessionId()
        )
    )
}

Apple Pay specific risk assessment

Leverage Apple Pay's built-in security for better risk assessment.

applePayConfig.onPreAuthorisation = {
    return ApplePayTransactionInitData(
        threeDSecureData: ThreeDSecureData(
            threeDSecureVersion: "2.1.0",
            electronicCommerceIndicator: .eci5,
            cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
            directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
            threeDSecureTransactionStatus: "Y" // Request no challenge (Apple Pay provides security)
        ),
        
        // Enhanced risk data
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            deviceSessionId: generateDeviceSessionId(),
            items: getCartItems(),
            fulfillments: [
                Fulfillment(
                    type: "SHIPPING",
                    address: getShippingAddress()
                )
            ],
            transaction: TransactionRiskData(
                deviceChannel: "IOS_APP",
                isRecurring: false,
                customerType: "EXISTING",
                // Apple Pay provides additional security context
                paymentMethod: "APPLE_PAY",
                deviceAuthentication: "BIOMETRIC", // Touch ID/Face ID used
                walletProvider: "APPLE"
            )
        )
    )
}

Step 4: Handle errors

Implement comprehensive error handling for Apple Pay + 3DS scenarios.

applePayConfig.onError = { error in
    print("Apple Pay + 3DS error: \(error.localizedDescription)")
    
    DispatchQueue.main.async {
        // Handle specific error types
        if let applePayError = error as? ApplePayValidationException {
            self.showError("Apple Pay configuration error. Please contact support.")
        } else if let paymentError = error as? ApplePayPaymentFailedException {
            self.showError("Payment processing failed. Please try again.")
        } else if let sessionError = error as? ApplePaySessionCancelledException {
            print("User cancelled Apple Pay")
            // Don't show error for user cancellation
        } else if error.localizedDescription.contains("3DS") {
            self.showError("Payment verification failed. Please try again.")
        } else if error.localizedDescription.contains("Network") {
            self.showError("Connection failed. Please check your internet and try again.")
        } else {
            self.showError("Payment failed. Please try again.")
        }
    }
}

applePayConfig.onPostAuthorisation = { result in
    if let failedResult = result as? FailedSubmitResult {
        print("Transaction failed: \(failedResult.errorReason)")
        
        DispatchQueue.main.async {
            // Handle specific failure scenarios
            switch failedResult.errorCode {
            case "AUTHENTICATION_FAILED":
                self.showError("Payment verification failed. Please try again.")
            case "CHALLENGE_TIMEOUT":
                self.showError("Verification timed out. Please try again.")
            case "AUTHENTICATION_REJECTED":
                self.showError("Payment was rejected by your bank.")
            case "INSUFFICIENT_FUNDS":
                self.showError("Insufficient funds. Please try a different payment method.")
            default:
                self.showError("Payment failed. Please try again or contact support.")
            }
        }
    }
}

Example

Here's a complete example of Apple Pay with 3DS implementation for iOS.

import UIKit
import PXPCheckoutSDK

class CheckoutViewController: UIViewController {
    
    private var applePayComponent: ApplePayButtonComponent?
    private var checkout: PxpCheckout?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupApplePayWith3DS()
    }
    
    private func setupApplePayWith3DS() {
        // Initialise the SDK
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: SessionConfig(
                sessionId: "your-session-id",
                allowedFundingTypes: AllowedFundingTypes(
                    wallets: WalletConfig(
                        applePay: ApplePayConfig(
                            merchantId: "merchant.com.yourcompany"
                        )
                    )
                )
            ),
            transactionData: TransactionData(
                amount: 150.00,
                currency: "USD",
                entryType: .ecom,
                intent: .sale,
                merchantTransactionId: "order-\(Int(Date().timeIntervalSince1970))",
                merchantTransactionDate: Date(),
                shopper: Shopper(
                    email: "customer@example.com",
                    firstName: "John",
                    lastName: "Doe"
                )
            )
        )
        
        do {
            checkout = try PxpCheckout.initialize(config: checkoutConfig)
            
            // Create Apple Pay component with 3DS
            let applePayConfig = createApplePayConfig()
            applePayComponent = try checkout?.create(.applePayButton, componentConfig: applePayConfig)
            
            // Add the component to your view
            if let componentView = applePayComponent?.render() {
                view.addSubview(componentView)
                setupConstraints(for: componentView)
            }
            
        } catch {
            print("Failed to initialize checkout: \(error)")
        }
    }
    
    private func createApplePayConfig() -> ApplePayButtonComponentConfig {
        let config = ApplePayButtonComponentConfig()
        
        // Basic configuration
        config.currencyCode = "USD"
        config.countryCode = "US"
        config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
        config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
        
        // Contact fields for 3DS
        config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
        config.requiredShippingContactFields = [.postalAddress, .name]
        
        // Payment items
        config.totalPaymentItem = ApplePayPaymentSummaryItem(
            amount: 150.00,
            label: "Total",
            type: .final
        )
        
        config.paymentItems = [
            ApplePayPaymentSummaryItem(amount: 140.00, label: "Premium Product", type: .final),
            ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
        ]
        
        // Button appearance
        config.buttonType = .buy
        config.buttonStyle = .black
        config.buttonRadius = 8.0
        
        // Step 1: Provide 3DS configuration
        config.onPreAuthorisation = { [weak self] in
            print("Configuring 3DS for Apple Pay transaction")
            
            return ApplePayTransactionInitData(
                // 3DS Authentication configuration
                threeDSecureData: ThreeDSecureData(
                    threeDSecureVersion: "2.1.0",
                    electronicCommerceIndicator: .eci5, // Authenticated
                    cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
                    directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
                    threeDSecureTransactionStatus: "Y"
                ),
                
                // Identity verification
                identityVerification: IdentityVerification(
                    nameVerification: true
                ),
                
                // Address verification
                addressVerification: AddressVerification(
                    countryCode: "US",
                    houseNumberOrName: "123 Main St",
                    postalCode: "10001"
                ),
                
                // Enhanced risk assessment
                riskScreeningData: RiskScreeningData(
                    performRiskScreening: true,
                    deviceSessionId: self?.generateDeviceSessionId() ?? "",
                    items: [
                        Item(
                            name: "Premium Product",
                            category: "Electronics",
                            price: 140.00,
                            quantity: 1,
                            sku: "PREM-001"
                        )
                    ],
                    fulfillments: [
                        Fulfillment(
                            type: "SHIPPING",
                            address: Address(
                                countryCode: "US",
                                postalCode: "10001",
                                city: "New York",
                                state: "NY"
                            )
                        )
                    ],
                    transaction: TransactionRiskData(
                        deviceChannel: "IOS_APP",
                        isRecurring: false,
                        customerType: "EXISTING",
                        paymentMethod: "APPLE_PAY",
                        deviceAuthentication: "BIOMETRIC",
                        walletProvider: "APPLE"
                    )
                )
            )
        }
        
        // Step 2: Handle successful/failed authorisation
        config.onPostAuthorisation = { [weak self] result in
            print("Apple Pay + 3DS result: \(result)")
            
            DispatchQueue.main.async {
                if let authorizedResult = result as? AuthorisedSubmitResult {
                    print("Payment successful!")
                    print("Transaction ID: \(authorizedResult.provider.code)")
                    
                    // Log 3DS authentication details
                    if let threeDSData = authorizedResult.threeDSData {
                        print("3DS Authentication Details:")
                        print("Version: \(threeDSData.threeDSecureVersion ?? "N/A")")
                        print("ECI: \(threeDSData.electronicCommerceIndicator?.rawValue ?? "N/A")")
                        print("CAVV: \(threeDSData.cardHolderAuthenticationVerificationValue ?? "N/A")")
                        print("DS Trans ID: \(threeDSData.directoryServerTransactionId ?? "N/A")")
                        
                        // Check liability shift
                        if threeDSData.electronicCommerceIndicator == .eci5 || 
                           threeDSData.electronicCommerceIndicator == .eci6 {
                            print("Liability shift achieved - protected from chargebacks")
                        }
                    }
                    
                    // Store transaction details
                    self?.storeTransactionRecord(
                        transactionId: authorizedResult.provider.code,
                        amount: 150.00,
                        currency: "USD",
                        paymentMethod: "APPLE_PAY",
                        threeDSStatus: authorizedResult.threeDSData != nil ? "AUTHENTICATED" : "NOT_REQUIRED"
                    )
                    
                    // Navigate to success screen
                    self?.showPaymentSuccess(transactionId: authorizedResult.provider.code)
                    
                } else if let failedResult = result as? FailedSubmitResult {
                    print("Payment failed: \(failedResult.errorReason)")
                    
                    // Handle specific failure scenarios
                    switch failedResult.errorCode {
                    case "AUTHENTICATION_FAILED":
                        self?.showError("Payment verification failed. Please try again.")
                    case "AUTHENTICATION_REJECTED":
                        self?.showError("Payment was rejected by your bank. Please try a different card.")
                    case "INSUFFICIENT_FUNDS":
                        self?.showError("Insufficient funds. Please try a different payment method.")
                    case "CARD_EXPIRED":
                        self?.showError("Card expired. Please update your payment method in Apple Pay.")
                    default:
                        self?.showError("Payment failed. Please try again or contact support.")
                    }
                }
            }
        }
        
        // Step 3: Error handling
        config.onError = { [weak self] error in
            print("Apple Pay + 3DS Error: \(error.localizedDescription)")
            
            DispatchQueue.main.async {
                if let _ = error as? ApplePayValidationException {
                    self?.showError("Apple Pay configuration error. Please contact support.")
                } else if let _ = error as? ApplePayPaymentFailedException {
                    self?.showError("Payment processing failed. Please try again.")
                } else if error.localizedDescription.contains("3DS") {
                    self?.showError("Payment verification failed. Please try again.")
                } else if error.localizedDescription.contains("Network") {
                    self?.showError("Connection failed. Please check your internet and try again.")
                } else {
                    self?.showError("An unexpected error occurred. Please try again.")
                }
            }
        }
        
        // Step 4: Handle cancellation
        config.onCancel = { error in
            print("Apple Pay cancelled by user")
            // User closed Apple Pay sheet - no error message needed
        }
        
        return config
    }
    
    // Helper functions
    private func generateDeviceSessionId() -> String {
        return "device_\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))_\(Int(Date().timeIntervalSince1970))"
    }
    
    private func showError(_ message: String) {
        let alert = UIAlertController(title: "Payment Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    private func showPaymentSuccess(transactionId: String) {
        let alert = UIAlertController(title: "Payment Successful", 
                                    message: "Transaction ID: \(transactionId)", 
                                    preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    private func storeTransactionRecord(transactionId: String, amount: Double, currency: String, paymentMethod: String, threeDSStatus: String) {
        let transactionData: [String: Any] = [
            "transactionId": transactionId,
            "amount": amount,
            "currency": currency,
            "paymentMethod": paymentMethod,
            "threeDSStatus": threeDSStatus,
            "timestamp": Date().timeIntervalSince1970
        ]
        
        // Store transaction details for audit/reconciliation
        UserDefaults.standard.set(transactionData, forKey: "last_transaction")
        
        // Send to analytics if needed
        print("Transaction stored: \(transactionData)")
    }
    
    private func setupConstraints(for componentView: UIView) {
        componentView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            componentView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            componentView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            componentView.widthAnchor.constraint(equalToConstant: 280),
            componentView.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

Callback data

This section describes the data received by the different callbacks as part of the Apple Pay 3DS flow on iOS.

onPreAuthorisation

The onPreAuthorisation callback receives no parameters for Apple Pay. It should return transaction initialisation data with 3DS configuration enabled.

Return data structure

ApplePayTransactionInitData(
    // 3D Secure configuration (required for 3DS flow)
    threeDSecureData: ThreeDSecureData(
        attemptN3d: false,
        challengeIndicator: "04",
        cardHolderName: "John Doe",
        email: "customer@example.com",
        homePhone: "+1234567890",
        mobilePhone: "+1234567890",
        workPhone: "+1234567890",
        billAddrCity: "New York",
        billAddrCountry: "840",
        billAddrLine1: "123 Main St",
        billAddrLine2: "Apt 4B",
        billAddrPostCode: "10001",
        billAddrState: "NY",
        shipAddrCity: "New York",
        shipAddrCountry: "840",
        shipAddrLine1: "123 Main St",
        shipAddrLine2: "Apt 4B",
        shipAddrPostCode: "10001",
        shipAddrState: "NY"
    ),
    
    // Identity verification (optional)
    identityVerification: IdentityVerification(
        nameVerification: true
    ),
    
    // Address verification (optional but recommended with 3DS)
    addressVerification: AddressVerification(
        countryCode: "US",
        houseNumberOrName: "123 Main St",
        postalCode: "10001"
    ),
    
    // Risk screening data (enhanced with 3DS)
    riskScreeningData: RiskScreeningData(
        performRiskScreening: true,
        deviceSessionId: "device_abc123_1704067200000",
        items: [
            Item(
                name: "Product Name",
                price: 99.99,
                quantity: 1
            )
        ],
        transaction: TransactionRiskData(
            paymentMethod: "APPLE_PAY",
            deviceAuthentication: "BIOMETRIC",
            walletProvider: "APPLE",
            authenticationMethod: "3DS_V2"
        )
    )
)

Here's an example of what to return:

config.onPreAuthorisation = {
    print("Preparing Apple Pay 3DS transaction")
    
    // Get customer information for 3DS authentication
    let customerInfo = await getCustomerInformation()
    let shippingInfo = await getShippingInformation()
    
    return ApplePayTransactionInitData(
        // 3DS configuration - required for 3DS flow
        threeDSecureData: ThreeDSecureData(
            attemptN3d: false,
            challengeIndicator: "04", // Request authentication
            cardHolderName: customerInfo.fullName,
            email: customerInfo.email,
            homePhone: customerInfo.phone,
            mobilePhone: customerInfo.mobile,
            workPhone: customerInfo.workPhone,
            
            // Billing address information
            billAddrCity: customerInfo.billingAddress.city,
            billAddrCountry: "840", // US country code
            billAddrLine1: customerInfo.billingAddress.line1,
            billAddrLine2: customerInfo.billingAddress.line2,
            billAddrPostCode: customerInfo.billingAddress.postalCode,
            billAddrState: customerInfo.billingAddress.state,
            
            // Shipping address information
            shipAddrCity: shippingInfo.city,
            shipAddrCountry: "840",
            shipAddrLine1: shippingInfo.line1,
            shipAddrLine2: shippingInfo.line2,
            shipAddrPostCode: shippingInfo.postalCode,
            shipAddrState: shippingInfo.state
        ),
        
        // Identity verification
        identityVerification: IdentityVerification(
            nameVerification: true
        ),
        
        // Address verification
        addressVerification: AddressVerification(
            countryCode: "US",
            houseNumberOrName: customerInfo.billingAddress.line1,
            postalCode: customerInfo.billingAddress.postalCode
        ),
        
        // Enhanced risk screening for 3DS
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            deviceSessionId: generateDeviceSessionId(),
            items: [
                Item(
                    name: "Premium Product",
                    category: "Electronics",
                    price: 99.99,
                    quantity: 1,
                    sku: "PROD-3DS-001"
                )
            ],
            fulfillments: [
                Fulfillment(
                    type: "SHIPPING",
                    address: Address(
                        countryCode: "US",
                        postalCode: shippingInfo.postalCode
                    ),
                    estimatedDelivery: "2024-01-15"
                )
            ],
            transaction: TransactionRiskData(
                deviceChannel: "IOS_APP",
                paymentMethod: "APPLE_PAY",
                deviceAuthentication: "BIOMETRIC", // Touch ID/Face ID
                walletProvider: "APPLE",
                authenticationMethod: "3DS_V2",
                processingType: "3ds",
                securityLevel: "MAXIMUM", // Due to Apple Pay + 3DS
                riskReduction: "APPLE_PAY_3DS",
                
                // Apple Pay with 3DS provides maximum security
                applePayWith3DS: ApplePaySecurityFeatures(
                    deviceBoundToken: true,
                    biometricAuthentication: true,
                    hardwareSecurityModule: true,
                    tokenization: true,
                    threeDSecureProtection: true
                ),
                
                // Enhanced 3DS context for iOS
                threeDSContext: ThreeDSContext(
                    deviceInfo: DeviceInfo(
                        platform: "iOS",
                        version: UIDevice.current.systemVersion,
                        model: UIDevice.current.model,
                        screenSize: "\(Int(UIScreen.main.bounds.width))x\(Int(UIScreen.main.bounds.height))",
                        locale: Locale.current.identifier,
                        timeZone: TimeZone.current.identifier
                    ),
                    customerAuthenticationInfo: CustomerAuthInfo(
                        authMethod: "biometric",
                        authTimestamp: Date().toISOString(),
                        authData: "apple_pay_biometric",
                        deviceId: UIDevice.current.identifierForVendor?.uuidString
                    )
                )
            )
        )
    )
}

onPostAuthorisation

The onPostAuthorisation callback receives the transaction result (BaseSubmitResult).

BaseSubmitResult types

The result can be one of several types:

AuthorisedSubmitResult - Successful 3DS authentication and authorisation:

AuthorisedSubmitResult(
    provider: ProviderResult(
        code: "AUTH123456",
        message: "Transaction approved with 3DS authentication"
    ),
    transactionId: "txn_abc123def456",
    threeDSecureResult: ThreeDSecureResult(
        status: "authenticated_and_authorized",
        eci: "05", // Electronic Commerce Indicator
        cavv: "jJ81HADVRtXfCBATEp01CJUAAAA=", // Cardholder Authentication Verification Value
        xid: "Nmp3NmJhZGI2MjAzOTI5Mw==",
        version: "2.1.0",
        directoryServerTransactionId: "c5b808e7-1de1-4069-a17b-f70d3b3b1645",
        acsTransactionId: "13c701a3-5a88-4c45-89e9-ef65e50b8bf9",
        challengeResult: "completed_successfully",
        challengeCompletionIndicator: "Y"
    ),
    riskResult: RiskResult(
        riskScore: 15,
        riskLevel: "LOW",
        riskFactors: ["apple_pay_3ds_protection"]
    )
)

DeclinedSubmitResult - 3DS authentication failed or transaction declined:

DeclinedSubmitResult(
    errorReason: "3DS authentication failed",
    errorCode: "3DS_AUTH_FAILED",
    threeDSecureResult: ThreeDSecureResult(
        status: "authentication_failed",
        eci: "07",
        version: "2.1.0",
        challengeResult: "abandoned_by_consumer",
        challengeCompletionIndicator: "N"
    ),
    declineReason: "Issuer declined after failed 3DS challenge"
)

FailedSubmitResult - Technical error during 3DS processing:

FailedSubmitResult(
    errorReason: "3DS server timeout",
    errorCode: "3DS_TIMEOUT",
    errorDetails: "Authentication request timed out",
    errorType: "NETWORK_ERROR"
)

Example implementation

config.onPostAuthorisation = { result in
    print("3DS Apple Pay transaction completed: \(result)")
    
    DispatchQueue.main.async {
        if let authorizedResult = result as? AuthorisedSubmitResult {
            // Successful 3DS authentication and authorisation
            print("3DS authentication successful!")
            print("Transaction ID: \(authorizedResult.transactionId)")
            print("Authorisation code: \(authorizedResult.provider.code)")
            print("3DS status: \(authorizedResult.threeDSecureResult.status)")
            print("ECI: \(authorizedResult.threeDSecureResult.eci)")
            print("CAVV: \(authorizedResult.threeDSecureResult.cavv)")
            
            // Store 3DS authentication results for compliance
            self.storeThreeDSResults(ThreeDSComplianceData(
                transactionId: authorizedResult.transactionId,
                eci: authorizedResult.threeDSecureResult.eci,
                cavv: authorizedResult.threeDSecureResult.cavv,
                xid: authorizedResult.threeDSecureResult.xid,
                version: authorizedResult.threeDSecureResult.version,
                challengeResult: authorizedResult.threeDSecureResult.challengeResult
            ))
            
            // Process order with high confidence
            self.processOrder(OrderData(
                transactionId: authorizedResult.transactionId,
                paymentMethod: "APPLE_PAY_3DS",
                securityLevel: "MAXIMUM",
                riskScore: authorizedResult.riskResult.riskScore,
                threeDSAuthenticated: true
            ))
            
            // Show success message
            self.showSuccessMessage("Payment successful with enhanced security!")
            
            // Navigate to order confirmation
            self.navigateToOrderConfirmation(transactionId: authorizedResult.transactionId)
            
        } else if let declinedResult = result as? DeclinedSubmitResult {
            // 3DS authentication failed or transaction declined
            print("3DS authentication or transaction failed")
            print("Error: \(declinedResult.errorReason)")
            print("3DS Status: \(declinedResult.threeDSecureResult?.status ?? "unknown")")
            
            // Handle specific 3DS failure scenarios
            if let challengeResult = declinedResult.threeDSecureResult?.challengeResult {
                switch challengeResult {
                case "abandoned_by_consumer":
                    self.showErrorMessage("Authentication was cancelled. Please try again.")
                case "authentication_failed":
                    self.showErrorMessage("Authentication failed. Please verify your identity and try again.")
                default:
                    self.showErrorMessage("Payment was declined. Please try a different payment method.")
                }
            } else {
                self.showErrorMessage("Payment was declined. Please try a different payment method.")
            }
            
            // Log for analysis
            self.logPaymentFailure(PaymentFailureData(
                errorCode: declinedResult.errorCode,
                errorReason: declinedResult.errorReason,
                threeDSStatus: declinedResult.threeDSecureResult?.status,
                challengeResult: declinedResult.threeDSecureResult?.challengeResult
            ))
            
        } else if let failedResult = result as? FailedSubmitResult {
            // Technical error during 3DS processing
            print("3DS processing error: \(failedResult.errorReason)")
            
            switch failedResult.errorCode {
            case "3DS_TIMEOUT":
                self.showErrorMessage("Authentication timed out. Please try again.")
            case "3DS_SERVER_ERROR":
                self.showErrorMessage("Authentication service unavailable. Please try again later.")
            default:
                self.showErrorMessage("Payment processing error. Please try again or contact support.")
            }
            
            // Log technical errors
            self.logTechnicalError(TechnicalErrorData(
                errorCode: failedResult.errorCode,
                errorReason: failedResult.errorReason,
                errorType: failedResult.errorType,
                timestamp: Date()
            ))
        }
    }
}

onError

The onError callback receives error information when the Apple Pay 3DS process encounters issues:

config.onError = { error in
    print("Apple Pay 3DS error: \(error)")
    
    DispatchQueue.main.async {
        if let validationError = error as? ApplePayValidationException {
            // Validation errors specific to 3DS setup
            if validationError.localizedDescription.contains("3DS") {
                self.showErrorMessage("3D Secure configuration error. Please contact support.")
            } else {
                self.showErrorMessage("Payment validation failed. Please check your information.")
            }
        } else if let networkError = error as? URLError {
            // Network errors during 3DS authentication
            self.showErrorMessage("Connection error during authentication. Please check your internet and try again.")
        } else if let pkError = error as? PKPaymentError {
            // Apple Pay specific errors with 3DS context
            switch pkError.code {
            case .paymentNotAllowed:
                self.showErrorMessage("Payment not allowed. Please check your settings.")
            case .paymentNetworkNotSupported:
                self.showErrorMessage("Card network not supported for 3D Secure. Please try a different card.")
            case .deviceCannotMakePayments:
                self.showErrorMessage("Apple Pay is not available on this device.")
            default:
                self.showErrorMessage("3D Secure authentication unavailable. Please try a different payment method.")
            }
        } else {
            // Generic errors
            self.showErrorMessage("Payment error occurred. Please try again.")
        }
        
        // Log all errors for debugging
        self.logError(ErrorData(
            error: error.localizedDescription,
            type: String(describing: type(of: error)),
            context: "3ds_apple_pay_ios",
            timestamp: Date()
        ))
    }
}

onCancel

The onCancel callback is triggered when the user cancels the Apple Pay process:

config.onCancel = { error in
    print("Apple Pay 3DS cancelled by user")
    
    DispatchQueue.main.async {
        // Track cancellation for analytics
        self.trackEvent("apple_pay_3ds_cancelled", parameters: [
            "step": error != nil ? "authentication" : "payment_sheet",
            "reason": error?.localizedDescription ?? "user_cancelled",
            "platform": "ios"
        ])
        
        // Show cancellation message
        self.showInfoMessage("Payment was cancelled. You can try again anytime.")
        
        // Reset payment form state
        self.resetPaymentForm()
    }
}

PKPaymentAuthorizationControllerDelegate methods

When using 3DS with Apple Pay on iOS, you may also need to implement additional delegate methods for enhanced functionality:

Shipping contact updates with 3DS context

func paymentAuthorizationController(
    _ controller: PKPaymentAuthorizationController,
    didSelectShippingContact contact: PKContact,
    handler completion: @escaping (PKPaymentRequestShippingContactUpdate) -> Void
) {
    // Calculate shipping with 3DS risk assessment
    let shippingCost = calculateShippingWithRiskAssessment(for: contact)
    let tax = calculateTaxWithCompliance(for: contact)
    let total = baseAmount + shippingCost + tax
    
    // Enhanced shipping update for 3DS
    let update = PKPaymentRequestShippingContactUpdate(
        errors: [],
        shippingMethods: [
            PKShippingMethod(
                label: "Standard Shipping (3DS Secure)",
                amount: NSDecimalNumber(value: shippingCost),
                type: .final,
                identifier: "standard_3ds",
                detail: "5-7 business days with enhanced security"
            )
        ],
        paymentSummaryItems: [
            PKPaymentSummaryItem(label: "Product", amount: NSDecimalNumber(value: baseAmount)),
            PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(value: tax)),
            PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: shippingCost)),
            PKPaymentSummaryItem(label: "Your Store (3DS Secure)", amount: NSDecimalNumber(value: total))
        ]
    )
    
    completion(update)
}

Payment method selection with 3DS validation

@available(iOS 15.0, *)
func paymentAuthorizationController(
    _ controller: PKPaymentAuthorizationController,
    didSelectPaymentMethod paymentMethod: PKPaymentMethod,
    handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void
) {
    // Validate payment method for 3DS compatibility
    let isThreeDSCompatible = validate3DSCompatibility(for: paymentMethod)
    
    let update = PKPaymentRequestPaymentMethodUpdate(
        errors: isThreeDSCompatible ? [] : [
            PKPaymentError(.paymentMethodInvalidError, 
                          userInfo: [NSLocalizedDescriptionKey: "This card does not support 3D Secure authentication"])
        ],
        paymentSummaryItems: [
            PKPaymentSummaryItem(label: "Product", amount: NSDecimalNumber(value: 99.99)),
            PKPaymentSummaryItem(label: "3DS Processing Fee", amount: NSDecimalNumber(value: 0.00)),
            PKPaymentSummaryItem(label: "Total (3DS Secure)", amount: NSDecimalNumber(value: 99.99))
        ]
    )
    
    completion(update)
}

private func validate3DSCompatibility(for paymentMethod: PKPaymentMethod) -> Bool {
    // Check if the payment network supports 3DS
    let threeDSNetworks: [PKPaymentNetwork] = [.visa, .masterCard, .amex]
    return threeDSNetworks.contains(paymentMethod.network)
}

Helper methods for 3DS data handling

// MARK: - 3DS Helper Methods

private func generateDeviceSessionId() -> String {
    let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
    let timestamp = Int(Date().timeIntervalSince1970)
    return "ios_3ds_\(deviceId)_\(timestamp)"
}

private func getCustomerInformation() async -> CustomerInfo {
    // Retrieve customer information for 3DS authentication
    return CustomerInfo(
        fullName: "John Doe",
        email: "customer@example.com",
        phone: "+1234567890",
        mobile: "+1234567890",
        workPhone: "+1234567890",
        billingAddress: Address(
            line1: "123 Main St",
            line2: "Apt 4B",
            city: "New York",
            state: "NY",
            postalCode: "10001",
            country: "US"
        )
    )
}

private func getShippingInformation() async -> Address {
    // Retrieve shipping information for 3DS
    return Address(
        line1: "123 Main St",
        line2: "Apt 4B",
        city: "New York",
        state: "NY",
        postalCode: "10001",
        country: "US"
    )
}

private func storeThreeDSResults(_ data: ThreeDSComplianceData) {
    // Store 3DS results for compliance and dispute resolution
    ComplianceManager.shared.store3DSResults(data)
}

private func processOrder(_ orderData: OrderData) {
    // Process order with 3DS authentication confidence
    OrderManager.shared.processSecureOrder(orderData)
}

private func showSuccessMessage(_ message: String) {
    let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}

private func showErrorMessage(_ message: String) {
    let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}

private func showInfoMessage(_ message: String) {
    let alert = UIAlertController(title: "Information", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}