66//
77
88@_spi ( STP) import StripeCore
9+ @_spi ( STP) import StripeFinancialConnectionsLite
910import 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
289396extension 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