diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift index 71dac6bee4b..cc5694d381a 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift @@ -242,6 +242,15 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { case link_pm = "Link PM" case passthrough + case link_card_brand = "Link Card" + + var value: String { + switch self { + case .link_pm: "LINK_PAYMENT_METHOD" + case .passthrough: "PASSTHROUGH" + case .link_card_brand: "LINK_CARD_BRAND" + } + } } enum UserOverrideCountry: String, PickerEnum { diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift index 8e40ecc1f0c..7acc188e973 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift @@ -458,6 +458,7 @@ extension PlaygroundController { "mode": settings.mode.rawValue, "automatic_payment_methods": settings.apmsEnabled == .on, "use_link": settings.linkMode == .link_pm, + "link_mode": settings.linkMode.value, "use_manual_confirmation": settings.integrationType == .deferred_mc, "require_cvc_recollection": settings.requireCVCRecollection == .on, "customer_session_component_name": "mobile_payment_element", diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift index ad54a6168d1..66a91b50880 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift @@ -219,6 +219,14 @@ protocol FinancialConnectionsAPI { bankAccountId: String ) -> Future + // TODO: Figure out return type for this request. + // We'll need to sort out the down-scoped client secret first. + func sharePaymentDetails( + consumerSessionClientSecret: String, + paymentDetailsId: String, + expectedPaymentMethodType: String + ) -> Future + func paymentMethods( consumerSessionClientSecret: String, paymentDetailsId: String @@ -944,6 +952,27 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { ) } + func sharePaymentDetails( + consumerSessionClientSecret: String, + paymentDetailsId: String, + expectedPaymentMethodType: String + ) -> Future { + let parameters: [String: Any] = [ + "request_surface": requestSurface, + "id": paymentDetailsId, + "credentials": [ + "consumer_session_client_secret": consumerSessionClientSecret + ], + "expected_payment_method_type": expectedPaymentMethodType, + "expand": ["payment_method"], + ] + return post( + resource: APIEndpointSharePaymentDetails, + parameters: parameters, + useConsumerPublishableKeyIfNeeded: false + ) + } + func paymentMethods( consumerSessionClientSecret: String, paymentDetailsId: String @@ -995,4 +1024,5 @@ private let APIEndpointPollAccountNumbers = "link_account_sessions/poll_account_ private let APIEndpointLinkAccountsSignUp = "consumers/accounts/sign_up" private let APIEndpointAttachLinkConsumerToLinkAccountSession = "consumers/attach_link_consumer_to_link_account_session" private let APIEndpointPaymentDetails = "consumers/payment_details" +private let APIEndpointSharePaymentDetails = "consumers/payment_details/share" private let APIEndpointPaymentMethods = "payment_methods" diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift index 6066790f65b..5963dee4266 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift @@ -126,6 +126,11 @@ struct FinancialConnectionsSessionManifest: Decodable { !livemode } + var isPantherPayment: Bool { + let isLinkPaymentMethod = paymentMethodType == .link + return isProductInstantDebits && isLinkPaymentMethod + } + init( accountholderCustomerEmailAddress: String? = nil, accountholderIsLinkConsumer: Bool? = nil, diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift index f89be5c9364..dbc2d41eaf7 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift @@ -513,10 +513,18 @@ extension NativeFlowController { } bankAccountDetails = paymentDetails.redactedPaymentDetails.bankAccountDetails - return self.dataManager.createPaymentMethod( - consumerSessionClientSecret: consumerSession.clientSecret, - paymentDetailsId: paymentDetails.redactedPaymentDetails.id - ) + if self.dataManager.manifest.isPantherPayment { + return self.dataManager.apiClient.sharePaymentDetails( + consumerSessionClientSecret: consumerSession.clientSecret, + paymentDetailsId: paymentDetails.redactedPaymentDetails.id, + expectedPaymentMethodType: "card" + ) + } else { + return self.dataManager.createPaymentMethod( + consumerSessionClientSecret: consumerSession.clientSecret, + paymentDetailsId: paymentDetails.redactedPaymentDetails.id + ) + } } .observe { result in switch result { @@ -801,7 +809,7 @@ extension NativeFlowController: ManualEntryViewControllerDelegate { // to the Link signup/save call later in the flow. We don't need them anymore since we know // they've failed us in some way at this point. dataManager.linkedAccounts = nil - + dataManager.paymentAccountResource = paymentAccountResource dataManager.accountNumberLast4 = accountNumberLast4 diff --git a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj index 854cb94fbc3..42ee5533b02 100644 --- a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj +++ b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 47AD56A9889DF5EFBBA9CEFB /* PollingViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADE49E72DD5EDA448D12D88 /* PollingViewTests.swift */; }; 47B19F96CCEA290541E3B988 /* CardSectionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D03000A6807B09BFD8E6CB1 /* CardSectionElement.swift */; }; 48DA2EFE0944E737B0F197B0 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B2AFFAD776D5F21DF837F1BD /* OHHTTPStubs */; }; + 493CA9F12C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */; }; 49803444CD948F1ED28FF021 /* PaymentSheetFormFactory+FormSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7A1EFF100C589FDFF4D516 /* PaymentSheetFormFactory+FormSpec.swift */; }; 49F62EDF394F18E5BB201D53 /* StripePaymentSheet.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AA6166F234C3A2129CBD573 /* StripePaymentSheet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4A1A0A542B824C830A200BE0 /* StubbedBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBF8498CCD12A5190F9267CD /* StubbedBackend.swift */; }; @@ -426,6 +427,7 @@ 45B6DC9BD9183495E5649369 /* LinkAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAccountService.swift; sourceTree = ""; }; 47C5DB8C01BA7137369C8B4D /* TextFieldElement+Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextFieldElement+Card.swift"; sourceTree = ""; }; 492B254E43F3BB9F9CEAEA06 /* PaymentSheetLoaderStubbedTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetLoaderStubbedTest.swift; sourceTree = ""; }; + 493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConsumerSession+PaymentMethodType.swift"; sourceTree = ""; }; 4BEFE8C0CFEAE73F9FD736D3 /* STPStringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStringUtils.swift; sourceTree = ""; }; 4C6AA41454A6757B3E26AE67 /* StripePaymentSheetTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripePaymentSheetTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4D595AA033BC84CB4E1C277F /* PaymentSheetFormFactorySnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetFormFactorySnapshotTest.swift; sourceTree = ""; }; @@ -941,6 +943,7 @@ F1E614E8481658A027599A92 /* STPAPIClient+Link.swift */, B662953D2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift */, 441C3414745D483C9A47ED0B /* VerificationSession.swift */, + 493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */, ); path = Link; sourceTree = ""; @@ -1788,6 +1791,7 @@ 229A4A578609A3711F02682E /* STPCardBrandChoice.swift in Sources */, 3EDFACA133567159875143C5 /* STPElementsSession.swift in Sources */, 1BFC617EED154D32BFCADAE7 /* SeparatorLabel.swift in Sources */, + 493CA9F12C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift in Sources */, 01D46644D87983FC4387B92C /* InstantDebitsOnlyFinancialConnectionsAuthManager.swift in Sources */, 367BB57FA826A82EEF074A70 /* PayWithLinkWebController.swift in Sources */, F3A34AD1CC2CBB899738C9D7 /* LinkInlineSignupElement.swift in Sources */, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift b/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift index 8fb136865da..2b897066a04 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift @@ -166,7 +166,12 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol { } } - func sharePaymentDetails(id: String, cvc: String?, completion: @escaping (Result) -> Void) { + func sharePaymentDetails( + id: String, + cvc: String?, + paymentMethodType: ConsumerSession.PaymentMethodType?, + completion: @escaping (Result) -> Void + ) { guard let session = currentSession else { assertionFailure() return completion( @@ -182,6 +187,7 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol { id: id, cvc: cvc, consumerAccountPublishableKey: publishableKey, + paymentMethodType: paymentMethodType, completion: completionWrapper ) } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession+PaymentMethodType.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession+PaymentMethodType.swift new file mode 100644 index 00000000000..74dcfdf02b7 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession+PaymentMethodType.swift @@ -0,0 +1,15 @@ +// +// ConsumerSession+PaymentMethodType.swift +// StripePaymentSheet +// +// Created by Mat Schmid on 2024-08-27. +// + +import Foundation + +extension ConsumerSession { + enum PaymentMethodType: String { + case card = "CARD" + case bankAccount = "BANK_ACCOUNT" + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession.swift index ee6fb7f5a3d..e6bab3a78c0 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession.swift @@ -129,6 +129,7 @@ extension ConsumerSession { id: String, cvc: String?, consumerAccountPublishableKey: String?, + paymentMethodType: PaymentMethodType?, completion: @escaping (Result) -> Void ) { apiClient.sharePaymentDetails( @@ -136,6 +137,7 @@ extension ConsumerSession { id: id, consumerAccountPublishableKey: consumerAccountPublishableKey, cvc: cvc, + paymentMethodType: paymentMethodType?.rawValue, completion: completion) } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetails.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetails.swift index 20897763728..a88f8dd2252 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetails.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetails.swift @@ -19,18 +19,28 @@ typealias ConsumerSessionWithPaymentDetails = (session: ConsumerSession, payment For internal SDK use only */ final class ConsumerPaymentDetails: Decodable { + enum PaymentDetailsType: String, Decodable { + case card = "CARD" + case bankAccount = "BANK_ACCOUNT" + case invalid = "PAYMENT_DETAILS_TYPE_INVALID" + } + let stripeID: String + let paymentDetailsType: PaymentDetailsType - init(stripeID: String) { + init(stripeID: String, paymentDetailsType: PaymentDetailsType) { self.stripeID = stripeID + self.paymentDetailsType = paymentDetailsType } private enum CodingKeys: String, CodingKey { case stripeID = "id" + case paymentDetailsType = "type" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.stripeID = try container.decode(String.self, forKey: .stripeID) + self.paymentDetailsType = try container.decode(PaymentDetailsType.self, forKey: .paymentDetailsType) } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift index 3aa434462ab..0e12b2f694a 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift @@ -200,6 +200,7 @@ extension STPAPIClient { id: String, consumerAccountPublishableKey: String?, cvc: String?, + paymentMethodType: String?, completion: @escaping (Result) -> Void ) { let endpoint: String = "consumers/payment_details/share" @@ -215,6 +216,10 @@ extension STPAPIClient { parameters["payment_method_options"] = ["card": ["cvc": cvc]] } + if let paymentMethodType { + parameters["expected_payment_method_type"] = paymentMethodType + } + APIRequest.post( with: self, endpoint: endpoint, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/Intent+Link.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/Intent+Link.swift index d3bbf88d194..85909fc3a63 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/Intent+Link.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/Intent+Link.swift @@ -26,6 +26,10 @@ extension STPElementsSession { linkSettings?.fundingSources } + var linkMode: LinkSettings.LinkMode? { + linkSettings?.linkMode + } + var disableLinkSignup: Bool { linkSettings?.disableSignup ?? false } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift index 4cc9578d2f8..02363f27af7 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift @@ -69,6 +69,9 @@ class IntentConfirmParams { case .instantDebits: let params = STPPaymentMethodParams(type: .link) self.init(params: params, type: type) + case .linkCardBrand: + let params = STPPaymentMethodParams(type: .card) + self.init(params: params, type: type) } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift index 5944eff8699..36404737474 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift @@ -16,7 +16,10 @@ extension PaymentSheet { enum PaymentMethodType: Equatable, Hashable { case stripe(STPPaymentMethodType) case external(ExternalPaymentMethod) + + // Synthetic payment methods: case instantDebits + case linkCardBrand static var analyticLogForIcon: Set = [] static let analyticLogForIconSemaphore = DispatchSemaphore(value: 1) @@ -27,7 +30,7 @@ extension PaymentSheet { return paymentMethodType.displayName case .external(let externalPaymentMethod): return externalPaymentMethod.label - case .instantDebits: + case .instantDebits, .linkCardBrand: return String.Localized.bank } } @@ -42,6 +45,8 @@ extension PaymentSheet { return externalPaymentMethod.type case .instantDebits: return "instant_debits" + case .linkCardBrand: + return "link_card_bank" } } @@ -103,7 +108,7 @@ extension PaymentSheet { } return DownloadManager.sharedManager.imagePlaceHolder() } - case .instantDebits: + case .instantDebits, .linkCardBrand: return Image.pm_type_us_bank.makeImage(overrideUserInterfaceStyle: forDarkBackground ? .dark : .light) } } @@ -114,7 +119,7 @@ extension PaymentSheet { return stpPaymentMethodType.iconRequiresTinting case .external: return false - case .instantDebits: + case .instantDebits, .linkCardBrand: return true } } @@ -173,6 +178,19 @@ extension PaymentSheet { if availabilityStatus == .supported { recommendedPaymentMethodTypes.append(.instantDebits) } + } else if + elementsSession.linkFundingSources?.contains(.bankAccount) == true, + !elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount), + elementsSession.linkSettings?.linkMode == .linkCardBrand + { + let availabilityStatus = configurationSatisfiesRequirements( + requirements: [.financialConnectionsSDK], + configuration: configuration, + intent: intent + ) + if availabilityStatus == .supported { + recommendedPaymentMethodTypes.append(.linkCardBrand) + } } if let merchantPaymentMethodOrder = configuration.paymentMethodOrder?.map({ $0.lowercased() }) { @@ -196,8 +214,10 @@ extension PaymentSheet { } // 3. Append the remaining PMs in recommendedPaymentMethodTypes reorderedPaymentMethodTypes.append(contentsOf: recommendedPaymentMethodTypes) + print("**** reorderedPaymentMethodTypes", reorderedPaymentMethodTypes) return reorderedPaymentMethodTypes } else { + print("**** recommendedPaymentMethodTypes", recommendedPaymentMethodTypes) return recommendedPaymentMethodTypes } } @@ -237,9 +257,9 @@ extension PaymentSheet { case .bacsDebit: return [.returnURL, .userSupportsDelayedPaymentMethods] case .cardPresent, .blik, .weChatPay, .grabPay, .FPX, .giropay, .przelewy24, .EPS, - .netBanking, .OXXO, .afterpayClearpay, .UPI, .link, .affirm, .paynow, .zip, .alma, - .mobilePay, .unknown, .alipay, .konbini, .promptPay, .swish, .twint, .multibanco, - .sunbit, .billie, .satispay: + .netBanking, .OXXO, .afterpayClearpay, .UPI, .link, .affirm, .paynow, + .zip, .alma, .mobilePay, .unknown, .alipay, .konbini, .promptPay, .swish, .twint, + .multibanco, .sunbit, .billie, .satispay: return [.unsupportedForSetup] @unknown default: return [.unsupportedForSetup] diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift index a1db37b5304..2df558fda2d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift @@ -410,8 +410,16 @@ extension PaymentSheet { switch result { case .success(let paymentDetails): if elementsSession.linkPassthroughModeEnabled { + let paymentMethodType = Self.getExpectedPaymentMethodType( + paymentMethodType: paymentDetails.paymentDetailsType, + linkMode: elementsSession.linkMode + ) // If passthrough mode, share payment details - linkAccount.sharePaymentDetails(id: paymentDetails.stripeID, cvc: paymentMethodParams.card?.cvc) { result in + linkAccount.sharePaymentDetails( + id: paymentDetails.stripeID, + cvc: paymentMethodParams.card?.cvc, + paymentMethodType: paymentMethodType + ) { result in switch result { case .success(let paymentDetailsShareResponse): confirmWithPaymentMethod(paymentDetailsShareResponse.paymentMethod, linkAccount, shouldSave) @@ -633,6 +641,18 @@ extension PaymentSheet { params.returnURL = configuration.returnURL return params } + + static func getExpectedPaymentMethodType( + paymentMethodType: ConsumerPaymentDetails.PaymentDetailsType?, + linkMode: LinkSettings.LinkMode? + ) -> ConsumerSession.PaymentMethodType? { + switch paymentMethodType { + case .card: .card + case .bankAccount: linkMode == .linkCardBrand ? .card : .bankAccount + case .invalid: nil + case .none: nil + } + } } /// A helper method to compare shipping details diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift index 3bac4088ed0..806a591d815 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift @@ -133,7 +133,7 @@ class PaymentSheetFormFactory { func make() -> PaymentMethodElement { switch paymentMethod { - case .instantDebits: + case .instantDebits, .linkCardBrand: return makeInstantDebits() case .external: return makeExternalPaymentMethodForm() diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/InstantDebitsPaymentMethodElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/InstantDebitsPaymentMethodElement.swift index b36f0fe81cc..8501f59dcc8 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/InstantDebitsPaymentMethodElement.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/InstantDebitsPaymentMethodElement.swift @@ -181,14 +181,13 @@ extension InstantDebitsPaymentMethodElement: PaymentMethodElement { // after a bank is linked, this gets hit to update func updateParams(params: IntentConfirmParams) -> IntentConfirmParams? { - if - let updatedParams = formElement.updateParams(params: params), - let linkedBank - { - updatedParams.instantDebitsLinkedBank = linkedBank - return updatedParams + guard let updatedParams = formElement.updateParams(params: params) else { + return nil } - return nil + + guard let linkedBank else { return nil } + updatedParams.instantDebitsLinkedBank = linkedBank + return updatedParams } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentMethodFormViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentMethodFormViewController.swift index 06a9c30e865..23538bd70c4 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentMethodFormViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentMethodFormViewController.swift @@ -215,7 +215,7 @@ extension PaymentMethodFormViewController { } else { return true } - } else if paymentMethodType == .instantDebits { + } else if paymentMethodType == .instantDebits || paymentMethodType == .linkCardBrand { // only override buy button (show "Continue") IF we don't have a linked bank return instantDebitsFormElement?.getLinkedBank() == nil } @@ -225,11 +225,12 @@ extension PaymentMethodFormViewController { var overridePrimaryButtonState: OverridePrimaryButtonState? { guard shouldOverridePrimaryButton else { return nil } let isEnabled: Bool = { - if paymentMethodType == .stripe(.USBankAccount) && usBankAccountFormElement?.canLinkAccount ?? false { - true - } else if paymentMethodType == .instantDebits && instantDebitsFormElement?.enableCTA ?? false { - true - } else { + switch paymentMethodType { + case .stripe(let paymentMethod): + paymentMethod == .USBankAccount && (usBankAccountFormElement?.canLinkAccount ?? false) + case .instantDebits, .linkCardBrand: + instantDebitsFormElement?.enableCTA ?? false + default: false } }() @@ -255,6 +256,8 @@ extension PaymentMethodFormViewController { handleCollectBankAccount(from: viewController) case .instantDebits: handleCollectInstantDebits(from: viewController) + case .linkCardBrand: + handleCollectInstantDebits(from: viewController) default: return } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift index 9bee755eeb8..effbd35b398 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift @@ -37,7 +37,7 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo return .new(confirmParams: params) case .external(let type): return .external(paymentMethod: type, billingDetails: params.paymentMethodParams.nonnil_billingDetails) - case .instantDebits: + case .instantDebits, .linkCardBrand: return .new(confirmParams: params) } case .saved(paymentMethod: let paymentMethod): diff --git a/StripePayments/StripePayments/Source/API Bindings/Models/Shared/LinkSettings.swift b/StripePayments/StripePayments/Source/API Bindings/Models/Shared/LinkSettings.swift index d9aa199eda6..9f98bd845ad 100644 --- a/StripePayments/StripePayments/Source/API Bindings/Models/Shared/LinkSettings.swift +++ b/StripePayments/StripePayments/Source/API Bindings/Models/Shared/LinkSettings.swift @@ -21,10 +21,17 @@ import Foundation case ephemeral } + @_spi(STP) @frozen public enum LinkMode: String { + case linkPaymentMethod = "LINK_PAYMENT_METHOD" + case passthrough = "PASSTHROUGH" + case linkCardBrand = "LINK_CARD_BRAND" + } + @_spi(STP) public let fundingSources: Set @_spi(STP) public let popupWebviewOption: PopupWebviewOption? @_spi(STP) public let passthroughModeEnabled: Bool? @_spi(STP) public let disableSignup: Bool? + @_spi(STP) public let linkMode: LinkMode? @_spi(STP) public let linkFlags: [String: Bool]? @_spi(STP) public let allResponseFields: [AnyHashable: Any] @@ -34,6 +41,7 @@ import Foundation popupWebviewOption: PopupWebviewOption?, passthroughModeEnabled: Bool?, disableSignup: Bool?, + linkMode: LinkMode?, linkFlags: [String: Bool]?, allResponseFields: [AnyHashable: Any] ) { @@ -41,6 +49,7 @@ import Foundation self.popupWebviewOption = popupWebviewOption self.passthroughModeEnabled = passthroughModeEnabled self.disableSignup = disableSignup + self.linkMode = linkMode self.linkFlags = linkFlags self.allResponseFields = allResponseFields } @@ -61,6 +70,7 @@ import Foundation let webviewOption = PopupWebviewOption(rawValue: response["link_popup_webview_option"] as? String ?? "") let passthroughModeEnabled = response["link_passthrough_mode_enabled"] as? Bool ?? false let disableSignup = response["link_mobile_disable_signup"] as? Bool ?? false + let linkMode = (response["link_mode"] as? String).flatMap { LinkMode(rawValue: $0) } // Collect the flags for the URL generator let linkFlags = response.reduce(into: [String: Bool]()) { partialResult, element in @@ -74,6 +84,7 @@ import Foundation popupWebviewOption: webviewOption, passthroughModeEnabled: passthroughModeEnabled, disableSignup: disableSignup, + linkMode: linkMode, linkFlags: linkFlags, allResponseFields: response ) as? Self