Learn how to use the Apple Pay component for iOS in your iOS application.
Every component follows the same four-step lifecycle:
- Initialise the PXP Checkout SDK with your configuration.
- Create the Apple Pay component with your specific configuration.
- Mount the component to display the Apple Pay button in your view.
- Handle payment results and lifecycle events.
To use the Apple Pay component, you first need to:
- Install Components for iOS.
- Complete the Apple Pay onboarding process in the Unity Portal.
- Configure your iOS app with Apple Pay entitlements.
- Ensure your merchant certificate is properly configured.
Apple Pay for iOS has specific requirements for optimal functionality.
- iOS 10.0+ for basic Apple Pay support.
- iOS 11.0+ for enhanced contact field support.
- iOS 15.0+ for coupon code support.
- 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.
- 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.
First, ensure your iOS app has the proper Apple Pay entitlements configured.
<?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><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>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)")
}
}
}| Property | Description |
|---|---|
environmentEnvironment required | The environment type. Possible values:
|
sessionSessionConfig required | Details about the checkout session. |
session.sessionIdString required | The unique session identifier. |
session.allowedFundingTypes.wallets.applePay.merchantIdString required | Your Apple Pay merchant identifier. |
ownerIdString required | The identifier of the owner related to the ownerType. |
ownerTypeOwnerType required | The type of owner. Possible values:
|
merchantShopperIdString required | A unique identifier for this shopper. |
transactionDataTransactionData required | Details about the transaction. |
transactionData.currencyString required | The currency code, in ISO 4217 format. |
transactionData.amountDouble required | The transaction amount. |
transactionData.entryTypeEntryType required | The entry type. Possible values:
|
transactionData.intentIntent required | The transaction intent. Possible values:
|
transactionData.merchantTransactionIdString required | A unique identifier for this transaction. |
transactionData.merchantTransactionDateDate required | The date and time of the transaction. |
transactionData.shopper.emailString required | The shopper's email address. |
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
}| Parameter | Description |
|---|---|
merchantDisplayNameString (≤ 64 characters) required | The name of your store as it appears to customers during payment. |
paymentDescriptionString (≤ 128 characters) required | A description of the payment that appears to customers. |
currencyCodeString required | The currency code in ISO 4217 format (e.g., "USD"). |
countryCodeString required | The merchant's country code in ISO 3166-1 alpha-2 format (e.g., "US"). |
supportedNetworks[PaymentNetwork] required | Supported card networks. Possible values:
|
merchantCapabilities[MerchantCapability] required | Payment processing capabilities. Possible values:
|
buttonTypeApplePaymentButtonType | The button type. Possible values:
|
buttonStyleApplePaymentButtonStyle | The button style. Possible values:
|
buttonRadiusCGFloat | The button corner radius (default: 4.0). |
totalPaymentItemApplePayPaymentSummaryItem | The total payment amount display. |
paymentItems[ApplePayPaymentSummaryItem] | Individual line items for the payment. |
requiredBillingContactFields[ContactField] | Required billing contact fields. Possible values:
|
requiredShippingContactFields[ContactField] | Required shipping contact fields. |
shippingMethods[ApplePayShippingMethod] | Available shipping methods. |
Create your view layout to include the Apple Pay button container.
@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
}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)
}
}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)
])
}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
}
}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
}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
}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()
}
}
}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)
)
}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.
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)
}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
}
}