Skip to content

Commit 847b7cb

Browse files
authored
Dark Mode style configuration API and Example app toggle (#4509)
## Summary #### [Commit 1](c0ad249): Creates a `Configuration` API for the `FinancialConnectionsSheet`, which is internal-only (for now). The configuration API allows consumers specify their preference for styling the sheet, with these options; - `automatic`: Appearance will reflect the device's theme, and dynamically switch accordingly. - `alwaysLight`: Appearance will always use colors appropriate for a light theme. - This is the default value for now. The default value is likely to switch over to `automatic` once we release this API. - `alwaysDark`: Appearance will always use colors appropriate for a dark theme. Usage of this API follows the same approach as the configuration API in MPE ([docs](https://docs.stripe.com/payments/accept-a-payment?platform=ios&ui=payment-sheet#dark-mode)). Here's what that looks like: ```swift var configuration = FinancialConnectionsSheet.Configuration() configuration.style = .alwaysDark financialConnectionsSheet.configuration = configuration ``` > [!IMPORTANT] >Reminder that the API described here is internal-only until a proper API review takes places. Anything mentioned here is subject to change between now and the public release. #### [Commit 2](c43205f): Adds a toggle and style picker in the Financial Connections Example app to test the API mentioned above. Unless the `Use dynamic style` toggle is enabled, the FC flow should always use a light theme. Otherwise, it should reflect the value in the style picker <img width="515" alt="image" src="https://github.com/user-attachments/assets/fc7a269e-eb12-4350-87e6-b2a857a6e32c" /> #### [Commit 3](9bedd68): Passes along the value set in the MPE `PaymentSheet.Configuration.UserInterfaceStyle` along to the FC configuration API, whenever FC is launched from payment sheet. This will ensure users integrating both MPE and FC won't need to make any integration changes to get their desired style configuration. > [!IMPORTANT] >Reminder that this will have no functional changes until this API is released. FC will continue to always present in "light mode" until we publicize the `Configuration` API, and remove the `ExperimentStore` guardrails we have in place. ## Motivation https://docs.google.com/document/d/1bGRPh8J8dvie-JEG4q43DycIBr5zY1GoGF-0RFf0L_Q/edit?usp=sharing ## Testing https://github.com/user-attachments/assets/960cb29a-161d-4fd5-a2f4-dba9676876ea ## Changelog N/a
1 parent e16dd1f commit 847b7cb

File tree

17 files changed

+256
-22
lines changed

17 files changed

+256
-22
lines changed

Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundConfiguration.swift

+66-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
@_spi(STP) import StripeFinancialConnections
910

1011
/// Provides an interface to customize the playground configuration
1112
/// (which is stored as a JSON in NSUserDefaults).
@@ -444,12 +445,6 @@ final class PlaygroundConfiguration {
444445

445446
// MARK: - Experimental
446447

447-
@UserDefault(
448-
key: "FINANCIAL_CONNECTIONS_EXAMPLE_USE_ASYNC_API_CLIENT",
449-
defaultValue: false
450-
)
451-
private static var useAsyncAPIClientStorage: Bool
452-
453448
private static let useAsyncAPIClientKey = "use_async_api_client"
454449
var useAsyncAPIClient: Bool {
455450
get {
@@ -462,8 +457,59 @@ final class PlaygroundConfiguration {
462457
set {
463458
// Save to configuration string
464459
configurationStore[Self.useAsyncAPIClientKey] = newValue
465-
// Save to user defaults
466-
Self.useAsyncAPIClientStorage = newValue
460+
// Save to experiment store
461+
ExperimentStore.shared.useAsyncAPIClient = newValue
462+
}
463+
}
464+
465+
private static let useDynamicStyleKey = "use_dynamic_style"
466+
var useDynamicStyle: Bool {
467+
get {
468+
if let useDynamicStyle = configurationStore[Self.useDynamicStyleKey] as? Bool {
469+
return useDynamicStyle
470+
} else {
471+
return false
472+
}
473+
}
474+
set {
475+
// Save to configuration string
476+
configurationStore[Self.useDynamicStyleKey] = newValue
477+
// Save to experiment store
478+
ExperimentStore.shared.supportsDynamicStyle = newValue
479+
}
480+
}
481+
482+
enum Style: String, CaseIterable, Identifiable, Hashable {
483+
case automatic = "automatic"
484+
case alwaysLight = "always_light"
485+
case alwaysDark = "always_dark"
486+
487+
var id: String {
488+
return rawValue
489+
}
490+
491+
var configurationValue: FinancialConnectionsSheet.Configuration.UserInterfaceStyle {
492+
switch self {
493+
case .automatic: return .automatic
494+
case .alwaysLight: return .alwaysLight
495+
case .alwaysDark: return .alwaysDark
496+
}
497+
}
498+
}
499+
500+
private static let styleKey = "dynamic_style"
501+
var style: PlaygroundConfiguration.Style {
502+
get {
503+
if let styleString = configurationStore[Self.styleKey] as? String,
504+
let style = PlaygroundConfiguration.Style(rawValue: styleString) {
505+
return style
506+
} else {
507+
return .alwaysLight
508+
}
509+
}
510+
set {
511+
// Save to configuration string
512+
configurationStore[Self.styleKey] = newValue.rawValue
467513
}
468514
}
469515

@@ -579,6 +625,18 @@ final class PlaygroundConfiguration {
579625
} else {
580626
self.useAsyncAPIClient = false
581627
}
628+
629+
if let useDynamicStyle = dictionary[Self.useDynamicStyleKey] as? Bool {
630+
self.useDynamicStyle = useDynamicStyle
631+
} else {
632+
self.useDynamicStyle = false
633+
}
634+
if let styleString = dictionary[Self.styleKey] as? String,
635+
let style = PlaygroundConfiguration.Style(rawValue: styleString) {
636+
self.style = style
637+
} else {
638+
self.style = .alwaysLight
639+
}
582640
}
583641
}
584642

Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundView.swift

+11
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ struct PlaygroundView: View {
179179

180180
Section(header: Text("Experimental")) {
181181
Toggle("Use async API client", isOn: viewModel.useAsyncAPIClient)
182+
183+
Toggle("Use dynamic style", isOn: viewModel.useDynamicStyle)
184+
if viewModel.useDynamicStyle.wrappedValue {
185+
Picker("Style", selection: viewModel.style) {
186+
ForEach(PlaygroundConfiguration.Style.allCases) {
187+
Text($0.rawValue)
188+
.tag($0)
189+
}
190+
}
191+
.pickerStyle(.segmented)
192+
}
182193
}
183194
}
184195

Example/FinancialConnections Example/FinancialConnections Example/Playground/PlaygroundViewModel.swift

+35
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,30 @@ final class PlaygroundViewModel: ObservableObject {
225225
)
226226
}
227227

228+
var useDynamicStyle: Binding<Bool> {
229+
Binding(
230+
get: {
231+
self.playgroundConfiguration.useDynamicStyle
232+
},
233+
set: {
234+
self.playgroundConfiguration.useDynamicStyle = $0
235+
self.objectWillChange.send()
236+
}
237+
)
238+
}
239+
240+
var style: Binding<PlaygroundConfiguration.Style> {
241+
Binding(
242+
get: {
243+
self.playgroundConfiguration.style
244+
},
245+
set: {
246+
self.playgroundConfiguration.style = $0
247+
self.objectWillChange.send()
248+
}
249+
)
250+
}
251+
228252
@Published var showConfigurationView = false
229253
private(set) lazy var playgroundConfigurationViewModel: PlaygroundManageConfigurationViewModel = {
230254
return PlaygroundManageConfigurationViewModel(
@@ -323,6 +347,7 @@ final class PlaygroundViewModel: ObservableObject {
323347
stripeAccount: self.playgroundConfiguration.merchant.stripeAccount,
324348
setupPlaygroundResponseJSON: setupPlaygroundResponse,
325349
useAsyncApiClient: self.playgroundConfiguration.useAsyncAPIClient,
350+
style: self.playgroundConfiguration.style,
326351
onEvent: { event in
327352
if self.liveEvents.wrappedValue == true {
328353
let message = "\(event.name.rawValue); \(event.metadata.dictionary)"
@@ -472,6 +497,7 @@ private func PresentFinancialConnectionsSheet(
472497
stripeAccount: String?,
473498
setupPlaygroundResponseJSON: [String: String],
474499
useAsyncApiClient: Bool,
500+
style: PlaygroundConfiguration.Style,
475501
onEvent: @escaping (FinancialConnectionsEvent) -> Void,
476502
completionHandler: @escaping (FinancialConnectionsSheet.Result) -> Void
477503
) {
@@ -518,6 +544,9 @@ private func PresentFinancialConnectionsSheet(
518544
)
519545
financialConnectionsSheet.apiClient.stripeAccount = stripeAccount
520546
financialConnectionsSheet.onEvent = onEvent
547+
var configuration = FinancialConnectionsSheet.Configuration()
548+
configuration.style = style.configurationValue
549+
financialConnectionsSheet.configuration = configuration
521550
let topMostViewController = UIViewController.topMostViewController()!
522551
if useCase == .token {
523552
financialConnectionsSheet.presentForToken(
@@ -619,6 +648,12 @@ private func PresentPaymentSheet(
619648
configuration.defaultBillingDetails.email = config.email
620649
configuration.defaultBillingDetails.phone = config.phone
621650

651+
switch config.style {
652+
case .automatic: configuration.style = .automatic
653+
case .alwaysLight: configuration.style = .alwaysLight
654+
case .alwaysDark: configuration.style = .alwaysDark
655+
}
656+
622657
let isUITest = (ProcessInfo.processInfo.environment["UITesting"] != nil)
623658
// disable app-to-app for UI tests
624659
configuration.returnURL = isUITest ? nil : "financial-connections-example://redirect"

StripeCore/StripeCore/Source/Connections Bindings/ElementsSessionContext.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,22 @@ import Foundation
4444
}
4545
}
4646

47+
/// Intermediary object between `PaymentSheet.Configuration.UserInterfaceStyle`
48+
/// and `FinancialConnectionsSheet.Configuration.UserInterfaceStyle`.
49+
@_spi(STP) @frozen public enum StyleConfig {
50+
case automatic
51+
case alwaysLight
52+
case alwaysDark
53+
}
54+
4755
@_spi(STP) public let amount: Int?
4856
@_spi(STP) public let currency: String?
4957
@_spi(STP) public let prefillDetails: PrefillDetails?
5058
@_spi(STP) public let intentId: IntentID?
5159
@_spi(STP) public let linkMode: LinkMode?
5260
@_spi(STP) public let billingDetails: BillingDetails?
5361
@_spi(STP) public let eligibleForIncentive: Bool
62+
@_spi(STP) public let styleConfig: StyleConfig?
5463

5564
@_spi(STP) public var billingAddress: BillingAddress? {
5665
BillingAddress(from: billingDetails)
@@ -70,7 +79,8 @@ import Foundation
7079
intentId: IntentID? = nil,
7180
linkMode: LinkMode? = nil,
7281
billingDetails: BillingDetails? = nil,
73-
eligibleForIncentive: Bool = false
82+
eligibleForIncentive: Bool = false,
83+
styleConfig: StyleConfig? = nil
7484
) {
7585
self.amount = amount
7686
self.currency = currency
@@ -79,6 +89,7 @@ import Foundation
7989
self.linkMode = linkMode
8090
self.billingDetails = billingDetails
8191
self.eligibleForIncentive = eligibleForIncentive
92+
self.styleConfig = styleConfig
8293
}
8394
}
8495

StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
49F047552C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F047542C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift */; };
7777
49F1B83A2D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */; };
7878
49F1B83E2D2EC82300136303 /* FinancialConnectionsAsyncAPIClient+Legacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F1B83D2D2EC82300136303 /* FinancialConnectionsAsyncAPIClient+Legacy.swift */; };
79+
49F883F82D496D7A00A104B0 /* ExperimentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F883F72D496D7A00A104B0 /* ExperimentStore.swift */; };
7980
4A0D015C978BD79BBFE6CE57 /* ManualEntryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4C39F5F9AF440B13F51A81 /* ManualEntryDataSource.swift */; };
8081
4A537AE0C50CAFF3889EFE28 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E41313B709F87B549D85F /* UIViewController+Extensions.swift */; };
8182
4DC8EB63806434ABF4C9CC43 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 782A419DCF59BE6AB6439D04 /* [email protected] */; };
@@ -342,6 +343,7 @@
342343
49F047542C63C2E5006BAD3E /* FinancialConnectionsPaymentDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsPaymentDetails.swift; sourceTree = "<group>"; };
343344
49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAsyncAPIClient.swift; sourceTree = "<group>"; };
344345
49F1B83D2D2EC82300136303 /* FinancialConnectionsAsyncAPIClient+Legacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FinancialConnectionsAsyncAPIClient+Legacy.swift"; sourceTree = "<group>"; };
346+
49F883F72D496D7A00A104B0 /* ExperimentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentStore.swift; sourceTree = "<group>"; };
345347
4A7B146AA6BF44921A249DB8 /* EmptyFinancialConnectionsAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyFinancialConnectionsAPIClient.swift; sourceTree = "<group>"; };
346348
4AFBF95DAE0783010A17EB58 /* FinancialConnectionsSession_only_accounts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = FinancialConnectionsSession_only_accounts.json; sourceTree = "<group>"; };
347349
4BFCD9C339634B71FC8F85E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -1041,6 +1043,7 @@
10411043
6A6F989B2C4F1BF00035C03D /* CreatePaneParameters.swift */,
10421044
49F047522C63B430006BAD3E /* StripeSchemeAddress.swift */,
10431045
49424EAF2D48050A0088F3D9 /* WebPrefillDetails.swift */,
1046+
49F883F72D496D7A00A104B0 /* ExperimentStore.swift */,
10441047
);
10451048
path = Shared;
10461049
sourceTree = "<group>";
@@ -1373,6 +1376,7 @@
13731376
4A537AE0C50CAFF3889EFE28 /* UIViewController+Extensions.swift in Sources */,
13741377
C38BEDD99477C83C91B105DD /* AccountPickerAccountLoadErrorView.swift in Sources */,
13751378
C1A079E8E76A02EBCB2588DA /* AccountPickerDataSource.swift in Sources */,
1379+
49F883F82D496D7A00A104B0 /* ExperimentStore.swift in Sources */,
13761380
BFF222008EEEDC3FACE342D9 /* AccountPickerFooterView.swift in Sources */,
13771381
C0831318A33A32BF2EAB641A /* AccountPickerHelpers.swift in Sources */,
13781382
1889ECB24D40EF331974C288 /* AccountPickerNoAccountEligibleErrorView.swift in Sources */,

StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostController.swift

+11-3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class HostController {
8080
private let apiClient: any FinancialConnectionsAPI
8181
private let clientSecret: String
8282
private let returnURL: String?
83+
private let configuration: FinancialConnectionsSheet.Configuration
8384
private let elementsSessionContext: ElementsSessionContext?
8485
private let analyticsClient: FinancialConnectionsAnalyticsClient
8586
private let analyticsClientV1: STPAnalyticsClientProtocol
@@ -92,7 +93,11 @@ class HostController {
9293
apiClient: apiClient,
9394
delegate: self
9495
)
95-
lazy var navigationController = FinancialConnectionsNavigationController(rootViewController: hostViewController)
96+
lazy var navigationController: FinancialConnectionsNavigationController = {
97+
let navigationController = FinancialConnectionsNavigationController(rootViewController: hostViewController)
98+
configuration.style.configure(navigationController)
99+
return navigationController
100+
}()
96101

97102
weak var delegate: HostControllerDelegate?
98103

@@ -102,16 +107,18 @@ class HostController {
102107
apiClient: any FinancialConnectionsAPI,
103108
analyticsClientV1: STPAnalyticsClientProtocol,
104109
clientSecret: String,
105-
elementsSessionContext: ElementsSessionContext?,
106110
returnURL: String?,
111+
configuration: FinancialConnectionsSheet.Configuration,
112+
elementsSessionContext: ElementsSessionContext?,
107113
publishableKey: String?,
108114
stripeAccount: String?
109115
) {
110116
self.apiClient = apiClient
111117
self.analyticsClientV1 = analyticsClientV1
112118
self.clientSecret = clientSecret
113-
self.elementsSessionContext = elementsSessionContext
114119
self.returnURL = returnURL
120+
self.configuration = configuration
121+
self.elementsSessionContext = elementsSessionContext
115122
self.analyticsClient = FinancialConnectionsAnalyticsClient()
116123
analyticsClient.setAdditionalParameters(
117124
linkAccountSessionClientSecret: clientSecret,
@@ -208,6 +215,7 @@ private extension HostController {
208215

209216
let dataManager = NativeFlowAPIDataManager(
210217
manifest: synchronizePayload.manifest,
218+
configuration: configuration,
211219
visualUpdate: synchronizePayload.visual,
212220
returnURL: returnURL,
213221
consentPaneModel: synchronizePayload.text?.consentPane,

StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSDK/FinancialConnectionsSDKImplementation.swift

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ public class FinancialConnectionsSDKImplementation: FinancialConnectionsSDKInter
3232
financialConnectionsSheet.apiClient = apiClient
3333
financialConnectionsSheet.elementsSessionContext = elementsSessionContext
3434
financialConnectionsSheet.onEvent = onEvent
35+
36+
var configuration = FinancialConnectionsSheet.Configuration()
37+
if let styleConfig = elementsSessionContext?.styleConfig {
38+
switch styleConfig {
39+
case .automatic: configuration.style = .automatic
40+
case .alwaysLight: configuration.style = .alwaysLight
41+
case .alwaysDark: configuration.style = .alwaysDark
42+
}
43+
}
44+
financialConnectionsSheet.configuration = configuration
3545
// Captures self explicitly until the callback is invoked
3646
financialConnectionsSheet.present(
3747
from: presentingViewController,

0 commit comments

Comments
 (0)