Skip to content

Commit 59f9821

Browse files
[BWA-155] Copy ConfigService to AuthenticatorShared (#1506)
1 parent da52a5f commit 59f9821

File tree

12 files changed

+619
-33
lines changed

12 files changed

+619
-33
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swiftlint:disable:this file_name
2+
3+
import BitwardenKit
4+
import Networking
5+
6+
extension APIService: ConfigAPIService {
7+
func getConfig() async throws -> ConfigResponseModel {
8+
try await apiUnauthenticatedService.send(ConfigRequest())
9+
}
10+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import BitwardenKit
2+
import BitwardenKitMocks
3+
import TestHelpers
4+
import XCTest
5+
6+
@testable import AuthenticatorShared
7+
8+
class ConfigAPIServiceTests: BitwardenTestCase {
9+
// MARK: Properties
10+
11+
var client: MockHTTPClient!
12+
var stateService: MockStateService!
13+
var subject: ConfigAPIService!
14+
15+
// MARK: Setup & Teardown
16+
17+
override func setUp() {
18+
super.setUp()
19+
20+
client = MockHTTPClient()
21+
stateService = MockStateService()
22+
subject = APIService(client: client)
23+
}
24+
25+
override func tearDown() {
26+
super.tearDown()
27+
28+
client = nil
29+
stateService = nil
30+
subject = nil
31+
}
32+
33+
// MARK: Tests
34+
35+
/// `getConfig()` performs the config request unauthenticated.
36+
func test_getConfig_unauthenticated() async throws {
37+
client.result = .httpSuccess(testData: .validServerConfig)
38+
39+
_ = try await subject.getConfig()
40+
41+
let request = try XCTUnwrap(client.requests.last)
42+
XCTAssertEqual(request.method, .get)
43+
XCTAssertEqual(request.url.absoluteString, "https://example.com/api/config")
44+
XCTAssertNil(request.body)
45+
XCTAssertNil(request.headers["Authorization"])
46+
}
47+
}

AuthenticatorShared/Core/Platform/Services/ConfigService.swift

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import OSLog
66
// MARK: - ConfigService
77

88
/// A protocol for a `ConfigService` that manages the app's config.
9-
/// This is significantly pared down from the `ConfigService` in the PM app.
109
///
1110
protocol ConfigService {
11+
/// A publisher that updates with a new value when a new server configuration is received.
12+
func configPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<MetaServerConfig?, Never>>
13+
1214
/// Retrieves the current configuration. This will return the on-disk configuration if available,
1315
/// or will retrieve it from the server if not. It will also retrieve the configuration from
1416
/// the server if it is outdated or if the `forceRefresh` argument is `true`. Configurations
@@ -110,9 +112,15 @@ class DefaultConfigService: ConfigService {
110112
/// The App Settings Store used for storing and retrieving values from User Defaults.
111113
private let appSettingsStore: AppSettingsStore
112114

115+
/// The API service to make config requests.
116+
private let configApiService: ConfigAPIService
117+
113118
/// The service used by the application to report non-fatal errors.
114119
private let errorReporter: ErrorReporter
115120

121+
/// A subject to notify any subscribers of new server configs.
122+
private let configSubject = CurrentValueSubject<MetaServerConfig?, Never>(nil)
123+
116124
/// The service used by the application to manage account state.
117125
private let stateService: StateService
118126

@@ -132,11 +140,13 @@ class DefaultConfigService: ConfigService {
132140
///
133141
init(
134142
appSettingsStore: AppSettingsStore,
143+
configApiService: ConfigAPIService,
135144
errorReporter: ErrorReporter,
136145
stateService: StateService,
137146
timeProvider: TimeProvider
138147
) {
139148
self.appSettingsStore = appSettingsStore
149+
self.configApiService = configApiService
140150
self.errorReporter = errorReporter
141151
self.stateService = stateService
142152
self.timeProvider = timeProvider
@@ -146,7 +156,26 @@ class DefaultConfigService: ConfigService {
146156

147157
@discardableResult
148158
func getConfig(forceRefresh: Bool, isPreAuth: Bool) async -> ServerConfig? {
149-
nil
159+
guard !forceRefresh else {
160+
await updateConfigFromServer(isPreAuth: isPreAuth)
161+
return try? await getStateServerConfig(isPreAuth: isPreAuth)
162+
}
163+
164+
let localConfig = try? await getStateServerConfig(isPreAuth: isPreAuth)
165+
166+
let localConfigExpired = localConfig?.date.addingTimeInterval(Constants.minimumConfigSyncInterval)
167+
?? Date.distantPast
168+
< timeProvider.presentTime
169+
170+
// if it's not forcing refresh we don't need to wait for the server call
171+
// to finish and we can move it to the background.
172+
if localConfig == nil || localConfigExpired {
173+
Task {
174+
await updateConfigFromServer(isPreAuth: isPreAuth)
175+
}
176+
}
177+
178+
return localConfig
150179
}
151180

152181
func getFeatureFlag(
@@ -161,7 +190,12 @@ class DefaultConfigService: ConfigService {
161190
}
162191
#endif
163192

164-
return FeatureFlag.initialValues[flag]?.boolValue
193+
guard flag.isRemotelyConfigured else {
194+
return FeatureFlag.initialValues[flag]?.boolValue ?? defaultValue
195+
}
196+
let configuration = await getConfig(forceRefresh: forceRefresh, isPreAuth: isPreAuth)
197+
return configuration?.featureStates[flag]?.boolValue
198+
?? FeatureFlag.initialValues[flag]?.boolValue
165199
?? defaultValue
166200
}
167201

@@ -171,7 +205,12 @@ class DefaultConfigService: ConfigService {
171205
forceRefresh: Bool = false,
172206
isPreAuth: Bool = false
173207
) async -> Int {
174-
FeatureFlag.initialValues[flag]?.intValue
208+
guard flag.isRemotelyConfigured else {
209+
return FeatureFlag.initialValues[flag]?.intValue ?? defaultValue
210+
}
211+
let configuration = await getConfig(forceRefresh: forceRefresh, isPreAuth: isPreAuth)
212+
return configuration?.featureStates[flag]?.intValue
213+
?? FeatureFlag.initialValues[flag]?.intValue
175214
?? defaultValue
176215
}
177216

@@ -181,16 +220,25 @@ class DefaultConfigService: ConfigService {
181220
forceRefresh: Bool = false,
182221
isPreAuth: Bool = false
183222
) async -> String? {
184-
FeatureFlag.initialValues[flag]?.stringValue
223+
guard flag.isRemotelyConfigured else {
224+
return FeatureFlag.initialValues[flag]?.stringValue ?? defaultValue
225+
}
226+
let configuration = await getConfig(forceRefresh: forceRefresh, isPreAuth: isPreAuth)
227+
return configuration?.featureStates[flag]?.stringValue
228+
?? FeatureFlag.initialValues[flag]?.stringValue
185229
?? defaultValue
186230
}
187231

232+
// MARK: Debug Feature Flags
233+
188234
func getDebugFeatureFlags() async -> [DebugMenuFeatureFlag] {
189235
let remoteFeatureFlags = await getConfig()?.featureStates ?? [:]
190236

191237
let flags = FeatureFlag.debugMenuFeatureFlags.map { feature in
192238
let userDefaultValue = appSettingsStore.debugFeatureFlag(name: feature.rawValue)
193-
let remoteFlagValue = remoteFeatureFlags[feature]?.boolValue ?? false
239+
let remoteFlagValue = remoteFeatureFlags[feature]?.boolValue
240+
?? FeatureFlag.initialValues[feature]?.boolValue
241+
?? false
194242

195243
return DebugMenuFeatureFlag(
196244
feature: feature,
@@ -232,6 +280,10 @@ class DefaultConfigService: ConfigService {
232280
return try? await stateService.getServerConfig()
233281
}
234282

283+
func configPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<MetaServerConfig?, Never>> {
284+
configSubject.eraseToAnyPublisher().values
285+
}
286+
235287
/// Sets the server config in state depending on if the call is being done before authentication.
236288
/// - Parameters:
237289
/// - config: Config to set
@@ -244,4 +296,50 @@ class DefaultConfigService: ConfigService {
244296
}
245297
try? await stateService.setServerConfig(config, userId: userId)
246298
}
299+
300+
/// Performs a call to the server to get the latest config and updates the local value.
301+
/// - Parameter isPreAuth: If true, the call is coming before the user is authenticated or when adding a new account
302+
private func updateConfigFromServer(isPreAuth: Bool) async {
303+
// The userId is needed here so we know which user trigger getting the config
304+
// which helps if this is done in background and the user somehow changes the user
305+
// while this is loading.
306+
let userId = try? await stateService.getActiveAccountId()
307+
308+
do {
309+
let configResponse = try await configApiService.getConfig()
310+
let serverConfig = ServerConfig(
311+
date: timeProvider.presentTime,
312+
responseModel: configResponse
313+
)
314+
try? await setStateServerConfig(serverConfig, isPreAuth: isPreAuth, userId: userId)
315+
316+
configSubject.send(MetaServerConfig(isPreAuth: isPreAuth, userId: userId, serverConfig: serverConfig))
317+
} catch {
318+
errorReporter.log(error: error)
319+
320+
guard !isPreAuth else {
321+
return
322+
}
323+
324+
let localConfig = try? await stateService.getServerConfig(userId: userId)
325+
guard localConfig == nil,
326+
let preAuthConfig = await stateService.getPreAuthServerConfig() else {
327+
return
328+
}
329+
330+
try? await setStateServerConfig(preAuthConfig, isPreAuth: false, userId: userId)
331+
}
332+
}
333+
}
334+
335+
/// Helper object to send updated server config object with extra metadata
336+
/// like whether it comes from pre-auth and the user ID it belongs to.
337+
/// This is useful for getting the config on background and establishing which was the original context.
338+
struct MetaServerConfig {
339+
/// If true, the call is coming before the user is authenticated or when adding a new account
340+
let isPreAuth: Bool
341+
/// The user ID the requested the server config.
342+
let userId: String?
343+
/// The server config.
344+
let serverConfig: ServerConfig?
247345
}

0 commit comments

Comments
 (0)