Skip to content

Commit 0197a57

Browse files
authored
Add Link warmup pane for deferred intents (#4272)
## Summary <!-- Simple summary of what was changed. --> This pull request improves the returning-user experience for IBP consumers when the flow is opened for deferred intents. With those, we don’t have a populated `accountholderCustomerEmailAddress`, so we need to manually look for a consumer account after the user accepts on the consent pane. It also updates the warmup pane to only lookup a consumer session if there isn’t one yet. ## Motivation <!-- Why are you making this change? If it's for fixing a bug, if possible, please include a code snippet or example project that demonstrates the issue. --> ## Testing <!-- How was the code tested? Be as specific as possible. --> ## Changelog <!-- Is this a notable change that affects users? If so, add a line to `CHANGELOG.md` and prefix the line with one of the following: - [Added] for new features. - [Changed] for changes in existing functionality. - [Deprecated] for soon-to-be removed features. - [Removed] for now removed features. - [Fixed] for any bug fixes. - [Security] in case of vulnerabilities. -->
1 parent 2caa27a commit 0197a57

File tree

5 files changed

+140
-16
lines changed

5 files changed

+140
-16
lines changed

StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentDataSource.swift

+76-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,28 @@ import Foundation
99
@_spi(STP) import StripeCore
1010

1111
protocol ConsentDataSource: AnyObject {
12+
var email: String? { get }
1213
var manifest: FinancialConnectionsSessionManifest { get }
1314
var consent: FinancialConnectionsConsent { get }
1415
var merchantLogo: [String]? { get }
1516
var analyticsClient: FinancialConnectionsAnalyticsClient { get }
1617

17-
func markConsentAcquired() -> Promise<FinancialConnectionsSessionManifest>
18+
func markConsentAcquired() -> Future<ConsentAcquiredResult>
19+
func completeAssertionIfNeeded(
20+
possibleError: Error?,
21+
api: FinancialConnectionsAPIClientLogger.API
22+
) -> Error?
23+
}
24+
25+
struct ConsentAcquiredResult {
26+
var manifest: FinancialConnectionsSessionManifest
27+
var consumerSession: ConsumerSessionData?
28+
var consumerPublishableKey: String?
29+
30+
var nextPane: FinancialConnectionsSessionManifest.NextPane {
31+
// If we have a consumer session, then provide the returning-user experience
32+
consumerSession != nil ? .networkingLinkLoginWarmup : manifest.nextPane
33+
}
1834
}
1935

2036
final class ConsentDataSourceImplementation: ConsentDataSource {
@@ -25,24 +41,80 @@ final class ConsentDataSourceImplementation: ConsentDataSource {
2541
private let apiClient: any FinancialConnectionsAPI
2642
private let clientSecret: String
2743
let analyticsClient: FinancialConnectionsAnalyticsClient
44+
private let elementsSessionContext: ElementsSessionContext?
45+
46+
var email: String? {
47+
elementsSessionContext?.prefillDetails?.email
48+
}
2849

2950
init(
3051
manifest: FinancialConnectionsSessionManifest,
3152
consent: FinancialConnectionsConsent,
3253
merchantLogo: [String]?,
3354
apiClient: any FinancialConnectionsAPI,
3455
clientSecret: String,
35-
analyticsClient: FinancialConnectionsAnalyticsClient
56+
analyticsClient: FinancialConnectionsAnalyticsClient,
57+
elementsSessionContext: ElementsSessionContext?
3658
) {
3759
self.manifest = manifest
3860
self.consent = consent
3961
self.merchantLogo = merchantLogo
4062
self.apiClient = apiClient
4163
self.clientSecret = clientSecret
4264
self.analyticsClient = analyticsClient
65+
self.elementsSessionContext = elementsSessionContext
66+
}
67+
68+
func markConsentAcquired() -> Future<ConsentAcquiredResult> {
69+
return apiClient.markConsentAcquired(clientSecret: clientSecret).chained { [weak self] manifest in
70+
guard let self, manifest.shouldLookupConsumerSession, let email else {
71+
let result = ConsentAcquiredResult(manifest: manifest)
72+
return Promise(value: result)
73+
}
74+
75+
let promise = Promise<ConsentAcquiredResult>()
76+
77+
apiClient.consumerSessionLookup(
78+
emailAddress: email,
79+
clientSecret: clientSecret,
80+
sessionId: manifest.id,
81+
emailSource: .customerObject,
82+
useMobileEndpoints: manifest.verified,
83+
pane: .consent
84+
).observe { lookupResult in
85+
switch lookupResult {
86+
case .success(let response):
87+
let result = ConsentAcquiredResult(
88+
manifest: manifest,
89+
consumerSession: response.consumerSession,
90+
consumerPublishableKey: response.publishableKey
91+
)
92+
promise.resolve(with: result)
93+
case .failure:
94+
let result = ConsentAcquiredResult(manifest: manifest)
95+
promise.resolve(with: result)
96+
}
97+
}
98+
99+
return promise
100+
}
43101
}
44102

45-
func markConsentAcquired() -> Promise<FinancialConnectionsSessionManifest> {
46-
return apiClient.markConsentAcquired(clientSecret: clientSecret)
103+
func completeAssertionIfNeeded(
104+
possibleError: Error?,
105+
api: FinancialConnectionsAPIClientLogger.API
106+
) -> Error? {
107+
guard manifest.verified else { return nil }
108+
return apiClient.completeAssertion(
109+
possibleError: possibleError,
110+
api: api,
111+
pane: .linkLogin
112+
)
113+
}
114+
}
115+
116+
private extension FinancialConnectionsSessionManifest {
117+
var shouldLookupConsumerSession: Bool {
118+
isLinkWithStripe == true && accountholderCustomerEmailAddress == nil
47119
}
48120
}

StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentViewController.swift

+17-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ protocol ConsentViewControllerDelegate: AnyObject {
1919
)
2020
func consentViewController(
2121
_ viewController: ConsentViewController,
22-
didConsentWithManifest manifest: FinancialConnectionsSessionManifest
22+
didConsentWithResult result: ConsentAcquiredResult
23+
)
24+
func consentViewControllerDidFailAttestationVerdict(
25+
_ viewController: ConsentViewController,
26+
prefillDetails: WebPrefillDetails
2327
)
2428
}
2529

@@ -146,9 +150,19 @@ class ConsentViewController: UIViewController {
146150
.observe(on: .main) { [weak self] result in
147151
guard let self = self else { return }
148152
switch result {
149-
case .success(let manifest):
150-
self.delegate?.consentViewController(self, didConsentWithManifest: manifest)
153+
case .success(let result):
154+
self.delegate?.consentViewController(self, didConsentWithResult: result)
151155
case .failure(let error):
156+
let attestationError = self.dataSource.completeAssertionIfNeeded(
157+
possibleError: error,
158+
api: .consumerSessionLookup
159+
)
160+
161+
if attestationError != nil {
162+
let prefillDetails = WebPrefillDetails(email: dataSource.email)
163+
self.delegate?.consentViewControllerDidFailAttestationVerdict(self, prefillDetails: prefillDetails)
164+
}
165+
152166
// we display no errors on failure
153167
self.dataSource
154168
.analyticsClient

StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift

+25-6
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,18 @@ extension NativeFlowController: ConsentViewControllerDelegate {
696696

697697
func consentViewController(
698698
_ viewController: ConsentViewController,
699-
didConsentWithManifest manifest: FinancialConnectionsSessionManifest
699+
didConsentWithResult result: ConsentAcquiredResult
700700
) {
701701
delegate?.nativeFlowController(
702702
self,
703703
didReceiveEvent: FinancialConnectionsEvent(name: .consentAcquired)
704704
)
705705

706-
dataManager.manifest = manifest
706+
dataManager.manifest = result.manifest
707+
dataManager.consumerSession = result.consumerSession
708+
dataManager.consumerPublishableKey = result.consumerPublishableKey
707709

708-
let nextPane = manifest.nextPane
710+
let nextPane = result.nextPane
709711
if nextPane == .networkingLinkLoginWarmup {
710712
presentPaneAsSheet(nextPane)
711713
} else {
@@ -727,6 +729,17 @@ extension NativeFlowController: ConsentViewControllerDelegate {
727729
pushPane(nextPane, parameters: parameters, animated: true)
728730
}
729731
}
732+
733+
func consentViewControllerDidFailAttestationVerdict(
734+
_ viewController: ConsentViewController,
735+
prefillDetails: WebPrefillDetails
736+
) {
737+
delegate?.nativeFlowController(
738+
self,
739+
shouldLaunchWebFlow: dataManager.manifest,
740+
prefillDetails: prefillDetails
741+
)
742+
}
730743
}
731744

732745
// MARK: - InstitutionPickerViewControllerDelegate
@@ -1030,13 +1043,18 @@ extension NativeFlowController: NetworkingLinkSignupViewControllerDelegate {
10301043

10311044
extension NativeFlowController: NetworkingLinkLoginWarmupViewControllerDelegate {
10321045

1033-
func networkingLinkLoginWarmupViewControllerDidSelectContinue(
1046+
func networkingLinkLoginWarmupViewControllerDidFindConsumerSession(
10341047
_ viewController: NetworkingLinkLoginWarmupViewController,
1035-
withSession consumerSession: ConsumerSessionData,
1048+
consumerSession: ConsumerSessionData,
10361049
consumerPublishableKey: String
10371050
) {
10381051
dataManager.consumerSession = consumerSession
10391052
dataManager.consumerPublishableKey = consumerPublishableKey
1053+
}
1054+
1055+
func networkingLinkLoginWarmupViewControllerDidSelectContinue(
1056+
_ viewController: NetworkingLinkLoginWarmupViewController
1057+
) {
10401058
pushPane(.networkingLinkVerification, animated: true)
10411059
}
10421060

@@ -1417,7 +1435,8 @@ private func CreatePaneViewController(
14171435
merchantLogo: dataManager.merchantLogo,
14181436
apiClient: dataManager.apiClient,
14191437
clientSecret: dataManager.clientSecret,
1420-
analyticsClient: dataManager.analyticsClient
1438+
analyticsClient: dataManager.analyticsClient,
1439+
elementsSessionContext: dataManager.elementsSessionContext
14211440
)
14221441
let consentViewController = ConsentViewController(dataSource: consentDataSource)
14231442
consentViewController.delegate = nativeFlowController

StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkLoginWarmup/NetworkingLinkLoginWarmupDataSource.swift

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ protocol NetworkingLinkLoginWarmupDataSource: AnyObject {
1212
var manifest: FinancialConnectionsSessionManifest { get }
1313
var analyticsClient: FinancialConnectionsAnalyticsClient { get }
1414
var email: String? { get }
15+
var hasConsumerSession: Bool { get }
1516

1617
func lookupConsumerSession() -> Future<LookupConsumerSessionResponse>
1718
func disableNetworking() -> Future<FinancialConnectionsSessionManifest>
@@ -34,6 +35,10 @@ final class NetworkingLinkLoginWarmupDataSourceImplementation: NetworkingLinkLog
3435
manifest.accountholderCustomerEmailAddress ?? elementsSessionContext?.prefillDetails?.email
3536
}
3637

38+
var hasConsumerSession: Bool {
39+
apiClient.consumerSession != nil && apiClient.consumerPublishableKey != nil
40+
}
41+
3742
init(
3843
manifest: FinancialConnectionsSessionManifest,
3944
apiClient: any FinancialConnectionsAPI,

StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkLoginWarmup/NetworkingLinkLoginWarmupViewController.swift

+17-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ typealias NetworkingLinkLoginWarmupFooterView = (footerView: UIView?, primaryBut
1414

1515
protocol NetworkingLinkLoginWarmupViewControllerDelegate: AnyObject {
1616
func networkingLinkLoginWarmupViewControllerDidSelectContinue(
17+
_ viewController: NetworkingLinkLoginWarmupViewController
18+
)
19+
func networkingLinkLoginWarmupViewControllerDidFindConsumerSession(
1720
_ viewController: NetworkingLinkLoginWarmupViewController,
18-
withSession consumerSession: ConsumerSessionData,
21+
consumerSession: ConsumerSessionData,
1922
consumerPublishableKey: String
2023
)
2124
func networkingLinkLoginWarmupViewControllerDidSelectCancel(
@@ -118,6 +121,16 @@ final class NetworkingLinkLoginWarmupViewController: SheetViewController {
118121
pane: .networkingLinkLoginWarmup
119122
)
120123

124+
if dataSource.hasConsumerSession {
125+
// We already have a consumer session, so let's us this one directly
126+
delegate?.networkingLinkLoginWarmupViewControllerDidSelectContinue(self)
127+
} else {
128+
// Otherwise, look it up so that we have it for the next pane
129+
lookupConsumerSessionAndContinue()
130+
}
131+
}
132+
133+
private func lookupConsumerSessionAndContinue() {
121134
warmupFooterView.primaryButton?.isLoading = true
122135

123136
dataSource
@@ -141,11 +154,12 @@ final class NetworkingLinkLoginWarmupViewController: SheetViewController {
141154
switch result {
142155
case .success(let response):
143156
if let consumerSession = response.consumerSession, let publishableKey = response.publishableKey {
144-
self.delegate?.networkingLinkLoginWarmupViewControllerDidSelectContinue(
157+
self.delegate?.networkingLinkLoginWarmupViewControllerDidFindConsumerSession(
145158
self,
146-
withSession: consumerSession,
159+
consumerSession: consumerSession,
147160
consumerPublishableKey: publishableKey
148161
)
162+
self.delegate?.networkingLinkLoginWarmupViewControllerDidSelectContinue(self)
149163
} else {
150164
let error = FinancialConnectionsSheetError.unknown(
151165
debugDescription: "Unexpected consumer lookup response without consumer session or publishable key"

0 commit comments

Comments
 (0)