Skip to content

Commit 89cca43

Browse files
committed
Replay JWT invalidated event to late-registered listeners
fireJwtInvalidated now buffers the externalId under a synchronized lock when no listeners are subscribed (e.g. during SDK init HYDRATE) and replays it when the first IUserJwtInvalidatedListener is added. Clears pending event on user switch (onModelReplaced) to prevent stale replay.
1 parent 03a333c commit 89cca43

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,45 @@ internal open class UserManager(
5555
private val jwtInvalidatedAppCallbackScope =
5656
CoroutineScope(SupervisorJob() + Dispatchers.Default)
5757

58+
private val jwtInvalidatedLock = Any()
59+
private var pendingJwtInvalidatedExternalId: String? = null
60+
5861
fun addJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
59-
jwtInvalidatedNotifier.subscribe(listener)
62+
val pendingExternalId: String?
63+
synchronized(jwtInvalidatedLock) {
64+
jwtInvalidatedNotifier.subscribe(listener)
65+
pendingExternalId = pendingJwtInvalidatedExternalId
66+
pendingJwtInvalidatedExternalId = null
67+
}
68+
pendingExternalId?.let {
69+
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(it))
70+
}
6071
}
6172

6273
fun removeJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) {
6374
jwtInvalidatedNotifier.unsubscribe(listener)
6475
}
6576

6677
/**
67-
* Schedules [IUserJwtInvalidatedListener] delivery on a background dispatcher so HYDRATE and
68-
* operation-repo paths can finish internal work before app code runs.
78+
* Fires [IUserJwtInvalidatedListener] to all subscribers asynchronously so the caller
79+
* (e.g. OperationRepo) is not blocked by developer code. If no listeners are registered yet
80+
* (e.g. during SDK init), stores the externalId so it can be replayed when the first
81+
* listener is added via [addJwtInvalidatedListener].
6982
*/
7083
fun fireJwtInvalidated(externalId: String) {
71-
jwtInvalidatedAppCallbackScope.launch {
72-
runCatching {
73-
jwtInvalidatedNotifier.fire { listener ->
74-
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(externalId))
84+
synchronized(jwtInvalidatedLock) {
85+
if (jwtInvalidatedNotifier.hasSubscribers) {
86+
jwtInvalidatedAppCallbackScope.launch {
87+
runCatching {
88+
jwtInvalidatedNotifier.fire { listener ->
89+
listener.onUserJwtInvalidated(UserJwtInvalidatedEvent(externalId))
90+
}
91+
}.onFailure {
92+
Logging.warn("Failed to deliver JWT invalidated event for externalId=$externalId", it)
93+
}
7594
}
76-
}.onFailure {
77-
Logging.warn("Failed to deliver JWT invalidated event for externalId=$externalId", it)
95+
} else {
96+
pendingJwtInvalidatedExternalId = externalId
7897
}
7998
}
8099
}
@@ -297,7 +316,11 @@ internal open class UserManager(
297316
override fun onModelReplaced(
298317
model: IdentityModel,
299318
tag: String,
300-
) { }
319+
) {
320+
synchronized(jwtInvalidatedLock) {
321+
pendingJwtInvalidatedExternalId = null
322+
}
323+
}
301324

302325
override fun onModelUpdated(
303326
args: ModelChangedArgs,

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.onesignal.user.internal
22

33
import com.onesignal.core.internal.language.ILanguageContext
44
import com.onesignal.mocks.MockHelper
5+
import com.onesignal.user.internal.identity.IdentityModel
56
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
67
import com.onesignal.user.internal.subscriptions.SubscriptionList
78
import io.kotest.assertions.throwables.shouldNotThrow
@@ -15,6 +16,8 @@ import io.mockk.mockk
1516
import io.mockk.runs
1617
import io.mockk.slot
1718
import io.mockk.verify
19+
import kotlin.reflect.full.memberFunctions
20+
import kotlin.reflect.jvm.isAccessible
1821

1922
class UserManagerTests : FunSpec({
2023

@@ -235,4 +238,27 @@ class UserManagerTests : FunSpec({
235238
)
236239
}
237240
}
241+
242+
test("onModelReplaced clears pendingJwtInvalidatedExternalId") {
243+
// Given
244+
val mockSubscriptionManager = mockk<ISubscriptionManager>()
245+
val userManager =
246+
UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), MockHelper.customEventController(), MockHelper.languageContext())
247+
248+
// Fire a JWT invalidated event with no subscribers, so it pends
249+
val fireMethod = UserManager::class.memberFunctions.first { it.name == "fireJwtInvalidated" }
250+
fireMethod.isAccessible = true
251+
fireMethod.call(userManager, "user-alice")
252+
253+
// Verify pending state is set
254+
val pendingField = UserManager::class.java.getDeclaredField("pendingJwtInvalidatedExternalId")
255+
pendingField.isAccessible = true
256+
pendingField.get(userManager) shouldBe "user-alice"
257+
258+
// When — user switches (model replaced)
259+
userManager.onModelReplaced(IdentityModel(), "test")
260+
261+
// Then — pending state should be cleared
262+
pendingField.get(userManager) shouldBe null
263+
}
238264
})

0 commit comments

Comments
 (0)