From e564e9eb78eb812f535f7778fb849a96f474204b Mon Sep 17 00:00:00 2001 From: Till Hellmund Date: Tue, 17 Dec 2024 18:05:06 +0100 Subject: [PATCH] Fetch available incentives after linking bank account --- .../InstantDebitsLinkedBank.swift | 5 +- .../project.pbxproj | 4 ++ .../FinancialConnectionsAPIClient.swift | 20 ++++++++ .../Models/AvailableIncentives.swift | 26 ++++++++++ .../Source/Native/NativeFlowController.swift | 50 +++++++++++++++++-- ...cialConnectionsWebFlowViewController.swift | 4 +- .../PaymentSheetAnalyticsHelperTest.swift | 6 ++- 7 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/AvailableIncentives.swift diff --git a/StripeCore/StripeCore/Source/Connections Bindings/InstantDebitsLinkedBank.swift b/StripeCore/StripeCore/Source/Connections Bindings/InstantDebitsLinkedBank.swift index e879b8016d4..f57a501c6c7 100644 --- a/StripeCore/StripeCore/Source/Connections Bindings/InstantDebitsLinkedBank.swift +++ b/StripeCore/StripeCore/Source/Connections Bindings/InstantDebitsLinkedBank.swift @@ -12,16 +12,19 @@ import Foundation public let bankName: String? public let last4: String? public let linkMode: LinkMode? + public let incentiveEligible: Bool public init( paymentMethod: LinkBankPaymentMethod, bankName: String?, last4: String?, - linkMode: LinkMode? + linkMode: LinkMode?, + incentiveEligible: Bool ) { self.paymentMethod = paymentMethod self.bankName = bankName self.last4 = last4 self.linkMode = linkMode + self.incentiveEligible = incentiveEligible } } diff --git a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj index d4d00ff12a8..a91fc4da8c8 100644 --- a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj +++ b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj @@ -193,6 +193,7 @@ CB734C25A19D38A87876FB2B /* FinancialConnectionsAnalyticsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AB8A480620B5C3567F453C /* FinancialConnectionsAnalyticsTest.swift */; }; CBEAB081DD7353928F485071 /* APIPollingHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 710183EE587F6FDA077FC150 /* APIPollingHelperTests.swift */; }; CBF7BE2271D309F2B1E794CC /* FinancialConnectionsDataAccessNotice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32ED8A7E94822F14AD94A698 /* FinancialConnectionsDataAccessNotice.swift */; }; + CBF7BE602D11DA5E00A4C172 /* AvailableIncentives.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF7BE5F2D11DA5E00A4C172 /* AvailableIncentives.swift */; }; CF47070B2A4CA27FEE9AE5FA /* generic_error@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6A764CF4DB5B5F6F488132A8 /* generic_error@3x.png */; }; D0C1EF46A418A8F8774B7418 /* FinancialConnectionsSession_both_accounts_la.json in Resources */ = {isa = PBXBuildFile; fileRef = F6CF7F1005B57D566E139DE3 /* FinancialConnectionsSession_both_accounts_la.json */; }; D0C6D94867FA04B1BF80D56D /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9AB787FE87EDD702B1BBF09 /* StripeCoreTestUtils.framework */; }; @@ -483,6 +484,7 @@ C93F7139E9BFB044902962D0 /* FinancialConnectionsImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsImage.swift; sourceTree = ""; }; CA2DA47ECE153F888FA675CE /* StripeiOS Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Debug.xcconfig"; sourceTree = ""; }; CB3C49A180D1697B03C79A59 /* UIViewController+KeyboardAvoiding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+KeyboardAvoiding.swift"; sourceTree = ""; }; + CBF7BE5F2D11DA5E00A4C172 /* AvailableIncentives.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableIncentives.swift; sourceTree = ""; }; CDD861E4EB8BA294545B7651 /* NetworkingLinkVerificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingLinkVerificationViewController.swift; sourceTree = ""; }; CE10909F3FC7D60E13B65226 /* et-EE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "et-EE"; path = "et-EE.lproj/Localizable.strings"; sourceTree = ""; }; CEC1BC95816DAD5AE9680662 /* FinancialConnectionsAccountFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAccountFetcher.swift; sourceTree = ""; }; @@ -745,6 +747,7 @@ isa = PBXGroup; children = ( 32249762D11692D5B34BBF38 /* ConsumerSession */, + CBF7BE5F2D11DA5E00A4C172 /* AvailableIncentives.swift */, D890BD770F4E33D23ABA37EA /* BankAccountToken.swift */, 359BF8ACFB35A16EBD96C4F0 /* FinancialConnectionsAccount.swift */, 5C837C27C2577391B91FF0E5 /* FinancialConnectionsAuthSession.swift */, @@ -1337,6 +1340,7 @@ C59DBA5A86A3331113D6ED7E /* LoadingView.swift in Sources */, 9B2CAE99344C26D524EDCF26 /* ModalPresentationWrapperViewController.swift in Sources */, 6ABFE5522B72BE630037437C /* PrepaneViews.swift in Sources */, + CBF7BE602D11DA5E00A4C172 /* AvailableIncentives.swift in Sources */, FE268512851E63E4E111DECD /* FinancialConnectionsSDKImplementation.swift in Sources */, E85DCFCA61299EF27B3201CF /* FinancialConnectionsSheet.swift in Sources */, F22DE4B785D51B318A1A3D08 /* FinancialConnectionsSheetError.swift in Sources */, diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift index 92330a12275..0862e0c616b 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift @@ -1108,6 +1108,25 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { ) } } + + func updateAvailableIncentives( + consumerSessionClientSecret: String, + sessionID: String + ) -> Future { + let parameters: [String: Any] = [ + "request_surface": requestSurface, + "credentials": [ + "consumer_session_client_secret": consumerSessionClientSecret + ], + "session_id": sessionID, + ] + + return post( + resource: APIEndpointAvailableIncentives, + parameters: parameters, + useConsumerPublishableKeyIfNeeded: true + ) + } } private let APIEndpointListAccounts = "link_account_sessions/list_accounts" @@ -1142,3 +1161,4 @@ private let APIEndpointAttachLinkConsumerToLinkAccountSession = "consumers/attac private let APIEndpointPaymentDetails = "consumers/payment_details" private let APIEndpointSharePaymentDetails = "consumers/payment_details/share" private let APIEndpointPaymentMethods = "payment_methods" +private let APIEndpointAvailableIncentives = "consumers/incentives/update_available" diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/AvailableIncentives.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/AvailableIncentives.swift new file mode 100644 index 00000000000..998fbbfc909 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/AvailableIncentives.swift @@ -0,0 +1,26 @@ +// +// AvailableIncentives.swift +// StripeFinancialConnections +// +// Created by Till Hellmund on 12/17/24. +// + +import Foundation + +struct AvailableIncentives: Decodable { + public let hasAny: Bool + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let data = try container.decode([LinkConsumerIncentive].self, forKey: .data) + hasAny = !data.isEmpty + } + + enum CodingKeys: String, CodingKey { + case data + } + + // We don't care about the incentives, we just need to know that there are + // *any* incentives. + private struct LinkConsumerIncentive: Decodable {} +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift index 09b16aabd05..a7628ec2b22 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift @@ -273,6 +273,11 @@ extension NativeFlowController { // MARK: - Other Helpers extension NativeFlowController { + + private struct PaymentMethodWithIncentiveEligibility { + let paymentMethod: LinkBankPaymentMethod + let incentiveEligible: Bool + } private func didSelectAnotherBank() { if dataManager.manifest.disableLinkMoreAccounts { @@ -544,14 +549,53 @@ extension NativeFlowController { ) } } + .chained { [weak self] paymentMethod -> Future in + guard let self else { + return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "data source deallocated")) + } + + let promise = Promise() + + if let incentiveEligibilitySession = elementsSessionContext?.incentiveEligibilitySession { + self.dataManager.apiClient.updateAvailableIncentives( + consumerSessionClientSecret: consumerSession.clientSecret, + sessionID: incentiveEligibilitySession.id + ).observe { result in + switch result { + case .success(let availableIncentives): + let result = PaymentMethodWithIncentiveEligibility( + paymentMethod: paymentMethod, + incentiveEligible: availableIncentives.hasAny + ) + promise.fullfill(with: .success(result)) + case .failure: + // TODO: Log error + let result = PaymentMethodWithIncentiveEligibility( + paymentMethod: paymentMethod, + incentiveEligible: false + ) + promise.fullfill(with: .success(result)) + } + } + } else { + let result = PaymentMethodWithIncentiveEligibility( + paymentMethod: paymentMethod, + incentiveEligible: false + ) + promise.fullfill(with: .success(result)) + } + + return promise + } .observe { result in switch result { - case .success(let paymentMethod): + case .success(let paymentMethodWithIncentiveEligibility): let linkedBank = InstantDebitsLinkedBank( - paymentMethod: paymentMethod, + paymentMethod: paymentMethodWithIncentiveEligibility.paymentMethod, bankName: bankAccountDetails?.bankName, last4: bankAccountDetails?.last4, - linkMode: linkMode + linkMode: linkMode, + incentiveEligible: paymentMethodWithIncentiveEligibility.incentiveEligible ) completion(.success(linkedBank)) case .failure(let error): diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsWebFlowViewController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsWebFlowViewController.swift index 02de354f894..4eea2e66555 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsWebFlowViewController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsWebFlowViewController.swift @@ -177,7 +177,9 @@ extension FinancialConnectionsWebFlowViewController { // backend can return "+" instead of a more-common encoding of "%20" for spaces .replacingOccurrences(of: "+", with: " "), last4: returnUrl.extractValue(forKey: "last4"), - linkMode: elementsSessionContext?.linkMode + linkMode: elementsSessionContext?.linkMode, + // TODO: Parse this from the return URL + incentiveEligible: false ) self.notifyDelegateOfSuccess(result: .instantDebits(instantDebitsLinkedBank)) } else { diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAnalyticsHelperTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAnalyticsHelperTest.swift index ae4d20921d3..39c1b7e50f9 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAnalyticsHelperTest.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAnalyticsHelperTest.swift @@ -323,13 +323,15 @@ final class PaymentSheetAnalyticsHelperTest: XCTestCase { paymentMethod: LinkBankPaymentMethod(id: "paymentMethodId"), bankName: nil, last4: nil, - linkMode: .linkPaymentMethod + linkMode: .linkPaymentMethod, + incentiveEligible: false ) let linkCardBrandLinkedBank = InstantDebitsLinkedBank( paymentMethod: LinkBankPaymentMethod(id: "paymentMethodId"), bankName: nil, last4: nil, - linkMode: .linkCardBrand + linkMode: .linkCardBrand, + incentiveEligible: false ) let instantDebitConfirmParams = IntentConfirmParams(type: .instantDebits)