Skip to content

Implementation

Learn how to use the Apple Pay component for iOS in your iOS application.

Overview

Every component follows the same four-step lifecycle:

  1. Initialise the PXP Checkout SDK with your configuration.
  2. Create the Apple Pay component with your specific configuration.
  3. Mount the component to display the Apple Pay button in your view.
  4. Handle payment results and lifecycle events.

Before you start

To use the Apple Pay component, you first need to:

Device and OS compatibility

Apple Pay for iOS has specific requirements for optimal functionality.

Supported iOS versions

  • iOS 10.0+ for basic Apple Pay support.
  • iOS 11.0+ for enhanced contact field support.
  • iOS 15.0+ for coupon code support.

Device requirements

  • iPhone: iPhone 6 or later with Touch ID or Face ID.
  • iPad: iPad Pro, iPad Air 2, iPad (5th generation) or later, iPad mini 3 or later.
  • Apple Watch: When paired with compatible iPhone.

Configuration requirements

  • The customer must have a supported payment method in their Wallet app.
  • The device must have Touch ID, Face ID, or passcode enabled.
  • App must have proper Apple Pay entitlements configured.

Step 1: Configure app entitlements

First, ensure your iOS app has the proper Apple Pay entitlements configured.

Entitlements.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.in-app-payments</key>
    <array>
        <string>merchant.com.yourcompany.yourapp</string>
    </array>
</dict>
</plist>

Info.plist privacy descriptions

<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to authenticate Apple Pay transactions</string>
<key>NSContactsUsageDescription</key>
<string>Access contacts for shipping and billing information</string>

Step 2: Initialise the iOS SDK

Import the PXP Checkout SDK and initialise it with Apple Pay support.

import UIKit
import PXPCheckoutSDK

class CheckoutViewController: UIViewController {
    
    private var checkout: PxpCheckout?
    private var applePayComponent: ApplePayButtonComponent?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initializeSDK()
    }
    
    private func initializeSDK() {
        let checkoutConfig = CheckoutConfig(
            environment: .test, // or .live for production
            session: SessionConfig(
                sessionId: "your-session-id",
                allowedFundingTypes: AllowedFundingTypes(
                    wallets: WalletFundingTypes(
                        applePay: ApplePayFundingType(
                            merchantId: "merchant.com.yourcompany.yourapp"
                        )
                    )
                )
            ),
            ownerId: "your-owner-id",
            ownerType: .merchant,
            merchantShopperId: "shopper-\(Int(Date().timeIntervalSince1970))",
            transactionData: TransactionData(
                amount: 25.00,
                currency: "USD",
                entryType: .mobileApp,
                intent: .capture,
                merchantTransactionId: "txn-\(Int(Date().timeIntervalSince1970))",
                merchantTransactionDate: Date(),
                shopper: ShopperData(
                    email: "customer@example.com"
                )
            )
        )
        
        do {
            checkout = try PxpCheckout.initialize(config: checkoutConfig)
            print("PXP Checkout SDK initialised successfully")
        } catch {
            print("Failed to initialise PXP Checkout SDK: \(error)")
        }
    }
}

Configuration parameters

PropertyDescription
environment
Environment
required
The environment type.

Possible values:
  • .test: For sandbox
  • .live: For production
session
SessionConfig
required
Details about the checkout session.
session.sessionId
String
required
The unique session identifier.
session.allowedFundingTypes.wallets.applePay.merchantId
String
required
Your Apple Pay merchant identifier.
ownerId
String
required
The identifier of the owner related to the ownerType.
ownerType
OwnerType
required
The type of owner.

Possible values:
  • .merchantGroup
  • .merchant Coming soon
  • .site Coming soon
merchantShopperId
String
required
A unique identifier for this shopper.
transactionData
TransactionData
required
Details about the transaction.
transactionData.currency
String
required
The currency code, in ISO 4217 format.
transactionData.amount
Double
required
The transaction amount.
transactionData.entryType
EntryType
required
The entry type.

Possible values:
  • .mobileApp: Mobile app transactions
  • .ecom: E-commerce transactions
  • .moto: Mail order/telephone order
transactionData.intent
Intent
required
The transaction intent.

Possible values:
  • .create: Create payment method
  • .confirm: Confirm payment
  • .capture: Capture authorised payment
  • .void: Void authorised payment
  • .refund: Refund captured payment
transactionData.merchantTransactionId
String
required
A unique identifier for this transaction.
transactionData.merchantTransactionDate
Date
required
The date and time of the transaction.
transactionData.shopper.email
String
required
The shopper's email address.

Step 3: Create the component configuration

Next, create the Apple Pay component configuration with your specific requirements.

private func createApplePayConfiguration() -> ApplePayButtonComponentConfig {
    let config = ApplePayButtonComponentConfig()
    
    // Basic configuration
    config.merchantDisplayName = "Your Store"
    config.paymentDescription = "Purchase from Your Store"
    config.currencyCode = "USD"
    config.countryCode = "US"
    config.supportedNetworks = [.visa, .masterCard, .amex]
    config.merchantCapabilities = [.threeDSecure, .emv]
    
    // Button styling
    config.buttonType = .buy
    config.buttonStyle = .black
    config.buttonRadius = 8.0
    
    // Payment items
    config.totalPaymentItem = ApplePayPaymentSummaryItem(
        amount: 25.00,
        label: "Total",
        type: .final
    )
    
    config.paymentItems = [
        ApplePayPaymentSummaryItem(amount: 20.00, label: "Product", type: .final),
        ApplePayPaymentSummaryItem(amount: 3.00, label: "Tax", type: .final),
        ApplePayPaymentSummaryItem(amount: 2.00, label: "Shipping", type: .final)
    ]
    
    // Contact fields
    config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
    config.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]
    
    // Shipping methods
    config.shippingMethods = [
        ApplePayShippingMethod(
            amount: 2.00,
            detail: "5-7 business days",
            identifier: "standard",
            label: "Standard Shipping"
        ),
        ApplePayShippingMethod(
            amount: 5.00,
            detail: "2-3 business days",
            identifier: "express",
            label: "Express Shipping"
        )
    ]
    
    // Event handlers
    config.onPreAuthorisation = { [weak self] in
        return await self?.handlePreAuthorisation()
    }
    
    config.onPostAuthorisation = { [weak self] result in
        self?.handlePostAuthorisation(result)
    }
    
    config.onError = { [weak self] error in
        self?.handleError(error)
    }
    
    config.onCancel = { [weak self] error in
        self?.handleCancellation(error)
    }
    
    return config
}

Configuration parameters

ParameterDescription
merchantDisplayName
String (≤ 64 characters)
required
The name of your store as it appears to customers during payment.
paymentDescription
String (≤ 128 characters)
required
A description of the payment that appears to customers.
currencyCode
String
required
The currency code in ISO 4217 format (e.g., "USD").
countryCode
String
required
The merchant's country code in ISO 3166-1 alpha-2 format (e.g., "US").
supportedNetworks
[PaymentNetwork]
required
Supported card networks.

Possible values:
  • .visa
  • .masterCard
  • .amex
  • .discover
  • .jcb
  • .unionPay
merchantCapabilities
[MerchantCapability]
required
Payment processing capabilities.

Possible values:
  • .threeDSecure: 3D Secure support
  • .emv: EMV support
  • .credit: Credit card support
  • .debit: Debit card support
buttonType
ApplePaymentButtonType
The button type.

Possible values:
  • .plain: Apple Pay logo only
  • .buy: Purchase button
  • .pay: Payment button
  • .donate: Donation button
  • .checkout: Checkout button
  • .book: Booking button
  • .subscribe: Subscription button
buttonStyle
ApplePaymentButtonStyle
The button style.

Possible values:
  • .black: Black background with white text
  • .white: White background with black text
  • .whiteOutline: White background with black border
  • .automatic: Adapts to system appearance (iOS 13+)
buttonRadius
CGFloat
The button corner radius (default: 4.0).
totalPaymentItem
ApplePayPaymentSummaryItem
The total payment amount display.
paymentItems
[ApplePayPaymentSummaryItem]
Individual line items for the payment.
requiredBillingContactFields
[ContactField]
Required billing contact fields.

Possible values:
  • .postalAddress
  • .name
  • .emailAddress
  • .phoneNumber
requiredShippingContactFields
[ContactField]
Required shipping contact fields.
shippingMethods
[ApplePayShippingMethod]
Available shipping methods.

Step 4: Setup the view layout

Create your view layout to include the Apple Pay button container.

Using Storyboard

@IBOutlet weak var paymentSummaryView: UIView!
@IBOutlet weak var applePayContainer: UIView!
@IBOutlet weak var errorMessageLabel: UILabel!
@IBOutlet weak var successMessageLabel: UILabel!

override func viewDidLoad() {
    super.viewDidLoad()
    setupUI()
    initializeSDK()
}

private func setupUI() {
    // Configure payment summary
    setupPaymentSummary()
    
    // Configure Apple Pay container
    applePayContainer.layer.cornerRadius = 8
    applePayContainer.backgroundColor = .systemBackground
    
    // Hide message labels initially
    errorMessageLabel.isHidden = true
    successMessageLabel.isHidden = true
}

private func setupPaymentSummary() {
    // Create payment summary views
    let stackView = UIStackView()
    stackView.axis = .vertical
    stackView.spacing = 8
    stackView.translatesAutoresizingMaskIntoConstraints = false
    
    // Add line items
    stackView.addArrangedSubview(createLineItem(label: "Premium T-Shirt", amount: "$20.00"))
    stackView.addArrangedSubview(createLineItem(label: "Sales Tax", amount: "$3.00"))
    stackView.addArrangedSubview(createLineItem(label: "Shipping", amount: "$2.00"))
    
    // Add separator
    let separator = UIView()
    separator.backgroundColor = .separator
    separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
    stackView.addArrangedSubview(separator)
    
    // Add total
    let totalView = createLineItem(label: "Total", amount: "$25.00", isTotal: true)
    stackView.addArrangedSubview(totalView)
    
    paymentSummaryView.addSubview(stackView)
    
    NSLayoutConstraint.activate([
        stackView.topAnchor.constraint(equalTo: paymentSummaryView.topAnchor, constant: 16),
        stackView.leadingAnchor.constraint(equalTo: paymentSummaryView.leadingAnchor, constant: 16),
        stackView.trailingAnchor.constraint(equalTo: paymentSummaryView.trailingAnchor, constant: -16),
        stackView.bottomAnchor.constraint(equalTo: paymentSummaryView.bottomAnchor, constant: -16)
    ])
}

private func createLineItem(label: String, amount: String, isTotal: Bool = false) -> UIView {
    let containerView = UIView()
    
    let labelView = UILabel()
    labelView.text = label
    labelView.font = isTotal ? .systemFont(ofSize: 18, weight: .semibold) : .systemFont(ofSize: 16)
    labelView.translatesAutoresizingMaskIntoConstraints = false
    
    let amountLabel = UILabel()
    amountLabel.text = amount
    amountLabel.font = isTotal ? .systemFont(ofSize: 18, weight: .semibold) : .systemFont(ofSize: 16)
    amountLabel.textAlignment = .right
    amountLabel.translatesAutoresizingMaskIntoConstraints = false
    
    containerView.addSubview(labelView)
    containerView.addSubview(amountLabel)
    
    NSLayoutConstraint.activate([
        labelView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
        labelView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
        labelView.topAnchor.constraint(equalTo: containerView.topAnchor),
        labelView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
        
        amountLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        amountLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
        amountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: labelView.trailingAnchor, constant: 8)
    ])
    
    return containerView
}

Using SwiftUI

import SwiftUI
import PXPCheckoutSDK

struct CheckoutView: View {
    @State private var applePayComponent: ApplePayButtonComponent?
    @State private var checkout: PxpCheckout?
    @State private var errorMessage: String = ""
    @State private var successMessage: String = ""
    @State private var showingError = false
    @State private var showingSuccess = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Complete your purchase")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            // Payment summary
            VStack(spacing: 12) {
                HStack {
                    Text("Premium T-Shirt")
                    Spacer()
                    Text("$20.00")
                }
                
                HStack {
                    Text("Sales Tax")
                    Spacer()
                    Text("$3.00")
                }
                
                HStack {
                    Text("Shipping")
                    Spacer()
                    Text("$2.00")
                }
                
                Divider()
                
                HStack {
                    Text("Total")
                        .fontWeight(.semibold)
                    Spacer()
                    Text("$25.00")
                        .fontWeight(.semibold)
                }
                .font(.title3)
            }
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(12)
            
            // Error/Success Messages
            if showingError {
                Text(errorMessage)
                    .foregroundColor(.red)
                    .padding()
                    .background(Color.red.opacity(0.1))
                    .cornerRadius(8)
            }
            
            if showingSuccess {
                Text(successMessage)
                    .foregroundColor(.green)
                    .padding()
                    .background(Color.green.opacity(0.1))
                    .cornerRadius(8)
            }
            
            // Apple Pay button container
            ApplePayButtonView(
                onComponentCreated: { component in
                    self.applePayComponent = component
                }
            )
            .frame(height: 50)
            .cornerRadius(8)
            
            Text("Or pay with credit card")
                .foregroundColor(.secondary)
                .font(.caption)
            
            Spacer()
        }
        .padding()
        .onAppear {
            initializeSDK()
        }
    }
    
    private func initializeSDK() {
        // Initialise SDK (same as UIKit example)
    }
}

Step 5: Create and mount the component

Create the Apple Pay component and add it to your view.

private func createAndMountApplePayComponent() {
    guard let checkout = checkout else {
        print("Checkout SDK not initialized")
        return
    }
    
    do {
        let config = createApplePayConfiguration()
        applePayComponent = try checkout.create(.applePayButton, componentConfig: config)
        
        if let componentView = applePayComponent?.render() {
            mountComponent(componentView)
        }
        
        print("Apple Pay component created and mounted successfully")
        
    } catch {
        print("Failed to create Apple Pay component: \(error)")
        showError("Apple Pay is not available on this device")
    }
}

private func mountComponent(_ componentView: UIView) {
    // Clear any existing subviews
    applePayContainer.subviews.forEach { $0.removeFromSuperview() }
    
    // Add the component view
    applePayContainer.addSubview(componentView)
    componentView.translatesAutoresizingMaskIntoConstraints = false
    
    NSLayoutConstraint.activate([
        componentView.topAnchor.constraint(equalTo: applePayContainer.topAnchor),
        componentView.leadingAnchor.constraint(equalTo: applePayContainer.leadingAnchor),
        componentView.trailingAnchor.constraint(equalTo: applePayContainer.trailingAnchor),
        componentView.bottomAnchor.constraint(equalTo: applePayContainer.bottomAnchor)
    ])
}

SwiftUI component wrapper

struct ApplePayButtonView: UIViewRepresentable {
    let onComponentCreated: (ApplePayButtonComponent) -> Void
    
    func makeUIView(context: Context) -> UIView {
        let containerView = UIView()
        
        // Create checkout and component
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: SessionConfig(sessionId: "your-session-id"),
            // ... other configuration
        )
        
        do {
            let checkout = try PxpCheckout.initialize(config: checkoutConfig)
            let config = createApplePayConfiguration()
            let component = try checkout.create(.applePayButton, componentConfig: config)
            
            if let componentView = component.render() {
                containerView.addSubview(componentView)
                componentView.translatesAutoresizingMaskIntoConstraints = false
                
                NSLayoutConstraint.activate([
                    componentView.topAnchor.constraint(equalTo: containerView.topAnchor),
                    componentView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                    componentView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                    componentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
                ])
            }
            
            onComponentCreated(component)
        } catch {
            print("Failed to create Apple Pay component: \(error)")
        }
        
        return containerView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // Updates handled by the component
    }
    
    private func createApplePayConfiguration() -> ApplePayButtonComponentConfig {
        // Same configuration as previous examples
    }
}

Step 6: Handle payment results

Implement the event handlers to process payment results and user interactions.

// MARK: - Event Handlers

private func handlePreAuthorisation() async -> ApplePayTransactionInitData? {
    print("Pre-authorisation started")
    
    DispatchQueue.main.async {
        self.showMessage("Processing payment...", type: .info)
    }
    
    return ApplePayTransactionInitData(
        riskScreeningData: RiskScreeningData(
            performRiskScreening: true,
            deviceSessionId: await getDeviceSessionId()
        )
    )
}

private func handlePostAuthorisation(_ result: BaseSubmitResult) {
    DispatchQueue.main.async {
        if let authorizedResult = result as? AuthorisedSubmitResult {
            self.showMessage("Payment successful! Redirecting...", type: .success)
            print("Transaction ID: \(authorizedResult.provider.code)")
            
            // Navigate to success screen after delay
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                self.navigateToSuccessScreen(transactionId: authorizedResult.provider.code)
            }
            
        } else if let declinedResult = result as? DeclinedSubmitResult {
            self.showMessage("Payment was declined: \(declinedResult.errorReason)", type: .error)
            
        } else if let failedResult = result as? FailedSubmitResult {
            self.showMessage("Payment failed: \(failedResult.errorReason)", type: .error)
        }
    }
}

private func handleError(_ error: Error) {
    print("Apple Pay error: \(error)")
    
    DispatchQueue.main.async {
        if let pkError = error as? PKPaymentError {
            self.handlePKPaymentError(pkError)
        } else if let validationError = error as? ApplePayValidationException {
            self.showMessage("Validation error: \(validationError.localizedDescription)", type: .error)
        } else {
            self.showMessage("Payment error: \(error.localizedDescription)", type: .error)
        }
    }
}

private func handlePKPaymentError(_ error: PKPaymentError) {
    switch error.code {
    case .paymentNotAllowed:
        showMessage("Payment not allowed. Please check your settings.", type: .error)
    case .paymentNetworkNotSupported:
        showMessage("Card network not supported. Please use a different card.", type: .error)
    case .deviceCannotMakePayments:
        showMessage("Apple Pay is not available on this device.", type: .error)
    default:
        showMessage("Payment error occurred. Please try again.", type: .error)
    }
}

private func handleCancellation(_ error: Error?) {
    print("Payment cancelled by user")
    DispatchQueue.main.async {
        self.showMessage("Payment was cancelled", type: .info)
    }
}

// MARK: - Helper methods

private func getDeviceSessionId() async -> String {
    return UIDevice.current.identifierForVendor?.uuidString ?? "unknown-device"
}

private func showMessage(_ message: String, type: MessageType) {
    errorMessageLabel.isHidden = true
    successMessageLabel.isHidden = true
    
    switch type {
    case .error:
        errorMessageLabel.text = message
        errorMessageLabel.isHidden = false
    case .success:
        successMessageLabel.text = message
        successMessageLabel.isHidden = false
    case .info:
        successMessageLabel.text = message
        successMessageLabel.textColor = .systemBlue
        successMessageLabel.isHidden = false
    }
}

private func navigateToSuccessScreen(transactionId: String) {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    if let successVC = storyboard.instantiateViewController(withIdentifier: "SuccessViewController") as? SuccessViewController {
        successVC.transactionId = transactionId
        navigationController?.pushViewController(successVC, animated: true)
    }
}

enum MessageType {
    case error, success, info
}

Step 7: Add advanced event handling

Implement shipping and payment method selection handlers.

private func setupAdvancedEventHandlers() {
    guard let config = applePayComponent?.config else { return }
    
    // Shipping contact selection
    config.onShippingContactSelected = { [weak self] contact in
        guard let self = self else { return PKPaymentRequestShippingContactUpdate(errors: []) }
        
        print("Shipping contact selected: \(contact)")
        
        // Calculate shipping and tax based on address
        let shippingCost = self.calculateShippingCost(for: contact.postalAddress)
        let tax = self.calculateTax(for: contact.postalAddress)
        let newTotal = 20.00 + shippingCost + tax
        
        return PKPaymentRequestShippingContactUpdate(
            errors: [],
            shippingMethods: [
                PKShippingMethod(
                    label: "Standard Shipping",
                    amount: NSDecimalNumber(value: shippingCost),
                    type: .final,
                    identifier: "standard",
                    detail: "5-7 business days"
                )
            ],
            paymentSummaryItems: [
                PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: 20.00)),
                PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: tax)),
                PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: shippingCost)),
                PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: newTotal))
            ]
        )
    }
    
    // Shipping method selection
    config.onShippingMethodSelected = { [weak self] method in
        guard let self = self else { return PKPaymentRequestShippingMethodUpdate(paymentSummaryItems: []) }
        
        print("Shipping method selected: \(method)")
        
        let baseAmount = 20.00
        let tax = 3.00
        let shippingCost = method.amount.doubleValue
        let newTotal = baseAmount + tax + shippingCost
        
        return PKPaymentRequestShippingMethodUpdate(
            paymentSummaryItems: [
                PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: baseAmount)),
                PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: tax)),
                PKPaymentSummaryItem(label: "Shipping", amount: method.amount),
                PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: newTotal))
            ]
        )
    }
    
    // Payment method selection (iOS 15.0+)
    if #available(iOS 15.0, *) {
        config.onPaymentMethodSelected = { paymentMethod in
            print("Payment method selected: \(paymentMethod)")
            
            return PKPaymentRequestPaymentMethodUpdate(
                paymentSummaryItems: [
                    PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: 20.00)),
                    PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: 3.00)),
                    PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: 2.00)),
                    PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: 25.00))
                ]
            )
        }
        
        // Coupon code handling
        config.onCouponCodeChanged = { [weak self] couponCode in
            guard let self = self else { return PKPaymentRequestCouponCodeUpdate(paymentSummaryItems: []) }
            
            print("Coupon code changed: \(couponCode)")
            
            let discount = self.calculateDiscount(for: couponCode)
            let newTotal = 25.00 - discount
            
            return PKPaymentRequestCouponCodeUpdate(
                errors: discount > 0 ? [] : [PKPaymentError(.couponCodeInvalid)],
                paymentSummaryItems: [
                    PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: 20.00)),
                    PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: 3.00)),
                    PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: 2.00)),
                    PKPaymentSummaryItem(label: "Discount", amount: NSDecimalNumber(value: -discount)),
                    PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: newTotal))
                ]
            )
        }
    }
}

// MARK: - Calculation methods

private func calculateShippingCost(for address: CNPostalAddress?) -> Double {
    guard let address = address else { return 5.00 }
    
    // Simple shipping calculation based on state/country
    if address.isoCountryCode == "US" {
        return address.state == "CA" ? 7.99 : 4.99
    }
    return 15.99 // International shipping
}

private func calculateTax(for address: CNPostalAddress?) -> Double {
    guard let address = address, address.isoCountryCode == "US" else { return 0.00 }
    
    let taxRates: [String: Double] = [
        "CA": 0.0875, // California
        "NY": 0.08,   // New York
        "TX": 0.0625  // Texas
    ]
    
    let rate = taxRates[address.state] ?? 0.06
    return 20.00 * rate
}

private func calculateDiscount(for couponCode: String) -> Double {
    let validCoupons: [String: Double] = [
        "SAVE10": 2.50,
        "SAVE20": 5.00,
        "WELCOME": 3.00
    ]
    
    return validCoupons[couponCode.uppercased()] ?? 0.00
}

Step 8: Component lifecycle management

Properly manage the component lifecycle to prevent memory leaks and ensure clean transitions.

class CheckoutViewController: UIViewController {
    
    // MARK: - Lifecycle
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Clean up component if navigating away
        if isMovingFromParent || isBeingDismissed {
            cleanupComponent()
        }
    }
    
    deinit {
        cleanupComponent()
    }
    
    private func cleanupComponent() {
        applePayComponent?.unmount()
        applePayComponent = nil
        checkout = nil
        print("Apple Pay component cleaned up")
    }
    
    // MARK: - Error recovery
    
    private func retryComponentCreation() {
        cleanupComponent()
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.initializeSDK()
            self.createAndMountApplePayComponent()
        }
    }
}

What's next?

Customise the look and feel

You can configure the appearance and behaviour of the Apple Pay component to fit your brand. We've documented all configurable parameters in the Customisation page.

// Custom button styling
config.buttonType = .buy
config.buttonStyle = .black
config.buttonRadius = 12.0

// Custom SwiftUI content
config.customContent = {
    return AnyView(
        HStack {
            Image(systemName: "applelogo")
                .foregroundColor(.white)
            Text("Buy with Apple Pay")
                .foregroundColor(.white)
                .fontWeight(.semibold)
        }
        .frame(maxWidth: .infinity, minHeight: 50)
        .background(
            LinearGradient(
                gradient: Gradient(colors: [Color.black, Color.gray]),
                startPoint: .leading,
                endPoint: .trailing
            )
        )
        .cornerRadius(12)
    )
}

Add more event handling

The Apple Pay component emits events based on user interaction, shipping changes, and payment method updates. For more information about all the available events, see the Events page.

Add error handling and validation

Error handling is crucial for payment components because they deal with sensitive financial data and complex validation rules. For more details about error handling, see the Data validation page.

// Comprehensive error handling
config.onError = { error in
    DispatchQueue.main.async {
        if let validationError = error as? ApplePayValidationException {
            self.handleValidationError(validationError)
        } else if let networkError = error as? URLError {
            self.handleNetworkError(networkError)
        } else {
            self.handleGenericError(error)
        }
    }
}

private func handleValidationError(_ error: ApplePayValidationException) {
    showMessage("Please check your payment information: \(error.localizedDescription)", type: .error)
}

private func handleNetworkError(_ error: URLError) {
    showMessage("Connection error. Please check your internet and try again.", type: .error)
}

private func handleGenericError(_ error: Error) {
    showMessage("An unexpected error occurred. Please try again.", type: .error)
}

Complete UIKit example

import UIKit
import PXPCheckoutSDK
import PassKit

class CompleteCheckoutViewController: UIViewController {
    
    // MARK: - Outlets
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var paymentSummaryView: UIView!
    @IBOutlet weak var applePayContainer: UIView!
    @IBOutlet weak var errorMessageLabel: UILabel!
    @IBOutlet weak var successMessageLabel: UILabel!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
    
    // MARK: - Properties
    private var checkout: PxpCheckout?
    private var applePayComponent: ApplePayButtonComponent?
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        initializeSDK()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if isMovingFromParent || isBeingDismissed {
            cleanupComponent()
        }
    }
    
    deinit {
        cleanupComponent()
    }
    
    // MARK: - Setup
    private func setupUI() {
        titleLabel.text = "Complete your purchase"
        titleLabel.font = UIFont.systemFont(ofSize: 28, weight: .bold)
        
        setupPaymentSummary()
        
        applePayContainer.layer.cornerRadius = 8
        applePayContainer.backgroundColor = .systemBackground
        
        errorMessageLabel.isHidden = true
        successMessageLabel.isHidden = true
        loadingIndicator.isHidden = true
        
        view.backgroundColor = .systemBackground
    }
    
    private func setupPaymentSummary() {
        paymentSummaryView.backgroundColor = UIColor.systemGray6
        paymentSummaryView.layer.cornerRadius = 12
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 12
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Add line items
        stackView.addArrangedSubview(createLineItem(label: "Premium T-Shirt", amount: "$20.00"))
        stackView.addArrangedSubview(createLineItem(label: "Sales Tax", amount: "$3.00"))
        stackView.addArrangedSubview(createLineItem(label: "Shipping", amount: "$2.00"))
        
        // Add separator
        let separator = UIView()
        separator.backgroundColor = .separator
        separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
        stackView.addArrangedSubview(separator)
        
        // Add total
        stackView.addArrangedSubview(createLineItem(label: "Total", amount: "$25.00", isTotal: true))
        
        paymentSummaryView.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: paymentSummaryView.topAnchor, constant: 16),
            stackView.leadingAnchor.constraint(equalTo: paymentSummaryView.leadingAnchor, constant: 16),
            stackView.trailingAnchor.constraint(equalTo: paymentSummaryView.trailingAnchor, constant: -16),
            stackView.bottomAnchor.constraint(equalTo: paymentSummaryView.bottomAnchor, constant: -16)
        ])
    }
    
    private func createLineItem(label: String, amount: String, isTotal: Bool = false) -> UIView {
        let containerView = UIView()
        
        let labelView = UILabel()
        labelView.text = label
        labelView.font = isTotal ? .systemFont(ofSize: 18, weight: .semibold) : .systemFont(ofSize: 16)
        labelView.translatesAutoresizingMaskIntoConstraints = false
        
        let amountLabel = UILabel()
        amountLabel.text = amount
        amountLabel.font = isTotal ? .systemFont(ofSize: 18, weight: .semibold) : .systemFont(ofSize: 16)
        amountLabel.textAlignment = .right
        amountLabel.translatesAutoresizingMaskIntoConstraints = false
        
        containerView.addSubview(labelView)
        containerView.addSubview(amountLabel)
        
        NSLayoutConstraint.activate([
            labelView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            labelView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            labelView.topAnchor.constraint(equalTo: containerView.topAnchor),
            labelView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            
            amountLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            amountLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            amountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: labelView.trailingAnchor, constant: 8)
        ])
        
        return containerView
    }
    
    // MARK: - SDK Initialisation
    private func initializeSDK() {
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: SessionConfig(
                sessionId: "your-session-id",
                allowedFundingTypes: AllowedFundingTypes(
                    wallets: WalletFundingTypes(
                        applePay: ApplePayFundingType(
                            merchantId: "merchant.com.yourcompany.yourapp"
                        )
                    )
                )
            ),
            ownerId: "your-owner-id",
            ownerType: .merchant,
            merchantShopperId: "shopper-\(Int(Date().timeIntervalSince1970))",
            transactionData: TransactionData(
                amount: 25.00,
                currency: "USD",
                entryType: .mobileApp,
                intent: .capture,
                merchantTransactionId: "txn-\(Int(Date().timeIntervalSince1970))",
                merchantTransactionDate: Date(),
                shopper: ShopperData(
                    email: "customer@example.com"
                )
            )
        )
        
        do {
            checkout = try PxpCheckout.initialize(config: checkoutConfig)
            createAndMountApplePayComponent()
        } catch {
            print("Failed to initialize SDK: \(error)")
            showMessage("Failed to initialize payment system", type: .error)
        }
    }
    
    private func createAndMountApplePayComponent() {
        guard let checkout = checkout else { return }
        
        // Check if Apple Pay is available
        guard PKPaymentAuthorizationController.canMakePayments() else {
            showMessage("Apple Pay is not available on this device", type: .error)
            return
        }
        
        do {
            let config = createApplePayConfiguration()
            applePayComponent = try checkout.create(.applePayButton, componentConfig: config)
            
            if let componentView = applePayComponent?.render() {
                mountComponent(componentView)
            }
            
        } catch {
            print("Failed to create Apple Pay component: \(error)")
            showMessage("Apple Pay is not available", type: .error)
        }
    }
    
    private func createApplePayConfiguration() -> ApplePayButtonComponentConfig {
        let config = ApplePayButtonComponentConfig()
        
        // Basic configuration
        config.merchantDisplayName = "Your Store Name"
        config.paymentDescription = "Premium T-Shirt Purchase"
        config.currencyCode = "USD"
        config.countryCode = "US"
        config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
        config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
        
        // Button styling
        config.buttonType = .buy
        config.buttonStyle = .black
        config.buttonRadius = 8.0
        
        // Payment items
        config.totalPaymentItem = ApplePayPaymentSummaryItem(
            amount: 25.00,
            label: "Your Store Name",
            type: .final
        )
        
        config.paymentItems = [
            ApplePayPaymentSummaryItem(amount: 20.00, label: "Premium T-Shirt", type: .final),
            ApplePayPaymentSummaryItem(amount: 3.00, label: "Sales Tax", type: .final),
            ApplePayPaymentSummaryItem(amount: 2.00, label: "Shipping", type: .final)
        ]
        
        // Contact fields
        config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
        config.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]
        
        // Shipping methods
        config.shippingMethods = [
            ApplePayShippingMethod(
                amount: 2.00,
                detail: "5-7 business days",
                identifier: "standard",
                label: "Standard Shipping"
            ),
            ApplePayShippingMethod(
                amount: 5.00,
                detail: "2-3 business days", 
                identifier: "express",
                label: "Express Shipping"
            )
        ]
        
        // Event handlers
        config.onPreAuthorisation = { [weak self] in
            return await self?.handlePreAuthorisation()
        }
        
        config.onPostAuthorisation = { [weak self] result in
            self?.handlePostAuthorisation(result)
        }
        
        config.onShippingContactSelected = { [weak self] contact in
            return self?.handleShippingContactSelected(contact) ?? PKPaymentRequestShippingContactUpdate(errors: [])
        }
        
        config.onShippingMethodSelected = { [weak self] method in
            return self?.handleShippingMethodSelected(method) ?? PKPaymentRequestShippingMethodUpdate(paymentSummaryItems: [])
        }
        
        config.onError = { [weak self] error in
            self?.handleError(error)
        }
        
        config.onCancel = { [weak self] error in
            self?.handleCancellation(error)
        }
        
        return config
    }
    
    private func mountComponent(_ componentView: UIView) {
        applePayContainer.subviews.forEach { $0.removeFromSuperview() }
        applePayContainer.addSubview(componentView)
        componentView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            componentView.topAnchor.constraint(equalTo: applePayContainer.topAnchor),
            componentView.leadingAnchor.constraint(equalTo: applePayContainer.leadingAnchor),
            componentView.trailingAnchor.constraint(equalTo: applePayContainer.trailingAnchor),
            componentView.bottomAnchor.constraint(equalTo: applePayContainer.bottomAnchor),
            componentView.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    // MARK: - Event Handlers
    private func handlePreAuthorisation() async -> ApplePayTransactionInitData? {
        DispatchQueue.main.async {
            self.showMessage("Processing payment...", type: .info)
            self.loadingIndicator.startAnimating()
            self.loadingIndicator.isHidden = false
        }
        
        return ApplePayTransactionInitData(
            riskScreeningData: RiskScreeningData(
                performRiskScreening: true,
                deviceSessionId: UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
            )
        )
    }
    
    private func handlePostAuthorisation(_ result: BaseSubmitResult) {
        DispatchQueue.main.async {
            self.loadingIndicator.stopAnimating()
            self.loadingIndicator.isHidden = true
            
            if let authorizedResult = result as? AuthorisedSubmitResult {
                self.showMessage("Payment successful! Redirecting...", type: .success)
                print("Transaction ID: \(authorizedResult.provider.code)")
                
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                    self.navigateToSuccessScreen(transactionId: authorizedResult.provider.code)
                }
                
            } else if let declinedResult = result as? DeclinedSubmitResult {
                self.showMessage("Payment declined: \(declinedResult.errorReason)", type: .error)
                
            } else if let failedResult = result as? FailedSubmitResult {
                self.showMessage("Payment failed: \(failedResult.errorReason)", type: .error)
            }
        }
    }
    
    private func handleShippingContactSelected(_ contact: PKContact) -> PKPaymentRequestShippingContactUpdate {
        let shippingCost = calculateShippingCost(for: contact.postalAddress)
        let tax = calculateTax(for: contact.postalAddress)
        let newTotal = 20.00 + shippingCost + tax
        
        return PKPaymentRequestShippingContactUpdate(
            errors: [],
            shippingMethods: [
                PKShippingMethod(
                    label: "Standard Shipping",
                    amount: NSDecimalNumber(value: shippingCost),
                    type: .final,
                    identifier: "standard",
                    detail: "5-7 business days"
                )
            ],
            paymentSummaryItems: [
                PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: 20.00)),
                PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: tax)),
                PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: shippingCost)),
                PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: newTotal))
            ]
        )
    }
    
    private func handleShippingMethodSelected(_ method: PKShippingMethod) -> PKPaymentRequestShippingMethodUpdate {
        let baseAmount = 20.00
        let tax = 3.00
        let shippingCost = method.amount.doubleValue
        let newTotal = baseAmount + tax + shippingCost
        
        return PKPaymentRequestShippingMethodUpdate(
            paymentSummaryItems: [
                PKPaymentSummaryItem(label: "Premium T-Shirt", amount: NSDecimalNumber(value: baseAmount)),
                PKPaymentSummaryItem(label: "Sales Tax", amount: NSDecimalNumber(value: tax)),
                PKPaymentSummaryItem(label: "Shipping", amount: method.amount),
                PKPaymentSummaryItem(label: "Your Store Name", amount: NSDecimalNumber(value: newTotal))
            ]
        )
    }
    
    private func handleError(_ error: Error) {
        DispatchQueue.main.async {
            self.loadingIndicator.stopAnimating()
            self.loadingIndicator.isHidden = true
            
            if let pkError = error as? PKPaymentError {
                self.handlePKPaymentError(pkError)
            } else if let validationError = error as? ApplePayValidationException {
                self.showMessage("Validation error: \(validationError.localizedDescription)", type: .error)
            } else {
                self.showMessage("Payment error: \(error.localizedDescription)", type: .error)
            }
        }
    }
    
    private func handlePKPaymentError(_ error: PKPaymentError) {
        switch error.code {
        case .paymentNotAllowed:
            showMessage("Payment not allowed. Please check your settings.", type: .error)
        case .paymentNetworkNotSupported:
            showMessage("Card not supported. Please use a different card.", type: .error)
        case .deviceCannotMakePayments:
            showMessage("Apple Pay is not available on this device.", type: .error)
        default:
            showMessage("Payment error occurred. Please try again.", type: .error)
        }
    }
    
    private func handleCancellation(_ error: Error?) {
        DispatchQueue.main.async {
            self.loadingIndicator.stopAnimating()
            self.loadingIndicator.isHidden = true
            self.showMessage("Payment was cancelled", type: .info)
        }
    }
    
    // MARK: - Helper Methods
    private func showMessage(_ message: String, type: MessageType) {
        errorMessageLabel.isHidden = true
        successMessageLabel.isHidden = true
        
        switch type {
        case .error:
            errorMessageLabel.text = message
            errorMessageLabel.isHidden = false
        case .success:
            successMessageLabel.text = message
            successMessageLabel.textColor = .systemGreen
            successMessageLabel.isHidden = false
        case .info:
            successMessageLabel.text = message
            successMessageLabel.textColor = .systemBlue
            successMessageLabel.isHidden = false
        }
    }
    
    private func calculateShippingCost(for address: CNPostalAddress?) -> Double {
        guard let address = address else { return 5.00 }
        return address.isoCountryCode == "US" ? (address.state == "CA" ? 7.99 : 4.99) : 15.99
    }
    
    private func calculateTax(for address: CNPostalAddress?) -> Double {
        guard let address = address, address.isoCountryCode == "US" else { return 0.00 }
        
        let taxRates: [String: Double] = ["CA": 0.0875, "NY": 0.08, "TX": 0.0625]
        let rate = taxRates[address.state] ?? 0.06
        return 20.00 * rate
    }
    
    private func navigateToSuccessScreen(transactionId: String) {
        let alert = UIAlertController(
            title: "Payment Successful",
            message: "Transaction ID: \(transactionId)",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            self.navigationController?.popViewController(animated: true)
        })
        present(alert, animated: true)
    }
    
    private func cleanupComponent() {
        applePayComponent?.unmount()
        applePayComponent = nil
        checkout = nil
    }
    
    enum MessageType {
        case error, success, info
    }
}