Skip to content

Commit 86b6c36

Browse files
committed
Fix stale JWT replay on user switch and IAM fetch with null ryw data
- UserManager.onModelReplaced: clear pendingJwtInvalidatedExternalId under jwtInvalidatedLock to prevent stale JWT events replaying to a new user - LoginUserOperationExecutor: call resolveConditionsWithID unconditionally to clean up stale IamFetchReadyCondition("") entries - InAppMessagesManager: remove null guards so IAMs are fetched even when the consistency condition resolves with null rywData; adjust onJwtUpdated to use pendingJwtRetryExternalId as sole sentinel - IInAppBackendService/InAppBackendService: accept nullable RywData and branch internally to fetch without ryw token when null Made-with: Cursor
1 parent 071ecef commit 86b6c36

8 files changed

Lines changed: 117 additions & 22 deletions

File tree

OneSignalSDK/detekt/detekt-baseline-in-app-messages.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
<ID>LongMethod:InAppRepository.kt$InAppRepository$override suspend fun cleanCachedInAppMessages()</ID>
6969
<ID>LongParameterList:IInAppBackendService.kt$IInAppBackendService$( appId: String, subscriptionId: String, variantId: String?, messageId: String, clickId: String?, isFirstClick: Boolean, )</ID>
7070
<ID>LongParameterList:InAppDisplayer.kt$InAppDisplayer$( private val _applicationService: IApplicationService, private val _lifecycle: IInAppLifecycleService, private val _promptFactory: IInAppMessagePromptFactory, private val _backend: IInAppBackendService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _time: ITime, )</ID>
71-
<ID>LongParameterList:InAppMessagesManager.kt$InAppMessagesManager$( private val _applicationService: IApplicationService, private val _sessionService: ISessionService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _userManager: IUserManager, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _outcomeEventsController: IOutcomeEventsController, private val _state: InAppStateService, private val _prefs: IInAppPreferencesController, private val _repository: IInAppRepository, private val _backend: IInAppBackendService, private val _triggerController: ITriggerController, private val _triggerModelStore: TriggerModelStore, private val _displayer: IInAppDisplayer, private val _lifecycle: IInAppLifecycleService, private val _languageContext: ILanguageContext, private val _time: ITime, private val _consistencyManager: IConsistencyManager, )</ID>
71+
<ID>LongParameterList:InAppMessagesManager.kt$InAppMessagesManager$( private val _applicationService: IApplicationService, private val _sessionService: ISessionService, private val _influenceManager: IInfluenceManager, private val _configModelStore: ConfigModelStore, private val _userManager: IUserManager, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _outcomeEventsController: IOutcomeEventsController, private val _state: InAppStateService, private val _prefs: IInAppPreferencesController, private val _repository: IInAppRepository, private val _backend: IInAppBackendService, private val _triggerController: ITriggerController, private val _triggerModelStore: TriggerModelStore, private val _displayer: IInAppDisplayer, private val _lifecycle: IInAppLifecycleService, private val _languageContext: ILanguageContext, private val _time: ITime, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, )</ID>
7272
<ID>LongParameterList:OneSignalAnimate.kt$OneSignalAnimate$( view: View, deltaFromY: Float, deltaToY: Float, duration: Int, interpolator: Interpolator?, animCallback: Animation.AnimationListener?, )</ID>
7373
<ID>MagicNumber:DraggableRelativeLayout.kt$DraggableRelativeLayout$3</ID>
7474
<ID>MagicNumber:DraggableRelativeLayout.kt$DraggableRelativeLayout$3000</ID>
@@ -103,12 +103,12 @@
103103
<ID>ReturnCount:DraggableRelativeLayout.kt$DraggableRelativeLayout.&lt;no name provided&gt;$override fun clampViewPositionVertical( child: View, top: Int, dy: Int, ): Int</ID>
104104
<ID>ReturnCount:DynamicTriggerController.kt$DynamicTriggerController$fun dynamicTriggerShouldFire(trigger: Trigger): Boolean</ID>
105105
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$override suspend fun getIAMData( appId: String, messageId: String, variantId: String?, ): GetIAMDataResponse</ID>
106-
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$private suspend fun attemptFetchWithRetries( baseUrl: String, rywData: RywData, sessionDurationProvider: () -&gt; Long, ): List&lt;InAppMessage&gt;?</ID>
106+
<ID>ReturnCount:InAppBackendService.kt$InAppBackendService$private suspend fun attemptFetchWithRetries( baseUrl: String, rywData: RywData, sessionDurationProvider: () -&gt; Long, jwt: String? = null, ): List&lt;InAppMessage&gt;?</ID>
107107
<ID>ReturnCount:InAppHydrator.kt$InAppHydrator$fun hydrateIAMMessageContent(jsonObject: JSONObject): InAppMessageContent?</ID>
108108
<ID>ReturnCount:InAppMessage.kt$InAppMessage$private fun parseEndTimeJson(json: JSONObject): Date?</ID>
109109
<ID>ReturnCount:InAppMessagePreviewHandler.kt$InAppMessagePreviewHandler$private fun inAppPreviewPushUUID(payload: JSONObject): String?</ID>
110110
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$override fun onMessageWasDisplayed(message: InAppMessage)</ID>
111-
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$private suspend fun fetchMessages(rywData: RywData)</ID>
111+
<ID>ReturnCount:InAppMessagesManager.kt$InAppMessagesManager$private suspend fun fetchMessages(rywData: RywData?)</ID>
112112
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun evaluateMessageTriggers(message: InAppMessage): Boolean</ID>
113113
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun isTriggerOnMessage( message: InAppMessage, triggersKeys: Collection&lt;String&gt;, ): Boolean</ID>
114114
<ID>ReturnCount:TriggerController.kt$TriggerController$override fun messageHasOnlyDynamicTriggers(message: InAppMessage): Boolean</ID>
@@ -124,7 +124,7 @@
124124
<ID>TooManyFunctions:InAppBackendService.kt$InAppBackendService : IInAppBackendService</ID>
125125
<ID>TooManyFunctions:InAppMessage.kt$InAppMessage : IInAppMessage</ID>
126126
<ID>TooManyFunctions:InAppMessageView.kt$InAppMessageView</ID>
127-
<ID>TooManyFunctions:InAppMessagesManager.kt$InAppMessagesManager : IInAppMessagesManagerIStartableServiceISubscriptionChangedHandlerISingletonModelStoreChangeHandlerIInAppLifecycleEventHandlerITriggerHandlerISessionLifecycleHandlerIApplicationLifecycleHandler</ID>
127+
<ID>TooManyFunctions:InAppMessagesManager.kt$InAppMessagesManager : IInAppMessagesManagerIStartableServiceISubscriptionChangedHandlerISingletonModelStoreChangeHandlerIInAppLifecycleEventHandlerITriggerHandlerISessionLifecycleHandlerIApplicationLifecycleHandlerIJwtUpdateListener</ID>
128128
<ID>TooManyFunctions:TriggerController.kt$TriggerController : ITriggerControllerIModelStoreChangeHandler</ID>
129129
<ID>TooManyFunctions:WebViewManager.kt$WebViewManager : IActivityLifecycleHandler</ID>
130130
<ID>UndocumentedPublicClass:TriggerModel.kt$TriggerModel : Model</ID>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,11 @@ internal open class UserManager(
316316
override fun onModelReplaced(
317317
model: IdentityModel,
318318
tag: String,
319-
) { }
319+
) {
320+
synchronized(jwtInvalidatedLock) {
321+
pendingJwtInvalidatedExternalId = null
322+
}
323+
}
320324

321325
override fun onModelUpdated(
322326
args: ModelChangedArgs,

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,8 @@ internal class LoginUserOperationExecutor(
230230

231231
if (response.rywData != null) {
232232
_consistencyManager.setRywData(backendOneSignalId, IamFetchRywTokenKey.USER, response.rywData)
233-
} else {
234-
_consistencyManager.resolveConditionsWithID(IamFetchReadyCondition.ID)
235233
}
234+
_consistencyManager.resolveConditionsWithID(IamFetchReadyCondition.ID)
236235

237236
val wasPossiblyAnUpsert = identities.isNotEmpty()
238237
val followUpOperations =

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
})

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,8 @@ internal class InAppMessagesManager(
160160
suspendifyOnIO {
161161
val updateConditionDeferred =
162162
_consistencyManager.getRywDataFromAwaitableCondition(IamFetchReadyCondition(newOneSignalId))
163-
val rywToken = updateConditionDeferred.await()
164-
if (rywToken != null) {
165-
fetchMessages(rywToken)
166-
}
163+
val rywData = updateConditionDeferred.await()
164+
fetchMessages(rywData)
167165
}
168166
}
169167
}
@@ -294,15 +292,12 @@ internal class InAppMessagesManager(
294292
val iamFetchCondition =
295293
_consistencyManager.getRywDataFromAwaitableCondition(IamFetchReadyCondition(onesignalId))
296294
val rywData = iamFetchCondition.await()
297-
298-
if (rywData != null) {
299-
fetchMessages(rywData)
300-
}
295+
fetchMessages(rywData)
301296
}
302297
}
303298

304299
// called when a new push subscription is added, or the app id is updated, or a new session starts
305-
private suspend fun fetchMessages(rywData: RywData) {
300+
private suspend fun fetchMessages(rywData: RywData?) {
306301
// We only want to fetch IAMs if we know the app is in the
307302
// foreground, as we don't want to do this for background
308303
// events (such as push received), wasting resources for
@@ -1046,9 +1041,10 @@ internal class InAppMessagesManager(
10461041
}
10471042

10481043
override fun onJwtUpdated(externalId: String) {
1049-
val retryExternalId = pendingJwtRetryExternalId
1044+
val retryExternalId = pendingJwtRetryExternalId ?: return
1045+
if (externalId != retryExternalId) return
1046+
10501047
val retryRywData = pendingJwtRetryRywData
1051-
if (retryExternalId == null || retryRywData == null || externalId != retryExternalId) return
10521048

10531049
Logging.debug("InAppMessagesManager.onJwtUpdated: JWT refreshed for $externalId, retrying IAM fetch")
10541050
pendingJwtRetryExternalId = null

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal interface IInAppBackendService {
2424
aliasLabel: String,
2525
aliasValue: String,
2626
subscriptionId: String,
27-
rywData: RywData,
27+
rywData: RywData?,
2828
sessionDurationProvider: () -> Long,
2929
jwt: String? = null,
3030
): List<InAppMessage>?

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ internal class InAppBackendService(
2929
aliasLabel: String,
3030
aliasValue: String,
3131
subscriptionId: String,
32-
rywData: RywData,
32+
rywData: RywData?,
3333
sessionDurationProvider: () -> Long,
3434
jwt: String?,
3535
): List<InAppMessage>? {
36+
val baseUrl = "apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions/$subscriptionId/iams"
37+
38+
if (rywData == null) {
39+
return fetchInAppMessagesWithoutRywToken(baseUrl, sessionDurationProvider, jwt)
40+
}
41+
3642
val rywDelay = rywData.rywDelay ?: DEFAULT_RYW_DELAY_MS
37-
delay(rywDelay) // Delay by the specified amount
43+
delay(rywDelay)
3844

39-
val baseUrl = "apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions/$subscriptionId/iams"
4045
return attemptFetchWithRetries(baseUrl, rywData, sessionDurationProvider, jwt)
4146
}
4247

OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,71 @@ class InAppMessagesManagerTests : FunSpec({
14361436
}
14371437
}
14381438

1439+
context("Null RYW data") {
1440+
test("fetchMessagesWhenConditionIsMet fetches without ryw token when condition resolves with null") {
1441+
// Given
1442+
every { mocks.userManager.onesignalId } returns "onesignal-id"
1443+
every { mocks.applicationService.isInForeground } returns true
1444+
every { mocks.pushSubscription.id } returns "subscription-id"
1445+
1446+
val nullRywDeferred = mockk<CompletableDeferred<RywData?>> {
1447+
coEvery { await() } returns null
1448+
}
1449+
coEvery {
1450+
mocks.consistencyManager.getRywDataFromAwaitableCondition(any<IamFetchReadyCondition>())
1451+
} returns nullRywDeferred
1452+
coEvery {
1453+
mocks.backend.listInAppMessages(any(), any(), any(), any(), isNull(), any(), any())
1454+
} returns listOf(mocks.createInAppMessage())
1455+
1456+
// When
1457+
mocks.inAppMessagesManager.onSessionStarted()
1458+
awaitIO()
1459+
1460+
// Then — should call listInAppMessages with null rywData
1461+
coVerify(exactly = 1) {
1462+
mocks.backend.listInAppMessages(any(), any(), any(), any(), isNull(), any(), any())
1463+
}
1464+
}
1465+
1466+
test("onJwtUpdated retries with null rywData when pendingJwtRetryExternalId is set") {
1467+
// Given
1468+
every { mocks.userManager.onesignalId } returns "onesignal-id"
1469+
every { mocks.applicationService.isInForeground } returns true
1470+
every { mocks.pushSubscription.id } returns "subscription-id"
1471+
mocks.identityModelStore.model.externalId = "test-external-id"
1472+
1473+
val nullRywDeferred = mockk<CompletableDeferred<RywData?>> {
1474+
coEvery { await() } returns null
1475+
}
1476+
coEvery {
1477+
mocks.consistencyManager.getRywDataFromAwaitableCondition(any<IamFetchReadyCondition>())
1478+
} returns nullRywDeferred
1479+
1480+
// First call throws 401, second (retry) succeeds
1481+
coEvery {
1482+
mocks.backend.listInAppMessages(any(), any(), any(), any(), isNull(), any(), any())
1483+
} throws BackendException(401, "Unauthorized") andThen listOf(mocks.createInAppMessage())
1484+
1485+
// Trigger initial fetch that will 401
1486+
mocks.inAppMessagesManager.onSessionStarted()
1487+
awaitIO()
1488+
1489+
mocks.getPendingJwtRetryExternalId(mocks.inAppMessagesManager) shouldBe "test-external-id"
1490+
mocks.getPendingJwtRetryRywData(mocks.inAppMessagesManager) shouldBe null
1491+
1492+
// When — JWT is refreshed
1493+
mocks.inAppMessagesManager.onJwtUpdated("test-external-id")
1494+
awaitIO()
1495+
1496+
// Then — should have retried, pending state cleared
1497+
coVerify(exactly = 2) {
1498+
mocks.backend.listInAppMessages(any(), any(), any(), any(), isNull(), any(), any())
1499+
}
1500+
mocks.getPendingJwtRetryExternalId(mocks.inAppMessagesManager) shouldBe null
1501+
}
1502+
}
1503+
14391504
context("JWT 401 Retry") {
14401505
test("fetchMessages stores pending retry state on 401 BackendException") {
14411506
// Given

0 commit comments

Comments
 (0)