From 8c6dd20fb2e72354b7dd5969a6383dabed704cdb Mon Sep 17 00:00:00 2001 From: Till Hellmund Date: Mon, 3 Feb 2025 17:18:59 -0500 Subject: [PATCH] Fixes and testing for merchant-initiated repair flows (#4522) ## Summary This pull request includes fixes for merchant-initiated repair flows in Financial Connections. We also now allow testing these flows from the playground by entering a customer and authorization ID. ## Motivation ## Testing ## Changelog --- .../Playground/PlaygroundConfiguration.swift | 30 +++++++++++++++++++ .../Playground/PlaygroundView.swift | 11 +++++++ .../Playground/PlaygroundViewModel.swift | 24 +++++++++++++++ .../Source/Native/NativeFlowController.swift | 18 +++++++++-- .../PartnerAuthViewController.swift | 19 +++++++++++- 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundConfiguration.swift b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundConfiguration.swift index 20712001c58..b3ff8d78345 100644 --- a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundConfiguration.swift +++ b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundConfiguration.swift @@ -371,6 +371,36 @@ final class PlaygroundConfiguration { configurationStore[Self.phoneKey] = newValue } } + + // MARK: - Relink Authorization + + private static let customerIdKey = "customer_id" + var customerId: String { + get { + if let customerId = configurationStore[Self.customerIdKey] as? String { + return customerId + } else { + return "" + } + } + set { + configurationStore[Self.customerIdKey] = newValue + } + } + + private static let relinkAuthorizationKey = "relink_authorization" + var relinkAuthorization: String { + get { + if let relinkAuthorization = configurationStore[Self.relinkAuthorizationKey] as? String { + return relinkAuthorization + } else { + return "" + } + } + set { + configurationStore[Self.relinkAuthorizationKey] = newValue + } + } // MARK: - Permissions diff --git a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundView.swift b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundView.swift index 6bcec5fd308..0659de679ef 100644 --- a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundView.swift +++ b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundView.swift @@ -114,6 +114,17 @@ struct PlaygroundView: View { .accessibility(identifier: "playground-phone") } } + + Section(header: Text("Relink")) { + TextField("Customer ID (cus_)", text: viewModel.customerId) + .keyboardType(.default) + .autocapitalization(.none) + .accessibility(identifier: "playground-customer-id") + + TextField("Relink authorization (fcauth_)", text: viewModel.relinkAuthorization) + .keyboardType(.default) + .accessibility(identifier: "playground-relink-authorization") + } Section(header: Text("PERMISSIONS")) { Toggle("Balances", isOn: viewModel.balancesPermission) diff --git a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundViewModel.swift b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundViewModel.swift index 662d9473043..688ad961033 100644 --- a/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundViewModel.swift +++ b/Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundViewModel.swift @@ -155,6 +155,30 @@ final class PlaygroundViewModel: ObservableObject { } ) } + + var customerId: Binding { + Binding( + get: { + self.playgroundConfiguration.customerId + }, + set: { + self.playgroundConfiguration.customerId = $0 + self.objectWillChange.send() + } + ) + } + + var relinkAuthorization: Binding { + Binding( + get: { + self.playgroundConfiguration.relinkAuthorization + }, + set: { + self.playgroundConfiguration.relinkAuthorization = $0 + self.objectWillChange.send() + } + ) + } var balancesPermission: Binding { Binding( diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift index 2c7f60814ec..5d27e4300d4 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift @@ -179,7 +179,10 @@ extension NativeFlowController { // // keeping this logic in `pushPane` is helpful because we want to // reuse `skipSuccessPane` and `manualEntryMode == .custom` logic - clearNavigationStack: Bool = false + clearNavigationStack: Bool = false, + // Useful for cases where we want to prevent the current pane from being shown again, + // but not affect any previous panes. + removeCurrent: Bool = false ) { if pane == .success && dataManager.manifest.skipSuccessPane == true { closeAuthFlow(error: nil) @@ -192,8 +195,11 @@ extension NativeFlowController { nativeFlowController: self, dataManager: dataManager ) - if clearNavigationStack, let paneViewController = paneViewController { + if clearNavigationStack, let paneViewController { setNavigationControllerViewControllers([paneViewController], animated: animated) + } else if removeCurrent, let paneViewController { + let viewControllers = Array(navigationController.viewControllers.dropLast()) + setNavigationControllerViewControllers(viewControllers + [paneViewController], animated: animated) } else { pushViewController(paneViewController, animated: animated) } @@ -829,6 +835,14 @@ extension NativeFlowController: PartnerAuthViewControllerDelegate { showErrorPane(forError: error, referrerPane: .partnerAuth) } + + func partnerAuthViewController( + _ viewController: PartnerAuthViewController, + didRequestNextPane nextPane: FinancialConnectionsSessionManifest.NextPane + ) { + dataManager.authSession = nil // clear any lingering auth sessions + pushPane(nextPane, animated: true, removeCurrent: true) + } } // MARK: - AccountPickerViewControllerDelegate diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthViewController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthViewController.swift index 4b5c6c03cc9..a89fb24fbda 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthViewController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthViewController.swift @@ -25,6 +25,10 @@ protocol PartnerAuthViewControllerDelegate: AnyObject { _ viewController: PartnerAuthViewController, didReceiveError error: Error ) + func partnerAuthViewController( + _ viewController: PartnerAuthViewController, + didRequestNextPane nextPane: FinancialConnectionsSessionManifest.NextPane + ) } final class PartnerAuthViewController: SheetViewController { @@ -157,7 +161,20 @@ final class PartnerAuthViewController: SheetViewController { }, didSelectCancel: { [weak self] in guard let self = self else { return } - self.delegate?.partnerAuthViewControllerDidRequestToGoBack(self) + let isModal = panePresentationStyle == .sheet + + self.dataSource.analyticsClient.log( + eventName: isModal ? "click.prepane.cancel" : "click.prepane.choose_another_bank", + pane: .partnerAuth + ) + + self.dataSource.cancelPendingAuthSessionIfNeeded() + + if isModal { + self.delegate?.partnerAuthViewControllerDidRequestToGoBack(self) + } else { + self.delegate?.partnerAuthViewController(self, didRequestNextPane: .institutionPicker) + } } ) self.prepaneViews = prepaneViews