Integrate 3D Secure (3DS) into your Apple Pay checkout for iOS applications.
By implementing 3DS authentication into your Apple Pay payment flow on iOS, you benefit from:
- Additional security: 3DS adds multiple layers of authentication and risk assessment.
- Liability shift: Successful 3DS authentication typically shifts fraud liability from merchant to card issuer.
- Higher success rate: Banks are more likely to approve 3DS-authenticated transactions.
- Apple Pay integration: 3DS works seamlessly with Apple Pay's built-in security features.
However, the 3DS payment flow may be longer than the non-3DS one due to the additional authentication steps. With Apple Pay on iOS, 3DS often runs in the background during the Apple Pay authorisation process.
The Apple Pay 3D Secure flow on iOS is made up of seven key steps.
The customer authorises payment using Touch ID, Face ID, or passcode in the Apple Pay sheet. The onPreAuthorisation callback in ApplePayButtonComponent is invoked with the Apple Pay payment token.
The SDK evaluates whether 3DS authentication is required based on factors like transaction amount, risk assessment, merchant configuration, or regulatory requirements.
The Apple Pay payment token is processed and prepared for 3DS authentication. The token contains encrypted payment data that will be used in the 3DS flow.
This is where you can provide additional transaction data for 3DS processing. The onPreAuthorisation callback allows you to specify risk screening data, address verification, and 3DS configuration.
The 3DS server evaluates the transaction risk using both Apple Pay data and additional merchant data:
- Frictionless flow: If the transaction is low-risk (enhanced by Apple Pay's built-in security), authentication completes automatically.
- Challenge flow: If additional verification is needed, the customer may complete a 3DS challenge, though this is less common with Apple Pay due to its inherent security.
The SDK sends the authorisation request to the payment gateway, including both the Apple Pay token and 3DS authentication data. This combines Apple Pay's security with 3DS protection.
You receive the final authorisation response from the payment gateway. The transaction is either approved or declined, with both Apple Pay and 3DS authentication confirmation.
To use 3D Secure with Apple Pay in your iOS application, you first need to:
- Enable 3DS in the Unity Portal:
- Go to Merchant setup > Merchant groups.
- Select a merchant group.
- Click the Services tab.
- Click Edit in the Card service row.
- Click Configure modules and enable ThreeD secure service.
- Configure Apple Pay:
- Set up your Apple Pay merchant identifier.
- Add Apple Pay capability to your iOS app.
- Configure your Apple Developer account with merchant IDs.
- Get 3DS credentials from your payment processor:
acquirerProfileId: Your acquirer profile identifier.providerId: Your 3DS provider identifier.- Test credentials for the sandbox environment.
Set up your CheckoutConfig to include both Apple Pay and 3DS-friendly data.
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(
sessionId: "your-session-id",
allowedFundingTypes: AllowedFundingTypes(
wallets: WalletConfig(
applePay: ApplePayConfig(
merchantId: "merchant.com.yourcompany"
)
)
)
),
transactionData: TransactionData(
amount: 99.99,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "order-123",
merchantTransactionDate: Date(),
// Include 3DS-friendly shopper data
shopper: Shopper(
email: "customer@example.com",
firstName: "John",
lastName: "Doe"
)
)
)Configure the Apple Pay component with 3DS capabilities.
let applePayConfig = ApplePayButtonComponentConfig()
// Basic payment configuration
applePayConfig.currencyCode = "USD"
applePayConfig.countryCode = "US"
// Supported payment networks
applePayConfig.supportedNetworks = [.visa, .masterCard, .amex, .discover]
// Merchant capabilities - IMPORTANT: Include .threeDSecure
applePayConfig.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Contact field requirements for 3DS
applePayConfig.requiredBillingContactFields = [.postalAddress, .name, .phoneNumber, .emailAddress]
applePayConfig.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]
// Payment items
applePayConfig.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: 99.99,
label: "Total",
type: .final
)
applePayConfig.paymentItems = [
ApplePayPaymentSummaryItem(amount: 89.99, label: "Product", type: .final),
ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
]
// Button appearance
applePayConfig.buttonType = .buy
applePayConfig.buttonStyle = .black
applePayConfig.buttonRadius = 8.0
// REQUIRED: Provide 3DS configuration and additional transaction data
applePayConfig.onPreAuthorisation = {
let logMessage = "onPreAuthorisation: Called before authorisation to update transaction details"
print(logMessage)
return ApplePayTransactionInitData(
// 3DS configuration
threeDSecureData: ThreeDSecureData(
threeDSecureVersion: "2.1.0",
electronicCommerceIndicator: .eci5, // Authenticated
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
threeDSecureTransactionStatus: "Y"
),
// Identity verification
identityVerification: IdentityVerification(
nameVerification: true
),
// Address verification (helps with 3DS risk assessment)
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
),
// Risk screening data for fraud detection
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: generateDeviceSessionId(),
items: [
Item(
name: "Product Name",
price: 89.99,
quantity: 1,
category: "Electronics"
)
],
fulfillments: [
Fulfillment(
type: "SHIPPING",
address: Address(
countryCode: "US",
postalCode: "10001",
city: "New York",
state: "NY"
)
)
]
)
)
}
// REQUIRED: Handle the final transaction result
applePayConfig.onPostAuthorisation = { result in
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Payment successful with Apple Pay + 3DS!")
print("Transaction ID: \(authorizedResult.provider.code)")
// Check if 3DS was used
if let threeDSData = authorizedResult.threeDSData {
print("3DS authentication confirmed")
print("ECI: \(threeDSData.electronicCommerceIndicator?.rawValue ?? "N/A")")
// Check liability shift
if threeDSData.electronicCommerceIndicator == .eci5 ||
threeDSData.electronicCommerceIndicator == .eci6 {
print("Liability shift achieved - protected from chargebacks")
}
}
// Navigate to success screen
DispatchQueue.main.async {
// Present success view controller or update UI
self.showPaymentSuccess(transactionId: authorizedResult.provider.code)
}
} else if let failedResult = result as? FailedSubmitResult {
print("Payment failed: \(failedResult.errorReason)")
DispatchQueue.main.async {
self.showError("Payment failed. Please try again.")
}
}
}
// OPTIONAL: Handle Apple Pay errors
applePayConfig.onError = { error in
print("Apple Pay error: \(error.localizedDescription)")
DispatchQueue.main.async {
// Handle specific 3DS + Apple Pay errors
if error.localizedDescription.contains("3DS") {
self.showError("Payment verification failed. Please try again.")
} else if error.localizedDescription.contains("Apple Pay") {
self.showError("Apple Pay is not available. Please try a different payment method.")
} else {
self.showError("Payment failed. Please try again.")
}
}
}
// OPTIONAL: Handle cancellation
applePayConfig.onCancel = { error in
print("Apple Pay cancelled by user")
// User closed Apple Pay sheet
}Use 3DS only for transactions above a certain amount.
applePayConfig.onPreAuthorisation = {
let transactionAmount = checkoutConfig.transactionData.amount
// Only use 3DS for amounts over $100
if transactionAmount > 100 {
return ApplePayTransactionInitData(
threeDSecureData: ThreeDSecureData(
threeDSecureVersion: "2.1.0",
electronicCommerceIndicator: .eci5,
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
threeDSecureTransactionStatus: "Y"
),
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
),
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: generateDeviceSessionId()
)
)
}
// For smaller amounts, just provide basic data
return ApplePayTransactionInitData(
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
)
)
}Handle different types of Apple Pay transactions with appropriate 3DS settings.
func get3DSConfig(for transactionType: TransactionType) -> ThreeDSecureData {
let baseVersion = "2.1.0"
let dsTransactionId = "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260"
switch transactionType {
case .payment:
return ThreeDSecureData(
threeDSecureVersion: baseVersion,
electronicCommerceIndicator: .eci5, // Authenticated
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: dsTransactionId,
threeDSecureTransactionStatus: "Y"
)
case .recurring:
return ThreeDSecureData(
threeDSecureVersion: baseVersion,
electronicCommerceIndicator: .eci6, // Recurring authenticated
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: dsTransactionId,
threeDSecureTransactionStatus: "Y"
)
case .addCard:
return ThreeDSecureData(
threeDSecureVersion: baseVersion,
electronicCommerceIndicator: .eci1, // Card verification
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: dsTransactionId,
threeDSecureTransactionStatus: "Y"
)
}
}
enum TransactionType {
case payment, recurring, addCard
}
applePayConfig.onPreAuthorisation = {
return ApplePayTransactionInitData(
threeDSecureData: get3DSConfig(for: .payment),
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
),
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: generateDeviceSessionId()
)
)
}Leverage Apple Pay's built-in security for better risk assessment.
applePayConfig.onPreAuthorisation = {
return ApplePayTransactionInitData(
threeDSecureData: ThreeDSecureData(
threeDSecureVersion: "2.1.0",
electronicCommerceIndicator: .eci5,
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
threeDSecureTransactionStatus: "Y" // Request no challenge (Apple Pay provides security)
),
// Enhanced risk data
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: generateDeviceSessionId(),
items: getCartItems(),
fulfillments: [
Fulfillment(
type: "SHIPPING",
address: getShippingAddress()
)
],
transaction: TransactionRiskData(
deviceChannel: "IOS_APP",
isRecurring: false,
customerType: "EXISTING",
// Apple Pay provides additional security context
paymentMethod: "APPLE_PAY",
deviceAuthentication: "BIOMETRIC", // Touch ID/Face ID used
walletProvider: "APPLE"
)
)
)
}Implement comprehensive error handling for Apple Pay + 3DS scenarios.
applePayConfig.onError = { error in
print("Apple Pay + 3DS error: \(error.localizedDescription)")
DispatchQueue.main.async {
// Handle specific error types
if let applePayError = error as? ApplePayValidationException {
self.showError("Apple Pay configuration error. Please contact support.")
} else if let paymentError = error as? ApplePayPaymentFailedException {
self.showError("Payment processing failed. Please try again.")
} else if let sessionError = error as? ApplePaySessionCancelledException {
print("User cancelled Apple Pay")
// Don't show error for user cancellation
} else if error.localizedDescription.contains("3DS") {
self.showError("Payment verification failed. Please try again.")
} else if error.localizedDescription.contains("Network") {
self.showError("Connection failed. Please check your internet and try again.")
} else {
self.showError("Payment failed. Please try again.")
}
}
}
applePayConfig.onPostAuthorisation = { result in
if let failedResult = result as? FailedSubmitResult {
print("Transaction failed: \(failedResult.errorReason)")
DispatchQueue.main.async {
// Handle specific failure scenarios
switch failedResult.errorCode {
case "AUTHENTICATION_FAILED":
self.showError("Payment verification failed. Please try again.")
case "CHALLENGE_TIMEOUT":
self.showError("Verification timed out. Please try again.")
case "AUTHENTICATION_REJECTED":
self.showError("Payment was rejected by your bank.")
case "INSUFFICIENT_FUNDS":
self.showError("Insufficient funds. Please try a different payment method.")
default:
self.showError("Payment failed. Please try again or contact support.")
}
}
}
}Here's a complete example of Apple Pay with 3DS implementation for iOS.
import UIKit
import PXPCheckoutSDK
class CheckoutViewController: UIViewController {
private var applePayComponent: ApplePayButtonComponent?
private var checkout: PxpCheckout?
override func viewDidLoad() {
super.viewDidLoad()
setupApplePayWith3DS()
}
private func setupApplePayWith3DS() {
// Initialise the SDK
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(
sessionId: "your-session-id",
allowedFundingTypes: AllowedFundingTypes(
wallets: WalletConfig(
applePay: ApplePayConfig(
merchantId: "merchant.com.yourcompany"
)
)
)
),
transactionData: TransactionData(
amount: 150.00,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "order-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date(),
shopper: Shopper(
email: "customer@example.com",
firstName: "John",
lastName: "Doe"
)
)
)
do {
checkout = try PxpCheckout.initialize(config: checkoutConfig)
// Create Apple Pay component with 3DS
let applePayConfig = createApplePayConfig()
applePayComponent = try checkout?.create(.applePayButton, componentConfig: applePayConfig)
// Add the component to your view
if let componentView = applePayComponent?.render() {
view.addSubview(componentView)
setupConstraints(for: componentView)
}
} catch {
print("Failed to initialize checkout: \(error)")
}
}
private func createApplePayConfig() -> ApplePayButtonComponentConfig {
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Contact fields for 3DS
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
config.requiredShippingContactFields = [.postalAddress, .name]
// Payment items
config.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: 150.00,
label: "Total",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 140.00, label: "Premium Product", type: .final),
ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
]
// Button appearance
config.buttonType = .buy
config.buttonStyle = .black
config.buttonRadius = 8.0
// Step 1: Provide 3DS configuration
config.onPreAuthorisation = { [weak self] in
print("Configuring 3DS for Apple Pay transaction")
return ApplePayTransactionInitData(
// 3DS Authentication configuration
threeDSecureData: ThreeDSecureData(
threeDSecureVersion: "2.1.0",
electronicCommerceIndicator: .eci5, // Authenticated
cardHolderAuthenticationVerificationValue: "AAABBBCCCDDDEEEFFFaaabbbcccdddeee",
directoryServerTransactionId: "8ac7ca4f-6068-4978-8b5e-c23c0c6d2260",
threeDSecureTransactionStatus: "Y"
),
// Identity verification
identityVerification: IdentityVerification(
nameVerification: true
),
// Address verification
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
),
// Enhanced risk assessment
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: self?.generateDeviceSessionId() ?? "",
items: [
Item(
name: "Premium Product",
category: "Electronics",
price: 140.00,
quantity: 1,
sku: "PREM-001"
)
],
fulfillments: [
Fulfillment(
type: "SHIPPING",
address: Address(
countryCode: "US",
postalCode: "10001",
city: "New York",
state: "NY"
)
)
],
transaction: TransactionRiskData(
deviceChannel: "IOS_APP",
isRecurring: false,
customerType: "EXISTING",
paymentMethod: "APPLE_PAY",
deviceAuthentication: "BIOMETRIC",
walletProvider: "APPLE"
)
)
)
}
// Step 2: Handle successful/failed authorisation
config.onPostAuthorisation = { [weak self] result in
print("Apple Pay + 3DS result: \(result)")
DispatchQueue.main.async {
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Payment successful!")
print("Transaction ID: \(authorizedResult.provider.code)")
// Log 3DS authentication details
if let threeDSData = authorizedResult.threeDSData {
print("3DS Authentication Details:")
print("Version: \(threeDSData.threeDSecureVersion ?? "N/A")")
print("ECI: \(threeDSData.electronicCommerceIndicator?.rawValue ?? "N/A")")
print("CAVV: \(threeDSData.cardHolderAuthenticationVerificationValue ?? "N/A")")
print("DS Trans ID: \(threeDSData.directoryServerTransactionId ?? "N/A")")
// Check liability shift
if threeDSData.electronicCommerceIndicator == .eci5 ||
threeDSData.electronicCommerceIndicator == .eci6 {
print("Liability shift achieved - protected from chargebacks")
}
}
// Store transaction details
self?.storeTransactionRecord(
transactionId: authorizedResult.provider.code,
amount: 150.00,
currency: "USD",
paymentMethod: "APPLE_PAY",
threeDSStatus: authorizedResult.threeDSData != nil ? "AUTHENTICATED" : "NOT_REQUIRED"
)
// Navigate to success screen
self?.showPaymentSuccess(transactionId: authorizedResult.provider.code)
} else if let failedResult = result as? FailedSubmitResult {
print("Payment failed: \(failedResult.errorReason)")
// Handle specific failure scenarios
switch failedResult.errorCode {
case "AUTHENTICATION_FAILED":
self?.showError("Payment verification failed. Please try again.")
case "AUTHENTICATION_REJECTED":
self?.showError("Payment was rejected by your bank. Please try a different card.")
case "INSUFFICIENT_FUNDS":
self?.showError("Insufficient funds. Please try a different payment method.")
case "CARD_EXPIRED":
self?.showError("Card expired. Please update your payment method in Apple Pay.")
default:
self?.showError("Payment failed. Please try again or contact support.")
}
}
}
}
// Step 3: Error handling
config.onError = { [weak self] error in
print("Apple Pay + 3DS Error: \(error.localizedDescription)")
DispatchQueue.main.async {
if let _ = error as? ApplePayValidationException {
self?.showError("Apple Pay configuration error. Please contact support.")
} else if let _ = error as? ApplePayPaymentFailedException {
self?.showError("Payment processing failed. Please try again.")
} else if error.localizedDescription.contains("3DS") {
self?.showError("Payment verification failed. Please try again.")
} else if error.localizedDescription.contains("Network") {
self?.showError("Connection failed. Please check your internet and try again.")
} else {
self?.showError("An unexpected error occurred. Please try again.")
}
}
}
// Step 4: Handle cancellation
config.onCancel = { error in
print("Apple Pay cancelled by user")
// User closed Apple Pay sheet - no error message needed
}
return config
}
// Helper functions
private func generateDeviceSessionId() -> String {
return "device_\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))_\(Int(Date().timeIntervalSince1970))"
}
private func showError(_ message: String) {
let alert = UIAlertController(title: "Payment Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showPaymentSuccess(transactionId: String) {
let alert = UIAlertController(title: "Payment Successful",
message: "Transaction ID: \(transactionId)",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func storeTransactionRecord(transactionId: String, amount: Double, currency: String, paymentMethod: String, threeDSStatus: String) {
let transactionData: [String: Any] = [
"transactionId": transactionId,
"amount": amount,
"currency": currency,
"paymentMethod": paymentMethod,
"threeDSStatus": threeDSStatus,
"timestamp": Date().timeIntervalSince1970
]
// Store transaction details for audit/reconciliation
UserDefaults.standard.set(transactionData, forKey: "last_transaction")
// Send to analytics if needed
print("Transaction stored: \(transactionData)")
}
private func setupConstraints(for componentView: UIView) {
componentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
componentView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
componentView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
componentView.widthAnchor.constraint(equalToConstant: 280),
componentView.heightAnchor.constraint(equalToConstant: 50)
])
}
}This section describes the data received by the different callbacks as part of the Apple Pay 3DS flow on iOS.
The onPreAuthorisation callback receives no parameters for Apple Pay. It should return transaction initialisation data with 3DS configuration enabled.
ApplePayTransactionInitData(
// 3D Secure configuration (required for 3DS flow)
threeDSecureData: ThreeDSecureData(
attemptN3d: false,
challengeIndicator: "04",
cardHolderName: "John Doe",
email: "customer@example.com",
homePhone: "+1234567890",
mobilePhone: "+1234567890",
workPhone: "+1234567890",
billAddrCity: "New York",
billAddrCountry: "840",
billAddrLine1: "123 Main St",
billAddrLine2: "Apt 4B",
billAddrPostCode: "10001",
billAddrState: "NY",
shipAddrCity: "New York",
shipAddrCountry: "840",
shipAddrLine1: "123 Main St",
shipAddrLine2: "Apt 4B",
shipAddrPostCode: "10001",
shipAddrState: "NY"
),
// Identity verification (optional)
identityVerification: IdentityVerification(
nameVerification: true
),
// Address verification (optional but recommended with 3DS)
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: "123 Main St",
postalCode: "10001"
),
// Risk screening data (enhanced with 3DS)
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: "device_abc123_1704067200000",
items: [
Item(
name: "Product Name",
price: 99.99,
quantity: 1
)
],
transaction: TransactionRiskData(
paymentMethod: "APPLE_PAY",
deviceAuthentication: "BIOMETRIC",
walletProvider: "APPLE",
authenticationMethod: "3DS_V2"
)
)
)Here's an example of what to return:
config.onPreAuthorisation = {
print("Preparing Apple Pay 3DS transaction")
// Get customer information for 3DS authentication
let customerInfo = await getCustomerInformation()
let shippingInfo = await getShippingInformation()
return ApplePayTransactionInitData(
// 3DS configuration - required for 3DS flow
threeDSecureData: ThreeDSecureData(
attemptN3d: false,
challengeIndicator: "04", // Request authentication
cardHolderName: customerInfo.fullName,
email: customerInfo.email,
homePhone: customerInfo.phone,
mobilePhone: customerInfo.mobile,
workPhone: customerInfo.workPhone,
// Billing address information
billAddrCity: customerInfo.billingAddress.city,
billAddrCountry: "840", // US country code
billAddrLine1: customerInfo.billingAddress.line1,
billAddrLine2: customerInfo.billingAddress.line2,
billAddrPostCode: customerInfo.billingAddress.postalCode,
billAddrState: customerInfo.billingAddress.state,
// Shipping address information
shipAddrCity: shippingInfo.city,
shipAddrCountry: "840",
shipAddrLine1: shippingInfo.line1,
shipAddrLine2: shippingInfo.line2,
shipAddrPostCode: shippingInfo.postalCode,
shipAddrState: shippingInfo.state
),
// Identity verification
identityVerification: IdentityVerification(
nameVerification: true
),
// Address verification
addressVerification: AddressVerification(
countryCode: "US",
houseNumberOrName: customerInfo.billingAddress.line1,
postalCode: customerInfo.billingAddress.postalCode
),
// Enhanced risk screening for 3DS
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: generateDeviceSessionId(),
items: [
Item(
name: "Premium Product",
category: "Electronics",
price: 99.99,
quantity: 1,
sku: "PROD-3DS-001"
)
],
fulfillments: [
Fulfillment(
type: "SHIPPING",
address: Address(
countryCode: "US",
postalCode: shippingInfo.postalCode
),
estimatedDelivery: "2024-01-15"
)
],
transaction: TransactionRiskData(
deviceChannel: "IOS_APP",
paymentMethod: "APPLE_PAY",
deviceAuthentication: "BIOMETRIC", // Touch ID/Face ID
walletProvider: "APPLE",
authenticationMethod: "3DS_V2",
processingType: "3ds",
securityLevel: "MAXIMUM", // Due to Apple Pay + 3DS
riskReduction: "APPLE_PAY_3DS",
// Apple Pay with 3DS provides maximum security
applePayWith3DS: ApplePaySecurityFeatures(
deviceBoundToken: true,
biometricAuthentication: true,
hardwareSecurityModule: true,
tokenization: true,
threeDSecureProtection: true
),
// Enhanced 3DS context for iOS
threeDSContext: ThreeDSContext(
deviceInfo: DeviceInfo(
platform: "iOS",
version: UIDevice.current.systemVersion,
model: UIDevice.current.model,
screenSize: "\(Int(UIScreen.main.bounds.width))x\(Int(UIScreen.main.bounds.height))",
locale: Locale.current.identifier,
timeZone: TimeZone.current.identifier
),
customerAuthenticationInfo: CustomerAuthInfo(
authMethod: "biometric",
authTimestamp: Date().toISOString(),
authData: "apple_pay_biometric",
deviceId: UIDevice.current.identifierForVendor?.uuidString
)
)
)
)
)
}The onPostAuthorisation callback receives the transaction result (BaseSubmitResult).
The result can be one of several types:
AuthorisedSubmitResult - Successful 3DS authentication and authorisation:
AuthorisedSubmitResult(
provider: ProviderResult(
code: "AUTH123456",
message: "Transaction approved with 3DS authentication"
),
transactionId: "txn_abc123def456",
threeDSecureResult: ThreeDSecureResult(
status: "authenticated_and_authorized",
eci: "05", // Electronic Commerce Indicator
cavv: "jJ81HADVRtXfCBATEp01CJUAAAA=", // Cardholder Authentication Verification Value
xid: "Nmp3NmJhZGI2MjAzOTI5Mw==",
version: "2.1.0",
directoryServerTransactionId: "c5b808e7-1de1-4069-a17b-f70d3b3b1645",
acsTransactionId: "13c701a3-5a88-4c45-89e9-ef65e50b8bf9",
challengeResult: "completed_successfully",
challengeCompletionIndicator: "Y"
),
riskResult: RiskResult(
riskScore: 15,
riskLevel: "LOW",
riskFactors: ["apple_pay_3ds_protection"]
)
)DeclinedSubmitResult - 3DS authentication failed or transaction declined:
DeclinedSubmitResult(
errorReason: "3DS authentication failed",
errorCode: "3DS_AUTH_FAILED",
threeDSecureResult: ThreeDSecureResult(
status: "authentication_failed",
eci: "07",
version: "2.1.0",
challengeResult: "abandoned_by_consumer",
challengeCompletionIndicator: "N"
),
declineReason: "Issuer declined after failed 3DS challenge"
)FailedSubmitResult - Technical error during 3DS processing:
FailedSubmitResult(
errorReason: "3DS server timeout",
errorCode: "3DS_TIMEOUT",
errorDetails: "Authentication request timed out",
errorType: "NETWORK_ERROR"
)config.onPostAuthorisation = { result in
print("3DS Apple Pay transaction completed: \(result)")
DispatchQueue.main.async {
if let authorizedResult = result as? AuthorisedSubmitResult {
// Successful 3DS authentication and authorisation
print("3DS authentication successful!")
print("Transaction ID: \(authorizedResult.transactionId)")
print("Authorisation code: \(authorizedResult.provider.code)")
print("3DS status: \(authorizedResult.threeDSecureResult.status)")
print("ECI: \(authorizedResult.threeDSecureResult.eci)")
print("CAVV: \(authorizedResult.threeDSecureResult.cavv)")
// Store 3DS authentication results for compliance
self.storeThreeDSResults(ThreeDSComplianceData(
transactionId: authorizedResult.transactionId,
eci: authorizedResult.threeDSecureResult.eci,
cavv: authorizedResult.threeDSecureResult.cavv,
xid: authorizedResult.threeDSecureResult.xid,
version: authorizedResult.threeDSecureResult.version,
challengeResult: authorizedResult.threeDSecureResult.challengeResult
))
// Process order with high confidence
self.processOrder(OrderData(
transactionId: authorizedResult.transactionId,
paymentMethod: "APPLE_PAY_3DS",
securityLevel: "MAXIMUM",
riskScore: authorizedResult.riskResult.riskScore,
threeDSAuthenticated: true
))
// Show success message
self.showSuccessMessage("Payment successful with enhanced security!")
// Navigate to order confirmation
self.navigateToOrderConfirmation(transactionId: authorizedResult.transactionId)
} else if let declinedResult = result as? DeclinedSubmitResult {
// 3DS authentication failed or transaction declined
print("3DS authentication or transaction failed")
print("Error: \(declinedResult.errorReason)")
print("3DS Status: \(declinedResult.threeDSecureResult?.status ?? "unknown")")
// Handle specific 3DS failure scenarios
if let challengeResult = declinedResult.threeDSecureResult?.challengeResult {
switch challengeResult {
case "abandoned_by_consumer":
self.showErrorMessage("Authentication was cancelled. Please try again.")
case "authentication_failed":
self.showErrorMessage("Authentication failed. Please verify your identity and try again.")
default:
self.showErrorMessage("Payment was declined. Please try a different payment method.")
}
} else {
self.showErrorMessage("Payment was declined. Please try a different payment method.")
}
// Log for analysis
self.logPaymentFailure(PaymentFailureData(
errorCode: declinedResult.errorCode,
errorReason: declinedResult.errorReason,
threeDSStatus: declinedResult.threeDSecureResult?.status,
challengeResult: declinedResult.threeDSecureResult?.challengeResult
))
} else if let failedResult = result as? FailedSubmitResult {
// Technical error during 3DS processing
print("3DS processing error: \(failedResult.errorReason)")
switch failedResult.errorCode {
case "3DS_TIMEOUT":
self.showErrorMessage("Authentication timed out. Please try again.")
case "3DS_SERVER_ERROR":
self.showErrorMessage("Authentication service unavailable. Please try again later.")
default:
self.showErrorMessage("Payment processing error. Please try again or contact support.")
}
// Log technical errors
self.logTechnicalError(TechnicalErrorData(
errorCode: failedResult.errorCode,
errorReason: failedResult.errorReason,
errorType: failedResult.errorType,
timestamp: Date()
))
}
}
}The onError callback receives error information when the Apple Pay 3DS process encounters issues:
config.onError = { error in
print("Apple Pay 3DS error: \(error)")
DispatchQueue.main.async {
if let validationError = error as? ApplePayValidationException {
// Validation errors specific to 3DS setup
if validationError.localizedDescription.contains("3DS") {
self.showErrorMessage("3D Secure configuration error. Please contact support.")
} else {
self.showErrorMessage("Payment validation failed. Please check your information.")
}
} else if let networkError = error as? URLError {
// Network errors during 3DS authentication
self.showErrorMessage("Connection error during authentication. Please check your internet and try again.")
} else if let pkError = error as? PKPaymentError {
// Apple Pay specific errors with 3DS context
switch pkError.code {
case .paymentNotAllowed:
self.showErrorMessage("Payment not allowed. Please check your settings.")
case .paymentNetworkNotSupported:
self.showErrorMessage("Card network not supported for 3D Secure. Please try a different card.")
case .deviceCannotMakePayments:
self.showErrorMessage("Apple Pay is not available on this device.")
default:
self.showErrorMessage("3D Secure authentication unavailable. Please try a different payment method.")
}
} else {
// Generic errors
self.showErrorMessage("Payment error occurred. Please try again.")
}
// Log all errors for debugging
self.logError(ErrorData(
error: error.localizedDescription,
type: String(describing: type(of: error)),
context: "3ds_apple_pay_ios",
timestamp: Date()
))
}
}The onCancel callback is triggered when the user cancels the Apple Pay process:
config.onCancel = { error in
print("Apple Pay 3DS cancelled by user")
DispatchQueue.main.async {
// Track cancellation for analytics
self.trackEvent("apple_pay_3ds_cancelled", parameters: [
"step": error != nil ? "authentication" : "payment_sheet",
"reason": error?.localizedDescription ?? "user_cancelled",
"platform": "ios"
])
// Show cancellation message
self.showInfoMessage("Payment was cancelled. You can try again anytime.")
// Reset payment form state
self.resetPaymentForm()
}
}When using 3DS with Apple Pay on iOS, you may also need to implement additional delegate methods for enhanced functionality:
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didSelectShippingContact contact: PKContact,
handler completion: @escaping (PKPaymentRequestShippingContactUpdate) -> Void
) {
// Calculate shipping with 3DS risk assessment
let shippingCost = calculateShippingWithRiskAssessment(for: contact)
let tax = calculateTaxWithCompliance(for: contact)
let total = baseAmount + shippingCost + tax
// Enhanced shipping update for 3DS
let update = PKPaymentRequestShippingContactUpdate(
errors: [],
shippingMethods: [
PKShippingMethod(
label: "Standard Shipping (3DS Secure)",
amount: NSDecimalNumber(value: shippingCost),
type: .final,
identifier: "standard_3ds",
detail: "5-7 business days with enhanced security"
)
],
paymentSummaryItems: [
PKPaymentSummaryItem(label: "Product", amount: NSDecimalNumber(value: baseAmount)),
PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(value: tax)),
PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: shippingCost)),
PKPaymentSummaryItem(label: "Your Store (3DS Secure)", amount: NSDecimalNumber(value: total))
]
)
completion(update)
}@available(iOS 15.0, *)
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didSelectPaymentMethod paymentMethod: PKPaymentMethod,
handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void
) {
// Validate payment method for 3DS compatibility
let isThreeDSCompatible = validate3DSCompatibility(for: paymentMethod)
let update = PKPaymentRequestPaymentMethodUpdate(
errors: isThreeDSCompatible ? [] : [
PKPaymentError(.paymentMethodInvalidError,
userInfo: [NSLocalizedDescriptionKey: "This card does not support 3D Secure authentication"])
],
paymentSummaryItems: [
PKPaymentSummaryItem(label: "Product", amount: NSDecimalNumber(value: 99.99)),
PKPaymentSummaryItem(label: "3DS Processing Fee", amount: NSDecimalNumber(value: 0.00)),
PKPaymentSummaryItem(label: "Total (3DS Secure)", amount: NSDecimalNumber(value: 99.99))
]
)
completion(update)
}
private func validate3DSCompatibility(for paymentMethod: PKPaymentMethod) -> Bool {
// Check if the payment network supports 3DS
let threeDSNetworks: [PKPaymentNetwork] = [.visa, .masterCard, .amex]
return threeDSNetworks.contains(paymentMethod.network)
}// MARK: - 3DS Helper Methods
private func generateDeviceSessionId() -> String {
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
let timestamp = Int(Date().timeIntervalSince1970)
return "ios_3ds_\(deviceId)_\(timestamp)"
}
private func getCustomerInformation() async -> CustomerInfo {
// Retrieve customer information for 3DS authentication
return CustomerInfo(
fullName: "John Doe",
email: "customer@example.com",
phone: "+1234567890",
mobile: "+1234567890",
workPhone: "+1234567890",
billingAddress: Address(
line1: "123 Main St",
line2: "Apt 4B",
city: "New York",
state: "NY",
postalCode: "10001",
country: "US"
)
)
}
private func getShippingInformation() async -> Address {
// Retrieve shipping information for 3DS
return Address(
line1: "123 Main St",
line2: "Apt 4B",
city: "New York",
state: "NY",
postalCode: "10001",
country: "US"
)
}
private func storeThreeDSResults(_ data: ThreeDSComplianceData) {
// Store 3DS results for compliance and dispute resolution
ComplianceManager.shared.store3DSResults(data)
}
private func processOrder(_ orderData: OrderData) {
// Process order with 3DS authentication confidence
OrderManager.shared.processSecureOrder(orderData)
}
private func showSuccessMessage(_ message: String) {
let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showErrorMessage(_ message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showInfoMessage(_ message: String) {
let alert = UIAlertController(title: "Information", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}