Skip to content

Commit 72822f5

Browse files
committed
Add support for id_consent_content pane
1 parent ab4cfaf commit 72822f5

12 files changed

+398
-58
lines changed

StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj

+24
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@
7676
49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */; };
7777
49C911392C597EAF00589E0D /* LinkLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C911352C597EAF00589E0D /* LinkLoginViewController.swift */; };
7878
49C9113B2C59932300589E0D /* LinkSignupFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C9113A2C59932300589E0D /* LinkSignupFormView.swift */; };
79+
49E489AC2D7F23FE00D09C1F /* IDConsentContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E489AB2D7F23FE00D09C1F /* IDConsentContentViewController.swift */; };
80+
49E489AE2D7F240900D09C1F /* IDConsentContentDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E489AD2D7F240900D09C1F /* IDConsentContentDataSource.swift */; };
81+
49E489B12D7F24C200D09C1F /* FinancialConnectionsIDContentConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E489B02D7F24C200D09C1F /* FinancialConnectionsIDContentConsent.swift */; };
82+
49E489B32D7F2B2F00D09C1F /* RoundedLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E489B22D7F2B2F00D09C1F /* RoundedLogoView.swift */; };
7983
49F047532C63B430006BAD3E /* StripeSchemeAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F047522C63B430006BAD3E /* StripeSchemeAddress.swift */; };
8084
49F047552C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F047542C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift */; };
8185
49F1B83A2D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */; };
@@ -347,6 +351,10 @@
347351
49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLoginDataSource.swift; sourceTree = "<group>"; };
348352
49C911352C597EAF00589E0D /* LinkLoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLoginViewController.swift; sourceTree = "<group>"; };
349353
49C9113A2C59932300589E0D /* LinkSignupFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LinkSignupFormView.swift; path = StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/LinkSignupFormView.swift; sourceTree = SOURCE_ROOT; };
354+
49E489AB2D7F23FE00D09C1F /* IDConsentContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDConsentContentViewController.swift; sourceTree = "<group>"; };
355+
49E489AD2D7F240900D09C1F /* IDConsentContentDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDConsentContentDataSource.swift; sourceTree = "<group>"; };
356+
49E489B02D7F24C200D09C1F /* FinancialConnectionsIDContentConsent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsIDContentConsent.swift; sourceTree = "<group>"; };
357+
49E489B22D7F2B2F00D09C1F /* RoundedLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLogoView.swift; sourceTree = "<group>"; };
350358
49F047522C63B430006BAD3E /* StripeSchemeAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeSchemeAddress.swift; sourceTree = "<group>"; };
351359
49F047542C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsPaymentDetails.swift; sourceTree = "<group>"; };
352360
49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAsyncAPIClient.swift; sourceTree = "<group>"; };
@@ -701,6 +709,15 @@
701709
path = LinkLogin;
702710
sourceTree = "<group>";
703711
};
712+
49E489AF2D7F240D00D09C1F /* IDConsentContent */ = {
713+
isa = PBXGroup;
714+
children = (
715+
49E489AB2D7F23FE00D09C1F /* IDConsentContentViewController.swift */,
716+
49E489AD2D7F240900D09C1F /* IDConsentContentDataSource.swift */,
717+
);
718+
path = IDConsentContent;
719+
sourceTree = "<group>";
720+
};
704721
53C3F536D69FCBC77C3E7D5F /* Common */ = {
705722
isa = PBXGroup;
706723
children = (
@@ -792,6 +809,7 @@
792809
6A384A832C24DD720044AB99 /* FinancialConnectionsGenericInfoScreen.swift */,
793810
49F047542C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift */,
794811
49AC00C12D7B5C90005BFD57 /* FinancialConnectionsSelectInstitution.swift */,
812+
49E489B02D7F24C200D09C1F /* FinancialConnectionsIDContentConsent.swift */,
795813
);
796814
path = Models;
797815
sourceTree = "<group>";
@@ -997,6 +1015,7 @@
9971015
5A844A24E4E9F4B7E3802DA9 /* AccountPicker */,
9981016
7BFDCD3B61A38A4BA3466780 /* AttachLinkedPaymentAccount */,
9991017
EB6632ED2E696DA6381326C0 /* Consent */,
1018+
49E489AF2D7F240D00D09C1F /* IDConsentContent */,
10001019
605E7A0FA65A081265FA6F54 /* InstitutionPicker */,
10011020
67E2F19935317C74CE2CEB8D /* LinkAccountPicker */,
10021021
92A9D27306891BBCBC4DBF49 /* ManualEntry */,
@@ -1034,6 +1053,7 @@
10341053
F669BB8F3DA862C425897705 /* HitTestView.swift */,
10351054
939D10B20D3ECA7BF7021BF8 /* InstitutionIconView.swift */,
10361055
6A1D43062B17AE37005A1EB0 /* RoundedIconView.swift */,
1056+
49E489B22D7F2B2F00D09C1F /* RoundedLogoView.swift */,
10371057
6A732CA12B69821300828CB1 /* RoundedTextField.swift */,
10381058
1D38B3C816EAB38AD242B064 /* SFSafariViewController+Extensions.swift */,
10391059
6A78140C2B30F15400168992 /* SheetViewController.swift */,
@@ -1361,6 +1381,7 @@
13611381
6A13B9FA2B4E182A00FFA327 /* SpinnerView.swift in Sources */,
13621382
6A384A842C24DD720044AB99 /* FinancialConnectionsGenericInfoScreen.swift in Sources */,
13631383
0D56BD448019185656DF9310 /* FinancialConnectionsCustomManualEntryRequiredError.swift in Sources */,
1384+
49E489B32D7F2B2F00D09C1F /* RoundedLogoView.swift in Sources */,
13641385
6E6E30D01D4E9629DB07E97B /* FinancialConnectionsNavigationController.swift in Sources */,
13651386
01C820ECDBFC041A741A5499 /* FlowRouter.swift in Sources */,
13661387
933F9DFE970FAB4715369086 /* HostController.swift in Sources */,
@@ -1396,6 +1417,7 @@
13961417
A10B5A3E5E8AE8767CF09C15 /* AccountPickerSelectionListView.swift in Sources */,
13971418
163E387D567068E4A64A4C13 /* AccountPickerSelectionView.swift in Sources */,
13981419
492651662C24C9E7001DDBCA /* TestModeAutofillBannerView.swift in Sources */,
1420+
49E489AE2D7F240900D09C1F /* IDConsentContentDataSource.swift in Sources */,
13991421
23DBA4240ED1727C47937A6B /* AccountPickerViewController.swift in Sources */,
14001422
E4D00DB842047E595DD85BEF /* CheckboxView.swift in Sources */,
14011423
6A1D43032B16B3DC005A1EB0 /* InstitutionCellView.swift in Sources */,
@@ -1443,7 +1465,9 @@
14431465
3AC5CA5F5529B55026342A54 /* NetworkingLinkSignupDataSource.swift in Sources */,
14441466
6A13B9862B4CD04100FFA327 /* RetrieveAccountsLoadingView.swift in Sources */,
14451467
6A3739162C4060BD00D1F765 /* AutoResizableUIView.swift in Sources */,
1468+
49E489B12D7F24C200D09C1F /* FinancialConnectionsIDContentConsent.swift in Sources */,
14461469
8DC6C2A239456994091BF3EE /* NetworkingLinkSignupFooterView.swift in Sources */,
1470+
49E489AC2D7F23FE00D09C1F /* IDConsentContentViewController.swift in Sources */,
14471471
6A3DA1F72C34B254005C3F6E /* GenericInfoFooterView.swift in Sources */,
14481472
54B51EA1F75B9607D7C29B08 /* NetworkingLinkSignupViewController.swift in Sources */,
14491473
3F835D5A1C797C1C9BCF05D0 /* NetworkingLinkStepUpVerificationBodyView.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// FinancialConnectionsIDContentConsent.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2025-03-10.
6+
//
7+
8+
import Foundation
9+
10+
struct FinancialConnectionsIDContentConsent: Decodable {
11+
let screen: FinancialConnectionsGenericInfoScreen
12+
let legalDetailsNotice: FinancialConnectionsLegalDetailsNotice
13+
}

StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSelectInstitution.swift

+5
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ import Foundation
99

1010
struct FinancialConnectionsSelectInstitution: Decodable {
1111
let manifest: FinancialConnectionsSessionManifest
12+
let text: Text?
13+
}
14+
15+
struct Text: Decodable {
16+
let idConsentContentPane: FinancialConnectionsIDContentConsent
1217
}

StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct FinancialConnectionsSessionManifest: Decodable {
2020
case authOptions = "auth_options"
2121
case bankAuthRepair = "bank_auth_repair"
2222
case consent = "consent"
23+
case idConsentContent = "id_consent_content"
2324
case institutionPicker = "institution_picker"
2425
case linkAccountPicker = "link_account_picker"
2526
case linkConsent = "link_consent"

StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentLogoView.swift

-23
Original file line numberDiff line numberDiff line change
@@ -106,29 +106,6 @@ final class ConsentLogoView: UIView {
106106
}
107107
}
108108

109-
private func CreateRoundedLogoView(urlString: String) -> UIView {
110-
let cornerRadius: CGFloat = 16.0
111-
let shadowContainerView = UIView()
112-
shadowContainerView.layer.shadowColor = FinancialConnectionsAppearance.Colors.shadow.cgColor
113-
shadowContainerView.layer.shadowOpacity = 0.18
114-
shadowContainerView.layer.shadowOffset = CGSize(width: 0, height: 3)
115-
shadowContainerView.layer.shadowRadius = 5
116-
shadowContainerView.layer.cornerRadius = cornerRadius
117-
let radius: CGFloat = 72.0
118-
let imageView = UIImageView()
119-
imageView.contentMode = .scaleAspectFill
120-
imageView.clipsToBounds = true
121-
imageView.layer.cornerRadius = cornerRadius
122-
imageView.setImage(with: urlString)
123-
imageView.translatesAutoresizingMaskIntoConstraints = false
124-
NSLayoutConstraint.activate([
125-
imageView.widthAnchor.constraint(equalToConstant: radius),
126-
imageView.heightAnchor.constraint(equalToConstant: radius),
127-
])
128-
shadowContainerView.addAndPinSubview(imageView)
129-
return shadowContainerView
130-
}
131-
132109
private func CreateEllipsisView(
133110
leftLogoUrl: String?,
134111
rightLogoUrl: String?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// IDConsentContentDataSource.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2025-03-10.
6+
//
7+
8+
import Foundation
9+
@_spi(STP) import StripeCore
10+
11+
protocol IDConsentContentDataSource: AnyObject {
12+
var manifest: FinancialConnectionsSessionManifest { get }
13+
var idConsentContent: FinancialConnectionsIDContentConsent { get }
14+
var analyticsClient: FinancialConnectionsAnalyticsClient { get }
15+
16+
func markConsentAcquired() -> Promise<FinancialConnectionsSessionManifest>
17+
}
18+
19+
final class IDConsentContentDataSourceImplementation: IDConsentContentDataSource {
20+
let manifest: FinancialConnectionsSessionManifest
21+
let idConsentContent: FinancialConnectionsIDContentConsent
22+
let analyticsClient: FinancialConnectionsAnalyticsClient
23+
24+
private let apiClient: any FinancialConnectionsAPI
25+
private let clientSecret: String
26+
27+
init(
28+
manifest: FinancialConnectionsSessionManifest,
29+
idConsentContent: FinancialConnectionsIDContentConsent,
30+
apiClient: any FinancialConnectionsAPI,
31+
clientSecret: String,
32+
analyticsClient: FinancialConnectionsAnalyticsClient
33+
) {
34+
self.manifest = manifest
35+
self.idConsentContent = idConsentContent
36+
self.apiClient = apiClient
37+
self.clientSecret = clientSecret
38+
self.analyticsClient = analyticsClient
39+
}
40+
41+
func markConsentAcquired() -> Promise<FinancialConnectionsSessionManifest> {
42+
return apiClient.markConsentAcquired(clientSecret: clientSecret)
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// IDConsentContentViewController.swift
3+
// StripeFinancialConnections
4+
//
5+
// Created by Mat Schmid on 2025-03-10.
6+
//
7+
8+
import Foundation
9+
@_spi(STP) import StripeCore
10+
@_spi(STP) import StripeUICore
11+
import UIKit
12+
13+
protocol IDConsentContentViewControllerDelegate: AnyObject {
14+
func idConsentContentViewController(
15+
_ viewController: IDConsentContentViewController,
16+
didRequestNextPane nextPane: FinancialConnectionsSessionManifest.NextPane,
17+
nextPaneOrDrawerOnSecondaryCta: String?
18+
)
19+
func idConsentContentViewController(
20+
_ viewController: IDConsentContentViewController,
21+
didConsentWithManifest manifest: FinancialConnectionsSessionManifest
22+
)
23+
}
24+
25+
class IDConsentContentViewController: UIViewController {
26+
private let dataSource: IDConsentContentDataSource
27+
weak var delegate: IDConsentContentViewControllerDelegate?
28+
29+
private lazy var footer: (footerView: UIView?, primaryButton: StripeUICore.Button?) = {
30+
return GenericInfoFooterViewAndPrimaryButton(
31+
footer: dataSource.idConsentContent.screen.footer,
32+
appearance: dataSource.manifest.appearance,
33+
didSelectPrimaryButton: didSelectAgree,
34+
didSelectSecondaryButton: {
35+
// This can't occur
36+
},
37+
didSelectURL: didSelectURLInTextFromBackend
38+
)
39+
}()
40+
41+
init(dataSource: IDConsentContentDataSource) {
42+
self.dataSource = dataSource
43+
super.init(nibName: nil, bundle: nil)
44+
}
45+
46+
required init?(coder: NSCoder) {
47+
fatalError("init(coder:) has not been implemented")
48+
}
49+
50+
override func viewDidLoad() {
51+
super.viewDidLoad()
52+
view.backgroundColor = FinancialConnectionsAppearance.Colors.background
53+
54+
let genericInfoScreen = dataSource.idConsentContent.screen
55+
let iconView: UIView? = {
56+
guard let imageUrl = genericInfoScreen.header?.icon?.default else { return nil }
57+
return CreateRoundedLogoView(urlString: imageUrl)
58+
}()
59+
60+
let headerAlignment: UIStackView.Alignment = {
61+
switch genericInfoScreen.header?.alignment {
62+
case .center: return .center
63+
case .right: return .trailing
64+
case .left: fallthrough
65+
case .unparsable: fallthrough
66+
case .none: return .leading
67+
}
68+
}()
69+
70+
let contentView = PaneLayoutView.createContentView(
71+
iconView: iconView,
72+
title: genericInfoScreen.header?.title,
73+
subtitle: genericInfoScreen.header?.subtitle,
74+
headerAlignment: headerAlignment,
75+
contentView: GenericInfoBodyView(
76+
body: genericInfoScreen.body,
77+
didSelectURL: didSelectURLInTextFromBackend
78+
)
79+
)
80+
81+
let paneLayoutView = PaneLayoutView(contentView: contentView, footerView: footer.footerView)
82+
paneLayoutView.addTo(view: view)
83+
84+
dataSource.analyticsClient.logPaneLoaded(pane: .idConsentContent)
85+
}
86+
87+
private func didSelectAgree() {
88+
dataSource.analyticsClient.log(
89+
eventName: "click.agree",
90+
pane: .idConsentContent
91+
)
92+
93+
footer.primaryButton?.isLoading = true
94+
dataSource.markConsentAcquired()
95+
.observe { [weak self] result in
96+
guard let self else { return }
97+
switch result {
98+
case .success(let manifest):
99+
self.delegate?.idConsentContentViewController(self, didConsentWithManifest: manifest)
100+
case .failure(let error):
101+
// we display no errors on failure
102+
self.dataSource
103+
.analyticsClient
104+
.logUnexpectedError(
105+
error,
106+
errorName: "ConsentAcquiredError",
107+
pane: .idConsentContent
108+
)
109+
}
110+
footer.primaryButton?.isLoading = false
111+
}
112+
}
113+
114+
private func didSelectURLInTextFromBackend(_ url: URL) {
115+
AuthFlowHelpers.handleURLInTextFromBackend(
116+
url: url,
117+
pane: .idConsentContent,
118+
analyticsClient: dataSource.analyticsClient,
119+
handleURL: { urlHost, nextPaneOrDrawerOnSecondaryCta in
120+
guard let urlHost, let address = StripeSchemeAddress(rawValue: urlHost) else {
121+
self.dataSource
122+
.analyticsClient
123+
.logUnexpectedError(
124+
FinancialConnectionsSheetError.unknown(
125+
debugDescription: "Unknown Stripe-scheme URL detected: \(urlHost ?? "nil")."
126+
),
127+
errorName: "ConsentStripeURLError",
128+
pane: .idConsentContent
129+
)
130+
return
131+
}
132+
133+
switch address {
134+
case .manualEntry:
135+
delegate?.idConsentContentViewController(
136+
self,
137+
didRequestNextPane: .manualEntry,
138+
nextPaneOrDrawerOnSecondaryCta: nextPaneOrDrawerOnSecondaryCta
139+
)
140+
case .dataAccessNotice:
141+
assertionFailure("ID Consent Content pane does not support Data Access Notice addresses")
142+
case .legalDatailsNotice:
143+
let legalDetailsNoticeModel = dataSource.idConsentContent.legalDetailsNotice
144+
let legalDetailsNoticeViewController = LegalDetailsNoticeViewController(
145+
legalDetailsNotice: legalDetailsNoticeModel,
146+
appearance: dataSource.manifest.appearance,
147+
didSelectUrl: { [weak self] url in
148+
self?.didSelectURLInTextFromBackend(url)
149+
}
150+
)
151+
legalDetailsNoticeViewController.present(on: self)
152+
case .linkAccountPicker:
153+
delegate?.idConsentContentViewController(
154+
self,
155+
didRequestNextPane: .linkAccountPicker,
156+
nextPaneOrDrawerOnSecondaryCta: nextPaneOrDrawerOnSecondaryCta
157+
)
158+
case .linkLogin:
159+
delegate?.idConsentContentViewController(
160+
self,
161+
didRequestNextPane: .networkingLinkLoginWarmup,
162+
nextPaneOrDrawerOnSecondaryCta: nextPaneOrDrawerOnSecondaryCta
163+
)
164+
}
165+
}
166+
)
167+
}
168+
}

StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionPickerViewController.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protocol InstitutionPickerViewControllerDelegate: AnyObject {
2323
func institutionPickerViewController(
2424
_ viewController: InstitutionPickerViewController,
2525
didFinishSelecting institution: FinancialConnectionsInstitution,
26-
manifest: FinancialConnectionsSessionManifest
26+
payload: FinancialConnectionsSelectInstitution
2727
)
2828
func institutionPickerViewControllerDidSelectManuallyAddYourAccount(
2929
_ viewController: InstitutionPickerViewController
@@ -238,11 +238,11 @@ class InstitutionPickerViewController: UIViewController {
238238
.observe { [weak self] result in
239239
guard let self else { return }
240240
switch result {
241-
case .success(let synchronizePayload):
241+
case .success(let selectInstitutionPayload):
242242
self.delegate?.institutionPickerViewController(
243243
self,
244244
didFinishSelecting: institution,
245-
manifest: synchronizePayload.manifest
245+
payload: selectInstitutionPayload
246246
)
247247
self.hideOverlayView()
248248
case .failure(let error):

0 commit comments

Comments
 (0)