passkit

$npx mdskill add dpearson2699/swift-ios-skills/passkit

Integrate Apple Pay payments and Wallet passes using PassKit.

  • Add Apple Pay buttons and create payment requests for goods and services.
  • Depends on PassKit framework, PKPaymentRequest, PKPaymentAuthorizationController, and PKPassLibrary.
  • Determines payment flow based on merchant capabilities, shipping/contact fields, and advanced request types.
  • Delivers payment authorization handling and Wallet pass distribution to the user or agent.

SKILL.md

.github/skills/passkitView on GitHub ↗
---
name: passkit
description: "Integrate Apple Pay payments and Wallet passes using PassKit. Use when adding Apple Pay buttons, creating payment requests, handling payment authorization, adding passes to Wallet, configuring merchant capabilities, managing shipping/contact fields, or working with PKPaymentRequest, PKPaymentAuthorizationController, PKPaymentButton, AddPassToWalletButton, PKPass, PKAddPassesViewController, PKPassLibrary, Wallet pass distribution, or Apple Pay checkout flows for physical goods, real-world services, donations, and eligible recurring payments."
---

# PassKit

Accept Apple Pay payments for physical goods, real-world services, donations,
and eligible recurring payments, and add passes to the user's Wallet. Covers
payment buttons, payment requests, authorization, Wallet passes, and merchant
configuration. Targets Swift 6.3 / iOS 26+.

For advanced Apple Pay flows, one `PKPaymentRequest` can set only one optional
advanced request type: recurring, automatic reload, deferred, Apple Pay Later
availability, or multi-token contexts. Use separate payment requests when a
checkout needs more than one of those modes.

## Contents

- [Setup](#setup)
- [Displaying the Apple Pay Button](#displaying-the-apple-pay-button)
- [Creating a Payment Request](#creating-a-payment-request)
- [Presenting the Payment Sheet](#presenting-the-payment-sheet)
- [Handling Payment Authorization](#handling-payment-authorization)
- [Wallet Passes](#wallet-passes)
- [Checking Pass Library](#checking-pass-library)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Setup

### Project Configuration

1. Enable the **Apple Pay** capability in Xcode
2. Create a Merchant ID in the Apple Developer portal (format: `merchant.com.example.app`)
3. Generate and install a Payment Processing Certificate for your merchant ID
4. Add the merchant ID to your entitlements

### Availability Check

Always verify the device can make payments before showing Apple Pay UI. If you
check for an active card with `canMakePayments(usingNetworks:capabilities:)`,
Apple's HIG expects Apple Pay to be a primary, prominent payment option wherever
you use that check.

```swift
import PassKit

func canMakePayments() -> Bool {
    // Check device supports Apple Pay at all
    guard PKPaymentAuthorizationController.canMakePayments() else {
        return false
    }
    // Check user has cards for the networks you support
    return PKPaymentAuthorizationController.canMakePayments(
        usingNetworks: [.visa, .masterCard, .amex, .discover],
        capabilities: .threeDSecure
    )
}
```

## Displaying the Apple Pay Button

### SwiftUI

Use the built-in `PayWithApplePayButton` view in SwiftUI. Use Apple-provided
button APIs for any control labeled Apple Pay; custom buttons must not include
the Apple Pay logo or "Apple Pay" text.

```swift
import SwiftUI
import PassKit

struct CheckoutView: View {
    var body: some View {
        PayWithApplePayButton(.buy) {
            startPayment()
        }
        .payWithApplePayButtonStyle(.black)
        .frame(height: 48)
        .padding()
    }
}
```

### UIKit

Use `PKPaymentButton` for UIKit-based interfaces.

```swift
let button = PKPaymentButton(
    paymentButtonType: .buy,
    paymentButtonStyle: .black
)
button.cornerRadius = 12
button.addTarget(self, action: #selector(startPayment), for: .touchUpInside)
```

**Button types:** `.plain`, `.buy`, `.setUp`, `.inStore`, `.donate`,
`.checkout`, `.continue`, `.book`, `.subscribe`, `.reload`, `.addMoney`,
`.topUp`, `.order`, `.rent`, `.support`, `.contribute`, `.tip`

## Creating a Payment Request

Build a `PKPaymentRequest` with your merchant details and the items being purchased.
PassKit amount APIs take `NSDecimalNumber`, not `Double`.

```swift
func createPaymentRequest() -> PKPaymentRequest {
    let request = PKPaymentRequest()
    request.merchantIdentifier = "merchant.com.example.app"
    request.countryCode = "US"
    request.currencyCode = "USD"
    request.supportedNetworks = [.visa, .masterCard, .amex, .discover]
    request.merchantCapabilities = .threeDSecure

    request.paymentSummaryItems = [
        PKPaymentSummaryItem(
            label: "Widget",
            amount: NSDecimalNumber(string: "9.99")
        ),
        PKPaymentSummaryItem(
            label: "Shipping",
            amount: NSDecimalNumber(string: "4.99")
        ),
        PKPaymentSummaryItem(
            label: "My Store",
            amount: NSDecimalNumber(string: "14.98")
        ) // Total
    ]

    return request
}
```

The **last item** in `paymentSummaryItems` is treated as the total and its label
appears in the Pay line on the payment sheet.

### Requesting Shipping and Contact Info

Request only the contact fields needed to price, fulfill, or legally process the
order.
Collect required product choices, optional notes, per-item shipping destinations,
and pickup locations before the Apple Pay button when the payment sheet cannot
collect them accurately.

```swift
request.requiredShippingContactFields = [.postalAddress, .emailAddress, .name]
request.requiredBillingContactFields = [.postalAddress]

let standard = PKShippingMethod(
    label: "Standard",
    amount: NSDecimalNumber(string: "4.99")
)
standard.identifier = "standard"
standard.detail = "5-7 business days"

let express = PKShippingMethod(
    label: "Express",
    amount: NSDecimalNumber(string: "9.99")
)
express.identifier = "express"
express.detail = "1-2 business days"

request.shippingMethods = [standard, express]

request.shippingType = .shipping // .delivery, .storePickup, .servicePickup
```

### Supported Networks

| Network | Constant |
|---|---|
| Visa | `.visa` |
| Mastercard | `.masterCard` |
| American Express | `.amex` |
| Discover | `.discover` |
| China UnionPay | `.chinaUnionPay` |
| JCB | `.JCB` |
| Maestro | `.maestro` |
| Electron | `.electron` |
| Interac | `.interac` |

Query available networks at runtime with `PKPaymentRequest.availableNetworks()`.

## Presenting the Payment Sheet

Use `PKPaymentAuthorizationController` (works in both SwiftUI and UIKit, no view controller needed). The controller's delegate is weak, so retain the controller for the life of the sheet.

```swift
final class CheckoutCoordinator: NSObject {
    private var paymentController: PKPaymentAuthorizationController?

    @MainActor
    func startPayment() {
        let controller = PKPaymentAuthorizationController(
            paymentRequest: createPaymentRequest()
        )
        paymentController = controller
        controller.delegate = self
        controller.present { [weak self] presented in
            if !presented {
                self?.paymentController = nil
            }
        }
    }
}
```

## Handling Payment Authorization

Implement `PKPaymentAuthorizationControllerDelegate` to process the payment token.

```swift
extension CheckoutCoordinator: PKPaymentAuthorizationControllerDelegate {
    func paymentAuthorizationController(
        _ controller: PKPaymentAuthorizationController,
        didAuthorizePayment payment: PKPayment,
        handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
    ) {
        // Send payment.token.paymentData to your payment processor
        Task {
            do {
                try await paymentService.process(payment.token)
                completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
            } catch {
                completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
            }
        }
    }

    func paymentAuthorizationControllerDidFinish(
        _ controller: PKPaymentAuthorizationController
    ) {
        controller.dismiss { [weak self] in
            self?.paymentController = nil
        }
    }
}
```

### Handling Shipping Changes

```swift
func paymentAuthorizationController(
    _ controller: PKPaymentAuthorizationController,
    didSelectShippingMethod shippingMethod: PKShippingMethod,
    handler completion: @escaping (PKPaymentRequestShippingMethodUpdate) -> Void
) {
    let updatedItems = recalculateItems(with: shippingMethod)
    let update = PKPaymentRequestShippingMethodUpdate(paymentSummaryItems: updatedItems)
    completion(update)
}
```

## Wallet Passes

### Adding a Pass to Wallet

Load signed `.pkpass` data, verify the device can add passes, then present
`PKAddPassesViewController` when you want the user to review the pass before
adding it. `PKPass(data:)` expects signed pass data and can throw invalid-data
or invalid-signature errors. Name `invalid-data` and `invalid-signature`
failures explicitly instead of hiding them behind a bare `try?` in review
guidance.

```swift
func addPassToWallet(data: Data) {
    guard PKAddPassesViewController.canAddPasses() else {
        return
    }

    do {
        let pass = try PKPass(data: data)
        guard let addController = PKAddPassesViewController(pass: pass) else {
            return
        }
        addController.delegate = self
        present(addController, animated: true)
    } catch {
        // Signed pass data is invalid or the signature cannot be validated.
        showRecoverablePassError(error)
    }
}
```

### SwiftUI Wallet Button

Use `AddPassToWalletButton` as the SwiftUI equivalent to `PKAddPassButton`.

```swift
import PassKit
import SwiftUI

struct AddPassButton: View {
    let passData: Data
    @State private var addedToWallet = false

    var body: some View {
        if PKAddPassesViewController.canAddPasses(),
           let pass = try? PKPass(data: passData) {
            AddPassToWalletButton([pass]) { added in
                addedToWallet = added
            }
            .addPassToWalletButtonStyle(.blackOutline)
            .frame(width: 250, height: 50)
        }
    }
}
```

## Checking Pass Library

Use `PKPassLibrary` to inspect and manage passes the user already has. Check
`PKPassLibrary.isPassLibraryAvailable()` before pass-library operations, but use
`PKAddPassesViewController.canAddPasses()` to decide whether the device can add
passes. `passes()` only returns passes your app can access through its
entitlements. When replacing an existing pass, check the Boolean result from
`replacePass(with:)` and handle failure. For signed pass bundle construction,
update web services, and `replacePass(with:)`, read
[references/wallet-passes.md](references/wallet-passes.md).

```swift
let library = PKPassLibrary()

// Check if a specific pass is already in Wallet
let hasPass = library.containsPass(pass)

// Retrieve passes your app can access
let passes = library.passes()

// Check if pass library is available
guard PKPassLibrary.isPassLibraryAvailable() else { return }
```

## Common Mistakes

### DON'T: Use StoreKit for physical goods

Apple Pay (PassKit) is for **physical goods, real-world services, donations, and
eligible recurring payments**. StoreKit is for virtual goods, app features, and
digital-content subscriptions. Using the wrong framework leads to App Review
rejection.

### DON'T: Hardcode merchant ID in multiple places

```swift
// WRONG: Merchant ID scattered across the codebase
let request1 = PKPaymentRequest()
request1.merchantIdentifier = "merchant.com.example.app"
// ...elsewhere:
let request2 = PKPaymentRequest()
request2.merchantIdentifier = "merchant.com.example.app" // easy to get out of sync

// CORRECT: Centralize configuration
enum PaymentConfig {
    static let merchantIdentifier = "merchant.com.example.app"
    static let countryCode = "US"
    static let currencyCode = "USD"
    static let supportedNetworks: [PKPaymentNetwork] = [.visa, .masterCard, .amex]
}
```

## Review Checklist

- [ ] Apple Pay capability enabled and merchant ID configured in Developer portal
- [ ] Payment Processing Certificate generated and installed
- [ ] `canMakePayments(usingNetworks:)` checked before showing Apple Pay button
- [ ] Apple Pay is prominent wherever active-card availability is checked
- [ ] Product choices, optional details, and complex shipping choices collected before payment sheet
- [ ] Last item in `paymentSummaryItems` is the total with merchant display name
- [ ] Payment summary and token-context amounts use `NSDecimalNumber`
- [ ] Payment token sent to server for processing (never decoded client-side)
- [ ] `PKPaymentAuthorizationController` retained while presented and cleared after finish
- [ ] `paymentAuthorizationControllerDidFinish` dismisses the controller
- [ ] Shipping method changes recalculate totals via delegate callback
- [ ] StoreKit used for virtual goods/digital content; Apple Pay used for physical goods, services, donations, and eligible recurring payments
- [ ] Wallet passes loaded from signed `.pkpass` bundles
- [ ] `PKPass(data:)` invalid-data and invalid-signature failures surfaced
- [ ] `PKPassLibrary.isPassLibraryAvailable()` used for pass operations, not add-pass capability
- [ ] `PKAddPassesViewController.canAddPasses()` checked before add-pass UI
- [ ] `PKPassLibrary.replacePass(with:)` Boolean result checked when replacing a pass
- [ ] Apple Pay button uses system-provided `PKPaymentButton` or `PayWithApplePayButton`
- [ ] Add-to-Wallet UI uses system-provided `PKAddPassButton`, `AddPassToWalletButton`, or `PKAddPassesViewController`
- [ ] Error states handled in authorization result (network failures, declined cards)

## References

- Extended patterns (recurring/deferred payments, coupon codes, multi-merchant, pass bundles, pass updates): [references/wallet-passes.md](references/wallet-passes.md)
- [PassKit framework](https://sosumi.ai/documentation/passkit)
- [PKPaymentRequest](https://sosumi.ai/documentation/passkit/pkpaymentrequest)
- [PKPaymentAuthorizationController](https://sosumi.ai/documentation/passkit/pkpaymentauthorizationcontroller)
- [PKPaymentButton](https://sosumi.ai/documentation/passkit/pkpaymentbutton)
- [PayWithApplePayButton](https://sosumi.ai/documentation/passkit/paywithapplepaybutton)
- [AddPassToWalletButton](https://sosumi.ai/documentation/passkit/addpasstowalletbutton)
- [PKPass](https://sosumi.ai/documentation/passkit/pkpass)
- [PKAddPassesViewController](https://sosumi.ai/documentation/passkit/pkaddpassesviewcontroller)
- [PKPassLibrary](https://sosumi.ai/documentation/passkit/pkpasslibrary)
- [PKPaymentNetwork](https://sosumi.ai/documentation/passkit/pkpaymentnetwork)
- [Apple Pay HIG](https://sosumi.ai/design/human-interface-guidelines/apple-pay)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.