Apple Best Practices and Guidelines
Comprehensive best practices for developing Tap to Pay on iPhone applications with Apple's ProximityReader framework and Koard SDK.
Overview
Building Tap to Pay on iPhone applications requires adherence to Apple's strict guidelines and best practices to ensure security, usability, and App Store approval. This guide covers essential practices for UI/UX design, security implementation, and the dual review process required for Tap to Pay applications.
Apple's Dual Review Process
Apple requires two distinct review processes for Tap to Pay on iPhone applications:
1. Tap to Pay Review
The Tap to Pay review is a comprehensive security and compliance assessment focused on:
- Security Implementation: Ensuring safe and secure payment processing
- Merchant Safety: Verifying that merchants can safely accept payments on their devices
- Branding and Messaging: Reviewing UI/UX workflows specifically around Tap to Pay functionality
- Payment Flow Design: Evaluating the complete payment experience from initiation to completion
- Error Handling: Assessing how payment errors and edge cases are managed
- Data Protection: Verifying compliance with Apple's data handling requirements
2. App Store Review
The standard App Store review process covers:
- General App Functionality: Core app features and user experience
- TestFlight Distribution: Ability to share the app via TestFlight for testing
- App Store Submission: Final approval for public distribution
- Guideline Compliance: Adherence to App Store Review Guidelines
Important: Both reviews must be passed successfully before your app can be distributed through the App Store.
User Experience Best Practices
Feedback and User Actions
Apple emphasizes providing clear feedback when users take explicit actions. This is crucial for Tap to Pay applications:
Provide Immediate Feedback
// Launch Tap to Pay screen with clear visual feedback
func presentTapToPayScreen() {
// Show loading indicator
showProgressIndicator()
// Launch Tap to Pay interface
Task {
do {
let reader = try await ProximityReader.readerIdentifier
// Present Tap to Pay UI
presentTapToPayInterface(reader: reader)
} catch {
// Handle error appropriately
handleTapToPayError(error)
}
}
}
Progress Indicators During prepare() Calls
func preparePaymentReader() {
// Show progress indicator while prepare() completes
showProgressIndicator(message: "Preparing payment reader...")
Task {
do {
// This is a long-running operation
let reader = try await ProximityReader.readerIdentifier
await prepareReader(reader)
// Hide progress indicator
hideProgressIndicator()
} catch {
hideProgressIndicator()
handlePreparationError(error)
}
}
}
Error Handling Best Practices
Avoid Modal Alerts for Background Operations
❌ Incorrect Approach:
// Never show modal alerts during background prepare() calls
func prepareReaderInBackground() {
Task {
do {
let reader = try await ProximityReader.readerIdentifier
await prepareReader(reader)
} catch {
// DON'T: Show modal alert during app launch
DispatchQueue.main.async {
let alert = UIAlertController(title: "Error", message: "Failed to prepare reader", preferredStyle: .alert)
self.present(alert, animated: true)
}
}
}
}
✅ Correct Approach:
// Use non-modal feedback for background operations
func prepareReaderInBackground() {
Task {
do {
let reader = try await ProximityReader.readerIdentifier
await prepareReader(reader)
} catch {
// Use banner notification or other non-modal method
DispatchQueue.main.async {
self.showBannerNotification(
message: "Payment reader preparation failed. Please try again.",
type: .error
)
}
}
}
}
Appropriate Error Display Methods
enum ErrorDisplayMethod {
case bannerNotification // For background operations
case modalAlert // For user-initiated actions
case inlineMessage // For form validation
case toastNotification // For non-critical errors
}
func displayError(_ error: Error, method: ErrorDisplayMethod) {
switch method {
case .bannerNotification:
showBannerNotification(message: error.localizedDescription)
case .modalAlert:
showModalAlert(title: "Error", message: error.localizedDescription)
case .inlineMessage:
showInlineError(message: error.localizedDescription)
case .toastNotification:
showToast(message: error.localizedDescription)
}
}
ProximityReader Framework Best Practices
Reader Management
Based on Apple's ProximityReader documentation, proper reader management is essential:
import ProximityReader
class TapToPayManager: ObservableObject {
@Published var isReaderAvailable = false
@Published var readerIdentifier: String?
func checkReaderAvailability() async {
do {
let identifier = try await ProximityReader.readerIdentifier
await MainActor.run {
self.readerIdentifier = identifier
self.isReaderAvailable = true
}
} catch {
await MainActor.run {
self.isReaderAvailable = false
self.readerIdentifier = nil
}
// Handle error appropriately
handleReaderError(error)
}
}
}
Secure Payment Processing
class SecurePaymentProcessor {
func processPayment(amount: Decimal, currency: String) async throws -> PaymentResult {
// Verify reader availability before processing
guard try await ProximityReader.readerIdentifier != nil else {
throw PaymentError.readerNotAvailable
}
// Process payment securely
let paymentData = try await capturePaymentData(amount: amount, currency: currency)
// Send to secure backend
return try await sendPaymentToBackend(paymentData)
}
private func capturePaymentData(amount: Decimal, currency: String) async throws -> PaymentData {
// Implementation for capturing payment data securely
// This should follow Apple's security guidelines
}
}
Security Best Practices
Data Protection and Privacy
class PaymentDataManager {
// Never store sensitive payment data
private let keychain = Keychain(service: "com.koard.payments")
func storeNonSensitiveData(_ data: PaymentMetadata) {
// Only store non-sensitive metadata
keychain["payment_id"] = data.paymentId
keychain["merchant_id"] = data.merchantId
// Never store card numbers, CVV, or other sensitive data
}
func processPaymentSecurely(_ paymentData: PaymentData) async throws {
// All sensitive processing should happen on secure backend
let encryptedData = try encryptPaymentData(paymentData)
try await sendToSecureBackend(encryptedData)
}
}
Entitlement Verification
class EntitlementManager {
func verifyTapToPayEntitlement() async -> Bool {
do {
_ = try await ProximityReader.readerIdentifier
return true
} catch {
// Handle entitlement errors
logEntitlementError(error)
return false
}
}
private func logEntitlementError(_ error: Error) {
// Log error for debugging but don't expose sensitive information
print("Entitlement verification failed: \(error.localizedDescription)")
}
}
UI/UX Design Guidelines
Payment Flow Design
class PaymentFlowViewController: UIViewController {
@IBOutlet weak var amountLabel: UILabel!
@IBOutlet weak var tapToPayButton: UIButton!
@IBOutlet weak var progressIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
setupAccessibility()
configurePaymentFlow()
}
private func setupAccessibility() {
// VoiceOver support
tapToPayButton.accessibilityLabel = "Pay with Tap to Pay"
tapToPayButton.accessibilityHint = "Double tap to initiate payment"
amountLabel.accessibilityLabel = "Total amount: $\(formattedAmount)"
}
private func configurePaymentFlow() {
// Clear payment intent
amountLabel.text = formattedAmount
amountLabel.font = UIFont.preferredFont(forTextStyle: .headline)
amountLabel.adjustsFontForContentSizeCategory = true
// Minimal steps - single tap to pay
tapToPayButton.setTitle("Tap to Pay", for: .normal)
}
}
Progress and Loading States
class PaymentProgressManager {
func showProgress(for operation: PaymentOperation) {
switch operation {
case .preparingReader:
showProgressIndicator(message: "Preparing payment reader...")
case .processingPayment:
showProgressIndicator(message: "Processing payment...")
case .completingTransaction:
showProgressIndicator(message: "Completing transaction...")
}
}
func hideProgress() {
hideProgressIndicator()
}
}
Testing and Validation
Comprehensive Testing Strategy
class TapToPayTests: XCTestCase {
func testReaderAvailability() async {
let manager = TapToPayManager()
await manager.checkReaderAvailability()
// Test on device with Tap to Pay capability
XCTAssertTrue(manager.isReaderAvailable)
}
func testPaymentFlow() async throws {
let processor = SecurePaymentProcessor()
let result = try await processor.processPayment(amount: 10.00, currency: "USD")
XCTAssertNotNil(result)
XCTAssertEqual(result.status, .success)
}
func testErrorHandling() {
// Test various error scenarios
let errorHandler = PaymentErrorHandler()
let networkError = PaymentError.networkError
let userMessage = errorHandler.getUserFriendlyMessage(for: networkError)
XCTAssertFalse(userMessage.isEmpty)
XCTAssertFalse(userMessage.contains("technical"))
}
}
TestFlight Preparation
// Prepare for TestFlight distribution
class TestFlightManager {
func prepareForTestFlight() {
// Ensure all test scenarios are covered
validatePaymentFlows()
testErrorScenarios()
verifyAccessibilityCompliance()
checkSecurityImplementation()
}
private func validatePaymentFlows() {
// Test all payment scenarios
// Verify UI/UX workflows
// Ensure proper error handling
}
}
App Store Review Preparation
Documentation Requirements
- Payment Flow Documentation: Complete walkthrough of payment process
- Security Implementation: Details of security measures and data protection
- Error Handling: Documentation of all error scenarios and user feedback
- Accessibility Compliance: VoiceOver and Dynamic Type support verification
- Test Account Credentials: Sandbox accounts for review team testing
Review Checklist
- Tap to Pay entitlement properly configured
- Reader availability checked before payment initiation
- Proper error handling for all scenarios
- No modal alerts during background operations
- Clear user feedback for all actions
- Accessibility compliance verified
- Security best practices implemented
- Test accounts provided for review
- Complete payment flow documented
Performance Optimization
Memory Management
class OptimizedPaymentManager {
weak var delegate: PaymentManagerDelegate?
private var readerSession: ProximityReader.Session?
func startPaymentSession() {
// Use weak references to avoid retain cycles
readerSession = ProximityReader.Session()
readerSession?.delegate = self
}
deinit {
// Clean up resources
readerSession?.invalidate()
}
}
Network Optimization
class NetworkOptimizer {
private let session: URLSession
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
func processPayment(_ data: PaymentData) async throws -> PaymentResult {
// Implement retry logic and timeout handling
return try await withRetry(maxAttempts: 3) {
try await sendPaymentRequest(data)
}
}
}
Resources and References
Apple Documentation
- ProximityReader Framework - Core framework for Tap to Pay functionality
- Apple Pay Developer Guide - Payment processing guidelines
- Human Interface Guidelines - UI/UX design principles
- App Store Review Guidelines - App Store submission requirements
Koard Resources
- Setting Up the Entitlement - Koard SDK integration guide
- Payment Lifecycle Guide - Complete payment flow documentation
- Test Cards Reference - Testing with test card numbers
- Security Guidelines - Security best practices
Additional Support
- Apple Developer Forums - Community support and discussions
- WWDC Sessions - Latest Tap to Pay and payment processing sessions
- Koard Developer Support - Direct support for Koard SDK integration