Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ The library offers functions for seamless integration and use of the Apple Walle
| **resumeAddCardToGoogleWallet** | Resumes the Push Provisioning flow for adding a card to the Google Wallet using existing token reference ID. | `data`: `AndroidResumeCardData` | `TokenizationStatus` | ❌ | ✅ |
| **listTokens** | Lists all tokens currently stored in the Google Wallet. | None | `TokenInfo[]` | ❌ | ✅ |
| **listAppleWalletPasses** | Lists Apple Wallet Secure Element passes available to the app, including activation state and whether the pass is remote (for example on Apple Watch). Filter the result for `activationState === 'requireActivation'` or `activationState === 'pending'` to build a pending-cards UI. | None | `IOSWalletPass[]` | ✅ | ❌ |
| **addCardToAppleWallet** | Initiates native Push Provisioning flow for adding a card to the Apple Wallet. When `IOSCardData.primaryAccountIdentifier` is set, the device picker only lists devices that do not already have the card. If PassKit cannot present the flow because the card is already on every device, the call fails with a clear error. | `data`: `IOSCardData`,<br>`issuerEncrypt-`<br>`PayloadCallback: IOSIssuerCallback` | `void` | ✅ | ❌ |
| **addCardToAppleWallet** | Initiates native Push Provisioning flow for adding a card to the Apple Wallet. When `IOSCardData.primaryAccountIdentifier` is set, the device picker only lists devices that do not already have the card. If PassKit cannot present the flow because the card is already on every device, the call fails with a clear error. If the issuer callback throws, the Add Payment Pass sheet is closed and the promise rejects with that error. | `data`: `IOSCardData`,<br>`issuerEncrypt-`<br>`PayloadCallback: IOSIssuerCallback` | `void` | ✅ | ❌ |
| **canAddCardToAppleWallet** | Checks whether the card can still be added to the iPhone or a paired Apple Watch. Pass `lastDigits` to use the same id lookup as `addCardToAppleWallet`. Returns `false` on Android. | `primaryAccountIdentifier: string`,<br>`lastDigits?: string` | `boolean` | ✅ | ✅ |
| **activateAppleWalletPass** | Activates an existing Secure Element pass in Apple Wallet using activation data from your issuer backend. Resolves `true` when the pass is already active. For `requireActivation`, returns Apple's result, which can be `false`. Rejects for any other state (`pending`, `suspended`), a missing pass, unavailable activation, or invalid or non-base64 activation data. | `data`: `IOSPassActivationData` | `boolean` | ✅ | ❌ |

Expand Down
4 changes: 4 additions & 0 deletions android/src/main/java/com/expensify/wallet/WalletModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ class WalletModule internal constructor(context: ReactApplicationContext) :
// no-op
}

override fun IOSFailAddPaymentPass(promise: Promise?) {
promise?.resolve(null)
}

override fun canAddCardToAppleWallet(primaryAccountIdentifier: String, lastDigits: String, promise: Promise) {
promise.resolve(false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public NativeWalletSpec(ReactApplicationContext reactContext) {
@DoNotStrip
public abstract void IOSHandleAddPaymentPassResponse(ReadableMap payload, Promise promise);

public abstract void IOSFailAddPaymentPass(Promise promise);

public abstract void activateAppleWalletPass(ReadableMap passData, Promise promise);

@ReactMethod
Expand Down
10 changes: 10 additions & 0 deletions ios/RNWallet.mm
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ - (instancetype)init {
}
}

RCT_REMAP_METHOD(IOSFailAddPaymentPass,
IOSFailAddPaymentPass:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->walletManager IOSFailAddPaymentPass];
resolve(nil);
});
}

RCT_REMAP_METHOD(getCardStatusBySuffix,
getCardStatusBySuffix:(NSString *)last4Digits
resolve:(RCTPromiseResolveBlock)resolve
Expand Down
10 changes: 10 additions & 0 deletions ios/WalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ open class WalletManager: UIViewController {
self.addPassHandler?(addPaymentPassRequest)
self.addPassHandler = nil
}

/// Closes the Add Payment Pass sheet when the issuer encrypt call fails.
/// The PassKit completion handler is dropped: there is no payload to give it.
@objc
public func IOSFailAddPaymentPass() {
self.addPassHandler = nil
self.addPaymentPassCompletionHandler = nil
self.addPassPrimaryAccountIdentifier = nil
hideModal()
}

private func getPassActivationState(matching condition: (PKSecureElementPass) -> Bool) -> NSNumber {
let paymentPasses = passLibrary.passes(of: .payment)
Expand Down
8 changes: 2 additions & 6 deletions ios/stucts/AddPassResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,9 @@ struct AddPassResponse {

func toNSDictionary() -> NSDictionary {
if status.rawValue > 0 {
var response: [String: Any] = [
return [
"status": status.rawValue,
]
if let deviceType = deviceType {
response["deviceType"] = deviceType.rawValue
}
return response as NSDictionary
] as NSDictionary
}

guard let nonce = nonce,
Expand Down
1 change: 1 addition & 0 deletions src/NativeWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface Spec extends TurboModule {
listAppleWalletPasses(): Promise<IOSWalletPassNative[]>;
IOSPresentAddPaymentPassView(cardData: IOSCardData): Promise<IOSAddPaymentPassData>;
IOSHandleAddPaymentPassResponse(payload: IOSEncryptPayload): Promise<IOSAddPaymentPassData | null>;
IOSFailAddPaymentPass(): Promise<void>;
activateAppleWalletPass(passData: IOSPassActivationData): Promise<boolean>;
canAddCardToAppleWallet(primaryAccountIdentifier: string, lastDigits: string): Promise<boolean>;
addListener: (eventType: string) => void;
Expand Down
12 changes: 11 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,17 @@ async function addCardToAppleWallet(
}

async function addPaymentPassToWallet(paymentPassData: IOSAddPaymentPassData): Promise<number> {
const responseData = await issuerEncryptPayloadCallback(paymentPassData.nonce, paymentPassData.nonceSignature, paymentPassData.certificates, toIOSDeviceType(paymentPassData.deviceType));
let responseData: IOSEncryptPayload;
try {
responseData = await issuerEncryptPayloadCallback(paymentPassData.nonce, paymentPassData.nonceSignature, paymentPassData.certificates, toIOSDeviceType(paymentPassData.deviceType));
} catch (error) {
try {
await wallet.IOSFailAddPaymentPass();
} catch {
// Closing the sheet is best-effort. The issuer error is what the user sees.
}
throw error;
}
const response = await wallet.IOSHandleAddPaymentPassResponse(responseData);
// Response is null when a pass is successfully added to the wallet or the user cancels the process
// In case the user presses the `Try again` option, new pass data is returned, and it should reenter the function
Expand Down
Loading