---
name: dpearson2699/passkit
source: https://app.decimal.ai/s/dpearson2699-passkit@1/SKILL.md
source_sha256: a60e29322a00
---

# 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)