Skip to content

Commit e1d6dfa

Browse files
nan-liclaude
andcommitted
feat(iv): IAM IV integration — alias-based fetch + JWT retry on 401
Adds IInAppBackendService.listInAppMessagesIv (alias-based URL, JWT bearer support, throws BackendException on 401/403) alongside the existing legacy listInAppMessages so Phase 1 users keep the legacy endpoint untouched. InAppMessagesManager: - Inject JwtTokenStore + IdentityVerificationService. - Subscribe to JwtTokenStore as IJwtUpdateListener so the IAM fetch can retry once the developer supplies a fresh JWT (via updateUserJwt) after a 401. - fetchMessages outer-gates on newCodePathsRun: dispatches to fetchIvOrSaveRetry under IV, falls through to legacy otherwise. - fetchIvOrSaveRetry inner-gates on ivBehaviorActive: external_id+JWT under IV; onesignal_id+null JWT for Phase 3 (exercises new endpoint structurally, no IV behavior). - @volatile pendingJwtRetry{ExternalId,RywData}; cleared on user-switch via existing identityModelChangeHandler.onModelReplaced. - 401 from IV fetch resets the rate-limiter so the retry isn't throttled. Cross-module visibility: drop `internal` modifier from JwtTokenStore, IdentityVerificationService, IJwtUpdateListener, IFeatureManager, FeatureFlag, FeatureActivationMode (still package-internal by convention via `.internal.` paths; Kotlin-visible cross-module). Test fixtures updated: InAppMessagesManagerTests, LogoutHelperTests, LoginUserOperationExecutorTests get the new ctor params. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 97a3799 commit e1d6dfa

11 files changed

Lines changed: 203 additions & 27 deletions

File tree

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/IdentityVerificationService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.onesignal.user.internal.jwt.JwtRequirement
2424
* Consumers (e.g. OperationRepo) wire post-HYDRATE behavior via [setOnJwtConfigHydratedHandler];
2525
* the handler fires once per HYDRATE with `ivRequired = useIdentityVerification == REQUIRED`.
2626
*/
27-
internal class IdentityVerificationService(
27+
class IdentityVerificationService(
2828
private val featureManager: IFeatureManager,
2929
private val configModelStore: ConfigModelStore,
3030
) : IStartableService, ISingletonModelStoreChangeHandler<ConfigModel> {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.onesignal.core.internal.features
33
/**
44
* Controls when remote config changes for a feature are applied.
55
*/
6-
internal enum class FeatureActivationMode {
6+
enum class FeatureActivationMode {
77
/**
88
* Apply config changes immediately during the current app run.
99
*/
@@ -20,7 +20,7 @@ internal enum class FeatureActivationMode {
2020
*
2121
* [key] values are **lowercase** strings as returned from remote config / Turbine `features` arrays.
2222
*/
23-
internal enum class FeatureFlag(
23+
enum class FeatureFlag(
2424
val key: String,
2525
val activationMode: FeatureActivationMode
2626
) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.onesignal.core.internal.config.ConfigModelStore
1010
import com.onesignal.debug.internal.logging.Logging
1111
import kotlinx.serialization.json.JsonObject
1212

13-
internal interface IFeatureManager {
13+
interface IFeatureManager {
1414
fun isEnabled(feature: FeatureFlag): Boolean
1515

1616
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package com.onesignal.user.internal.jwt
55
* Listeners should call [JwtTokenStore.getJwt] for the current value — event delivery
66
* order is not guaranteed to match mutation order across concurrent writers.
77
*/
8-
internal interface IJwtUpdateListener {
8+
interface IJwtUpdateListener {
99
/** Fired when a JWT was added or refreshed (`putJwt`), or when stale entries are pruned. */
1010
fun onJwtUpdated(externalId: String) {}
1111

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import org.json.JSONObject
1414
* can still resolve their JWT at execution time. Storage is unconditional; *usage* of JWTs is
1515
* gated on `IdentityVerificationService.ivBehaviorActive`.
1616
*/
17-
internal class JwtTokenStore(
17+
class JwtTokenStore(
1818
private val _prefs: IPreferencesService,
1919
) : IEventNotifier<IJwtUpdateListener> {
2020
private val tokens: MutableMap<String, String> = mutableMapOf()

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ class LogoutHelperTests : FunSpec({
4949
userSwitcher = mockUserSwitcher,
5050
operationRepo = mockOperationRepo,
5151
configModel = mockConfigModel,
52+
subscriptionModelStore = mockk(relaxed = true),
53+
identityVerificationService = mockk(relaxed = true) {
54+
every { newCodePathsRun } returns false
55+
every { ivBehaviorActive } returns false
56+
},
5257
lock = logoutLock,
5358
)
5459

@@ -80,6 +85,11 @@ class LogoutHelperTests : FunSpec({
8085
userSwitcher = mockUserSwitcher,
8186
operationRepo = mockOperationRepo,
8287
configModel = mockConfigModel,
88+
subscriptionModelStore = mockk(relaxed = true),
89+
identityVerificationService = mockk(relaxed = true) {
90+
every { newCodePathsRun } returns false
91+
every { ivBehaviorActive } returns false
92+
},
8393
lock = logoutLock,
8494
)
8595

@@ -120,6 +130,11 @@ class LogoutHelperTests : FunSpec({
120130
userSwitcher = mockUserSwitcher,
121131
operationRepo = mockOperationRepo,
122132
configModel = mockConfigModel,
133+
subscriptionModelStore = mockk(relaxed = true),
134+
identityVerificationService = mockk(relaxed = true) {
135+
every { newCodePathsRun } returns false
136+
every { ivBehaviorActive } returns false
137+
},
123138
lock = logoutLock,
124139
)
125140

@@ -153,6 +168,11 @@ class LogoutHelperTests : FunSpec({
153168
userSwitcher = mockUserSwitcher,
154169
operationRepo = mockOperationRepo,
155170
configModel = mockConfigModel,
171+
subscriptionModelStore = mockk(relaxed = true),
172+
identityVerificationService = mockk(relaxed = true) {
173+
every { newCodePathsRun } returns false
174+
every { ivBehaviorActive } returns false
175+
},
156176
lock = logoutLock,
157177
)
158178

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class LoginUserOperationExecutorTests : FunSpec({
7979
mockSubscriptionsModelStore,
8080
MockHelper.configModelStore(),
8181
MockHelper.languageContext(),
82-
getJwtTokenStore(), getIdentityVerificationService(),
82+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
8383
)
8484
val operations =
8585
listOf<Operation>(
@@ -124,7 +124,7 @@ class LoginUserOperationExecutorTests : FunSpec({
124124
mockSubscriptionsModelStore,
125125
MockHelper.configModelStore(),
126126
MockHelper.languageContext(),
127-
getJwtTokenStore(), getIdentityVerificationService(),
127+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
128128
)
129129
val operations =
130130
listOf<Operation>(
@@ -153,7 +153,7 @@ class LoginUserOperationExecutorTests : FunSpec({
153153
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
154154

155155
val loginUserOperationExecutor =
156-
LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
156+
LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
157157
val operations =
158158
listOf<Operation>(
159159
LoginUserOperation(appId, localOneSignalId, null, null),
@@ -181,7 +181,7 @@ class LoginUserOperationExecutorTests : FunSpec({
181181
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
182182

183183
val loginUserOperationExecutor =
184-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
184+
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
185185
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", null))
186186

187187
// When
@@ -219,7 +219,7 @@ class LoginUserOperationExecutorTests : FunSpec({
219219
mockSubscriptionsModelStore,
220220
MockHelper.configModelStore(),
221221
MockHelper.languageContext(),
222-
getJwtTokenStore(), getIdentityVerificationService(),
222+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
223223
)
224224
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", null))
225225

@@ -248,7 +248,7 @@ class LoginUserOperationExecutorTests : FunSpec({
248248
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
249249

250250
val loginUserOperationExecutor =
251-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
251+
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
252252
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
253253

254254
// When
@@ -284,7 +284,7 @@ class LoginUserOperationExecutorTests : FunSpec({
284284
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
285285

286286
val loginUserOperationExecutor =
287-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
287+
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
288288
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
289289

290290
// When
@@ -320,7 +320,7 @@ class LoginUserOperationExecutorTests : FunSpec({
320320
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
321321

322322
val loginUserOperationExecutor =
323-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
323+
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
324324
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
325325

326326
// When
@@ -358,7 +358,7 @@ class LoginUserOperationExecutorTests : FunSpec({
358358
val mockSubscriptionsModelStore = mockk<SubscriptionModelStore>()
359359

360360
val loginUserOperationExecutor =
361-
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService())
361+
LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true))
362362
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId"))
363363

364364
// When
@@ -409,7 +409,7 @@ class LoginUserOperationExecutorTests : FunSpec({
409409
mockSubscriptionsModelStore,
410410
MockHelper.configModelStore(),
411411
MockHelper.languageContext(),
412-
getJwtTokenStore(), getIdentityVerificationService(),
412+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
413413
)
414414
val operations =
415415
listOf<Operation>(
@@ -514,7 +514,7 @@ class LoginUserOperationExecutorTests : FunSpec({
514514
mockSubscriptionsModelStore,
515515
MockHelper.configModelStore(),
516516
MockHelper.languageContext(),
517-
getJwtTokenStore(), getIdentityVerificationService(),
517+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
518518
)
519519
val operations =
520520
listOf<Operation>(
@@ -603,7 +603,7 @@ class LoginUserOperationExecutorTests : FunSpec({
603603
mockSubscriptionsModelStore,
604604
MockHelper.configModelStore(),
605605
MockHelper.languageContext(),
606-
getJwtTokenStore(), getIdentityVerificationService(),
606+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
607607
)
608608
val operations =
609609
listOf<Operation>(
@@ -678,7 +678,7 @@ class LoginUserOperationExecutorTests : FunSpec({
678678
mockSubscriptionsModelStore,
679679
MockHelper.configModelStore(),
680680
MockHelper.languageContext(),
681-
getJwtTokenStore(), getIdentityVerificationService(),
681+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
682682
)
683683
val operations =
684684
listOf<Operation>(
@@ -744,7 +744,7 @@ class LoginUserOperationExecutorTests : FunSpec({
744744
mockSubscriptionsModelStore,
745745
MockHelper.configModelStore(),
746746
MockHelper.languageContext(),
747-
getJwtTokenStore(), getIdentityVerificationService(),
747+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
748748
)
749749
// anonymous Login request
750750
val operations = listOf<Operation>(LoginUserOperation(appId, localOneSignalId, null, null))
@@ -791,7 +791,7 @@ class LoginUserOperationExecutorTests : FunSpec({
791791
mockSubscriptionsModelStore,
792792
MockHelper.configModelStore(),
793793
MockHelper.languageContext(),
794-
getJwtTokenStore(), getIdentityVerificationService(),
794+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
795795
)
796796

797797
// send PUSH then EMAIL (local IDs 1,2) — order differs from backend response
@@ -856,7 +856,7 @@ class LoginUserOperationExecutorTests : FunSpec({
856856
mockSubscriptionsModelStore,
857857
configModelStore,
858858
MockHelper.languageContext(),
859-
getJwtTokenStore(), getIdentityVerificationService(),
859+
getJwtTokenStore(), getIdentityVerificationService(), mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
860860
)
861861

862862
val ops =
@@ -915,6 +915,7 @@ class LoginUserOperationExecutorTests : FunSpec({
915915
MockHelper.languageContext(),
916916
getJwtTokenStore(),
917917
getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true),
918+
mockk<com.onesignal.common.consistency.models.IConsistencyManager>(relaxed = true),
918919
)
919920

920921
// LoginUserOperation has existingOnesignalId AND externalId — the input shape that

0 commit comments

Comments
 (0)