proximity-reader
ProximityReader iOS Development Skill
Build contactless payment, loyalty, and ID verification features using Apple's ProximityReader framework on iPhone — no additional hardware required.
When to Use This Skill
- Integrating Tap to Pay on iPhone (contactless payment acceptance)
- Reading loyalty cards / VAS passes from Apple Wallet
- Implementing Store and Forward for offline payment scenarios
- Building ID verification with the Verifier API (mobile driver's licenses, national IDs)
- Showing merchant education UI via
ProximityReaderDiscovery - Debugging
PaymentCardReaderErrororMobileDocumentReaderError
Framework Overview
ProximityReader (iOS 15.4+, iPadOS 15.4+, Mac Catalyst 15.4+) enables an iPhone to act as a contactless reader for:
| Domain | Key Classes | Min iOS |
|---|---|---|
| Payments | PaymentCardReader, PaymentCardReaderSession |
15.4 |
| Loyalty (VAS) | VASRequest, VASReadResult |
15.4 |
| Store & Forward | StoreAndForwardPaymentCardReaderSession, PaymentCardReaderStore |
17.0+ |
| ID Verification | MobileDocumentReader, MobileDocumentReaderSession |
17.0+ |
| Merchant Discovery | ProximityReaderDiscovery |
18.0+ |
Prerequisites & Entitlements
Before writing any code, ensure:
- Entitlement: Request the Tap to Pay on iPhone entitlement from Apple via your developer account. Without it, the framework won't function.
- Payment Service Provider (PSP): You must coordinate with a Level 3 certified PSP (e.g. Stripe, Adyen, Square, Windcave). The PSP provides the reader token (JWT) required to initialize the reader.
- Device: iPhone XS or later. No additional NFC hardware needed.
- Xcode: Add the
com.apple.developer.proximity-reader.payment.acceptanceentitlement to your app's entitlements file.
For the Verifier API (ID reading), a separate entitlement and server-side reader token generation is required. Read references/verifier-api.md for details.
Architecture at a Glance
┌─────────────────────────────────────────────────┐
│ Your App │
├──────────┬──────────┬───────────┬───────────────┤
│ Payment │ Loyalty │ Store & │ ID │
│ Flow │ (VAS) │ Forward │ Verification │
├──────────┴──────────┴───────────┴───────────────┤
│ ProximityReader Framework │
├─────────────────────────────────────────────────┤
│ Secure Element / NFC Hardware │
└─────────────────────────────────────────────────┘
Quick Reference: Common Patterns
1. Payment Card Reading (Tap to Pay)
import ProximityReader
// 1. Create and configure the reader
let reader = PaymentCardReader()
// 2. Obtain token from your PSP
let token = PaymentCardReader.Token(rawValue: pspProvidedJWT)
// 3. Link the merchant account (first use only)
if try await !reader.isAccountLinked(using: token) {
try await reader.linkAccount(using: token)
}
// 4. Create a session and prepare
let session = try await PaymentCardReaderSession(reader: reader, token: token)
try await session.prepare()
// 5. Read a payment card
let request = PaymentCardTransactionRequest(
amount: Decimal(29.99),
currencyCode: "USD",
type: .purchase
)
let result: PaymentCardReadResult = try await session.readPaymentCard(request)
// Send result.paymentCardData to your PSP for processing
2. Loyalty Card (VAS) Reading
// Can be combined with payment or standalone
let vasRequest = VASRequest(
merchantIdentifier: "pass.com.example.loyalty",
localizedDescription: "Example Loyalty Program"
)
// Read loyalty card alongside payment
let result = try await session.readPaymentCard(
request,
vasRequest: vasRequest
)
// Access loyalty data
if let vasResult = result.vasReadResult {
// Process loyalty identifiers
}
3. Store and Forward (Offline)
let sfSession = try await StoreAndForwardPaymentCardReaderSession(
reader: reader,
token: token
)
try await sfSession.prepare()
// Transactions are stored locally
let result = try await sfSession.readPaymentCard(request)
// Later, when online: retrieve and send batches
let store = PaymentCardReaderStore()
let batches: [StoreAndForwardBatch] = try await store.allBatches()
for batch in batches {
// Send batch.data to PSP
// On success, delete the batch
try await store.delete(using: batch.deletionToken)
}
4. Mobile Document / ID Reading (Verifier API)
Read references/verifier-api.md for the full setup including server-side reader token generation.
import ProximityReader
let docReader = MobileDocumentReader()
let readerToken = try await fetchReaderToken() // From your server
let session = try await MobileDocumentReaderSession(
reader: docReader,
readerToken: readerToken
)
try await session.prepare()
// Request a driver's license display
let displayRequest = MobileDriversLicenseDisplayRequest(
retainedElements: [.givenName, .familyName, .portrait, .dateOfBirth, .ageOver21]
)
try await session.readDocument(displayRequest)
// Or request validated data
let dataRequest = MobileDriversLicenseDataRequest(
retainedElements: [.givenName, .familyName, .dateOfBirth]
)
let documentResult = try await session.readDocument(dataRequest)
5. Merchant Discovery UI
// iOS 18+: Show Apple-provided merchant education
let discovery = ProximityReaderDiscovery()
try await discovery.present()
Error Handling
Always wrap ProximityReader calls in do-catch. The two main error types are:
do {
try await session.readPaymentCard(request)
} catch let error as PaymentCardReaderError {
switch error {
case .notAllowed:
// Missing entitlement or not authorized
case .unsupported:
// Device doesn't support Tap to Pay
case .networkError:
// Connectivity issue
case .invalidReaderToken:
// Token from PSP is invalid or expired
case .readerBusy:
// Another read session is active
case .backgrounded:
// App moved to background during read — must call prepare() again
default:
break
}
} catch let error as PaymentCardReaderSession.ReadError {
switch error {
case .cancelled:
// Customer cancelled the tap
case .invalidAmount:
// Amount was negative or zero
case .notReady:
// prepare() was not called
default:
break
}
}
For the Verifier API:
catch let error as MobileDocumentReaderError {
// Handle session preparation and document request errors
}
Critical Implementation Notes
- Always call
prepare()after the app returns to the foreground. The reader session is invalidated when backgrounded. Safe to call multiple times. - Call
prepare()after each transaction to reset internal state for the next transaction. - The reader token (JWT) comes from your PSP, not from Apple directly. Each PSP has their own token generation flow.
- PIN entry is supported on iOS 16.4+ for contactless cards that require it.
- Testing: Use
ProximityReaderStubfor simulator testing. Real NFC reads require a physical device. - Thread safety: ProximityReader uses Swift concurrency (async/await). All calls should be made from the main actor or appropriate actor context.
- Store and Forward: Batches persist on device. Always delete after successful processing to avoid data accumulation.
Deeper Reference Docs
For more detailed implementation guides, read these reference files:
| Reference | When to Read |
|---|---|
references/api-reference.md |
Full class/struct/enum listing with all properties and methods |
references/verifier-api.md |
Complete Verifier API setup including server-side token generation |
references/integration-patterns.md |
PSP integration patterns (Stripe, Adyen, Square), SwiftUI patterns, MVVM architecture |
references/troubleshooting.md |
Common errors, debugging tips, device compatibility |
Code Generation Guidelines
When generating ProximityReader code:
- Always
import ProximityReader - Use Swift concurrency (async/await) — the entire API is async
- Wrap all reader operations in do-catch with specific error handling
- Include
prepare()calls after foregrounding and after each transaction - Never hardcode PSP tokens — always fetch from server/PSP SDK
- For SwiftUI: use
.task {}modifier or@MainActorview models - Always check device capability before attempting to use the reader
- Follow Apple HIG: the tap payment sheet is system-provided — don't recreate it
More from eyadkelleh/carplay_claude_skill
carplay
CarPlay framework for iOS in-car applications — audio, communication, navigation, parking, EV charging, quick food ordering, fueling, driving task, public safety, and voice-based conversational apps. Includes widgets, live activities, CarPlay Ultra, and instrument cluster support.
21sirikit
Integrate Siri voice interactions, Shortcuts, and intelligent suggestions into iOS/watchOS apps using Apple's SiriKit framework. Use when implementing Intents extensions, custom intents, Siri Shortcuts, voice phrase handling, intent resolution/confirmation/handling, IntentsUI for custom Siri interfaces, donating shortcuts, or App Intents migration. Covers system intent domains (messaging, payments, ride booking, workouts, media) and custom intent definition.
1swiftui
SwiftUI framework for building user interfaces
1mapkit
Help developers integrate Apple MapKit into iOS/macOS apps. Use this skill when users ask to add a map to their app, display maps, show user location on a map, add markers/pins/annotations, implement map clustering, get directions/routing between locations, search for places/points of interest, implement MapKit features, work with MKMapView, SwiftUI Map, MKAnnotation, MKOverlay, MKDirections, MKLocalSearch, or any MapKit-related development task.
1apple-appclip
>
1widgetkit
Build iOS/macOS/watchOS/visionOS widgets, Live Activities, watch complications, and controls using Apple's WidgetKit framework. Use when creating widget extensions, timeline providers, configurable widgets, Lock Screen widgets, Smart Stack widgets, Live Activities with ActivityKit, interactive widgets with buttons/toggles, or watch complications. Covers all widget families (systemSmall/Medium/Large/ExtraLarge, accessoryCircular/Rectangular/Inline/Corner) and rendering modes.
1