Skip to content

Commit 1a4124a

Browse files
author
AR Abdul Azeez
committed
Fix login race condition and JWT FAIL_UNAUTHORIZED blocking callers
1. Split LoginHelper into switchUser() + enqueueLogin() so the non-suspend login() can synchronously swap user models (instant, no network) while the backend create-user call fires in the background. This prevents ANRs on main thread while ensuring tags target the correct user after each login() call. loginSuspend() still uses the full login() which awaits the backend operation. 2. Wake enqueueAndWait waiters on FAIL_UNAUTHORIZED and FAIL_PAUSE_OPREPO before re-queuing operations. Re-queued items use waiter=null so future retries don't reference stale waiters. This prevents loginSuspend from hanging forever when a JWT is invalid or the op repo pauses. Fixes SDK-4338 Made-with: Cursor
1 parent 258c8e6 commit 1a4124a

3 files changed

Lines changed: 62 additions & 31 deletions

File tree

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,11 @@ internal class OperationRepo(
288288
if (externalId != null) {
289289
_jwtTokenStore.invalidateJwt(externalId)
290290
Logging.warn("Operation execution failed with 401 Unauthorized, JWT invalidated for user: $externalId. Operations re-queued.")
291+
ops.forEach { it.waiter?.wake(false) }
291292
synchronized(queue) {
292-
ops.reversed().forEach { queue.add(0, it) }
293+
ops.reversed().forEach {
294+
queue.add(0, OperationQueueItem(it.operation, waiter = null, bucket = it.bucket, retries = it.retries))
295+
}
293296
}
294297
dispatchJwtInvalidatedToApp(externalId)
295298
} else {
@@ -331,9 +334,12 @@ internal class OperationRepo(
331334
Logging.error("Operation execution failed with eventual retry, pausing the operation repo: $operations")
332335
// keep the failed operation and pause the operation repo from executing
333336
paused = true
337+
ops.forEach { it.waiter?.wake(false) }
334338
// add back all operations to the front of the queue to be re-executed.
335339
synchronized(queue) {
336-
ops.reversed().forEach { queue.add(0, it) }
340+
ops.reversed().forEach {
341+
queue.add(0, OperationQueueItem(it.operation, waiter = null, bucket = it.bucket, retries = it.retries))
342+
}
337343
}
338344
}
339345
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -384,14 +384,16 @@ internal class OneSignalImp(
384384

385385
if (isBackgroundThreadingEnabled) {
386386
waitForInit(operationName = "login")
387-
suspendifyOnIO { loginHelper.login(externalId, jwtBearerToken) }
387+
val context = loginHelper.switchUser(externalId, jwtBearerToken) ?: return
388+
suspendifyOnIO { loginHelper.enqueueLogin(context) }
388389
} else {
389390
if (!isInitialized) {
390391
throw IllegalStateException("Must call 'initWithContext' before 'login'")
391392
}
393+
val context = loginHelper.switchUser(externalId, jwtBearerToken) ?: return
392394
Thread {
393395
runBlocking(runtimeIoDispatcher) {
394-
loginHelper.login(externalId, jwtBearerToken)
396+
loginHelper.enqueueLogin(context)
395397
}
396398
}.start()
397399
}
@@ -407,11 +409,7 @@ internal class OneSignalImp(
407409
if (!isInitialized) {
408410
throw IllegalStateException("Must call 'initWithContext' before 'logout'")
409411
}
410-
Thread {
411-
runBlocking(runtimeIoDispatcher) {
412-
logoutHelper.logout()
413-
}
414-
}.start()
412+
logoutHelper.logout()
415413
}
416414
}
417415

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/LoginHelper.kt

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,30 @@ class LoginHelper(
1515
private val jwtTokenStore: JwtTokenStore,
1616
private val lock: Any,
1717
) {
18-
suspend fun login(
18+
internal data class LoginEnqueueContext(
19+
val appId: String,
20+
val newIdentityOneSignalId: String,
21+
val externalId: String,
22+
val existingOneSignalId: String?,
23+
)
24+
25+
/**
26+
* Synchronously switches local user models under the login/logout lock.
27+
* Returns context needed for [enqueueLogin], or null if the user was
28+
* already logged in with [externalId] (no switch needed).
29+
*/
30+
internal fun switchUser(
1931
externalId: String,
2032
jwtBearerToken: String? = null,
21-
) {
22-
var currentIdentityExternalId: String? = null
23-
var currentIdentityOneSignalId: String? = null
24-
var newIdentityOneSignalId: String = ""
25-
33+
): LoginEnqueueContext? {
2634
synchronized(lock) {
27-
currentIdentityExternalId = identityModelStore.model.externalId
28-
currentIdentityOneSignalId = identityModelStore.model.onesignalId
35+
val currentExternalId = identityModelStore.model.externalId
36+
val currentOneSignalId = identityModelStore.model.onesignalId
2937

30-
if (currentIdentityExternalId == externalId) {
38+
if (currentExternalId == externalId) {
3139
jwtTokenStore.putJwt(externalId, jwtBearerToken)
3240
operationRepo.forceExecuteOperations()
33-
return
41+
return null
3442
}
3543

3644
jwtTokenStore.putJwt(externalId, jwtBearerToken)
@@ -39,28 +47,47 @@ class LoginHelper(
3947
identityModel.externalId = externalId
4048
}
4149

42-
newIdentityOneSignalId = identityModelStore.model.onesignalId
43-
}
50+
val newOneSignalId = identityModelStore.model.onesignalId
4451

45-
val existingOneSignalId =
46-
if (configModel.useIdentityVerification == true) {
47-
null
48-
} else {
49-
if (currentIdentityExternalId == null) currentIdentityOneSignalId else null
50-
}
52+
val existingOneSignalId =
53+
if (configModel.useIdentityVerification == true) {
54+
null
55+
} else {
56+
if (currentExternalId == null) currentOneSignalId else null
57+
}
5158

59+
return LoginEnqueueContext(configModel.appId, newOneSignalId, externalId, existingOneSignalId)
60+
}
61+
}
62+
63+
/**
64+
* Enqueues the [LoginUserOperation] and suspends until it completes.
65+
*/
66+
internal suspend fun enqueueLogin(context: LoginEnqueueContext) {
5267
val result =
5368
operationRepo.enqueueAndWait(
5469
LoginUserOperation(
55-
configModel.appId,
56-
newIdentityOneSignalId,
57-
externalId,
58-
existingOneSignalId,
70+
context.appId,
71+
context.newIdentityOneSignalId,
72+
context.externalId,
73+
context.existingOneSignalId,
5974
),
6075
)
6176

6277
if (!result) {
6378
Logging.warn("Could not login user")
6479
}
6580
}
81+
82+
/**
83+
* Full login: switches user models then waits for the backend operation.
84+
* Used by [loginSuspend] where the caller can afford to suspend.
85+
*/
86+
suspend fun login(
87+
externalId: String,
88+
jwtBearerToken: String? = null,
89+
) {
90+
val context = switchUser(externalId, jwtBearerToken) ?: return
91+
enqueueLogin(context)
92+
}
6693
}

0 commit comments

Comments
 (0)