Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: check for existence of pass on paired watch #1219

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

### New features

- Added the `hasPairedAppleWatch` option to `canAddCardToWallet`. [#1219](https://github.com/stripe/stripe-react-native/pull/1219)

## Fixes

- Fixed an issue where builds would error with the message `'const' enums are not supported.` [see commit](https://github.com/stripe/stripe-react-native/commit/f882bfa588aa6d23a980b4b43d2cca660ca1dd2a)
- Fixed an issue where the `canAddCardToWallet` method would sometimes wrongly return `false` with a `details.status` of `MISSING_CONFIGURATION` in production builds. [#1215](https://github.com/stripe/stripe-react-native/pull/1215)
- Fixed an issue on Android where, for certain countries, the postal code would not be enabled but would still be required. [#1213](https://github.com/stripe/stripe-react-native/pull/1213)
- Fixed an issue on iOS where `canAddCardToWallet` would return `false` if the card had already been provisioned on a paired device like an Apple Watch, but had not yet been provisioned on the current device, and would also return `false` if the card had been provisioned on the current device, but not on a paired Apple Watch. [#1219](https://github.com/stripe/stripe-react-native/pull/1219)

## 0.21.0 - 2022-11-15

Expand Down
61 changes: 61 additions & 0 deletions ios/PaymentPassFinder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// PKPaymentPassFinder.swift
// stripe-react-native
//
// Created by Charles Cruzan on 10/6/22.
//

import Foundation

internal class PaymentPassFinder: NSObject {
enum PassLocation: String {
case CURRENT_DEVICE
case PAIRED_DEVICE
}

class func findPassWithLast4(last4: String, hasPairedAppleWatch: Bool, completion: @escaping ((Bool, [PassLocation]) -> Void)) {
let existingPassOnDevice: PKPass? = {
if #available(iOS 13.4, *) {
return PKPassLibrary().passes(of: PKPassType.secureElement)
.first(where: { $0.secureElementPass?.primaryAccountNumberSuffix == last4 && $0.secureElementPass?.passActivationState != .deactivated && !$0.isRemotePass })
} else {
return PKPassLibrary().passes(of: PKPassType.payment)
.first(where: { $0.paymentPass?.primaryAccountNumberSuffix == last4 && $0.paymentPass?.passActivationState != .deactivated && !$0.isRemotePass })
}
}()

var passLocations: [PassLocation] = []
if (existingPassOnDevice != nil) {
passLocations.append(.CURRENT_DEVICE)
}

// We're done here if the user does not have a paired Apple Watch
if (!hasPairedAppleWatch) {
completion(
passLocations.count < 1,
passLocations
)
return
}

let existingPassOnPairedDevices: PKPass? = {
if #available(iOS 13.4, *) {
return PKPassLibrary().remoteSecureElementPasses
.first(where: { $0.secureElementPass?.primaryAccountNumberSuffix == last4 && $0.secureElementPass?.passActivationState != .deactivated })
} else {
return PKPassLibrary().remotePaymentPasses()
.first(where: { $0.paymentPass?.primaryAccountNumberSuffix == last4 && $0.paymentPass?.passActivationState != .deactivated })
}
}()


if (existingPassOnPairedDevices != nil) {
passLocations.append(.PAIRED_DEVICE)
}

completion(
passLocations.count < 2,
passLocations
)
}
}
55 changes: 39 additions & 16 deletions ios/PushProvisioning/PushProvisioningUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,33 @@ internal class PushProvisioningUtils {
class func canAddCardToWallet(
last4: String,
primaryAccountIdentifier: String,
testEnv: Bool
) -> (canAddCard: Bool, status: AddCardToWalletStatus?) {
testEnv: Bool,
hasPairedAppleWatch: Bool,
completion: @escaping (_ canAddCard: Bool, _ status: AddCardToWalletStatus?) -> Void
) {
if (!PKAddPassesViewController.canAddPasses()) {
return (false, AddCardToWalletStatus.UNSUPPORTED_DEVICE)
completion(false, AddCardToWalletStatus.UNSUPPORTED_DEVICE)
}

var status : AddCardToWalletStatus? = nil
var canAddCard = PushProvisioningUtils.canAddPaymentPass(
let canAddCard = canAddPaymentPass(
primaryAccountIdentifier: primaryAccountIdentifier,
isTestMode: testEnv)

if (!canAddCard) {
status = AddCardToWalletStatus.MISSING_CONFIGURATION
} else if (PushProvisioningUtils.passExistsWith(last4: last4)) {
canAddCard = false
status = AddCardToWalletStatus.CARD_ALREADY_EXISTS
completion(canAddCard, AddCardToWalletStatus.MISSING_CONFIGURATION)
} else {
PaymentPassFinder.findPassWithLast4(last4: last4, hasPairedAppleWatch: hasPairedAppleWatch) { canAddCardToADevice, passLocations in
var status: AddCardToWalletStatus? = nil
if (!canAddCardToADevice) {
status = AddCardToWalletStatus.CARD_ALREADY_EXISTS
} else if (passLocations.contains(.PAIRED_DEVICE)) {
status = AddCardToWalletStatus.CARD_EXISTS_ON_PAIRED_DEVICE
} else if (passLocations.contains(.CURRENT_DEVICE)) {
status = AddCardToWalletStatus.CARD_EXISTS_ON_CURRENT_DEVICE
}
completion(canAddCardToADevice, status)
}
}

return (canAddCard, status)
}

class func canAddPaymentPass(primaryAccountIdentifier: String, isTestMode: Bool) -> Bool {
Expand All @@ -45,20 +53,35 @@ internal class PushProvisioningUtils {
}
}

class func passExistsWith(last4: String) -> Bool {
let existingPass: PKPass? = {
class func getPassLocation(last4: String) -> PaymentPassFinder.PassLocation? {
let existingPassOnDevice: PKPass? = {
if #available(iOS 13.4, *) {
return PKPassLibrary().passes(of: PKPassType.secureElement).first(where: {$0.secureElementPass?.primaryAccountNumberSuffix == last4})
return PKPassLibrary().passes(of: PKPassType.secureElement)
.first(where: { $0.secureElementPass?.primaryAccountNumberSuffix == last4 && $0.secureElementPass?.passActivationState != .suspended && !$0.isRemotePass })
} else {
return PKPassLibrary().passes(of: PKPassType.payment).first(where: {$0.paymentPass?.primaryAccountNumberSuffix == last4})
return PKPassLibrary().passes(of: PKPassType.payment)
.first(where: { $0.paymentPass?.primaryAccountNumberSuffix == last4 && $0.paymentPass?.passActivationState != .suspended && !$0.isRemotePass })
}
}()
return existingPass != nil

let existingPassOnPairedDevices: PKPass? = {
if #available(iOS 13.4, *) {
return PKPassLibrary().remoteSecureElementPasses
.first(where: { $0.secureElementPass?.primaryAccountNumberSuffix == last4 && $0.secureElementPass?.passActivationState != .suspended })
} else {
return PKPassLibrary().remotePaymentPasses()
.first(where: { $0.paymentPass?.primaryAccountNumberSuffix == last4 && $0.paymentPass?.passActivationState != .suspended })
}
}()

return existingPassOnDevice != nil ? PaymentPassFinder.PassLocation.CURRENT_DEVICE : (existingPassOnPairedDevices != nil ? PaymentPassFinder.PassLocation.PAIRED_DEVICE : nil)
}

enum AddCardToWalletStatus: String {
case UNSUPPORTED_DEVICE
case MISSING_CONFIGURATION
case CARD_ALREADY_EXISTS
case CARD_EXISTS_ON_CURRENT_DEVICE
case CARD_EXISTS_ON_PAIRED_DEVICE
}
}
20 changes: 12 additions & 8 deletions ios/StripeSdk.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1116,13 +1116,17 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap
resolve(Errors.createError(ErrorType.Failed, "You must provide `cardLastFour`"))
return
}
let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: last4,
primaryAccountIdentifier: params["primaryAccountIdentifier"] as? String ?? "",
testEnv: params["testEnv"] as? Bool ?? false)
resolve([
"canAddCard": canAddCard,
"details": ["status": status?.rawValue],
])
PushProvisioningUtils.canAddCardToWallet(
last4: last4,
primaryAccountIdentifier: params["primaryAccountIdentifier"] as? String ?? "",
testEnv: params["testEnv"] as? Bool ?? false,
hasPairedAppleWatch: params["hasPairedAppleWatch"] as? Bool ?? false)
{ canAddCard, status in
resolve([
"canAddCard": canAddCard,
"details": ["status": status?.rawValue],
])
}
}

@objc(isCardInWallet:resolver:rejecter:)
Expand All @@ -1135,7 +1139,7 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap
resolve(Errors.createError(ErrorType.Failed, "You must provide `cardLastFour`"))
return
}
resolve(["isInWallet": PushProvisioningUtils.passExistsWith(last4: last4)])
resolve(["isInWallet": PushProvisioningUtils.getPassLocation(last4: last4) != nil])
}

@objc(collectBankAccountToken:resolver:rejecter:)
Expand Down
25 changes: 14 additions & 11 deletions ios/Tests/PushProvisioningTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ import XCTest

class PushProvisioningTests: XCTestCase {
func testCanAddCardToWalletInTestMode() throws {
let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: "4242",
PushProvisioningUtils.canAddCardToWallet(last4: "4242",
primaryAccountIdentifier: "",
testEnv: true)
XCTAssertEqual(canAddCard, true)
XCTAssertEqual(status, nil)
testEnv: true, hasPairedAppleWatch: false) { canAddCard, status in
XCTAssertEqual(canAddCard, true)
XCTAssertEqual(status, nil)
}
}

func testCanAddCardToWalletInLiveMode() throws {
let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: "4242",
PushProvisioningUtils.canAddCardToWallet(last4: "4242",
primaryAccountIdentifier: "",
testEnv: false)
XCTAssertEqual(canAddCard, false)
XCTAssertEqual(status, PushProvisioningUtils.AddCardToWalletStatus.MISSING_CONFIGURATION)
testEnv: false,
hasPairedAppleWatch: false) { canAddCard, status in
XCTAssertEqual(canAddCard, false)
XCTAssertEqual(status, PushProvisioningUtils.AddCardToWalletStatus.MISSING_CONFIGURATION)
}
}

func testCanAddPaymentPassInTestMode() throws {
Expand All @@ -42,8 +45,8 @@ class PushProvisioningTests: XCTestCase {

func testCheckIfPassExists() throws {
XCTAssertEqual(
PushProvisioningUtils.passExistsWith(last4: "4242"),
false
PushProvisioningUtils.getPassLocation(last4: "4242"),
nil
)
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"bootstrap": "yarn example && yarn && yarn pods",
"bootstrap-no-pods": "yarn example && yarn",
"docs": "yarn typedoc ./src/index.tsx --out ./docs/api-reference --tsconfig ./tsconfig.json --readme none --sort source-order",
"run-example-ios": "cd example;ENVFILE=.env.ci react-native run-ios --configuration Release --simulator \"iPhone 13 (15.5)\"",
"run-example-ios": "cd example;ENVFILE=.env.ci react-native run-ios --configuration Release --simulator \"iPhone 13 (15.2)\"",
"run-example-android": "cd example;ENVFILE=.env.ci react-native run-android --variant=release",
"test:e2e:ios": "mkdir -p .tmp/screenshots && node ./run-appium-tests.js ios",
"test:e2e:android": "mkdir -p .tmp/screenshots && node ./run-appium-tests.js android",
Expand Down
20 changes: 10 additions & 10 deletions src/types/PlatformPay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ export type ApplePaySheetError =
message?: string;
};

export const enum ApplePaySheetErrorType {
export enum ApplePaySheetErrorType {
InvalidShippingAddress = 'InvalidShippingAddress',
UnserviceableShippingAddress = 'UnserviceableShippingAddress',
InvalidCouponCode = 'InvalidCouponCode',
ExpiredCouponCode = 'ExpiredCouponCode',
}

export const enum ContactField {
export enum ContactField {
EmailAddress = 'emailAddress',
Name = 'name',
PhoneNumber = 'phoneNumber',
PhoneticName = 'phoneticName',
PostalAddress = 'postalAddress',
}

export const enum InvalidShippingField {
export enum InvalidShippingField {
Street = 'street',
City = 'city',
SubAdministrativeArea = 'subAdministrativeArea',
Expand Down Expand Up @@ -76,7 +76,7 @@ export type ApplePayPaymentMethodParams = {
couponCode?: string;
};

export const enum ApplePayMerchantCapability {
export enum ApplePayMerchantCapability {
/** Required. This value must be supplied. */
Supports3DS = 'supports3DS',
/** Optional. If present, only transactions that are categorized as credit cards are allowed. */
Expand All @@ -86,7 +86,7 @@ export const enum ApplePayMerchantCapability {
}

/** A type that indicates how to ship purchased items. */
export const enum ApplePayShippingType {
export enum ApplePayShippingType {
/** Default. */
Shipping = 'shipping',
Delivery = 'delivery',
Expand Down Expand Up @@ -140,7 +140,7 @@ export type GooglePayPaymentMethodParams = {
};
};

export const enum BillingAddressFormat {
export enum BillingAddressFormat {
/** Collect name, street address, locality, region, country code, and postal code. */
Full = 'FULL',
/** Collect name, country code, and postal code (default). */
Expand All @@ -161,7 +161,7 @@ export type ConfirmParams = {
applePay?: ApplePayBaseParams;
};

export const enum ButtonType {
export enum ButtonType {
/** A button with the Apple Pay or Google Pay logo only, useful when an additional call to action isn't needed. */
Default = 0,
/** A button useful for product purchases. */
Expand Down Expand Up @@ -203,7 +203,7 @@ export const enum ButtonType {
}

/** iOS only. */
export const enum ButtonStyle {
export enum ButtonStyle {
/** A white button with black lettering. */
White = 0,
/** A white button with black lettering and a black outline. */
Expand All @@ -221,7 +221,7 @@ export type CartSummaryItem =
| RecurringCartSummaryItem;

/** iOS only. */
export const enum PaymentType {
export enum PaymentType {
Deferred = 'Deferred',
Immediate = 'Immediate',
Recurring = 'Recurring',
Expand Down Expand Up @@ -261,7 +261,7 @@ export type RecurringCartSummaryItem = {
};

/** iOS only. */
export const enum IntervalUnit {
export enum IntervalUnit {
Minute = 'minute',
Hour = 'hour',
Day = 'day',
Expand Down
10 changes: 8 additions & 2 deletions src/types/PushProvisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type GooglePayCardToken = {
status: GooglePayCardTokenStatus;
};

export const enum GooglePayCardTokenStatus {
export enum GooglePayCardTokenStatus {
/** */
TOKEN_STATE_NEEDS_IDENTITY_VERIFICATION = 'TOKEN_STATE_NEEDS_IDENTITY_VERIFICATION',
/** */
Expand Down Expand Up @@ -43,6 +43,8 @@ export type CanAddCardToWalletParams = {
cardLastFour: string;
/** iOS only. Set this to `true` until shipping through TestFlight || App Store. If true, you must be using live cards, and have the proper iOS entitlement set up. See https://stripe.com/docs/issuing/cards/digital-wallets?platform=react-native#requesting-access-for-ios */
testEnv?: boolean;
/** iOS only. Set this to `true` if: your user has an Apple Watch device currently paired, and you want to check that device for the presence of the specified card. */
hasPairedAppleWatch?: boolean;
};

export type CanAddCardToWalletResult =
Expand All @@ -60,11 +62,15 @@ export type CanAddCardToWalletResult =
error: StripeError<GooglePayError>;
};

export const enum CanAddCardToWalletStatus {
export enum CanAddCardToWalletStatus {
/** You are missing configuration required for Push Provisioning. Make sure you've completed all steps at https://stripe.com/docs/issuing/cards/digital-wallets?platform=react-native. */
MISSING_CONFIGURATION = 'MISSING_CONFIGURATION',
/** This device doesn't support adding a card to the native wallet. */
UNSUPPORTED_DEVICE = 'UNSUPPORTED_DEVICE',
/** This card already exists on this device and any paired devices. */
CARD_ALREADY_EXISTS = 'CARD_ALREADY_EXISTS',
/** This card already exists on this device, but not on the paired device. */
CARD_EXISTS_ON_CURRENT_DEVICE = 'CARD_EXISTS_ON_CURRENT_DEVICE',
/** This card already exists on the paired device, but not on this device. */
CARD_EXISTS_ON_PAIRED_DEVICE = 'CARD_EXISTS_ON_PAIRED_DEVICE',
}
2 changes: 1 addition & 1 deletion wdio.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ exports.config = {
browserName: '',
appiumVersion: '1.22.2',
platformName: 'iOS',
platformVersion: '15.5',
platformVersion: '15.2',
deviceName: 'iPhone 13',
app: 'example/ios/DerivedData/StripeSdkExample/Build/Products/Release-iphonesimulator/StripeSdkExample.app',
automationName: 'XCUITest',
Expand Down