@@ -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///
1110protocol 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