Skip to content

Commit

Permalink
fix: check for existence of pass on paired watch (#1219)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliecruzan-stripe authored Dec 2, 2022
1 parent 9a1a163 commit de9727e
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 49 deletions.
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

0 comments on commit de9727e

Please sign in to comment.