From 3b8e1f6f983c662ef027ed8bbb7976195d3ce3b8 Mon Sep 17 00:00:00 2001 From: Mat Schmid Date: Wed, 25 Sep 2024 12:07:09 -0400 Subject: [PATCH] [BANKCON-14524] Allow pay by bank when linkCardBrand criteria is met & dedupe instant debits (#4021) ## Summary Per the [Panther project plan](https://docs.google.com/document/d/1ErJVA3lLvNspPe3A8uYP9feK8Yvfq-Sw5aatNIvlGxk/edit?usp=sharing), this adds Link Card Brand as a payment method when the following criteria is met: - `link_funding_sources` contains `BANK_ACCOUNT` - `US_BANK_ACCOUNT` is not an available payment method. - `link_mode` is `LINK_CARD_BRAND` This also makes sure that both Instant Debits and Link Card Brand won't both be shown at the same time, since they appear as identical payment methods to a user (same name, icon, and elements form). ## Motivation Building Panther support! ## Testing Added a unit test, and manually verified the new payment method shows up when the conditions are met: https://github.com/user-attachments/assets/28a1f8d6-abab-4143-ae1f-0a468340718d ## Changelog N/a --- .../PaymentSheetUITest.swift | 2 +- .../PaymentSheet/PaymentMethodType.swift | 37 ++++++++++++++++--- .../PaymentSheetPaymentMethodTypeTest.swift | 27 ++++++++++++-- .../STPFixtures+PaymentSheet.swift | 20 +++++++++- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 1172f110583..70de67d5b14 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -196,7 +196,7 @@ class PaymentSheetStandardUITests: PaymentSheetUITestCase { // `mc_load_succeeded` event `selected_lpm` should be "apple_pay", the default payment method. XCTAssertEqual(analyticsLog[2][string: "selected_lpm"], "apple_pay") app.buttons["+ Add"].waitForExistenceAndTap() - XCTAssertTrue(app.staticTexts["Add a card"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["Card information"].waitForExistence(timeout: 2)) // Should fire the `mc_form_shown` event w/ `selected_lpm` = card XCTAssertEqual(analyticsLog.last?[string: "event"], "mc_form_shown") diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift index fdd474ceabb..51a3a354576 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift @@ -165,12 +165,29 @@ extension PaymentSheet { // External Payment Methods + elementsSession.externalPaymentMethods.map { PaymentMethodType.external($0) } - if - elementsSession.orderedPaymentMethodTypes.contains(.link), - !elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount), - !intent.isDeferredIntent, + // We should manually add Instant Debits as a payment method when: + // - Link is an available payment method. + // - US Bank Account is *not* an available payment method. + // - Not a deferred intent flow. + // - Link Funding Sources contains Bank Account. + var eligibleForInstantDebits: Bool { + elementsSession.orderedPaymentMethodTypes.contains(.link) && + !elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount) && + !intent.isDeferredIntent && elementsSession.linkFundingSources?.contains(.bankAccount) == true - { + } + + // We should manually add Link Card Brand as a payment method when: + // - Link Funding Sources contains Bank Account. + // - US Bank Account is *not* an available payment method. + // - Link Card Brand is the Link Mode + var eligibleForLinkCardBrand: Bool { + elementsSession.linkFundingSources?.contains(.bankAccount) == true && + !elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount) && + elementsSession.linkSettings?.linkMode == .linkCardBrand + } + + if eligibleForInstantDebits { let availabilityStatus = configurationSatisfiesRequirements( requirements: [.financialConnectionsSDK], configuration: configuration, @@ -179,6 +196,16 @@ extension PaymentSheet { if availabilityStatus == .supported { recommendedPaymentMethodTypes.append(.instantDebits) } + // Else if here so we don't show both Instant Debits and Link Card Brand together. + } else if eligibleForLinkCardBrand { + let availabilityStatus = configurationSatisfiesRequirements( + requirements: [.financialConnectionsSDK], + configuration: configuration, + intent: intent + ) + if availabilityStatus == .supported { + recommendedPaymentMethodTypes.append(.linkCardBrand) + } } if let merchantPaymentMethodOrder = configuration.paymentMethodOrder?.map({ $0.lowercased() }) { diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetPaymentMethodTypeTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetPaymentMethodTypeTest.swift index dd2ba7ed42e..77ab4523042 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetPaymentMethodTypeTest.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetPaymentMethodTypeTest.swift @@ -340,6 +340,8 @@ class PaymentSheetPaymentMethodTypeTest: XCTestCase { XCTAssertEqual(elementsSession.orderedPaymentMethodTypes, [.klarna, .card]) } + // MARK: - Payment Method Types + func testPaymentIntentFilteredPaymentMethodTypes() { let intent = Intent._testPaymentIntent(paymentMethodTypes: [.card, .klarna, .przelewy24]) var configuration = PaymentSheet.Configuration() @@ -407,6 +409,23 @@ class PaymentSheetPaymentMethodTypeTest: XCTestCase { XCTAssertEqual(types, [.stripe(.card)]) } + func testPaymentMethodTypesLinkCardBrand() { + let intent = Intent._testPaymentIntent(paymentMethodTypes: [.card]) + let configuration = PaymentSheet.Configuration() + let types = PaymentSheet.PaymentMethodType.filteredPaymentMethodTypes( + from: intent, + elementsSession: ._testValue( + intent: intent, + linkMode: .linkCardBrand, + linkFundingSources: [.card, .bankAccount] + ), + configuration: configuration + ) + XCTAssertEqual(types, [.stripe(.card), .linkCardBrand]) + } + + // MARK: Other + func testUnknownPMTypeIsUnsupported() { let setupIntent = STPFixtures.makeSetupIntent(paymentMethodTypes: [.unknown]) let paymentMethod = STPPaymentMethod.type(from: "luxe_bucks") @@ -518,7 +537,7 @@ extension STPFixtures { captureMethod: String = "automatic", confirmationMethod: String = "automatic", shippingProvided: Bool = false, - paymentMethodJson: [String:Any]? = nil + paymentMethodJson: [String: Any]? = nil ) -> STPPaymentIntent { var json = STPTestUtils.jsonNamed(STPTestJSONPaymentIntent)! if let setupFutureUsage = setupFutureUsage { @@ -539,7 +558,7 @@ extension STPFixtures { } if let paymentMethodJson = paymentMethodJson { json["payment_method"] = paymentMethodJson - + } if let paymentMethodOptions = paymentMethodOptions { json["payment_method_options"] = paymentMethodOptions.dictionaryValue @@ -550,7 +569,7 @@ extension STPFixtures { static func makeSetupIntent( paymentMethodTypes: [STPPaymentMethodType] = [.card], usage: String = "off_session", - paymentMethodJson: [String:Any]? = nil + paymentMethodJson: [String: Any]? = nil ) -> STPSetupIntent { var json = STPTestUtils.jsonNamed(STPTestJSONSetupIntent)! json["usage"] = usage @@ -559,7 +578,7 @@ extension STPFixtures { } if let paymentMethodJson = paymentMethodJson { json["payment_method"] = paymentMethodJson - + } return STPSetupIntent.decodedObject(fromAPIResponse: json)! } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift index 15079f05c27..9dd6cb1bb38 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift @@ -37,6 +37,8 @@ extension STPElementsSession { customerSessionData: [String: Any]? = nil, cardBrandChoiceData: [String: Any]? = nil, isLinkPassthroughModeEnabled: Bool? = nil, + linkMode: LinkSettings.LinkMode? = nil, + linkFundingSources: Set = [], disableLinkSignup: Bool? = nil ) -> STPElementsSession { var json = STPTestUtils.jsonNamed("ElementsSession")! @@ -70,6 +72,12 @@ extension STPElementsSession { json[jsonDict: "link_settings"]!["link_passthrough_mode_enabled"] = isLinkPassthroughModeEnabled } + if let linkMode { + json[jsonDict: "link_settings"]!["link_mode"] = linkMode.rawValue + } + + json[jsonDict: "link_settings"]!["link_funding_sources"] = linkFundingSources.map(\.rawValue) + if let disableLinkSignup { json[jsonDict: "link_settings"]!["link_mobile_disable_signup"] = disableLinkSignup } @@ -78,7 +86,11 @@ extension STPElementsSession { return elementsSession } - static func _testValue(intent: Intent) -> STPElementsSession { + static func _testValue( + intent: Intent, + linkMode: LinkSettings.LinkMode? = nil, + linkFundingSources: Set = [] + ) -> STPElementsSession { let paymentMethodTypes: [String] = { switch intent { case .paymentIntent(let paymentIntent): @@ -89,7 +101,11 @@ extension STPElementsSession { return intentConfig.paymentMethodTypes ?? [] } }() - return STPElementsSession._testValue(paymentMethodTypes: paymentMethodTypes) + return STPElementsSession._testValue( + paymentMethodTypes: paymentMethodTypes, + linkMode: linkMode, + linkFundingSources: linkFundingSources + ) } }