Skip to content

Commit 17cfc75

Browse files
committed
Use FC Lite as fallback flow in FC SDK
1 parent 097b046 commit 17cfc75

12 files changed

Lines changed: 546 additions & 641 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ let package = Package(
175175
),
176176
.target(
177177
name: "StripeFinancialConnections",
178-
dependencies: ["StripeCore", "StripeUICore"],
178+
dependencies: ["StripeCore", "StripeUICore", "StripeFinancialConnectionsLite"],
179179
path: "StripeFinancialConnections/StripeFinancialConnections",
180180
exclude: ["Info.plist"],
181181
resources: [

StripeFinancialConnections.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Pod::Spec.new do |s|
1919
s.weak_framework = 'SwiftUI'
2020
s.dependency 'StripeCore', "#{s.version}"
2121
s.dependency 'StripeUICore', "#{s.version}"
22+
s.dependency 'StripeFinancialConnectionsLite', "#{s.version}"
2223
s.source_files = 'StripeFinancialConnections/StripeFinancialConnections/**/*.swift'
2324
s.ios.resource_bundle = { 'StripeFinancialConnectionsBundle' => ['StripeFinancialConnections/StripeFinancialConnections/Resources/**/*.{lproj,png}', 'StripeFinancialConnections/StripeFinancialConnections/PrivacyInfo.xcprivacy'] }
2425
end

StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
9CE29EA549C4BFA447AB82E0 /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C650E82A4195A7566AA54298 /* StripeCore.framework */; };
1515
D0C6D94867FA04B1BF80D56D /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9AB787FE87EDD702B1BBF09 /* StripeCoreTestUtils.framework */; };
1616
D50E771043434AD80EA28628 /* StripeUICore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6652EBE38C47B36962AD370A /* StripeUICore.framework */; };
17+
FC11FEDA000000000000AA02 /* StripeFinancialConnectionsLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC11FEDA000000000000AA01 /* StripeFinancialConnectionsLite.framework */; };
18+
FC11FEDA000000000000AA03 /* StripeFinancialConnectionsLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC11FEDA000000000000AA01 /* StripeFinancialConnectionsLite.framework */; };
1719
/* End PBXBuildFile section */
1820

1921
/* Begin PBXContainerItemProxy section */
@@ -62,6 +64,7 @@
6264
CFEEBA73EBBCE02A50B2DB7A /* StripeFinancialConnectionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeFinancialConnectionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6365
E37D8CE9CD73443A9AAF2AE8 /* StripeFinancialConnections.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeFinancialConnections.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6466
F9AB787FE87EDD702B1BBF09 /* StripeCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; };
67+
FC11FEDA000000000000AA01 /* StripeFinancialConnectionsLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeFinancialConnectionsLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6568
/* End PBXFileReference section */
6669

6770
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -99,6 +102,7 @@
99102
D0C6D94867FA04B1BF80D56D /* StripeCoreTestUtils.framework in Frameworks */,
100103
864C5159C62C562C655B53F7 /* StripeFinancialConnections.framework in Frameworks */,
101104
971E6F5E78BC3265CD80D0C6 /* StripeUICore.framework in Frameworks */,
105+
FC11FEDA000000000000AA03 /* StripeFinancialConnectionsLite.framework in Frameworks */,
102106
);
103107
runOnlyForDeploymentPostprocessing = 0;
104108
};
@@ -108,6 +112,7 @@
108112
files = (
109113
06445472B3008395FCA92FEC /* StripeCore.framework in Frameworks */,
110114
D50E771043434AD80EA28628 /* StripeUICore.framework in Frameworks */,
115+
FC11FEDA000000000000AA02 /* StripeFinancialConnectionsLite.framework in Frameworks */,
111116
);
112117
runOnlyForDeploymentPostprocessing = 0;
113118
};
@@ -163,6 +168,7 @@
163168
E37D8CE9CD73443A9AAF2AE8 /* StripeFinancialConnections.framework */,
164169
CFEEBA73EBBCE02A50B2DB7A /* StripeFinancialConnectionsTests.xctest */,
165170
6652EBE38C47B36962AD370A /* StripeUICore.framework */,
171+
FC11FEDA000000000000AA01 /* StripeFinancialConnectionsLite.framework */,
166172
);
167173
name = Products;
168174
sourceTree = "<group>";
@@ -277,6 +283,8 @@
277283
"zh-Hans",
278284
"zh-Hant",
279285
Base,
286+
"nn-NO",
287+
th,
280288
);
281289
mainGroup = 7879CBA341D7E807714A831B;
282290
packageReferences = (

StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostController.swift

Lines changed: 147 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
@_spi(STP) import StripeCore
9+
@_spi(STP) import StripeFinancialConnectionsLite
910
import UIKit
1011

1112
/// An internal result type that helps us handle both
@@ -100,6 +101,7 @@ class HostController {
100101
private let analyticsClientV1: STPAnalyticsClientProtocol
101102

102103
private var nativeFlowController: NativeFlowController?
104+
private var financialConnectionsLite: FinancialConnectionsLite?
103105
private var linkAccountSessionId: String?
104106
lazy var hostViewController = HostViewController(
105107
analyticsClientV1: analyticsClientV1,
@@ -190,7 +192,7 @@ extension HostController: HostViewControllerDelegate {
190192

191193
switch flow {
192194
case .webInstantDebits, .webFinancialConnections:
193-
continueWithWebFlow(synchronizePayload.manifest)
195+
continueWithFCLite(synchronizePayload.manifest)
194196
case .nativeInstantDebits, .nativeFinancialConnections:
195197
continueWithNativeFlow(synchronizePayload)
196198
}
@@ -206,36 +208,166 @@ extension HostController: HostViewControllerDelegate {
206208

207209
// MARK: - Helpers
208210

209-
private extension HostController {
211+
extension HostController {
210212

211-
func continueWithWebFlow(_ manifest: FinancialConnectionsSessionManifest, prefillDetails: WebPrefillDetails? = nil) {
213+
private func continueWithFCLite(
214+
_ manifest: FinancialConnectionsSessionManifest,
215+
prefillDetails: WebPrefillDetails? = nil
216+
) {
212217
delegate?.hostController(
213218
self,
214219
didReceiveEvent: FinancialConnectionsEvent(
215220
name: .flowLaunchedInBrowser
216221
)
217222
)
218223

224+
let financialConnectionsLite = FinancialConnectionsLite(
225+
clientSecret: clientSecret,
226+
returnUrl: returnURL.flatMap(URL.init(string:)),
227+
apiClient: apiClient.backingAPIClient
228+
)
229+
financialConnectionsLite.elementsSessionContext = elementsSessionContext
230+
financialConnectionsLite.consumerPublishableKey = apiClient.consumerPublishableKey
231+
financialConnectionsLite.prefillDetails = prefillDetails
232+
self.financialConnectionsLite = financialConnectionsLite
233+
234+
let presentingViewController = navigationController.topViewController ?? navigationController
235+
financialConnectionsLite.present(from: presentingViewController) { [weak self] result in
236+
guard let self else { return }
237+
self.handleFCLiteResult(result, manifest: manifest)
238+
}
239+
}
240+
241+
/// Adapts FC Lite's `FinancialConnectionsSDKResult` into the `HostControllerResult` expected by
242+
/// the rest of the SDK. FC Lite only returns a lightweight linked bank for a successful Financial
243+
/// Connections result, so we re-fetch the full session here to satisfy the host surface.
244+
func handleFCLiteResult(
245+
_ result: FinancialConnectionsSDKResult,
246+
manifest: FinancialConnectionsSessionManifest
247+
) {
248+
switch result {
249+
case .completed(let completed):
250+
handleFCLiteCompletion(completed, manifest: manifest)
251+
case .cancelled:
252+
handleFCLiteCancellation(manifest: manifest)
253+
case .failed(let error):
254+
notifyDelegateOfFailureEvents(error: error)
255+
finishFromFCLite(.failed(error: error))
256+
@unknown default:
257+
let error = FinancialConnectionsSheetError.unknown(
258+
debugDescription: "Unhandled FinancialConnectionsSDKResult case"
259+
)
260+
notifyDelegateOfFailureEvents(error: error)
261+
finishFromFCLite(.failed(error: error))
262+
}
263+
}
264+
265+
private func handleFCLiteCompletion(
266+
_ completed: FinancialConnectionsSDKResult.Completed,
267+
manifest: FinancialConnectionsSessionManifest
268+
) {
269+
switch completed {
270+
case .financialConnections:
271+
// FC Lite only returns a lightweight linked bank, so re-fetch the full session.
272+
fetchSession { [weak self] fetchResult in
273+
guard let self else { return }
274+
switch fetchResult {
275+
case .success(let session):
276+
self.notifyDelegateOfSuccessEvent(session: session)
277+
self.finishFromFCLite(
278+
.completed(.financialConnections(session)).updateWith(manifest)
279+
)
280+
case .failure(let error):
281+
self.notifyDelegateOfFailureEvents(error: error)
282+
self.finishFromFCLite(.failed(error: error))
283+
}
284+
}
285+
case .instantDebits(let linkedBank):
286+
notifyDelegateOfSuccessEvent(session: nil)
287+
finishFromFCLite(.completed(.instantDebits(linkedBank)))
288+
case .linkedAccount(let id):
289+
notifyDelegateOfSuccessEvent(session: nil)
290+
finishFromFCLite(.completed(.linkedAccount(id: id)))
291+
@unknown default:
292+
let error = FinancialConnectionsSheetError.unknown(
293+
debugDescription: "Unhandled FinancialConnectionsSDKResult.Completed case"
294+
)
295+
notifyDelegateOfFailureEvents(error: error)
296+
finishFromFCLite(.failed(error: error))
297+
}
298+
}
299+
300+
/// FC Lite reports a plain cancellation, but the web fallback used to convert a
301+
/// custom-manual-entry cancellation into a dedicated error. To preserve that behavior we
302+
/// re-fetch the session on cancel (for Financial Connections only) and check its status.
303+
func handleFCLiteCancellation(manifest: FinancialConnectionsSessionManifest) {
304+
guard !manifest.isProductInstantDebits else {
305+
notifyDelegateOfCancelEvent()
306+
finishFromFCLite(.canceled)
307+
return
308+
}
309+
310+
fetchSession { [weak self] fetchResult in
311+
guard let self else { return }
312+
if case .success(let session) = fetchResult,
313+
session.status == .cancelled,
314+
session.statusDetails?.cancelled?.reason == .customManualEntry {
315+
self.finishFromFCLite(.failed(error: FinancialConnectionsCustomManualEntryRequiredError()))
316+
} else {
317+
self.notifyDelegateOfCancelEvent()
318+
self.finishFromFCLite(.canceled)
319+
}
320+
}
321+
}
322+
323+
private func fetchSession(
324+
completion: @escaping (Result<StripeAPI.FinancialConnectionsSession, Error>) -> Void
325+
) {
219326
let accountFetcher = FinancialConnectionsAccountAPIFetcher(api: apiClient, clientSecret: clientSecret)
220327
let sessionFetcher = FinancialConnectionsSessionAPIFetcher(
221328
api: apiClient,
222329
clientSecret: clientSecret,
223330
accountFetcher: accountFetcher
224331
)
225-
let webFlowViewController = FinancialConnectionsWebFlowViewController(
226-
clientSecret: clientSecret,
227-
apiClient: apiClient,
228-
manifest: manifest,
229-
sessionFetcher: sessionFetcher,
230-
returnURL: returnURL,
231-
elementsSessionContext: elementsSessionContext,
232-
prefillDetailsOverride: prefillDetails
332+
sessionFetcher.fetchSession().observe { result in
333+
completion(result)
334+
}
335+
}
336+
337+
private func finishFromFCLite(_ result: HostControllerResult) {
338+
let linkAccountSessionId = result.linkAccountSessionId ?? self.linkAccountSessionId
339+
let viewController = navigationController.topViewController ?? navigationController
340+
delegate?.hostController(
341+
self,
342+
viewController: viewController,
343+
didFinish: result,
344+
linkAccountSessionId: linkAccountSessionId
233345
)
234-
webFlowViewController.delegate = self
235-
navigationController.setViewControllers([webFlowViewController], animated: true)
236346
}
237347

238-
func continueWithNativeFlow(_ synchronizePayload: FinancialConnectionsSynchronize) {
348+
private func notifyDelegateOfSuccessEvent(session: StripeAPI.FinancialConnectionsSession?) {
349+
delegate?.hostController(
350+
self,
351+
didReceiveEvent: FinancialConnectionsEvent(
352+
name: .success,
353+
metadata: FinancialConnectionsEvent.Metadata(
354+
manualEntry: session?.paymentAccount?.isManualEntry ?? false
355+
)
356+
)
357+
)
358+
}
359+
360+
private func notifyDelegateOfCancelEvent() {
361+
delegate?.hostController(self, didReceiveEvent: FinancialConnectionsEvent(name: .cancel))
362+
}
363+
364+
private func notifyDelegateOfFailureEvents(error: Error) {
365+
FinancialConnectionsEvent
366+
.events(fromError: error)
367+
.forEach { delegate?.hostController(self, didReceiveEvent: $0) }
368+
}
369+
370+
private func continueWithNativeFlow(_ synchronizePayload: FinancialConnectionsSynchronize) {
239371
navigationController.configureAppearanceForNative()
240372

241373
let dataManager = NativeFlowAPIDataManager(
@@ -259,31 +391,6 @@ private extension HostController {
259391
}
260392
}
261393

262-
// MARK: - ConnectionsWebFlowViewControllerDelegate
263-
264-
extension HostController: FinancialConnectionsWebFlowViewControllerDelegate {
265-
266-
func webFlowViewController(
267-
_ viewController: FinancialConnectionsWebFlowViewController,
268-
didFinish result: HostControllerResult
269-
) {
270-
let linkAccountSessionId = result.linkAccountSessionId ?? linkAccountSessionId
271-
delegate?.hostController(
272-
self,
273-
viewController: viewController,
274-
didFinish: result,
275-
linkAccountSessionId: linkAccountSessionId
276-
)
277-
}
278-
279-
func webFlowViewController(
280-
_ webFlowViewController: UIViewController,
281-
didReceiveEvent event: FinancialConnectionsEvent
282-
) {
283-
delegate?.hostController(self, didReceiveEvent: event)
284-
}
285-
}
286-
287394
// MARK: - NativeFlowControllerDelegate
288395

289396
extension HostController: NativeFlowControllerDelegate {
@@ -316,7 +423,7 @@ extension HostController: NativeFlowControllerDelegate {
316423
shouldLaunchWebFlow manifest: FinancialConnectionsSessionManifest,
317424
prefillDetails: WebPrefillDetails
318425
) {
319-
continueWithWebFlow(manifest, prefillDetails: prefillDetails)
426+
continueWithFCLite(manifest, prefillDetails: prefillDetails)
320427
}
321428
}
322429

0 commit comments

Comments
 (0)