From ee379ec71f992d2fca2fa6913dcf077366bb8634 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Wed, 30 Oct 2024 14:38:29 -0700 Subject: [PATCH 1/8] added fingerprint check validation --- .../PaymentSheetDeferredValidator.swift | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift index 1f1c1480ed2..f27df9528f9 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift @@ -8,6 +8,7 @@ import Foundation import StripePayments @_spi(STP) import StripeCore +@_spi(STP) import StripePayments struct PaymentSheetDeferredValidator { /// Note: We don't validate amount (for any payment method) because there are use cases where the amount can change slightly between PM collection and confirmation. @@ -36,7 +37,7 @@ struct PaymentSheetDeferredValidator { throw PaymentSheetError.deferredIntentValidationFailed(message: "Your PaymentIntent confirmationMethod (\(paymentIntent.confirmationMethod)) can only be used with PaymentSheet.FlowController.") } } - + static func validate(setupIntent: STPSetupIntent, intentConfiguration: PaymentSheet.IntentConfiguration, paymentMethod: STPPaymentMethod) throws { @@ -48,12 +49,41 @@ struct PaymentSheetDeferredValidator { } try validatePaymentMethodId(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod) } - + static func validatePaymentMethodId(intentPaymentMethod: STPPaymentMethod?, paymentMethod: STPPaymentMethod) throws { guard let intentPaymentMethod = intentPaymentMethod else { return } guard intentPaymentMethod.stripeId == paymentMethod.stripeId else { + if intentPaymentMethod.type == paymentMethod.type { + switch paymentMethod.type.identifier { + case "card": + try validateFingerprint(intentFingerprint: intentPaymentMethod.card?.fingerprint, fingerprint: paymentMethod.card?.fingerprint) + return + case "us_bank_account": + try validateFingerprint(intentFingerprint: intentPaymentMethod.usBankAccount?.fingerprint, fingerprint: paymentMethod.usBankAccount?.fingerprint) + return + default: + break + } + } let errorMessage = """ \nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(paymentMethod.stripeId). + + To resolve this issue, you can: + 1. Create a new Intent each time before you call the `confirmHandler`, or + 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. + """ + let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodIdMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage)) + STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic) + throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage) + } + } + + static func validateFingerprint(intentFingerprint: String?, fingerprint: String?) throws { + guard let intentFingerprint = intentFingerprint else { return } + guard let fingerprint = fingerprint else { return } + guard intentFingerprint == fingerprint else { + let errorMessage = """ + \nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(fingerprint). To resolve this issue, you can: 1. Create a new Intent each time before you call the `confirmHandler`, or @@ -64,6 +94,7 @@ struct PaymentSheetDeferredValidator { throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage) } } + } // MARK: - Validation helpers From cb1ae836e31e5327b910fbb0bf557acb485a5600 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 10:42:07 -0700 Subject: [PATCH 2/8] implemented suggestions --- .../Source/Analytics/STPAnalyticEvent.swift | 1 + .../PaymentSheet/PaymentSheet+DeferredAPI.swift | 4 ++-- .../PaymentSheet/PaymentSheetDeferredValidator.swift | 9 +++++---- .../PaymentSheetDeferredValidatorTests.swift | 12 ++++++------ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift b/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift index e575b4ed618..a7f70cf74f7 100644 --- a/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift +++ b/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift @@ -217,6 +217,7 @@ import Foundation case paymentSheetFormCompleted = "mc_form_completed" case paymentSheetCardNumberCompleted = "mc_card_number_completed" case paymentSheetDeferredIntentPaymentMethodIdMismatch = "mc_deferred_intent_payment_method_id_mismatch" + case paymentSheetDeferredIntentPaymentMethodFingerprintMismatch = "mc_deferred_intent_payment_method_fingerprint_mismatch" // MARK: - v1/elements/session case paymentSheetElementsSessionLoadFailed = "mc_elements_session_load_failed" diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift index d4ebc55269e..30e4daede2d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift @@ -82,7 +82,7 @@ extension PaymentSheet { } } else { // 4b. Server-side confirmation - try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod) + try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod) paymentHandler.handleNextAction( for: paymentIntent, with: authenticationContext, @@ -110,7 +110,7 @@ extension PaymentSheet { } } else { // 4b. Server-side confirmation - try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod) + try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod) paymentHandler.handleNextAction( for: setupIntent, with: authenticationContext, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift index f27df9528f9..c358fa2e077 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift @@ -28,7 +28,7 @@ struct PaymentSheetDeferredValidator { guard paymentIntent.captureMethod == captureMethod else { throw PaymentSheetError.deferredIntentValidationFailed(message: "Your PaymentIntent captureMethod (\(paymentIntent.captureMethod)) does not match the PaymentSheet.IntentConfiguration amount (\(captureMethod)).") } - try validatePaymentMethodId(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod) + try validatePaymentMethod(intentPaymentMethod: paymentIntent.paymentMethod, paymentMethod: paymentMethod) /* Manual confirmation is only available using FlowController because merchants own the final step of confirmation. Showing a successful payment in the complete flow may be misleading when merchants still need to do a final confirmation which could fail e.g., bad network @@ -47,13 +47,14 @@ struct PaymentSheetDeferredValidator { guard setupIntent.usage == setupFutureUsage else { throw PaymentSheetError.deferredIntentValidationFailed(message: "Your SetupIntent usage (\(setupIntent.usage)) does not match the PaymentSheet.IntentConfiguration setupFutureUsage (\(String(describing: setupFutureUsage))).") } - try validatePaymentMethodId(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod) + try validatePaymentMethod(intentPaymentMethod: setupIntent.paymentMethod, paymentMethod: paymentMethod) } - static func validatePaymentMethodId(intentPaymentMethod: STPPaymentMethod?, paymentMethod: STPPaymentMethod) throws { + static func validatePaymentMethod(intentPaymentMethod: STPPaymentMethod?, paymentMethod: STPPaymentMethod) throws { guard let intentPaymentMethod = intentPaymentMethod else { return } guard intentPaymentMethod.stripeId == paymentMethod.stripeId else { if intentPaymentMethod.type == paymentMethod.type { + // Payment methods of type card and us_bank_account can be cloned, leading to mismatched pm ids, but their fingerprints should still match switch paymentMethod.type.identifier { case "card": try validateFingerprint(intentFingerprint: intentPaymentMethod.card?.fingerprint, fingerprint: paymentMethod.card?.fingerprint) @@ -89,7 +90,7 @@ struct PaymentSheetDeferredValidator { 1. Create a new Intent each time before you call the `confirmHandler`, or 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. """ - let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodIdMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage)) + let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodFingerprintMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage)) STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic) throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage) } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift index c1af5c635d6..0721b986a45 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift @@ -79,7 +79,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { paymentMethodJson["id"] = testCard.stripeId let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson) - XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardPi.paymentMethod, + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod, paymentMethod: testCard)) } @@ -92,7 +92,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { guard let intentPaymentMethod = testCardPi.paymentMethod else { return } - XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardPi.paymentMethod, + XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod, paymentMethod: testUSBankAccount)) { error in XCTAssertEqual("\(error)", """ An error occurred in PaymentSheet. \nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(testUSBankAccount.stripeId). @@ -110,7 +110,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { func testPaymentIntentNilPaymentMethod() throws { let testCard = STPPaymentMethod._testCard() let nilPaymentMethodPi = STPFixtures.makePaymentIntent() - XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: nilPaymentMethodPi.paymentMethod, + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: nilPaymentMethodPi.paymentMethod, paymentMethod: testCard)) } @@ -120,7 +120,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { paymentMethodJson["id"] = testCard.stripeId let testCardSi = STPFixtures.makeSetupIntent(paymentMethodJson: paymentMethodJson) - XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardSi.paymentMethod, + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardSi.paymentMethod, paymentMethod: testCard)) } @@ -133,7 +133,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { guard let intentPaymentMethod = testCardSi.paymentMethod else { return } - XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: testCardSi.paymentMethod, + XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardSi.paymentMethod, paymentMethod: testUSBankAccount)) { error in XCTAssertEqual("\(error)", """ An error occurred in PaymentSheet. \nThere is a mismatch between the payment method ID on your Intent: \(intentPaymentMethod.stripeId) and the payment method passed into the `confirmHandler`: \(testUSBankAccount.stripeId). @@ -151,7 +151,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { func testSetupIntentNilPaymentMethod() throws { let testCard = STPPaymentMethod._testCard() let nilPaymentMethodSi = STPFixtures.makeSetupIntent() - XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethodId(intentPaymentMethod: nilPaymentMethodSi.paymentMethod, + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: nilPaymentMethodSi.paymentMethod, paymentMethod: testCard)) } From 67993a277edb58e363a2e5ed563f5a91ccf5e8be Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 11:01:53 -0700 Subject: [PATCH 3/8] consolidated analytic error event --- StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift | 3 +-- .../Source/PaymentSheet/PaymentSheetDeferredValidator.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift b/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift index a7f70cf74f7..0c88c7bd953 100644 --- a/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift +++ b/StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift @@ -216,8 +216,7 @@ import Foundation case paymentSheetFormInteracted = "mc_form_interacted" case paymentSheetFormCompleted = "mc_form_completed" case paymentSheetCardNumberCompleted = "mc_card_number_completed" - case paymentSheetDeferredIntentPaymentMethodIdMismatch = "mc_deferred_intent_payment_method_id_mismatch" - case paymentSheetDeferredIntentPaymentMethodFingerprintMismatch = "mc_deferred_intent_payment_method_fingerprint_mismatch" + case paymentSheetDeferredIntentPaymentMethodMismatch = "mc_deferred_intent_payment_method_mismatch" // MARK: - v1/elements/session case paymentSheetElementsSessionLoadFailed = "mc_elements_session_load_failed" diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift index c358fa2e077..1cee73e0f0a 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift @@ -73,7 +73,7 @@ struct PaymentSheetDeferredValidator { 1. Create a new Intent each time before you call the `confirmHandler`, or 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. """ - let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodIdMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage)) + let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage), additionalNonPIIParams: ["field": "payment method ID"]) STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic) throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage) } @@ -90,7 +90,7 @@ struct PaymentSheetDeferredValidator { 1. Create a new Intent each time before you call the `confirmHandler`, or 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. """ - let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodFingerprintMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage)) + let errorAnalytic = ErrorAnalytic(event: .paymentSheetDeferredIntentPaymentMethodMismatch, error: PaymentSheetError.unknown(debugDescription: errorMessage), additionalNonPIIParams: ["field": "fingerprint"]) STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic) throw PaymentSheetError.deferredIntentValidationFailed(message: errorMessage) } From 7d62f7d518db959387e0cdf82944c073fac98804 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 11:13:55 -0700 Subject: [PATCH 4/8] fix test from refactor --- .../PaymentSheet/PaymentSheetDeferredValidatorTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift index 0721b986a45..9c0832c34b5 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift @@ -103,7 +103,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { """) } let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last - XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodIdMismatch.rawValue) + XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue) XCTAssertNotNil(analyticEvent?["error_code"] as? String) } @@ -144,7 +144,7 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { """) } let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last - XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodIdMismatch.rawValue) + XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue) XCTAssertNotNil(analyticEvent?["error_code"] as? String) } From 991dbe6f797f61173f193eb97c1e5ef8ac68d5f6 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 13:07:49 -0700 Subject: [PATCH 5/8] switch on type --- .../Source/PaymentSheet/PaymentSheetDeferredValidator.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift index 1cee73e0f0a..f1f3f51291a 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift @@ -55,11 +55,11 @@ struct PaymentSheetDeferredValidator { guard intentPaymentMethod.stripeId == paymentMethod.stripeId else { if intentPaymentMethod.type == paymentMethod.type { // Payment methods of type card and us_bank_account can be cloned, leading to mismatched pm ids, but their fingerprints should still match - switch paymentMethod.type.identifier { - case "card": + switch paymentMethod.type { + case .card: try validateFingerprint(intentFingerprint: intentPaymentMethod.card?.fingerprint, fingerprint: paymentMethod.card?.fingerprint) return - case "us_bank_account": + case .USBankAccount: try validateFingerprint(intentFingerprint: intentPaymentMethod.usBankAccount?.fingerprint, fingerprint: paymentMethod.usBankAccount?.fingerprint) return default: From a025009ac83824abc626c510eba4a752a11e42c9 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 13:09:42 -0700 Subject: [PATCH 6/8] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e74bc6dd6..61d920d871e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * [Fixed] Fixed an animation glitch when dismissing PaymentSheet in React Native. * [Fixed] Fixed an issue with FlowController in vertical layout where the payment method could incorrectly be preserved across a call to `update` when it's no longer valid. * [Fixed] Fixed a potential deadlock when `paymentOption` is accessed from Swift concurrency. - +* [Added] Added a check to `PaymentSheetDeferredValidator.validatePaymentMethod` for mismatched card or US bank account fingerprints ## 23.32.0 2024-10-21 ### PaymentSheet From 8216e2f93dc30c84236ea0d83d171ac5cfccf293 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 15:44:32 -0700 Subject: [PATCH 7/8] updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d920d871e..b32594b2509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * [Fixed] Fixed an animation glitch when dismissing PaymentSheet in React Native. * [Fixed] Fixed an issue with FlowController in vertical layout where the payment method could incorrectly be preserved across a call to `update` when it's no longer valid. * [Fixed] Fixed a potential deadlock when `paymentOption` is accessed from Swift concurrency. -* [Added] Added a check to `PaymentSheetDeferredValidator.validatePaymentMethod` for mismatched card or US bank account fingerprints +* [Fixed] Fixed deferred intent validation to handle cloned payment methods ([#4195](https://github.com/stripe/stripe-ios/issues/4195) ## 23.32.0 2024-10-21 ### PaymentSheet From 0cc420d97185ff588bc6183dac4a955e915b15e5 Mon Sep 17 00:00:00 2001 From: Joyce Qin Date: Thu, 31 Oct 2024 20:19:04 -0700 Subject: [PATCH 8/8] added unit tests --- .../PaymentSheetDeferredValidatorTests.swift | 80 +++++++++++++++++++ .../STPFixtures+PaymentSheet.swift | 1 + .../SavedPaymentMethodManagerTest.swift | 22 +++++ 3 files changed, 103 insertions(+) diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift index 9c0832c34b5..7db9571ee9f 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift @@ -107,6 +107,86 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase { XCTAssertNotNil(analyticEvent?["error_code"] as? String) } + func testPaymentIntentMatchedCardFingerprint() throws { + let testCard = STPPaymentMethod._testCard() + var paymentMethodJson = STPPaymentMethod.paymentMethodJson + paymentMethodJson["id"] = "pm_mismatch_id" + paymentMethodJson["card"] = testCard.card + let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson) + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod, + paymentMethod: testCard)) + } + + func testPaymentIntentMismatchedCardFingerprint() throws { + let testCard = STPPaymentMethod._testCard() + var paymentMethodJson = STPPaymentMethod.paymentMethodJson + paymentMethodJson["id"] = "pm_mismatch_id" + paymentMethodJson["card"] = ["fingerprint": "mismatch_fingerprint"] + let testCardPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson) + guard let intentPaymentMethod = testCardPi.paymentMethod else { + return + } + guard let intentPaymentMethodFingerprint = intentPaymentMethod.card?.fingerprint else { + return + } + guard let testCardFingerprint = testCard.card?.fingerprint else { + return + } + XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testCardPi.paymentMethod, + paymentMethod: testCard)) { error in + XCTAssertEqual("\(error)", """ + An error occurred in PaymentSheet. \nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentPaymentMethodFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(testCardFingerprint). + + To resolve this issue, you can: + 1. Create a new Intent each time before you call the `confirmHandler`, or + 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. + """) + } + let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last + XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue) + XCTAssertNotNil(analyticEvent?["error_code"] as? String) + } + + func testPaymentIntentMatchedUSBankAccountFingerprint() throws { + let testUSBankAccount = STPPaymentMethod._testUSBankAccount() + var paymentMethodJson = STPPaymentMethod.usBankAccountJson + paymentMethodJson["id"] = "pm_mismatch_id" + paymentMethodJson["us_bank_account"] = testUSBankAccount.usBankAccount + let testUSBankAccountPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson) + XCTAssertNoThrow(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testUSBankAccountPi.paymentMethod, + paymentMethod: testUSBankAccount)) + } + + func testPaymentIntentMismatchedUSBankAccountFingerprint() throws { + let testUSBankAccount = STPPaymentMethod._testUSBankAccount() + var paymentMethodJson = STPPaymentMethod.usBankAccountJson + paymentMethodJson["id"] = "pm_mismatch_id" + paymentMethodJson["us_bank_account"] = ["fingerprint": "mismatch_fingerprint"] + let testUSBankAccountPi = STPFixtures.makePaymentIntent(paymentMethodJson: paymentMethodJson) + guard let intentPaymentMethod = testUSBankAccountPi.paymentMethod else { + return + } + guard let intentPaymentMethodFingerprint = intentPaymentMethod.card?.fingerprint else { + return + } + guard let testUSBankAccountFingerprint = testUSBankAccount.usBankAccount?.fingerprint else { + return + } + XCTAssertThrowsError(try PaymentSheetDeferredValidator.validatePaymentMethod(intentPaymentMethod: testUSBankAccountPi.paymentMethod, + paymentMethod: testUSBankAccount)) { error in + XCTAssertEqual("\(error)", """ + An error occurred in PaymentSheet. \nThere is a mismatch between the fingerprint of the payment method on your Intent: \(intentPaymentMethodFingerprint) and the fingerprint of the payment method passed into the `confirmHandler`: \(testUSBankAccountFingerprint). + + To resolve this issue, you can: + 1. Create a new Intent each time before you call the `confirmHandler`, or + 2. Update the existing Intent with the desired `paymentMethod` before calling the `confirmHandler`. + """) + } + let analyticEvent = STPAnalyticsClient.sharedClient._testLogHistory.last + XCTAssertEqual(analyticEvent?["event"] as? String, STPAnalyticEvent.paymentSheetDeferredIntentPaymentMethodMismatch.rawValue) + XCTAssertNotNil(analyticEvent?["error_code"] as? String) + } + func testPaymentIntentNilPaymentMethod() throws { let testCard = STPPaymentMethod._testCard() let nilPaymentMethodPi = STPFixtures.makePaymentIntent() diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift index fd15db852e4..c7167d4d47b 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift @@ -159,6 +159,7 @@ extension STPPaymentMethod { "card": [ "last4": "4242", "brand": "visa", + "fingerprint": "B8XXs2y2JsVBtB9f", ], ])! } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentMethodManagerTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentMethodManagerTest.swift index f6f591cdedc..d6699b8b498 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentMethodManagerTest.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentMethodManagerTest.swift @@ -169,9 +169,31 @@ extension STPPaymentMethod { "card": [ "last4": "4242", "brand": "visa", + "fingerprint": "B8XXs2y2JsVBtB9f", ], ] } + + static var usBankAccountJson: [String: Any] { + return [ + "id": "pm_123", + "type": "us_bank_account", + "us_bank_account": [ + "account_holder_type": "individual", + "account_type": "checking", + "bank_name": "STRIPE TEST BANK", + "fingerprint": "ickfX9sbxIyAlbuh", + "last4": "6789", + "networks": [ + "preferred": "ach", + "supported": [ + "ach", + ], + ] as [String: Any], + "routing_number": "110000000", + ] as [String: Any], + ] + } static var paymentMethodsJson: [String: Any] = [ "data": [