Skip to content

Commit 474f1b3

Browse files
committed
refactor(iv): replace Boolean? with JwtRequirement enum
`ConfigModel.useIdentityVerification` was `Boolean?` where `null` silently meant "pre-HYDRATE, not yet known" — easy to miss and easy to confuse with the SDK-side `FeatureFlag.IDENTITY_VERIFICATION`. Replace with a tri-state `JwtRequirement` enum (UNKNOWN / NOT_REQUIRED / REQUIRED) that names the customer-config side explicitly and ties to the backend param (`jwt_required`). - New `JwtRequirement` with a `fromBoolean(Boolean?)` helper so `ConfigModelStoreListener` can map the backend's nullable boolean. - `ConfigModel.useIdentityVerification` is now `internal var … : JwtRequirement` (internal because the type is internal-only). Backing storage is unchanged — still a nullable boolean under the hood, so no cache migration needed. - `IdentityVerificationGates.update` takes `jwtRequirement: JwtRequirement`; `required` is derived locally as `== REQUIRED`. - Tests updated to use enum values.
1 parent 790e997 commit 474f1b3

8 files changed

Lines changed: 79 additions & 34 deletions

File tree

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.onesignal.core.internal.config
22

33
import com.onesignal.common.modeling.Model
44
import com.onesignal.core.internal.http.OneSignalService.ONESIGNAL_API_BASE_URL
5+
import com.onesignal.user.internal.jwt.JwtRequirement
56
import org.json.JSONArray
67
import org.json.JSONObject
78

@@ -236,11 +237,18 @@ class ConfigModel : Model() {
236237
setBooleanProperty(::enterprise.name, value)
237238
}
238239

239-
/** Mirrors backend `jwt_required`. `null` = pre-HYDRATE (distinguish from `false`). */
240-
var useIdentityVerification: Boolean?
241-
get() = getOptBooleanProperty(::useIdentityVerification.name)
240+
/** Mirrors backend `jwt_required`. Pre-HYDRATE callers see [JwtRequirement.UNKNOWN]. */
241+
internal var useIdentityVerification: JwtRequirement
242+
get() = JwtRequirement.fromBoolean(getOptBooleanProperty(::useIdentityVerification.name))
242243
set(value) {
243-
setOptBooleanProperty(::useIdentityVerification.name, value)
244+
setOptBooleanProperty(
245+
::useIdentityVerification.name,
246+
when (value) {
247+
JwtRequirement.UNKNOWN -> null
248+
JwtRequirement.NOT_REQUIRED -> false
249+
JwtRequirement.REQUIRED -> true
250+
},
251+
)
244252
}
245253

246254
/**

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.onesignal.core.internal.config.ConfigModel
1010
import com.onesignal.core.internal.config.ConfigModelStore
1111
import com.onesignal.core.internal.startup.IStartableService
1212
import com.onesignal.debug.internal.logging.Logging
13+
import com.onesignal.user.internal.jwt.JwtRequirement
1314
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
1415
import kotlinx.coroutines.delay
1516
import java.net.HttpURLConnection
@@ -85,7 +86,9 @@ internal class ConfigModelStoreListener(
8586

8687
// these are only copied from the backend params when the backend has set them.
8788
params.enterprise?.let { config.enterprise = it }
88-
params.useIdentityVerification?.let { config.useIdentityVerification = it }
89+
params.useIdentityVerification?.let {
90+
config.useIdentityVerification = JwtRequirement.fromBoolean(it)
91+
}
8992
params.firebaseAnalytics?.let { config.firebaseAnalytics = it }
9093
params.restoreTTLFilter?.let { config.restoreTTLFilter = it }
9194
params.clearGroupOnSummaryClick?.let { config.clearGroupOnSummaryClick = it }

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal enum class FeatureFlag(
2424
val key: String,
2525
val activationMode: FeatureActivationMode
2626
) {
27+
// Threading mode is selected once per app startup to avoid mixed-mode behavior mid-session.
2728
// Remote key (lowercase) must match backend / Turbine flag id.
2829
SDK_BACKGROUND_THREADING(
2930
"sdk_background_threading",

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
@@ -150,7 +150,7 @@ internal class FeatureManager(
150150
FeatureFlag.IDENTITY_VERIFICATION ->
151151
IdentityVerificationGates.update(
152152
featureFlagOn = enabled,
153-
jwtRequired = model.useIdentityVerification,
153+
jwtRequirement = model.useIdentityVerification,
154154
source = "FeatureManager:${feature.activationMode}"
155155
)
156156
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,21 @@ internal object IdentityVerificationGates {
2323
/** Idempotent. [source] is logged for traceability when gates change. */
2424
fun update(
2525
featureFlagOn: Boolean,
26-
jwtRequired: Boolean?,
26+
jwtRequirement: JwtRequirement,
2727
source: String,
2828
) {
2929
val previousNewCode = newCodePathsRun
3030
val previousIvActive = ivBehaviorActive
3131

32-
newCodePathsRun = featureFlagOn || (jwtRequired == true)
33-
ivBehaviorActive = jwtRequired == true
32+
val required = jwtRequirement == JwtRequirement.REQUIRED
33+
newCodePathsRun = featureFlagOn || required
34+
ivBehaviorActive = required
3435

3536
if (previousNewCode != newCodePathsRun || previousIvActive != ivBehaviorActive) {
3637
Logging.info(
3738
"OneSignal: IdentityVerificationGates updated: " +
3839
"newCodePathsRun=$newCodePathsRun, ivBehaviorActive=$ivBehaviorActive " +
39-
"(source=$source, featureFlagOn=$featureFlagOn, jwtRequired=$jwtRequired)",
40+
"(source=$source, featureFlagOn=$featureFlagOn, jwtRequirement=$jwtRequirement)",
4041
)
4142
} else {
4243
Logging.debug(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.onesignal.user.internal.jwt
2+
3+
/**
4+
* Customer-side JWT requirement, mirrored from the backend `jwt_required` remote param.
5+
* Explicit [UNKNOWN] so callers can distinguish pre-HYDRATE (no value yet) from
6+
* [NOT_REQUIRED] (customer opted out).
7+
*
8+
* Represents only the customer-config side of Identity Verification; do not confuse
9+
* with [com.onesignal.core.internal.features.FeatureFlag.IDENTITY_VERIFICATION] (our
10+
* SDK-side rollout switch).
11+
*/
12+
internal enum class JwtRequirement {
13+
/** Remote params have not been fetched yet. Treat as non-IV until known. */
14+
UNKNOWN,
15+
16+
/** Customer config `jwt_required=false`. No JWT signing. */
17+
NOT_REQUIRED,
18+
19+
/** Customer config `jwt_required=true`. IV-specific behavior active. */
20+
REQUIRED,
21+
;
22+
23+
companion object {
24+
fun fromBoolean(value: Boolean?): JwtRequirement =
25+
when (value) {
26+
null -> UNKNOWN
27+
false -> NOT_REQUIRED
28+
true -> REQUIRED
29+
}
30+
}
31+
}

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/features/FeatureManagerTests.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.onesignal.common.threading.ThreadingMode
55
import com.onesignal.core.internal.config.ConfigModel
66
import com.onesignal.core.internal.config.ConfigModelStore
77
import com.onesignal.user.internal.jwt.IdentityVerificationGates
8+
import com.onesignal.user.internal.jwt.JwtRequirement
89
import io.kotest.core.spec.style.FunSpec
910
import io.kotest.matchers.shouldBe
1011
import io.mockk.every
@@ -16,13 +17,13 @@ import io.mockk.runs
1617
class FeatureManagerTests : FunSpec({
1718
beforeEach {
1819
ThreadingMode.useBackgroundThreading = false
19-
IdentityVerificationGates.update(false, null, "test-reset")
20+
IdentityVerificationGates.update(false, JwtRequirement.UNKNOWN, "test-reset")
2021
}
2122

2223
fun stubConfigModel(model: ConfigModel) {
2324
every { model.sdkRemoteFeatureFlags } returns emptyList()
2425
every { model.sdkRemoteFeatureFlagMetadata } returns null
25-
every { model.useIdentityVerification } returns null
26+
every { model.useIdentityVerification } returns JwtRequirement.UNKNOWN
2627
}
2728

2829
test("initial state enables BACKGROUND_THREADING when key is present in sdk remote flags") {
@@ -180,7 +181,7 @@ class FeatureManagerTests : FunSpec({
180181
test("ERROR STATE: flag off + jwt_required=true → both gates true (customer config wins)") {
181182
val initialModel = mockk<ConfigModel>()
182183
stubConfigModel(initialModel)
183-
every { initialModel.useIdentityVerification } returns true
184+
every { initialModel.useIdentityVerification } returns JwtRequirement.REQUIRED
184185
val configModelStore = mockk<ConfigModelStore>()
185186
every { configModelStore.model } returns initialModel
186187
every { configModelStore.subscribe(any()) } just runs
@@ -196,7 +197,7 @@ class FeatureManagerTests : FunSpec({
196197
val initialModel = mockk<ConfigModel>()
197198
stubConfigModel(initialModel)
198199
every { initialModel.sdkRemoteFeatureFlags } returns listOf(FeatureFlag.IDENTITY_VERIFICATION.key)
199-
every { initialModel.useIdentityVerification } returns true
200+
every { initialModel.useIdentityVerification } returns JwtRequirement.REQUIRED
200201
val configModelStore = mockk<ConfigModelStore>()
201202
every { configModelStore.model } returns initialModel
202203
every { configModelStore.subscribe(any()) } just runs
@@ -220,7 +221,7 @@ class FeatureManagerTests : FunSpec({
220221

221222
val updatedModel = mockk<ConfigModel>()
222223
stubConfigModel(updatedModel)
223-
every { updatedModel.useIdentityVerification } returns true
224+
every { updatedModel.useIdentityVerification } returns JwtRequirement.REQUIRED
224225

225226
manager.onModelReplaced(updatedModel, ModelChangeTags.HYDRATE)
226227

@@ -240,7 +241,7 @@ class FeatureManagerTests : FunSpec({
240241
val updatedModel = mockk<ConfigModel>()
241242
stubConfigModel(updatedModel)
242243
every { updatedModel.sdkRemoteFeatureFlags } returns listOf(FeatureFlag.IDENTITY_VERIFICATION.key)
243-
every { updatedModel.useIdentityVerification } returns false
244+
every { updatedModel.useIdentityVerification } returns JwtRequirement.NOT_REQUIRED
244245

245246
manager.onModelReplaced(updatedModel, ModelChangeTags.HYDRATE)
246247

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,91 @@ import io.kotest.matchers.shouldBe
66
class IdentityVerificationGatesTests : FunSpec({
77
// Singleton state leaks across tests; reset before each.
88
beforeEach {
9-
IdentityVerificationGates.update(false, null, "test-reset")
9+
IdentityVerificationGates.update(false, JwtRequirement.UNKNOWN, "test-reset")
1010
}
1111

1212
test("defaults to newCodePathsRun=false and ivBehaviorActive=false") {
1313
IdentityVerificationGates.newCodePathsRun shouldBe false
1414
IdentityVerificationGates.ivBehaviorActive shouldBe false
1515
}
1616

17-
test("featureFlagOn=false, jwtRequired=null: both gates are false (safe default)") {
17+
test("featureFlagOn=false, jwtRequirement=UNKNOWN: both gates are false (safe default)") {
1818
IdentityVerificationGates.update(
1919
featureFlagOn = false,
20-
jwtRequired = null,
20+
jwtRequirement = JwtRequirement.UNKNOWN,
2121
source = "test",
2222
)
2323
IdentityVerificationGates.newCodePathsRun shouldBe false
2424
IdentityVerificationGates.ivBehaviorActive shouldBe false
2525
}
2626

27-
test("featureFlagOn=false, jwtRequired=false: both gates are false") {
27+
test("featureFlagOn=false, jwtRequirement=NOT_REQUIRED: both gates are false") {
2828
IdentityVerificationGates.update(
2929
featureFlagOn = false,
30-
jwtRequired = false,
30+
jwtRequirement = JwtRequirement.NOT_REQUIRED,
3131
source = "test",
3232
)
3333
IdentityVerificationGates.newCodePathsRun shouldBe false
3434
IdentityVerificationGates.ivBehaviorActive shouldBe false
3535
}
3636

37-
test("ERROR STATE — featureFlagOn=false, jwtRequired=true: both gates true (customer config wins)") {
37+
test("ERROR STATE — featureFlagOn=false, jwtRequirement=REQUIRED: both gates true (customer config wins)") {
3838
IdentityVerificationGates.update(
3939
featureFlagOn = false,
40-
jwtRequired = true,
40+
jwtRequirement = JwtRequirement.REQUIRED,
4141
source = "test",
4242
)
4343
IdentityVerificationGates.newCodePathsRun shouldBe true
4444
IdentityVerificationGates.ivBehaviorActive shouldBe true
4545
}
4646

47-
test("featureFlagOn=true, jwtRequired=null: newCodePathsRun true, ivBehaviorActive false") {
47+
test("featureFlagOn=true, jwtRequirement=UNKNOWN: newCodePathsRun true, ivBehaviorActive false") {
4848
IdentityVerificationGates.update(
4949
featureFlagOn = true,
50-
jwtRequired = null,
50+
jwtRequirement = JwtRequirement.UNKNOWN,
5151
source = "test",
5252
)
5353
IdentityVerificationGates.newCodePathsRun shouldBe true
5454
IdentityVerificationGates.ivBehaviorActive shouldBe false
5555
}
5656

57-
test("featureFlagOn=true, jwtRequired=false: newCodePathsRun true, ivBehaviorActive false (Phase 3)") {
57+
test("featureFlagOn=true, jwtRequirement=NOT_REQUIRED: newCodePathsRun true, ivBehaviorActive false (Phase 3)") {
5858
IdentityVerificationGates.update(
5959
featureFlagOn = true,
60-
jwtRequired = false,
60+
jwtRequirement = JwtRequirement.NOT_REQUIRED,
6161
source = "test",
6262
)
6363
IdentityVerificationGates.newCodePathsRun shouldBe true
6464
IdentityVerificationGates.ivBehaviorActive shouldBe false
6565
}
6666

67-
test("featureFlagOn=true, jwtRequired=true: both gates true (full IV)") {
67+
test("featureFlagOn=true, jwtRequirement=REQUIRED: both gates true (full IV)") {
6868
IdentityVerificationGates.update(
6969
featureFlagOn = true,
70-
jwtRequired = true,
70+
jwtRequirement = JwtRequirement.REQUIRED,
7171
source = "test",
7272
)
7373
IdentityVerificationGates.newCodePathsRun shouldBe true
7474
IdentityVerificationGates.ivBehaviorActive shouldBe true
7575
}
7676

7777
test("updating to the same values is a no-op but still reflects in reads") {
78-
IdentityVerificationGates.update(true, true, "first")
79-
IdentityVerificationGates.update(true, true, "second")
78+
IdentityVerificationGates.update(true, JwtRequirement.REQUIRED, "first")
79+
IdentityVerificationGates.update(true, JwtRequirement.REQUIRED, "second")
8080
IdentityVerificationGates.newCodePathsRun shouldBe true
8181
IdentityVerificationGates.ivBehaviorActive shouldBe true
8282
}
8383

8484
test("transition: non-IVIV-active → off") {
85-
IdentityVerificationGates.update(false, false, "phase-1-non-iv")
85+
IdentityVerificationGates.update(false, JwtRequirement.NOT_REQUIRED, "phase-1-non-iv")
8686
IdentityVerificationGates.newCodePathsRun shouldBe false
8787
IdentityVerificationGates.ivBehaviorActive shouldBe false
8888

89-
IdentityVerificationGates.update(true, true, "phase-2-iv-on")
89+
IdentityVerificationGates.update(true, JwtRequirement.REQUIRED, "phase-2-iv-on")
9090
IdentityVerificationGates.newCodePathsRun shouldBe true
9191
IdentityVerificationGates.ivBehaviorActive shouldBe true
9292

93-
IdentityVerificationGates.update(false, false, "kill-switch")
93+
IdentityVerificationGates.update(false, JwtRequirement.NOT_REQUIRED, "kill-switch")
9494
IdentityVerificationGates.newCodePathsRun shouldBe false
9595
IdentityVerificationGates.ivBehaviorActive shouldBe false
9696
}

0 commit comments

Comments
 (0)