Perform Apple Pay recurring, deferred, and automatic reload transactions for iOS applications.
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.
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
)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.")
}
}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 = 1For 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()
}
}
}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
}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()
}
}
}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()
}
}
}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
}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
}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.")
}
}
}
}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?
}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
}