Repository navigation
feat: pass optional primaryAccountIdentifier to Apple Wallet - #91
Brunonascdev wants to merge 6 commits into
Conversation
PassKit uses this id to recognize a reissued card after the PAN suffix changes.
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
JakubKorytko
left a comment
There was a problem hiding this comment.
note: #56 also asks for the selected device in the issuer callback, so this covers only the first part of it and the issue should stay open after merge, unless you want to expand this PR
|
Heads up: #93 just landed and adds an old arch branch to the |
|
@Brunonascdev gentle bump |
Keep primaryAccountIdentifier on both the new and old architecture paths so the old arch does not drop the field.
PassKit returns a nil enrollment controller in that case, which surfaced as a generic configuration error.
PassKit does not report the device the user picked, so infer it from existing passes and pass it to the issuer callback. Expose canAddCardToAppleWallet so the Add button stays up until every device has the card.
|
@JakubKorytko, I merged This also covers the rest of #56 and #89. |
Ignore remote passes in the iPhone list, recover the stored account id from a unique last-digits match, and only use the all-devices check when PassKit itself refuses to present.
JakubKorytko
left a comment
There was a problem hiding this comment.
Main thing is the last-digits fallback for primaryAccountIdentifier, the rest is smaller
| return identifier | ||
| } | ||
|
|
||
| let suffixMatches = passes.filter { $0.primaryAccountNumberSuffix.hasSuffix(lastDigits) } |
There was a problem hiding this comment.
Not sure about this fallback. With no exact match we use the id of the only pass with the same last digits, deactivated passes included. A reissued card (new FPAN ID, same last 4) would get the old card's id, so PassKit hides the devices that have the old card and the add can fail with "already added to all available devices". I would keep the caller's id unless it matches exactly. Is there a real case where the issuer id differs from what PassKit reports?
| } | ||
|
|
||
| @objc public func canAddCardToAppleWallet(primaryAccountIdentifier: NSString) -> Bool { | ||
| return passLibrary.canAddSecureElementPass(primaryAccountIdentifier: primaryAccountIdentifier as String) |
There was a problem hiding this comment.
If the fallback stays: this checks the raw id, but the add flow may swap it for the last-digits match. Then this returns true, the button shows, and the add fails as "already added". Both should use the same resolved id
| configuration.primaryAccountIdentifier = primaryAccountIdentifier | ||
| } | ||
|
|
||
| let targetDevice = provisioningTargetDevice(for: primaryAccountIdentifier) |
There was a problem hiding this comment.
This re-reads both pass lists right after resolvedPrimaryAccountIdentifier already did. Also deviceType is computed once before the sheet opens and reused on "Try again", so it can go stale. Computing it in the delegate callback would fix both
| let onWatch = hasProvisionedPass(identifier: primaryAccountIdentifier, in: passLibrary.remoteSecureElementPasses) | ||
|
|
||
| if onPhone && !onWatch { | ||
| return "watch" |
There was a problem hiding this comment.
The device names are bare strings here and again in src/index.tsx. A typo on either side quietly turns into unknown. A small Swift enum with raw values for phone, watch and unknown would keep them in one place on this side.
|
|
||
| async function canAddCardToAppleWallet(primaryAccountIdentifier: string): Promise<boolean> { | ||
| if (Platform.OS === 'android') { | ||
| throw new Error('canAddCardToAppleWallet is not available on Android'); |
There was a problem hiding this comment.
This is a yes/no check apps will call when deciding whether to show the button. Throwing on Android means every caller needs its own platform check. listTokens returns an empty value on iOS instead of throwing, so returning false here would match that and be easier to use.
| - **Certificates** - an array of NSData objects. Each object contains a DER encoded X.509 certificate, with the leaf first and root last. You must download the root CA to validate the entire chain. | ||
| - **Encrypted Pass Data**- an encrypted JSON file containing the sensitive information needed to add a card to Apple Pay (Base 64 encoded). [More details here.](https://developer.apple.com/documentation/passkit/pkaddpaymentpassrequest/encryptedpassdata) | ||
| - **Ephemeral Public Key** - a key used by elliptic curve cryptography (ECC) (Base 64 encoded). | ||
| - **Primary Account Identifier** - an opaque issuer id (also called the FPAN ID) that PassKit uses to recognize the same card across reissues and devices. Pass it in `IOSCardData` and to `canAddCardToAppleWallet`. On iOS, `getCardStatusByIdentifier` uses this same value. If no pass matches the given id exactly, the library uses the id of the only pass with the same last digits. |
There was a problem hiding this comment.
This reads like getCardStatusByIdentifier also does the last-digits fallback, but it only does an exact match, and canAddCardToAppleWallet does not do it either. So the same id can give different answers from different functions. Either make them match or say exactly which call does the fallback.
| | **IOSIssuerCallback** | This callback is invoked with a nonce, its signature, and a certificate array obtained from Apple. It is expected that you will forward these details to your server or the card issuer's API to securely encrypt the payload required for adding cards to the Apple Wallet. | `(nonce: string, nonceSignature: string, certificate: string[]) => IOSEncryptPayload` | | ||
| | **IOSIssuerCallback** | This callback is invoked with a nonce, its signature, a certificate array obtained from Apple, and the device the card is being added to. Forward these details to your server or the card issuer's API to securely encrypt the payload required for adding cards to the Apple Wallet. `deviceType` is a best-effort value; see `IOSDeviceType`. | `(nonce: string, nonceSignature: string, certificate: string[], deviceType: IOSDeviceType) => IOSEncryptPayload` | | ||
| | **IOSDeviceType** | Best-effort device the card is being provisioned to. PassKit does not report the selected device, so this is inferred from existing passes when `primaryAccountIdentifier` is set. If no pass matches that id exactly, the library uses the id of the only pass with the same last digits. Passes on a paired Apple Watch are never counted as iPhone passes. `phone` means the card is already on the paired Apple Watch only. `watch` means it is already on the iPhone only. `unknown` means the card is on neither device, or no identifier was provided, so the user may still choose either device. Passes in the `deactivated` state count as not present. | `'phone' \| 'watch' \| 'unknown'` | | ||
| | **IOSAddPaymentPassData** | Data Apple returns when the user continues the provisioning flow, including the device the card is being added to. | `status: number`,<br>`nonce: string`,<br>`nonceSignature: string`,<br>`certificates: string[]`,<br>`deviceType?: string` | |
There was a problem hiding this comment.
Apple does not return deviceType, the library guesses it (see IOSDeviceType above). Worth saying that here so it does not read like a PassKit field.
Details
IOSCardDataaccepts an optionalprimaryAccountIdentifier. Issuers that reissue a card keep one opaque id while the PAN suffix changes, and PassKit uses that id to recognize the existing pass and to offer only the devices that do not already have it. The native bridge forwards the field on both the new and the old architecture, and only when it is non-empty. Callers that omit it keep the previous behavior.If no pass matches the given id exactly, the library uses the id of the only pass with the same last digits. That covers the case where Wallet stores a different id than the one the app passed. Two different cards with the same last digits fall back to the id the caller provided. Passes on a paired Apple Watch are never counted as iPhone passes.
canAddSecureElementPassis not used to block provisioning, because it has been reported to return false after an iPhone add even when a paired Watch can still take the card. If PassKit itself refuses to present the flow and that check says the card is already on every device, the call fails with "This card has already been added to all available devices." instead of the generic controller configuration error.canAddCardToAppleWallet(primaryAccountIdentifier)reports whether the card can still be added to the iPhone or a paired Apple Watch. Keep the Add button visible while it returns true.The issuer callback now receives a best-effort
deviceType(phone,watch, orunknown). PassKit does not report the device the user selected, so the library infers it from existing passes whenprimaryAccountIdentifieris set:watchwhen the card is only on the iPhone,phonewhen it is only on the paired Watch, andunknownwhen the user can still choose either device or no identifier was provided. Passes in thedeactivatedstate count as not present.Related Issues
Fixes #56
Fixes #89
Manual Tests
No automated coverage for the PassKit configuration. Typecheck, lint, and the example app typecheck pass. The wallet Swift sources typecheck against the iOS simulator SDK.
Confirming that PassKit filters the device list, and that
deviceTypematches the remaining device, still needs a device and an issuer that supplies the id.