@@ -6,9 +6,11 @@ import OSLog
6
6
// MARK: - ConfigService
7
7
8
8
/// A protocol for a `ConfigService` that manages the app's config.
9
- /// This is significantly pared down from the `ConfigService` in the PM app.
10
9
///
11
10
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
+
12
14
/// Retrieves the current configuration. This will return the on-disk configuration if available,
13
15
/// or will retrieve it from the server if not. It will also retrieve the configuration from
14
16
/// the server if it is outdated or if the `forceRefresh` argument is `true`. Configurations
@@ -110,9 +112,15 @@ class DefaultConfigService: ConfigService {
110
112
/// The App Settings Store used for storing and retrieving values from User Defaults.
111
113
private let appSettingsStore : AppSettingsStore
112
114
115
+ /// The API service to make config requests.
116
+ private let configApiService : ConfigAPIService
117
+
113
118
/// The service used by the application to report non-fatal errors.
114
119
private let errorReporter : ErrorReporter
115
120
121
+ /// A subject to notify any subscribers of new server configs.
122
+ private let configSubject = CurrentValueSubject < MetaServerConfig ? , Never > ( nil )
123
+
116
124
/// The service used by the application to manage account state.
117
125
private let stateService : StateService
118
126
@@ -132,11 +140,13 @@ class DefaultConfigService: ConfigService {
132
140
///
133
141
init (
134
142
appSettingsStore: AppSettingsStore ,
143
+ configApiService: ConfigAPIService ,
135
144
errorReporter: ErrorReporter ,
136
145
stateService: StateService ,
137
146
timeProvider: TimeProvider
138
147
) {
139
148
self . appSettingsStore = appSettingsStore
149
+ self . configApiService = configApiService
140
150
self . errorReporter = errorReporter
141
151
self . stateService = stateService
142
152
self . timeProvider = timeProvider
@@ -146,7 +156,26 @@ class DefaultConfigService: ConfigService {
146
156
147
157
@discardableResult
148
158
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
150
179
}
151
180
152
181
func getFeatureFlag(
@@ -161,7 +190,12 @@ class DefaultConfigService: ConfigService {
161
190
}
162
191
#endif
163
192
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
165
199
?? defaultValue
166
200
}
167
201
@@ -171,7 +205,12 @@ class DefaultConfigService: ConfigService {
171
205
forceRefresh: Bool = false ,
172
206
isPreAuth: Bool = false
173
207
) 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
175
214
?? defaultValue
176
215
}
177
216
@@ -181,16 +220,25 @@ class DefaultConfigService: ConfigService {
181
220
forceRefresh: Bool = false ,
182
221
isPreAuth: Bool = false
183
222
) 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
185
229
?? defaultValue
186
230
}
187
231
232
+ // MARK: Debug Feature Flags
233
+
188
234
func getDebugFeatureFlags( ) async -> [ DebugMenuFeatureFlag ] {
189
235
let remoteFeatureFlags = await getConfig ( ) ? . featureStates ?? [ : ]
190
236
191
237
let flags = FeatureFlag . debugMenuFeatureFlags. map { feature in
192
238
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
194
242
195
243
return DebugMenuFeatureFlag (
196
244
feature: feature,
@@ -232,6 +280,10 @@ class DefaultConfigService: ConfigService {
232
280
return try ? await stateService. getServerConfig ( )
233
281
}
234
282
283
+ func configPublisher( ) async throws -> AsyncThrowingPublisher < AnyPublisher < MetaServerConfig ? , Never > > {
284
+ configSubject. eraseToAnyPublisher ( ) . values
285
+ }
286
+
235
287
/// Sets the server config in state depending on if the call is being done before authentication.
236
288
/// - Parameters:
237
289
/// - config: Config to set
@@ -244,4 +296,50 @@ class DefaultConfigService: ConfigService {
244
296
}
245
297
try ? await stateService. setServerConfig ( config, userId: userId)
246
298
}
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 ?
247
345
}
0 commit comments