@@ -203,41 +203,48 @@ class DevCycleClient private constructor(
203203 @JvmOverloads
204204 @Synchronized
205205 fun identifyUser (user : DevCycleUser , callback : DevCycleCallback <Map <String , BaseConfigVariable >>? = null) {
206+ // Validate user before modifying any client state
207+ val updatedUser: PopulatedUser = try {
208+ if (this @DevCycleClient.user.userId == user.userId) {
209+ this @DevCycleClient.user.copyUserAndUpdateFromDevCycleUser(user)
210+ } else {
211+ // Apply the same validation logic as in DevCycleClientBuilder
212+ DevCycleClient .validateDevCycleUser(user, dvcSharedPrefs)
213+ PopulatedUser .fromUserParam(user, context)
214+ }
215+ } catch (t: Throwable ) {
216+ // On invalid user, return error and keep existing user/config
217+ callback?.onError(t)
218+ return
219+ }
220+
206221 flushEvents()
207222
208- val updatedUser: PopulatedUser = if (this @DevCycleClient.user.userId == user.userId) {
209- this @DevCycleClient.user.copyUserAndUpdateFromDevCycleUser(user)
210- } else {
211- // Apply the same validation logic as in DevCycleClientBuilder
212- DevCycleClient .validateDevCycleUser(user, dvcSharedPrefs)
213- PopulatedUser .fromUserParam(user, context)
214- }
223+ // Store previous state for recovery
224+ val previousUser = latestIdentifiedUser
215225 latestIdentifiedUser = updatedUser
216226
217- if (isExecuting.get()) {
218- configRequestQueue.add(UserAndCallback (updatedUser, callback))
219- DevCycleLogger .d(" Queued identifyUser request for user_id %s" , updatedUser.userId)
220- return
221- }
227+ fetchConfigForUser(updatedUser, object : DevCycleCallback <Map <String , BaseConfigVariable >> {
228+ override fun onSuccess (variables : Map <String , BaseConfigVariable >) {
229+ callback?.onSuccess(variables)
230+ }
222231
223- isExecuting.set(true )
224- coroutineScope.launch {
225- withContext(coroutineContext) {
226- try {
227- fetchConfig(updatedUser)
232+ override fun onError (error : Throwable ) {
233+ DevCycleLogger .d(" Error fetching config for user_id %s: %s" , updatedUser.userId, error.message)
234+
235+ // In the event that the config request fails (i.e. the device is offline)
236+ // Fallback to using a Cached Configuration for the User if available
237+ val hasCachedConfig = tryLoadCachedConfigForUser(updatedUser)
238+ if (hasCachedConfig) {
239+ // Successfully used cached config, return success
228240 config?.variables?.let { callback?.onSuccess(it) }
229- } catch (t: Throwable ) {
230- DevCycleLogger .d(" Error fetching config for user_id %s" , updatedUser.userId)
231- // In the event that the config request fails (i.e. the device is offline)
232- // Fallback to using a Cached Configuration for the User if available
233- useCachedConfigForUser(updatedUser)
234- callback?.onError(t)
235- } finally {
236- handleQueuedConfigRequests()
237- isExecuting.set(false )
241+ } else {
242+ // No cached config available, restore previous state and return error
243+ latestIdentifiedUser = previousUser
244+ callback?.onError(error)
238245 }
239246 }
240- }
247+ })
241248 }
242249
243250 /* *
@@ -249,31 +256,32 @@ class DevCycleClient private constructor(
249256 @JvmOverloads
250257 @Synchronized
251258 fun resetUser (callback : DevCycleCallback <Map <String , BaseConfigVariable >>? = null) {
252- val newUser: PopulatedUser = PopulatedUser .buildAnonymous(this .context)
253- latestIdentifiedUser = newUser
254-
255- if (isExecuting.get()) {
256- configRequestQueue.add(UserAndCallback (newUser, callback))
257- DevCycleLogger .d(" Queued resetUser request for new anonymous user" )
258- return
259+ val cachedAnonUserId = dvcSharedPrefs.getAnonUserId()
260+ if (cachedAnonUserId != null ) {
261+ dvcSharedPrefs.clearAnonUserId()
259262 }
260263
264+ // Store previous state for recovery
265+ val previousUser = latestIdentifiedUser
266+ val newUser = PopulatedUser .buildAnonymous(this .context, dvcSharedPrefs)
267+ latestIdentifiedUser = newUser
268+
261269 flushEvents()
262270
263- coroutineScope.launch {
264- withContext(coroutineContext) {
265- isExecuting.set(true )
266- try {
267- fetchConfig(newUser)
268- config?.variables?.let { callback?.onSuccess(it) }
269- } catch (t: Throwable ) {
270- callback?.onError(t)
271- } finally {
272- handleQueuedConfigRequests()
273- isExecuting.set(false )
271+ fetchConfigForUser(newUser, object : DevCycleCallback <Map <String , BaseConfigVariable >> {
272+ override fun onSuccess (variables : Map <String , BaseConfigVariable >) {
273+ callback?.onSuccess(variables)
274+ }
275+
276+ override fun onError (error : Throwable ) {
277+ // Restore the original anonymous user id and previous user on error
278+ if (cachedAnonUserId != null ) {
279+ dvcSharedPrefs.setAnonUserId(cachedAnonUserId)
274280 }
281+ latestIdentifiedUser = previousUser
282+ callback?.onError(error)
275283 }
276- }
284+ })
277285 }
278286
279287 fun close (callback : DevCycleCallback <String >? = null) {
@@ -510,6 +518,32 @@ class DevCycleClient private constructor(
510518 }
511519 }
512520
521+ private fun fetchConfigForUser (
522+ user : PopulatedUser ,
523+ callback : DevCycleCallback <Map <String , BaseConfigVariable >>
524+ ) {
525+ if (isExecuting.get()) {
526+ configRequestQueue.add(UserAndCallback (user, callback))
527+ return
528+ }
529+
530+ isExecuting.set(true )
531+ coroutineScope.launch {
532+ withContext(coroutineContext) {
533+ try {
534+ fetchConfig(user)
535+ config?.variables?.let { callback?.onSuccess(it) }
536+ } catch (t: Throwable ) {
537+ callback?.onError(t)
538+ } finally {
539+ handleQueuedConfigRequests()
540+ isExecuting.set(false )
541+ }
542+ }
543+ }
544+ }
545+
546+
513547 private fun refetchConfig (
514548 sse : Boolean = false,
515549 lastModified : Long? = null,
@@ -557,6 +591,20 @@ class DevCycleClient private constructor(
557591 }
558592 }
559593
594+ private fun tryLoadCachedConfigForUser (user : PopulatedUser ): Boolean {
595+ val cachedConfig = if (disableConfigCache) null else dvcSharedPrefs.getConfig(user)
596+
597+ if (cachedConfig != null ) {
598+ config = cachedConfig
599+ isConfigCached.set(true )
600+ DevCycleLogger .d(" Loaded config from cache for user_id %s" , user.userId)
601+ observable.configUpdated(config)
602+ return true
603+ } else {
604+ return false
605+ }
606+ }
607+
560608 class DevCycleClientBuilder {
561609 private var context: Context ? = null
562610 private var customLifecycleHandler: Handler ? = null
0 commit comments