Learn about customisation options for the Apple Pay component for iOS applications.
The Apple Pay component comes with responsive and accessible default styling that follows Apple's design guidelines, but is designed to be customisable to match your brand and application requirements.
You can find Apple's official recommendations around button styling in their PKPaymentButton documentation and PassKit framework reference.
The default implementation uses Apple's native PKPaymentButton
component with the iOS SDK:
let config = ApplePayButtonComponentConfig()
// Basic styling configuration
config.buttonType = .buy
config.buttonStyle = .black
config.buttonRadius = 4.0
Type | Description | Use case |
---|---|---|
.plain | Apple Pay logo only. |
|
.buy | Standard purchase button. |
|
.setUp | Setup button. |
|
.inStore | In-store payment button. |
|
.donate | Donation button. |
|
.checkout | Checkout button. |
|
.book | Booking button. |
|
.subscribe | Subscribe button. |
|
.reload | Reload button. |
|
.addMoney | Add money button. |
|
.topUp | Top-up button. |
|
.order | Order button. |
|
.rent | Rent button. |
|
.support | Support button. |
|
.contribute | Contribution button. |
|
.tip | Tip button. |
|
Style | Description |
---|---|
.black | Black background with white text. |
.white | White background with black text. |
.whiteOutline | White background with a black border and text. |
.automatic | Adapts to the system's appearance (iOS 13+). |
When you need more control over the button appearance, you can customize various properties:
let config = ApplePayButtonComponentConfig()
// Button appearance customization
config.buttonType = .buy
config.buttonStyle = .black
config.buttonRadius = 8.0
// Optional: Custom SwiftUI content for advanced customization
config.customContent = {
return AnyView(
HStack(spacing: 8) {
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(8)
)
}
The Apple Pay component accepts the main configuration class that controls its appearance and behaviour.
// Definition
class ApplePayButtonComponentConfig: BaseComponentConfig {
// Basic payment configuration
var merchantDisplayName: String
var paymentDescription: String
var currencyCode: String
var countryCode: String
var supportedNetworks: [PaymentNetwork]
var merchantCapabilities: [MerchantCapability]
// Button appearance
var buttonType: ApplePaymentButtonType = .plain
var buttonStyle: ApplePaymentButtonStyle = .black
var buttonRadius: CGFloat = 4.0
// Payment items
var totalPaymentItem: ApplePayPaymentSummaryItem?
var paymentItems: [ApplePayPaymentSummaryItem]?
// Contact fields
var requiredBillingContactFields: [ContactField]?
var requiredShippingContactFields: [ContactField]?
// Shipping configuration
var shippingMethods: [ApplePayShippingMethod]?
var shippingType: ShippingType?
// Advanced features
var recurringRequest: ApplePayRecurringPaymentRequest?
var deferredPaymentRequest: ApplePayDeferredPaymentRequest?
var automaticReloadPaymentRequest: ApplePayAutomaticReloadPaymentRequest?
// Custom content
var customContent: (() -> AnyView)?
// Consent component
weak var applePayConsentComponent: ApplePayConsentComponent?
// Event handlers
var onPreAuthorisation: (() async -> ApplePayTransactionInitData?)?
var onPostAuthorisation: ((BaseSubmitResult) -> Void)?
var onShippingContactSelected: ((PKContact) async -> PKPaymentRequestShippingContactUpdate)?
var onShippingMethodSelected: ((PKShippingMethod) -> PKPaymentRequestShippingMethodUpdate)?
var onPaymentMethodSelected: ((PKPaymentMethod) -> PKPaymentRequestPaymentMethodUpdate)?
var onCouponCodeChanged: ((String) async -> PKPaymentRequestCouponCodeUpdate)?
var onError: ((Error) -> Void)?
var onCancel: ((Error?) -> Void)?
}
let config = ApplePayButtonComponentConfig()
// Required fields
config.merchantDisplayName = "Acme Store"
config.paymentDescription = "Product Purchase"
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Payment items
config.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: 99.99,
label: "Total",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 89.99, label: "Product", type: .final),
ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
]
// Button styling
config.buttonType = .buy
config.buttonStyle = .black
config.buttonRadius = 8.0
// Contact fields
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
// Event handlers
config.onPreAuthorisation = {
return ApplePayTransactionInitData(
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: "session-123"
)
)
}
config.onPostAuthorisation = { result in
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Payment successful: \(authorizedResult.provider.code)")
} else if let failedResult = result as? FailedSubmitResult {
print("Payment failed: \(failedResult.errorReason)")
}
}
config.onShippingContactSelected = { contact in
let shippingCost = 5.99
let tax = 2.00
let total = 89.99 + shippingCost + tax
return PKPaymentRequestShippingContactUpdate(
errors: [],
shippingMethods: [
PKShippingMethod(label: "Standard Shipping", amount: NSDecimalNumber(value: shippingCost))
],
paymentSummaryItems: [
PKPaymentSummaryItem(label: "Product", amount: NSDecimalNumber(value: 89.99)),
PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(value: shippingCost)),
PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(value: tax)),
PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(value: total))
]
)
}
config.onError = { error in
print("Apple Pay error: \(error)")
// Show error to user
}
config.onCancel = { error in
print("Apple Pay cancelled by user")
// Handle cancellation
}
Property | Description |
---|---|
merchantDisplayName stringrequired | The display name for your business. |
paymentDescription stringrequired | A description of what the payment is for. |
currencyCode stringrequired | The currency code (ISO 4217, e.g., "USD"). |
countryCode stringrequired | The country code (ISO 3166-1 alpha-2, e.g., "US"). |
supportedNetworks [PaymentNetwork]required | The supported payment networks. |
merchantCapabilities [MerchantCapability]required | The merchant capabilities. |
buttonType ApplePaymentButtonType | The button type. Defaults to .plain . |
buttonStyle ApplePaymentButtonStyle | The button style. Defaults to .black . |
buttonRadius CGFloat | The button corner radius. Defaults to 4.0 . |
totalPaymentItem ApplePayPaymentSummaryItem? | The total payment amount display. |
paymentItems [ApplePayPaymentSummaryItem]? | The line items to display. |
requiredBillingContactFields [ContactField]? | Required billing contact fields. |
requiredShippingContactFields [ContactField]? | Required shipping contact fields. |
shippingMethods [ApplePayShippingMethod]? | Available shipping methods. |
customContent (() -> AnyView)? | Custom SwiftUI content for the button. |
applePayConsentComponent ApplePayConsentComponent? | Consent component for data processing. |
onPreAuthorisation (() async -> ApplePayTransactionInitData?)? | Event handler for before payment authorisation. |
onPostAuthorisation ((BaseSubmitResult) -> Void)? | Event handler for after payment authorisation. |
onShippingContactSelected | Event handler for shipping address changes. |
onShippingMethodSelected | Event handler for shipping method changes. |
onPaymentMethodSelected | Event handler for payment method changes. |
onCouponCodeChanged | Event handler for coupon code changes (iOS 15.0+). |
onError ((Error) -> Void)? | Event handler for errors. |
onCancel ((Error?) -> Void)? | Event handler for cancellation. |
When you need complete control over the button appearance, use custom SwiftUI content:
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.merchantDisplayName = "Urban Fashion Boutique"
config.paymentDescription = "Designer Jacket Purchase"
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Custom SwiftUI button
config.customContent = {
return AnyView(
HStack(spacing: 12) {
// Apple Pay logo
Image(systemName: "applelogo")
.font(.title2)
.foregroundColor(.white)
// Custom text
VStack(alignment: .leading, spacing: 2) {
Text("Pay with")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
Text("Apple Pay")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
}
Spacer()
// Security indicator
Image(systemName: "lock.shield.fill")
.font(.title3)
.foregroundColor(.green)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, minHeight: 52)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.black,
Color.gray.opacity(0.8)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
.cornerRadius(12)
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
)
}
import UIKit
import PXPCheckoutSDK
class BasicCustomizationViewController: UIViewController {
private var applePayComponent: ApplePayButtonComponent?
private var checkout: PxpCheckout?
override func viewDidLoad() {
super.viewDidLoad()
setupBasicApplePay()
}
private func setupBasicApplePay() {
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(sessionId: "your-session-id"),
transactionData: TransactionData(
amount: 129.99,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "order-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date()
)
)
do {
checkout = try PxpCheckout.initialize(config: checkoutConfig)
let applePayConfig = createBasicApplePayConfig()
applePayComponent = try checkout?.create(.applePayButton, componentConfig: applePayConfig)
if let componentView = applePayComponent?.render() {
view.addSubview(componentView)
setupConstraints(for: componentView)
}
} catch {
print("Failed to initialize: \(error)")
}
}
private func createBasicApplePayConfig() -> ApplePayButtonComponentConfig {
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.merchantDisplayName = "Acme Electronics Store"
config.paymentDescription = "Wireless Headphones 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: 129.99,
label: "Total",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 119.99, label: "Wireless Headphones", type: .final),
ApplePayPaymentSummaryItem(amount: 10.00, label: "Tax", type: .final)
]
// Contact fields
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
config.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]
// Event handlers
config.onPostAuthorisation = { [weak self] result in
DispatchQueue.main.async {
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Payment successful: \(authorizedResult.provider.code)")
self?.navigateToOrderConfirmation(transactionId: authorizedResult.provider.code)
} else if let failedResult = result as? FailedSubmitResult {
print("Payment failed: \(failedResult.errorReason)")
self?.showErrorMessage("Payment was declined. Please try a different payment method.")
}
}
}
config.onError = { [weak self] error in
print("Apple Pay Error: \(error)")
DispatchQueue.main.async {
self?.showErrorMessage("Payment failed. Please try again.")
}
}
config.onCancel = { error in
print("User cancelled Apple Pay")
// Track cancellation analytics
}
return config
}
private func setupConstraints(for componentView: UIView) {
componentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
componentView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
componentView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
componentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
componentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
componentView.heightAnchor.constraint(equalToConstant: 50)
])
}
private func navigateToOrderConfirmation(transactionId: String) {
// Navigate to order confirmation screen
let alert = UIAlertController(title: "Order Successful", message: "Order ID: \(transactionId)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showErrorMessage(_ message: String) {
let alert = UIAlertController(title: "Payment Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
import SwiftUI
import PXPCheckoutSDK
struct CustomApplePayView: View {
@State private var applePayComponent: ApplePayButtonComponent?
@State private var checkout: PxpCheckout?
@State private var showingOrderDetails = false
@State private var orderTotal: Double = 89.97
var body: some View {
VStack(spacing: 20) {
Text("Outdoor Adventure Gear")
.font(.largeTitle)
.fontWeight(.bold)
VStack(spacing: 10) {
HStack {
Text("Camping Tent")
Spacer()
Text("$69.99")
}
HStack {
Text("Sleeping Bag")
Spacer()
Text("$14.99")
}
HStack {
Text("Tax")
Spacer()
Text("$4.99")
}
Divider()
HStack {
Text("Total")
.fontWeight(.semibold)
Spacer()
Text("$\(orderTotal, specifier: "%.2f")")
.fontWeight(.semibold)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
// Custom Apple Pay Button
ApplePayButtonWrapper(
onComponentCreated: { component in
self.applePayComponent = component
}
)
.frame(height: 52)
.cornerRadius(12)
Button("Show Order Details") {
showingOrderDetails = true
}
.foregroundColor(.blue)
}
.padding()
.onAppear {
setupCustomApplePay()
}
.sheet(isPresented: $showingOrderDetails) {
OrderDetailsView()
}
}
private func setupCustomApplePay() {
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(sessionId: "your-session-id"),
transactionData: TransactionData(
amount: orderTotal,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "camping-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date()
)
)
do {
checkout = try PxpCheckout.initialize(config: checkoutConfig)
} catch {
print("Failed to initialize: \(error)")
}
}
}
struct ApplePayButtonWrapper: UIViewRepresentable {
let onComponentCreated: (ApplePayButtonComponent) -> Void
func makeUIView(context: Context) -> UIView {
let containerView = UIView()
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(sessionId: "your-session-id"),
transactionData: TransactionData(
amount: 89.97,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "order-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date()
)
)
do {
let checkout = try PxpCheckout.initialize(config: checkoutConfig)
let config = createCustomApplePayConfig()
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 createCustomApplePayConfig() -> ApplePayButtonComponentConfig {
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.merchantDisplayName = "Outdoor Adventure Gear"
config.paymentDescription = "Camping Equipment Order"
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Custom SwiftUI button
config.customContent = {
return AnyView(
HStack(spacing: 12) {
// Apple logo
Image(systemName: "applelogo")
.font(.title2)
.foregroundColor(.white)
// Button text
VStack(alignment: .leading, spacing: 2) {
Text("Buy with")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
Text("Apple Pay")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
}
Spacer()
// Animated shimmer effect
Rectangle()
.frame(width: 2, height: 20)
.foregroundColor(.white.opacity(0.6))
.animation(
Animation.linear(duration: 1.5)
.repeatForever(autoreverses: false),
value: UUID()
)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, minHeight: 52)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color(red: 0.1, green: 0.1, blue: 0.1),
Color(red: 0.3, green: 0.3, blue: 0.3)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(
LinearGradient(
gradient: Gradient(colors: [
Color.white.opacity(0.2),
Color.clear
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
)
.cornerRadius(12)
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
)
}
// Payment items
config.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: 89.97,
label: "Total",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 69.99, label: "Camping Tent", type: .final),
ApplePayPaymentSummaryItem(amount: 14.99, label: "Sleeping Bag", type: .final),
ApplePayPaymentSummaryItem(amount: 4.99, label: "Tax", type: .final)
]
// Contact fields
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
config.requiredShippingContactFields = [.postalAddress, .name, .phoneNumber]
// Shipping methods
config.shippingMethods = [
ApplePayShippingMethod(
amount: 0.00,
detail: "5-7 business days",
identifier: "standard",
label: "Standard Shipping"
),
ApplePayShippingMethod(
amount: 12.99,
detail: "2-3 business days",
identifier: "express",
label: "Express Shipping"
)
]
// Event handlers
config.onShippingMethodSelected = { method in
let baseAmount = 84.98 // Subtotal + tax
let shippingCost = method.amount.doubleValue
let newTotal = baseAmount + shippingCost
return PKPaymentRequestShippingMethodUpdate(
paymentSummaryItems: [
PKPaymentSummaryItem(label: "Camping Tent", amount: NSDecimalNumber(value: 69.99)),
PKPaymentSummaryItem(label: "Sleeping Bag", amount: NSDecimalNumber(value: 14.99)),
PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(value: 4.99)),
PKPaymentSummaryItem(label: "Shipping (\(method.label))", amount: method.amount),
PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(value: newTotal))
]
)
}
config.onPostAuthorisation = { result in
DispatchQueue.main.async {
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Order successful: \(authorizedResult.provider.code)")
// Send order confirmation and navigate
} else if let failedResult = result as? FailedSubmitResult {
print("Order failed: \(failedResult.errorReason)")
// Show error notification
}
}
}
config.onError = { error in
print("Apple Pay Error: \(error)")
// Handle error
}
return config
}
}
struct OrderDetailsView: View {
var body: some View {
NavigationView {
VStack {
Text("Order Details")
.font(.largeTitle)
.padding()
Text("Your camping gear order is being processed.")
.padding()
Spacer()
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
// Dismiss view
}
}
}
}
}
}
import UIKit
import PXPCheckoutSDK
class SubscriptionViewController: UIViewController {
@IBOutlet weak var subscriptionPlanLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
@IBOutlet weak var featuresStackView: UIStackView!
@IBOutlet weak var applePayContainer: UIView!
private var applePayComponent: ApplePayButtonComponent?
private var checkout: PxpCheckout?
private let subscriptionPrice: Double = 14.99
override func viewDidLoad() {
super.viewDidLoad()
setupSubscriptionUI()
setupRecurringApplePay()
}
private func setupSubscriptionUI() {
subscriptionPlanLabel.text = "StreamMax Premium"
priceLabel.text = "$\(subscriptionPrice, specifier: "%.2f")/month"
// Add feature list
let features = [
"Ad-free streaming",
"4K Ultra HD quality",
"Download for offline viewing",
"Stream on up to 4 devices",
"Exclusive content access"
]
features.forEach { feature in
let featureView = createFeatureView(text: feature)
featuresStackView.addArrangedSubview(featureView)
}
}
private func createFeatureView(text: String) -> UIView {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8
stackView.alignment = .center
let checkmark = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
checkmark.tintColor = .systemGreen
checkmark.widthAnchor.constraint(equalToConstant: 20).isActive = true
checkmark.heightAnchor.constraint(equalToConstant: 20).isActive = true
let label = UILabel()
label.text = text
label.font = UIFont.systemFont(ofSize: 16)
label.textColor = .label
stackView.addArrangedSubview(checkmark)
stackView.addArrangedSubview(label)
return stackView
}
private func setupRecurringApplePay() {
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(sessionId: "your-session-id"),
transactionData: TransactionData(
amount: subscriptionPrice,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "subscription-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date()
)
)
do {
checkout = try PxpCheckout.initialize(config: checkoutConfig)
let subscriptionConfig = createSubscriptionApplePayConfig()
applePayComponent = try checkout?.create(.applePayButton, componentConfig: subscriptionConfig)
if let componentView = applePayComponent?.render() {
applePayContainer.addSubview(componentView)
setupApplePayConstraints(for: componentView)
}
} catch {
print("Failed to initialize: \(error)")
}
}
private func createSubscriptionApplePayConfig() -> ApplePayButtonComponentConfig {
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.merchantDisplayName = "StreamMax Entertainment"
config.paymentDescription = "Premium Streaming Subscription"
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Subscription-specific button styling
config.buttonType = .subscribe
config.buttonStyle = .black
config.buttonRadius = 8.0
// Custom gradient button using SwiftUI
config.customContent = {
return AnyView(
HStack(spacing: 10) {
Image(systemName: "applelogo")
.font(.title2)
.foregroundColor(.white)
VStack(alignment: .leading, spacing: 2) {
Text("Subscribe with")
.font(.caption)
.foregroundColor(.white.opacity(0.9))
Text("Apple Pay")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("$14.99")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
Text("per month")
.font(.caption2)
.foregroundColor(.white.opacity(0.8))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, minHeight: 48)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color(red: 1.0, green: 0.42, blue: 0.21), // FF6B35
Color(red: 0.97, green: 0.58, blue: 0.12) // F7931E
]),
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(8)
.shadow(color: Color(red: 1.0, green: 0.42, blue: 0.21).opacity(0.3), radius: 4, x: 0, y: 2)
)
}
// Payment items
config.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: subscriptionPrice,
label: "First Month",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 12.99, label: "Premium Subscription", type: .final),
ApplePayPaymentSummaryItem(amount: 2.00, label: "Tax", type: .final)
]
// Recurring payment configuration
config.recurringRequest = ApplePayRecurringPaymentRequest(
paymentDescription: "StreamMax Premium Monthly Subscription",
regularBilling: ApplePayRecurringPaymentSummaryItem(
label: "Monthly Premium Subscription",
amount: NSDecimalNumber(value: subscriptionPrice)
),
managementURL: "https://streammax.com/manage-subscription",
tokenNotificationURL: "https://api.streammax.com/webhooks/subscription"
)
// Configure recurring schedule
let calendar = Calendar.current
let nextMonth = calendar.date(byAdding: .month, value: 1, to: Date()) ?? Date()
config.recurringRequest?.regularBilling.startDate = nextMonth
config.recurringRequest?.regularBilling.intervalUnit = .month
config.recurringRequest?.regularBilling.intervalCount = 1
// Contact fields
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
// Event handlers
config.onPreAuthorisation = { [weak self] in
// Get user consent for recurring payments
let userConsent = await self?.getUserSubscriptionConsent() ?? false
guard userConsent else {
throw SubscriptionError.userDeclinedTerms
}
return ApplePayTransactionInitData(
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
deviceSessionId: await self?.getDeviceFingerprint() ?? "",
transaction: TransactionRiskData(
subscriptionType: "premium",
userTier: "new_subscriber"
)
)
)
}
config.onPostAuthorisation = { [weak self] result in
DispatchQueue.main.async {
if let authorizedResult = result as? AuthorisedSubmitResult {
print("Subscription setup successful: \(authorizedResult.provider.code)")
// Create subscription record
self?.createSubscription(
transactionId: authorizedResult.provider.code,
subscriptionPlan: "premium",
billingCycle: "monthly",
nextBillingDate: nextMonth
)
// Navigate to welcome screen
self?.navigateToWelcomeScreen(subscriptionId: authorizedResult.provider.code)
} else if let failedResult = result as? FailedSubmitResult {
print("Subscription setup failed: \(failedResult.errorReason)")
self?.showError("Subscription setup failed. Please try again.")
}
}
}
config.onError = { [weak self] error in
print("Subscription Error: \(error)")
DispatchQueue.main.async {
self?.showError("Unable to set up subscription. Please try again or contact support.")
}
}
return config
}
// Helper methods
private func getUserSubscriptionConsent() async -> Bool {
return await withCheckedContinuation { continuation in
DispatchQueue.main.async {
let alert = UIAlertController(
title: "Subscription Terms",
message: "You will be charged $14.99 monthly starting 30 days from today. You can cancel anytime in your account settings.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Accept & Subscribe", style: .default) { _ in
continuation.resume(returning: true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
continuation.resume(returning: false)
})
self.present(alert, animated: true)
}
}
}
private func getDeviceFingerprint() async -> String {
return "device_\(UIDevice.current.identifierForVendor?.uuidString ?? "unknown")_\(Int(Date().timeIntervalSince1970))"
}
private func createSubscription(transactionId: String, subscriptionPlan: String, billingCycle: String, nextBillingDate: Date) {
// Store subscription details
let subscriptionData: [String: Any] = [
"transactionId": transactionId,
"plan": subscriptionPlan,
"billingCycle": billingCycle,
"nextBillingDate": nextBillingDate,
"createdAt": Date()
]
UserDefaults.standard.set(subscriptionData, forKey: "subscription_\(transactionId)")
print("Subscription created: \(subscriptionData)")
}
private func navigateToWelcomeScreen(subscriptionId: String) {
let alert = UIAlertController(
title: "Welcome to StreamMax Premium!",
message: "Your subscription is now active. Subscription ID: \(subscriptionId)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Start Streaming", style: .default))
present(alert, animated: true)
}
private func showError(_ message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func setupApplePayConstraints(for componentView: UIView) {
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: 48)
])
}
}
enum SubscriptionError: Error {
case userDeclinedTerms
}
import UIKit
import PXPCheckoutSDK
class DynamicApplePayManager: NSObject {
enum PaymentState {
case idle, loading, success, error
}
private var component: ApplePayButtonComponent?
private var containerView: UIView
private var state: PaymentState = .idle
private var stateIndicator: UIView?
init(containerView: UIView) {
self.containerView = containerView
super.init()
}
func create(theme: Theme = .light) -> DynamicApplePayManager {
let checkoutConfig = CheckoutConfig(
environment: .test,
session: SessionConfig(sessionId: "your-session-id"),
transactionData: TransactionData(
amount: 189.99,
currency: "USD",
entryType: .ecom,
intent: .sale,
merchantTransactionId: "gaming-\(Int(Date().timeIntervalSince1970))",
merchantTransactionDate: Date()
)
)
do {
let checkout = try PxpCheckout.initialize(config: checkoutConfig)
let config = createDynamicConfig(theme: theme)
component = try checkout.create(.applePayButton, componentConfig: config)
} catch {
print("Failed to create component: \(error)")
}
return self
}
func mount() -> DynamicApplePayManager {
guard let component = component else { return self }
if let componentView = component.render() {
containerView.addSubview(componentView)
setupConstraints(for: componentView)
setupStateIndicator()
}
return self
}
private func createDynamicConfig(theme: Theme) -> ApplePayButtonComponentConfig {
let config = ApplePayButtonComponentConfig()
// Basic configuration
config.merchantDisplayName = "TechGadget Pro Store"
config.paymentDescription = "Wireless Gaming Headset Purchase"
config.currencyCode = "USD"
config.countryCode = "US"
config.supportedNetworks = [.visa, .masterCard, .amex, .discover]
config.merchantCapabilities = [.threeDSecure, .emv, .credit, .debit]
// Dynamic button with state management
config.customContent = { [weak self] in
return AnyView(self?.createDynamicButton(theme: theme) ?? EmptyView().eraseToAnyView())
}
// Payment items
config.totalPaymentItem = ApplePayPaymentSummaryItem(
amount: 189.99,
label: "Total",
type: .final
)
config.paymentItems = [
ApplePayPaymentSummaryItem(amount: 169.99, label: "Gaming Headset Pro X", type: .final),
ApplePayPaymentSummaryItem(amount: 12.99, label: "Extended Warranty", type: .final),
ApplePayPaymentSummaryItem(amount: 7.01, label: "Tax", type: .final)
]
// Contact fields
config.requiredBillingContactFields = [.postalAddress, .name, .emailAddress]
// Event handlers with state management
config.onPostAuthorisation = { [weak self] result in
DispatchQueue.main.async {
if let _ = result as? AuthorisedSubmitResult {
self?.setState(.success)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
// Navigate to success screen
}
} else {
self?.setState(.error)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self?.setState(.idle)
}
}
}
}
config.onError = { [weak self] error in
DispatchQueue.main.async {
self?.setState(.error)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self?.setState(.idle)
}
}
}
return config
}
private func createDynamicButton(theme: Theme) -> some View {
let colors = theme.colors
return HStack(spacing: 12) {
// Apple Pay logo
Image(systemName: "applelogo")
.font(.title2)
.foregroundColor(.white)
// Button text
Text("Pay")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
Spacer()
// Dynamic state indicator
Group {
switch state {
case .idle:
EmptyView()
case .loading:
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
case .success:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.title3)
case .error:
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
.font(.title3)
}
}
.frame(width: 20, height: 20)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, minHeight: 52)
.background(backgroundColor(for: state, theme: theme))
.cornerRadius(8)
.onTapGesture {
if state == .idle {
setState(.loading)
}
}
}
private func backgroundColor(for state: PaymentState, theme: Theme) -> Color {
switch state {
case .idle:
return theme.colors.background
case .loading:
return theme.colors.background.opacity(0.8)
case .success:
return Color.green
case .error:
return Color.red
}
}
private func setState(_ newState: PaymentState) {
state = newState
updateButtonAppearance()
}
private func setupStateIndicator() {
// Additional state indicators if needed
}
private func updateButtonAppearance() {
// Trigger view update through component refresh
}
private func setupConstraints(for componentView: UIView) {
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),
componentView.heightAnchor.constraint(equalToConstant: 52)
])
}
}
// Theme support
struct Theme {
let colors: ThemeColors
static let light = Theme(colors: ThemeColors(
background: Color.black,
text: Color.white,
hover: Color.gray,
success: Color.green,
error: Color.red
))
static let dark = Theme(colors: ThemeColors(
background: Color(red: 0.11, green: 0.11, blue: 0.12),
text: Color.white,
hover: Color(red: 0.17, green: 0.17, blue: 0.18),
success: Color(red: 0.19, green: 0.82, blue: 0.35),
error: Color(red: 1.0, green: 0.27, blue: 0.23)
))
}
struct ThemeColors {
let background: Color
let text: Color
let hover: Color
let success: Color
let error: Color
}
// Extension for type erasure
extension View {
func eraseToAnyView() -> AnyView {
return AnyView(self)
}
}
// Usage
// let applePayManager = DynamicApplePayManager(containerView: applePayContainerView)
// applePayManager.create(theme: .dark).mount()