feat: [JWT-3] feature flags, JWT config, and the Identity Verification gate - #1707
feat: [JWT-3] feature flags, JWT config, and the Identity Verification gate#1707nan-li wants to merge 2 commits into
Conversation
5411cbc to
d0b8588
Compare
…gate The foundation the rest of Identity Verification is built on. Nothing consumes the gate yet, so this changes no behavior beyond the params handling below. OSUserJwtConfig holds whether the app requires a token as a three-state answer — unknown until remote params say — because "not yet known" and "not required" have to be told apart before anything can be sent. OSFeatureManager carries the flags remote params enable, and OSIdentityVerificationService is the single place that answers whether Identity Verification is on. Remote params now hydrate that requirement from jwt_required. A response that omits the key means Identity Verification is off for the app, while an empty response answers nothing and leaves the cached requirement standing. Because the operation repo will hold every queued operation until the requirement is known, a params request that failed was going to cost a first-launch app every tag, session count and event for the rest of the session: nothing retried it. It now retries with a backoff over five attempts and, failing that, tries again on the next session. Replaces the unread require_user_id_auth key with jwt_required. The requiresUserAuth property it wrote is removed in the next PR. Co-authored-by: Cursor <cursoragent@cursor.com>
d0b8588 to
1b80146
Compare
There was a problem hiding this comment.
Multi-model adversarial review (interrogate)
Intent
Add the Identity Verification foundation without wiring consumers:
OSFeatureManager, three-stateOSUserJwtConfig(jwt_required), andOSIdentityVerificationService; hydrate from remote params; clear new cache keys on app-id change; retry failed params downloads so first-launch sessions are not stranded with an unknown requirement.
Reviewers
- A: claude-fable-5-thinking-xhigh — 10 findings
- B: gpt-5.6-sol-xhigh — 6 findings
- C: cursor-grok-4.5-high-fast — 5 findings
- D: claude-opus-5-thinking-high — 14 findings
Act On
- Stale
appIdin params retry (A/B/C/D) —scheduleDownloadIOSParamsRetryWithAppId:capturesappIdand only guards_downloadedParameters/_didCallDownloadParameters.handleAppIdChange:resets those flags, so a pending retry for app A can run after a switch to B and rehydrate IV + other remote params from the wrong app. - App-id change clears disk but not in-memory singletons (A/B/C/D) —
OSUD_USE_IDENTITY_VERIFICATION/OSUD_SDK_FEATURE_FLAGSare removed, butOSUserJwtConfig.shared/OSFeatureManager.sharedkeep the previous app’s values.refreshIfUnknown()cannot repair a non-unknown in-memory value. Add a productionreset()and call it fromhandleAppIdChange:. - No tests for the
OneSignal.mretry/hydrate path (A/B/C/D) — Swift unit tests cover the leaf types; the new retry state machine (four entry points, three statics,dispatch_after) is untested. That is where the highest-severity bugs live.
Consider
- Nested retry + indiscriminate 4xx (A/B/C/D) —
OneSignalClientalready retries 0/5xx up to 5 attempts; the outer loop multiplies that and also retries permanent 4xx / missing-app-id failures. - Empty-body 200 strands
unknown(A/B/D) — client can callsuccessBlock(nil); hydration is skipped but_downloadedParameters = truestill blocks further retries/session refetch. - Hydration handler double-delivery / stale last value (A/B/C/D) — register-then-read outside the lock races URLSession-queue hydrate. No production consumers yet, but this is the deferred-work signal.
- Orphan
requiresUserAuth(A/B/C/D) — writer removed;@objc publicproperty remains permanentlyfalse. PR text says removal is a later PR — prefer deleting in this wave if the stack allows. refreshIfUnknown/refreshIfEmptyunwired (D, noted by others) — prewarm helpers have no production caller;recoverIfDeferredalready exists inOneSignal.m.
Noted
- Unused JWT notification/token defines (
OS_ON_JWT_CONFIG_*, etc.) before posters exist - Feature-flag hydration deferred (scoped;
newCodePathsRun==ivBehaviorActiveuntil then) - Doc comments claim
OneSignalUserManagerImplreferences that do not exist yet - Hold lock across
UserDefaults synchronize; case-canonicalize onisEnabledlookup
Dismissed
- Collapsing feature manager + jwt config + gate into one type in this PR — stack intentionally lands the foundation before consumers; defer structural judo to the wiring PR unless reset/retry fixes force a reshape.
- “No consumers yet so races don’t matter” — foundation contract will be trusted by the next PRs; fix the retry/app-id issues now.
Agreement Map
All four models independently flagged (1) stale app-id retry, (2) disk-only app-id clear, and (3) missing OneSignal.m tests. Nested client/outer retry and observer race were near-unanimous. Lone-model pressure was mostly on collapsing types and resilient-storage mirroring — useful context, lower weight for this PR’s stated scope.
Lead verdict: Do not merge until Act On #1–#2 are fixed; add at least a focused test for the retry/app-id interaction before wiring consumers.
Sent by Cursor Automation: Untitled
Outer iOS-params retries now follow OSResponseStatusRetryable so classified 4xx are not re-armed for the session. Hydration handlers snapshot requirement under the same lock as registration. Co-authored-by: Cursor <cursoragent@cursor.com>


Description
One Line Summary
Add the Identity Verification foundation: feature flags, three-state JWT requirement config, the IV gate service, and remote-params hydration with retry — nothing consumes the gate yet.
Details
Motivation
Identity Verification needs a known answer to “does this app require a token?” before user-scoped work can be sent. That answer is three-state (unknown / on / off), comes from remote params (
jwt_required), and must survive a failed first-launch params download without stranding the session’s queued work forever.Scope
OSFeatureManager,OSUserJwtConfig,OSIdentityVerificationServiceOneSignal.mparams hydration fromjwt_required, retry/backoff, cache clears for the new keysrequire_user_id_authwithjwt_required(therequiresUserAuthproperty itself is removed in a later PR)Targets
5.6-main(already includes the project-file normalize and read-your-write consistency fixes).Testing
Unit testing
OSFeatureManagerTestsOSUserJwtConfigTestsOSIdentityVerificationServiceTestsManual testing
Built in sequence as part of the local JWT stack against an iOS Simulator.
Affected code checklist
Checklist
Overview
Testing
Final pass
Made with Cursor