Skip to content

Recurring payments

Perform Apple Pay recurring, deferred, and automatic reload transactions for iOS applications.

Overview

Apple Pay supports three types of recurring payment scenarios:

  • Recurring payments: For subscription-based payments where the customer signs up and pays initially, and you then automatically charge them at specified intervals (monthly, yearly, etc.). Apple Pay handles the subscription lifecycle with proper customer consent.
  • Deferred payments: For transactions where authorisation happens now, but payment is processed later (e.g., hotel bookings, pre-orders). Apple Pay provides secure payment promises.
  • Automatic reload payments: For topping up stored value accounts when balance falls below a threshold (e.g., gift cards, transit cards). Apple Pay manages the reload triggers automatically.

All Apple Pay recurring payment types provide enhanced security, customer control, and transparent billing through Apple's ecosystem.

Recurring payments

Step 1: Set up the SDK config

To set up a recurring Apple Pay payment, you need to include recurring payment configuration in your Apple Pay component setup.

// Configure subscription transaction data
let transactionData = TransactionData(
    amount: 9.99,
    currency: "USD",
    entryType: .ecom,
    intent: .sale,
    merchantTransactionId: "sub-setup-123",
    merchantTransactionDate: Date(),
    shopper: Shopper(
        email: "customer@example.com",
        firstName: "John",
        lastName: "Doe"
    )
)

// Create complete SDK configuration
let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: SessionConfig(
        sessionId: "your-session-id",
        allowedFundingTypes: AllowedFundingTypes(
            wallets: WalletConfig(
                applePay: ApplePayConfig(
                    merchantId: "merchant.com.yourcompany"
                )
            )
        )
    ),
    transactionData: transactionData // ← This connects Step 1 to Step 2
)

Step 2: Create your Apple Pay component with recurring configuration

Next, you're going to use your checkoutConfig to create the Apple Pay component with recurring payment configuration. When the customer authorises with Apple Pay, the recurring payments will start.

let applePayConfig = ApplePayButtonComponentConfig()

// Basic configuration
applePayConfig.merchantDisplayName = "Subscription Service"
applePayConfig.paymentDescription = "Monthly Premium Subscription"
applePayConfig.currencyCode = "USD"
applePayConfig.countryCode = "US"
applePayConfig.supportedNetworks = [.visa, .masterCard, .amex]
applePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Payment items for first payment
applePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 9.99,
    label: "First Month",
    type: .final
)

// Button appearance - use subscribe type
applePayConfig.buttonType = .subscribe // Use subscribe button type
applePayConfig.buttonStyle = .black
applePayConfig.buttonRadius = 8.0

// Recurring payment configuration
applePayConfig.recurringRequest = ApplePayRecurringPaymentRequest(
    paymentDescription: "Monthly Premium Subscription",
    regularBilling: ApplePayRecurringPaymentSummaryItem(
        label: "Monthly Subscription",
        amount: NSDecimalNumber(value: 9.99)
    ),
    managementURL: "https://yoursite.com/manage-subscription",
    tokenNotificationURL: "https://yoursite.com/webhook/subscription" // Optional
)

// Set recurring payment schedule
applePayConfig.recurringRequest?.regularBilling.startDate = Calendar.current.date(byAdding: .month, value: 1, to: Date())
applePayConfig.recurringRequest?.regularBilling.endDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())
applePayConfig.recurringRequest?.regularBilling.intervalUnit = .month
applePayConfig.recurringRequest?.regularBilling.intervalCount = 1

// Post-authorisation callback
applePayConfig.onPostAuthorisation = { [weak self] result in
    if let authorizedResult = result as? AuthorisedSubmitResult {
        print("Recurring Apple Pay subscription started!")
        print("Transaction ID: \(authorizedResult.provider.code)")
        
        // Store recurring payment information
        self?.storeRecurringPayment(RecurringPaymentInfo(
            transactionId: authorizedResult.provider.code,
            customerId: "customer@example.com",
            subscriptionType: "monthly",
            amount: 9.99,
            currency: "USD",
            startDate: Calendar.current.date(byAdding: .month, value: 1, to: Date()) ?? Date(),
            endDate: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()
        ))
        
        // Navigate to success screen
        DispatchQueue.main.async {
            self?.navigateToSubscriptionSuccess()
        }
    }
}

applePayConfig.onError = { [weak self] error in
    print("Apple Pay subscription error: \(error)")
    DispatchQueue.main.async {
        self?.showError("Failed to start subscription. Please try again.")
    }
}

Advanced recurring payment with trial period

For subscriptions with trial periods, you can configure both trial and regular billing:

let applePayConfig = ApplePayButtonComponentConfig()

// Basic configuration
applePayConfig.merchantDisplayName = "SaaS Platform"
applePayConfig.paymentDescription = "Annual Subscription with Trial"
applePayConfig.currencyCode = "USD"
applePayConfig.countryCode = "US"
applePayConfig.supportedNetworks = [.visa, .masterCard, .amex]
applePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Payment items for trial period
applePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 0.00, // Free trial
    label: "Trial Period",
    type: .final
)

// Recurring payment configuration with trial
applePayConfig.recurringRequest = ApplePayRecurringPaymentRequest(
    paymentDescription: "Annual SaaS Subscription",
    regularBilling: ApplePayRecurringPaymentSummaryItem(
        label: "Annual Subscription",
        amount: NSDecimalNumber(value: 299.99)
    ),
    managementURL: "https://yoursite.com/manage-subscription",
    tokenNotificationURL: "https://yoursite.com/webhook/subscription"
)

// Set trial billing
applePayConfig.recurringRequest?.trialBilling = ApplePayRecurringPaymentSummaryItem(
    label: "14-Day Free Trial",
    amount: NSDecimalNumber(value: 0.00)
)

// Configure trial period
let calendar = Calendar.current
let trialStart = Date()
let trialEnd = calendar.date(byAdding: .day, value: 14, to: trialStart) ?? Date()
let regularStart = calendar.date(byAdding: .day, value: 1, to: trialEnd) ?? Date()

applePayConfig.recurringRequest?.trialBilling?.startDate = trialStart
applePayConfig.recurringRequest?.trialBilling?.endDate = trialEnd
applePayConfig.recurringRequest?.trialBilling?.intervalUnit = .day
applePayConfig.recurringRequest?.trialBilling?.intervalCount = 14

// Configure regular billing after trial
applePayConfig.recurringRequest?.regularBilling.startDate = regularStart
applePayConfig.recurringRequest?.regularBilling.intervalUnit = .year
applePayConfig.recurringRequest?.regularBilling.intervalCount = 1

Deferred payments

Step 1: Set up deferred payment configuration

For deferred payments (e.g., hotel bookings or pre-orders), configure the payment to be charged at a future date:

let deferredApplePayConfig = ApplePayButtonComponentConfig()

// Basic configuration
deferredApplePayConfig.merchantDisplayName = "Travel Agency"
deferredApplePayConfig.paymentDescription = "Hotel Booking"
deferredApplePayConfig.currencyCode = "USD"
deferredApplePayConfig.countryCode = "US"
deferredApplePayConfig.supportedNetworks = [.visa, .masterCard, .amex]
deferredApplePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Payment items
deferredApplePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 200.00,
    label: "Hotel Booking",
    type: .final
)

// Deferred payment configuration
deferredApplePayConfig.deferredPaymentRequest = ApplePayDeferredPaymentRequest(
    paymentDescription: "Hotel Booking - Payment Due at Check-in",
    deferredBilling: ApplePayDeferredPaymentSummaryItem(
        label: "Hotel Payment",
        amount: NSDecimalNumber(value: 200.00),
        deferredPaymentDate: Calendar.current.date(byAdding: .day, value: 30, to: Date()) ?? Date() // Check-in date
    ),
    managementURL: "https://yoursite.com/manage-booking",
    tokenNotificationURL: "https://yoursite.com/webhook/deferred-payment"
)

// Optional cancellation deadline
let cancellationDate = Calendar.current.date(byAdding: .day, value: 25, to: Date()) ?? Date()
deferredApplePayConfig.deferredPaymentRequest?.freeCancellationDate = cancellationDate

// Post-authorisation callback
deferredApplePayConfig.onPostAuthorisation = { [weak self] result in
    if let authorizedResult = result as? AuthorisedSubmitResult {
        print("Deferred Apple Pay payment authorized!")
        
        // Store deferred payment information
        self?.storeDeferredPayment(DeferredPaymentInfo(
            authorizationId: authorizedResult.provider.code,
            customerId: "customer@example.com",
            bookingReference: "HTL-\(Int(Date().timeIntervalSince1970))",
            amount: 200.00,
            currency: "USD",
            chargeDate: Calendar.current.date(byAdding: .day, value: 30, to: Date()) ?? Date(),
            cancellationDeadline: Calendar.current.date(byAdding: .day, value: 25, to: Date()) ?? Date()
        ))
        
        // Navigate to booking confirmed screen
        DispatchQueue.main.async {
            self?.navigateToBookingConfirmed()
        }
    }
}

Step 2: Handle deferred payment execution

When the deferred payment date arrives, you'll need to process the actual charge:

// This would typically run in a background job on the deferred payment date
func processDeferredPayment(deferredPaymentId: String) async throws {
    do {
        let deferredPayment = try await getDeferredPayment(deferredPaymentId)
        
        // Process the actual charge using the stored authorisation
        let chargeResult = try await chargeDeferredApplePayment(ChargeRequest(
            originalAuthorizationId: deferredPayment.authorizationId,
            amount: deferredPayment.amount,
            currency: deferredPayment.currency,
            merchantTransactionId: "deferred-\(deferredPaymentId)"
        ))
        
        if chargeResult.success {
            print("Deferred payment charged successfully")
            try await updateDeferredPaymentStatus(deferredPaymentId, status: .charged)
            try await sendPaymentConfirmation(deferredPayment.customerId)
        } else {
            print("Deferred payment failed: \(chargeResult.error ?? "Unknown error")")
            try await updateDeferredPaymentStatus(deferredPaymentId, status: .failed)
            try await sendPaymentFailureNotification(deferredPayment.customerId)
        }
    } catch {
        print("Error processing deferred payment: \(error)")
        throw error
    }
}

struct DeferredPaymentInfo {
    let authorizationId: String
    let customerId: String
    let bookingReference: String
    let amount: Double
    let currency: String
    let chargeDate: Date
    let cancellationDeadline: Date
}

struct ChargeRequest {
    let originalAuthorizationId: String
    let amount: Double
    let currency: String
    let merchantTransactionId: String
}

enum DeferredPaymentStatus {
    case authorized, charged, failed, cancelled
}

Automatic reload payments

Step 1: Set up automatic reload configuration

For automatic reload payments (e.g., gift cards, transit cards), configure the reload trigger and amount:

let autoReloadApplePayConfig = ApplePayButtonComponentConfig()

// Basic configuration
autoReloadApplePayConfig.merchantDisplayName = "Gift Card Store"
autoReloadApplePayConfig.paymentDescription = "Gift Card Purchase"
autoReloadApplePayConfig.currencyCode = "USD"
autoReloadApplePayConfig.countryCode = "US"
autoReloadApplePayConfig.supportedNetworks = [.visa, .masterCard, .amex]
autoReloadApplePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Payment items for initial purchase
autoReloadApplePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 50.00,
    label: "Gift Card",
    type: .final
)

// Automatic reload configuration
autoReloadApplePayConfig.automaticReloadPaymentRequest = ApplePayAutomaticReloadPaymentRequest(
    paymentDescription: "Automatic Reload for Gift Card",
    automaticReloadBilling: ApplePayAutomaticReloadPaymentSummaryItem(
        label: "Auto Reload",
        amount: NSDecimalNumber(value: 25.00),
        automaticReloadPaymentThresholdAmount: NSDecimalNumber(value: 10.00) // Reload when balance drops below $10
    ),
    managementURL: "https://yoursite.com/manage-gift-card",
    tokenNotificationURL: "https://yoursite.com/webhook/auto-reload"
)

// Post-authorisation callback
autoReloadApplePayConfig.onPostAuthorisation = { [weak self] result in
    if let authorizedResult = result as? AuthorisedSubmitResult {
        print("Apple Pay automatic reload set up!")
        
        // Store automatic reload configuration
        self?.storeAutoReloadConfig(AutoReloadConfig(
            transactionId: authorizedResult.provider.code,
            customerId: "customer@example.com",
            giftCardId: "GC-\(Int(Date().timeIntervalSince1970))",
            initialAmount: 50.00,
            reloadAmount: 25.00,
            thresholdAmount: 10.00,
            currency: "USD"
        ))
        
        // Navigate to gift card success screen
        DispatchQueue.main.async {
            self?.navigateToGiftCardSuccess()
        }
    }
}

Step 2: Handle automatic reload triggers

Monitor account balances and trigger automatic reloads when thresholds are reached:

// This would typically run as a scheduled job to check balances
func checkAutoReloadTriggers() async throws {
    let autoReloadAccounts = try await getActiveAutoReloadAccounts()
    
    for account in autoReloadAccounts {
        let currentBalance = try await getAccountBalance(account.giftCardId)
        
        if currentBalance <= account.thresholdAmount {
            do {
                print("Triggering auto-reload for account \(account.giftCardId)")
                
                // Process automatic reload using stored authorization
                let reloadResult = try await processAutoReload(AutoReloadRequest(
                    originalTransactionId: account.transactionId,
                    amount: account.reloadAmount,
                    currency: account.currency,
                    accountId: account.giftCardId,
                    merchantTransactionId: "reload-\(account.giftCardId)-\(Int(Date().timeIntervalSince1970))"
                ))
                
                if reloadResult.success {
                    // Update account balance
                    try await updateAccountBalance(
                        account.giftCardId,
                        newBalance: currentBalance + account.reloadAmount
                    )
                    
                    // Send notification to customer
                    try await sendAutoReloadNotification(account.customerId, notification: AutoReloadNotification(
                        amount: account.reloadAmount,
                        newBalance: currentBalance + account.reloadAmount,
                        transactionId: reloadResult.transactionId
                    ))
                    
                    print("Auto-reload completed successfully")
                } else {
                    print("Auto-reload failed: \(reloadResult.error ?? "Unknown error")")
                    try await sendAutoReloadFailureNotification(account.customerId)
                }
            } catch {
                print("Error processing auto-reload: \(error)")
            }
        }
    }
}

struct AutoReloadConfig {
    let transactionId: String
    let customerId: String
    let giftCardId: String
    let initialAmount: Double
    let reloadAmount: Double
    let thresholdAmount: Double
    let currency: String
}

struct AutoReloadRequest {
    let originalTransactionId: String
    let amount: Double
    let currency: String
    let accountId: String
    let merchantTransactionId: String
}

struct AutoReloadNotification {
    let amount: Double
    let newBalance: Double
    let transactionId: String
}

// Schedule to run every hour to check for reload triggers
func scheduleAutoReloadChecks() {
    Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { _ in
        Task {
            try await checkAutoReloadTriggers()
        }
    }
}

Combined payment types

You can combine multiple payment types in a single Apple Pay transaction:

let combinedApplePayConfig = ApplePayButtonComponentConfig()

// Basic configuration
combinedApplePayConfig.merchantDisplayName = "Premium Service"
combinedApplePayConfig.paymentDescription = "Premium Subscription with Auto-Reload"
combinedApplePayConfig.currencyCode = "USD"
combinedApplePayConfig.countryCode = "US"
combinedApplePayConfig.supportedNetworks = [.visa, .masterCard, .amex]
combinedApplePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]

// Payment items for first payment
combinedApplePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
    amount: 29.99,
    label: "First Payment",
    type: .final
)

// Recurring payment for subscription
combinedApplePayConfig.recurringRequest = ApplePayRecurringPaymentRequest(
    paymentDescription: "Monthly Premium Subscription",
    regularBilling: ApplePayRecurringPaymentSummaryItem(
        label: "Monthly Premium",
        amount: NSDecimalNumber(value: 29.99)
    ),
    managementURL: "https://yoursite.com/manage-subscription",
    tokenNotificationURL: "https://yoursite.com/webhook/subscription"
)

// Configure recurring schedule
let calendar = Calendar.current
combinedApplePayConfig.recurringRequest?.regularBilling.startDate = calendar.date(byAdding: .month, value: 1, to: Date())
combinedApplePayConfig.recurringRequest?.regularBilling.endDate = calendar.date(byAdding: .year, value: 1, to: Date())
combinedApplePayConfig.recurringRequest?.regularBilling.intervalUnit = .month
combinedApplePayConfig.recurringRequest?.regularBilling.intervalCount = 1

// Automatic reload for credits
combinedApplePayConfig.automaticReloadPaymentRequest = ApplePayAutomaticReloadPaymentRequest(
    paymentDescription: "Auto Reload Credits",
    automaticReloadBilling: ApplePayAutomaticReloadPaymentSummaryItem(
        label: "Credit Reload",
        amount: NSDecimalNumber(value: 10.00),
        automaticReloadPaymentThresholdAmount: NSDecimalNumber(value: 5.00)
    ),
    managementURL: "https://yoursite.com/manage-credits"
)

// Post-authorisation callback
combinedApplePayConfig.onPostAuthorisation = { [weak self] result in
    if let authorizedResult = result as? AuthorisedSubmitResult {
        print("Combined Apple Pay payment configured!")
        
        // Store both recurring and auto-reload configurations
        self?.storeCombinedPaymentConfig(CombinedPaymentConfig(
            transactionId: authorizedResult.provider.code,
            customerId: "customer@example.com",
            subscription: SubscriptionConfig(
                amount: 29.99,
                interval: "monthly",
                startDate: calendar.date(byAdding: .month, value: 1, to: Date()) ?? Date(),
                endDate: calendar.date(byAdding: .year, value: 1, to: Date()) ?? Date()
            ),
            autoReload: AutoReloadConfig(
                transactionId: authorizedResult.provider.code,
                customerId: "customer@example.com",
                giftCardId: "GC-\(Int(Date().timeIntervalSince1970))",
                initialAmount: 0,
                reloadAmount: 10.00,
                thresholdAmount: 5.00,
                currency: "USD"
            )
        ))
    }
}

struct CombinedPaymentConfig {
    let transactionId: String
    let customerId: String
    let subscription: SubscriptionConfig
    let autoReload: AutoReloadConfig
}

struct SubscriptionConfig {
    let amount: Double
    let interval: String
    let startDate: Date
    let endDate: Date
}

For recurring payments, you should also implement a consent component to clearly communicate the recurring nature of the payment:

// Create Apple Pay consent component
let applePayConsentConfig = ApplePayConsentComponentConfig(
    label: "I agree to store my Device Primary Account Number (DPAN) for recurring payments and authorise monthly charges of $9.99",
    checkedColor: UIColor.systemBlue,
    uncheckedColor: UIColor.systemGray,
    fontSize: 16.0,
    checked: false
)

let applePayConsentComponent = try checkout.create(.applePayConsent, componentConfig: applePayConsentConfig)

// Connect consent to Apple Pay button
applePayConfig.applePayConsentComponent = applePayConsentComponent as? ApplePayConsentComponent

// Alternative: Use callback for consent
applePayConfig.onGetConsent = { [weak self] in
    return self?.recurringConsentSwitch.isOn ?? false
}

Customer management URLs

Apple Pay requires management URLs for recurring payments where customers can:

  • View upcoming charges.
  • Modify subscription details.
  • Cancel subscriptions.
  • Update payment information.
// Example management functionality
class SubscriptionManager {
    
    func getSubscriptions(for customerId: String) async throws -> [Subscription] {
        let url = URL(string: "https://yourapi.com/customers/\(customerId)/subscriptions")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Subscription].self, from: data)
    }
    
    func cancelSubscription(_ subscriptionId: String) async throws -> Bool {
        // Cancel with Apple Pay notification
        try await notifyApplePayCancellation(subscriptionId)
        
        // Cancel in your system
        let url = URL(string: "https://yourapi.com/subscriptions/\(subscriptionId)/cancel")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode == 200
    }
    
    func updateSubscription(_ subscriptionId: String, updates: SubscriptionUpdate) async throws -> Bool {
        let url = URL(string: "https://yourapi.com/subscriptions/\(subscriptionId)")!
        var request = URLRequest(url: url)
        request.httpMethod = "PATCH"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(updates)
        
        let (_, response) = try await URLSession.shared.data(for: request)
        return (response as? HTTPURLResponse)?.statusCode == 200
    }
    
    private func notifyApplePayCancellation(_ subscriptionId: String) async throws {
        // Implement Apple Pay notification for cancellation
        // This ensures Apple Pay is aware of the cancellation
    }
}

struct Subscription: Codable {
    let id: String
    let customerId: String
    let amount: Double
    let currency: String
    let status: SubscriptionStatus
    let nextPaymentDate: Date
}

struct SubscriptionUpdate: Codable {
    let amount: Double?
    let nextPaymentDate: Date?
    let status: SubscriptionStatus?
}

enum SubscriptionStatus: String, Codable {
    case active, paused, cancelled, failed
}

Error handling and edge cases

Recurring payment failures

Handle various failure scenarios for recurring payments:

let applePayConfig = ApplePayButtonComponentConfig()

applePayConfig.onError = { [weak self] error in
    print("Apple Pay recurring payment error: \(error)")
    
    DispatchQueue.main.async {
        if error.localizedDescription.contains("recurring") {
            self?.showError("Unable to set up recurring payments. Please try again.")
        } else if error.localizedDescription.contains("deferred") {
            self?.showError("Unable to authorise future payment. Please try again.")
        } else if error.localizedDescription.contains("reload") {
            self?.showError("Unable to set up automatic reload. Please try again.")
        } else {
            self?.showError("Payment setup failed. Please try again.")
        }
    }
}

applePayConfig.onPostAuthorisation = { [weak self] result in
    if let failedResult = result as? FailedSubmitResult {
        DispatchQueue.main.async {
            // Handle specific recurring payment failures
            switch failedResult.errorCode {
            case "RECURRING_NOT_SUPPORTED":
                self?.showError("Recurring payments are not supported for this card.")
            case "DEFERRED_NOT_SUPPORTED":
                self?.showError("Deferred payments are not supported for this card.")
            case "AUTO_RELOAD_NOT_SUPPORTED":
                self?.showError("Automatic reload is not supported for this card.")
            case "INSUFFICIENT_FUNDS":
                self?.showError("Insufficient funds for recurring payment setup.")
            default:
                self?.showError("Payment authorisation failed. Please try again.")
            }
        }
    }
}

Webhook handling

Implement webhooks to handle Apple Pay notifications for recurring payments:

// Example webhook handler using Vapor framework
import Vapor

func routes(_ app: Application) throws {
    app.post("webhook", "apple-pay") { req -> HTTPStatus in
        let webhookData = try req.content.decode(ApplePayWebhook.self)
        
        switch webhookData.type {
        case .recurringPaymentCharged:
            try await handleRecurringPaymentCharged(webhookData.data)
        case .recurringPaymentFailed:
            try await handleRecurringPaymentFailed(webhookData.data)
        case .subscriptionCancelled:
            try await handleSubscriptionCancelled(webhookData.data)
        case .autoReloadTriggered:
            try await handleAutoReloadTriggered(webhookData.data)
        case .deferredPaymentDue:
            try await handleDeferredPaymentDue(webhookData.data)
        default:
            print("Unknown webhook type: \(webhookData.type)")
        }
        
        return .ok
    }
}

struct ApplePayWebhook: Content {
    let type: WebhookType
    let data: WebhookData
}

enum WebhookType: String, Codable {
    case recurringPaymentCharged = "RECURRING_PAYMENT_CHARGED"
    case recurringPaymentFailed = "RECURRING_PAYMENT_FAILED"
    case subscriptionCancelled = "SUBSCRIPTION_CANCELLED"
    case autoReloadTriggered = "AUTO_RELOAD_TRIGGERED"
    case deferredPaymentDue = "DEFERRED_PAYMENT_DUE"
}

struct WebhookData: Content {
    let subscriptionId: String?
    let customerId: String
    let amount: Double?
    let currency: String?
    let failureReason: String?
    let nextRetryDate: Date?
}

func handleRecurringPaymentCharged(_ data: WebhookData) async throws {
    // Update subscription status
    try await updateSubscriptionStatus(data.subscriptionId!, status: .active)
    
    // Send receipt to customer
    try await sendRecurringPaymentReceipt(data.customerId, data: data)
    
    // Update customer balance/access
    try await updateCustomerAccess(data.customerId, subscriptionType: "premium")
}

func handleRecurringPaymentFailed(_ data: WebhookData) async throws {
    // Update subscription status
    try await updateSubscriptionStatus(data.subscriptionId!, status: .failed)
    
    // Send payment failure notification
    try await sendPaymentFailureNotification(data.customerId, details: PaymentFailureDetails(
        reason: data.failureReason ?? "Unknown",
        nextAttempt: data.nextRetryDate
    ))
    
    // Implement grace period or suspend access
    try await suspendCustomerAccess(data.customerId, subscriptionType: "premium")
}

struct PaymentFailureDetails {
    let reason: String
    let nextAttempt: Date?
}

Complete implementation example

Here's a comprehensive example showing all recurring payment types working together:

import UIKit
import PXPCheckoutSDK

class RecurringPaymentsViewController: UIViewController {
    
    private var applePayComponent: ApplePayButtonComponent?
    private var applePayConsentComponent: ApplePayConsentComponent?
    private var checkout: PxpCheckout?
    
    @IBOutlet weak var paymentTypeSegmentedControl: UISegmentedControl!
    @IBOutlet weak var recurringConsentSwitch: UISwitch!
    @IBOutlet weak var applePayContainer: UIView!
    @IBOutlet weak var consentContainer: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupRecurringPayments()
    }
    
    private func setupRecurringPayments() {
        let checkoutConfig = CheckoutConfig(
            environment: .test,
            session: SessionConfig(sessionId: "your-session-id"),
            transactionData: TransactionData(
                amount: 29.99,
                currency: "USD",
                entryType: .ecom,
                intent: .sale,
                merchantTransactionId: "recurring-\(Int(Date().timeIntervalSince1970))",
                merchantTransactionDate: Date()
            )
        )
        
        do {
            checkout = try PxpCheckout.initialize(config: checkoutConfig)
            setupConsentComponent()
            updatePaymentConfiguration()
        } catch {
            print("Failed to initialize: \(error)")
        }
    }
    
    private func setupConsentComponent() {
        let consentConfig = ApplePayConsentComponentConfig(
            label: "I agree to store my payment information for recurring payments",
            checkedColor: UIColor.systemBlue,
            uncheckedColor: UIColor.systemGray,
            fontSize: 16.0,
            checked: false
        )
        
        do {
            applePayConsentComponent = try checkout?.create(.applePayConsent, componentConfig: consentConfig) as? ApplePayConsentComponent
            
            if let consentView = applePayConsentComponent?.render() {
                consentContainer.addSubview(consentView)
                setupConstraints(for: consentView, in: consentContainer)
            }
        } catch {
            print("Failed to create consent component: \(error)")
        }
    }
    
    @IBAction func paymentTypeChanged(_ sender: UISegmentedControl) {
        updatePaymentConfiguration()
    }
    
    private func updatePaymentConfiguration() {
        guard let checkout = checkout else { return }
        
        // Remove existing component
        applePayComponent = nil
        applePayContainer.subviews.forEach { $0.removeFromSuperview() }
        
        let config: ApplePayButtonComponentConfig
        
        switch paymentTypeSegmentedControl.selectedSegmentIndex {
        case 0: // Recurring
            config = createRecurringPaymentConfig()
        case 1: // Deferred
            config = createDeferredPaymentConfig()
        case 2: // Auto-reload
            config = createAutoReloadPaymentConfig()
        default:
            config = createRecurringPaymentConfig()
        }
        
        do {
            applePayComponent = try checkout.create(.applePayButton, componentConfig: config)
            
            if let componentView = applePayComponent?.render() {
                applePayContainer.addSubview(componentView)
                setupConstraints(for: componentView, in: applePayContainer)
            }
        } catch {
            print("Failed to create Apple Pay component: \(error)")
        }
    }
    
    private func createRecurringPaymentConfig() -> ApplePayButtonComponentConfig {
        let config = ApplePayButtonComponentConfig()
        
        // Basic configuration
        config.merchantDisplayName = "Subscription Service"
        config.paymentDescription = "Monthly Premium Subscription"
        config.currencyCode = "USD"
        config.countryCode = "US"
        config.supportedNetworks = [.visa, .masterCard, .amex]
        config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
        config.buttonType = .subscribe
        config.buttonStyle = .black
        config.buttonRadius = 8.0
        
        // Payment items
        config.totalPaymentItem = ApplePayPaymentSummaryItem(
            amount: 29.99,
            label: "First Month",
            type: .final
        )
        
        // Recurring configuration
        config.recurringRequest = ApplePayRecurringPaymentRequest(
            paymentDescription: "Monthly Premium Subscription",
            regularBilling: ApplePayRecurringPaymentSummaryItem(
                label: "Monthly Subscription",
                amount: NSDecimalNumber(value: 29.99)
            ),
            managementURL: "https://yoursite.com/manage-subscription"
        )
        
        let calendar = Calendar.current
        config.recurringRequest?.regularBilling.startDate = calendar.date(byAdding: .month, value: 1, to: Date())
        config.recurringRequest?.regularBilling.intervalUnit = .month
        config.recurringRequest?.regularBilling.intervalCount = 1
        
        // Connect consent component
        config.applePayConsentComponent = applePayConsentComponent
        
        // Callbacks
        setupCallbacks(for: config, type: .recurring)
        
        return config
    }
    
    private func createDeferredPaymentConfig() -> ApplePayButtonComponentConfig {
        let config = ApplePayButtonComponentConfig()
        
        // Basic configuration
        config.merchantDisplayName = "Travel Agency"
        config.paymentDescription = "Hotel Booking"
        config.currencyCode = "USD"
        config.countryCode = "US"
        config.supportedNetworks = [.visa, .masterCard, .amex]
        config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
        config.buttonType = .book
        config.buttonStyle = .black
        config.buttonRadius = 8.0
        
        // Payment items
        config.totalPaymentItem = ApplePayPaymentSummaryItem(
            amount: 200.00,
            label: "Hotel Booking",
            type: .final
        )
        
        // Deferred configuration
        config.deferredPaymentRequest = ApplePayDeferredPaymentRequest(
            paymentDescription: "Hotel Payment Due at Check-in",
            deferredBilling: ApplePayDeferredPaymentSummaryItem(
                label: "Hotel Payment",
                amount: NSDecimalNumber(value: 200.00),
                deferredPaymentDate: Calendar.current.date(byAdding: .day, value: 30, to: Date()) ?? Date()
            ),
            managementURL: "https://yoursite.com/manage-booking"
        )
        
        // Callbacks
        setupCallbacks(for: config, type: .deferred)
        
        return config
    }
    
    private func createAutoReloadPaymentConfig() -> ApplePayButtonComponentConfig {
        let config = ApplePayButtonComponentConfig()
        
        // Basic configuration
        config.merchantDisplayName = "Gift Card Store"
        config.paymentDescription = "Gift Card with Auto-Reload"
        config.currencyCode = "USD"
        config.countryCode = "US"
        config.supportedNetworks = [.visa, .masterCard, .amex]
        config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
        config.buttonType = .addMoney
        config.buttonStyle = .black
        config.buttonRadius = 8.0
        
        // Payment items
        config.totalPaymentItem = ApplePayPaymentSummaryItem(
            amount: 50.00,
            label: "Gift Card",
            type: .final
        )
        
        // Auto-reload configuration
        config.automaticReloadPaymentRequest = ApplePayAutomaticReloadPaymentRequest(
            paymentDescription: "Automatic Gift Card Reload",
            automaticReloadBilling: ApplePayAutomaticReloadPaymentSummaryItem(
                label: "Auto Reload",
                amount: NSDecimalNumber(value: 25.00),
                automaticReloadPaymentThresholdAmount: NSDecimalNumber(value: 10.00)
            ),
            managementURL: "https://yoursite.com/manage-gift-card"
        )
        
        // Callbacks
        setupCallbacks(for: config, type: .autoReload)
        
        return config
    }
    
    private func setupCallbacks(for config: ApplePayButtonComponentConfig, type: PaymentType) {
        config.onPostAuthorisation = { [weak self] result in
            DispatchQueue.main.async {
                if let authorizedResult = result as? AuthorisedSubmitResult {
                    self?.handleSuccessfulPayment(result: authorizedResult, type: type)
                } else if let failedResult = result as? FailedSubmitResult {
                    self?.handleFailedPayment(result: failedResult, type: type)
                }
            }
        }
        
        config.onError = { [weak self] error in
            DispatchQueue.main.async {
                self?.handlePaymentError(error: error, type: type)
            }
        }
    }
    
    private func handleSuccessfulPayment(result: AuthorisedSubmitResult, type: PaymentType) {
        let message: String
        
        switch type {
        case .recurring:
            message = "Recurring subscription set up successfully!"
        case .deferred:
            message = "Deferred payment authorized successfully!"
        case .autoReload:
            message = "Auto-reload configured successfully!"
        }
        
        let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    private func handleFailedPayment(result: FailedSubmitResult, type: PaymentType) {
        let message = "Payment failed: \(result.errorReason)"
        let alert = UIAlertController(title: "Payment Failed", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    private func handlePaymentError(error: Error, type: PaymentType) {
        let message = "Error: \(error.localizedDescription)"
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    // Helper methods
    private func setupConstraints(for view: UIView, in container: UIView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            view.topAnchor.constraint(equalTo: container.topAnchor),
            view.bottomAnchor.constraint(equalTo: container.bottomAnchor)
        ])
    }
    
    enum PaymentType {
        case recurring, deferred, autoReload
    }
}

// Data models for storing payment information
struct RecurringPaymentInfo {
    let transactionId: String
    let customerId: String
    let subscriptionType: String
    let amount: Double
    let currency: String
    let startDate: Date
    let endDate: Date
}