feat: [JWT-7] in-app messages under Identity Verification - #1712
Conversation
There was a problem hiding this comment.
Multimodal adversarial review (interrogate)
Verdict: Direction matches the JWT stack (address/sign via user manager, defer until addressable, IAM 401 ≠ invalidate). Two correctness gaps should land before merge: a park that can strand the fetch after a replacement token already arrived, and a user-change path that does not actually drop displayed/queued IAMs.
Intent
Make the IAM fetch address and sign the current user under Identity Verification via the user manager, wait when it cannot yet be addressed, reattempt on JWT config hydration / token supply, discard fetches after a user change, and deliberately not treat IAM 401s as JWT invalidation.
Reviewers
- A:
claude-fable-5-thinking-xhigh— 11 findings - B:
gpt-5.6-sol-xhigh— 3 findings - C:
cursor-grok-4.5-high-fast— 5 findings - D:
claude-opus-5-thinking-high— 10 findings
Act On
- Stale 401 / missed JWT wakeup can park the fetch for the rest of the session (A, B, C, D) —
handleUnauthorizedFetchalways defers on a signed 401 and never compares the rejected token to the currentvalidJwt. IfOS_ON_USER_JWT_UPDATEDfired while the request was in flight (including the RYW wait), nothing is deferred yet, so the wakeup no-ops; the later 401 then parks with no further signal. Same check-then-act shape exists for nil authorization → defer vs a concurrentstoreJwt. Pipeline JWT invalidation already guards “replacement landed while rejected request was in flight”; this path needs the same. onUserWillChangedoes not drop presented/queued IAMs (A, B, C, D) — Onlymessages = @[].messageDisplayQueueand the showingviewControllerare untouched, so user A’s IAM can keep showing (and the next queued A message can present after dismiss) under user B. Conflicts with the generation comment’s “not showing one user's messages to another.” Mirror the dismiss path already used bysetInAppMessagingPaused:and clear the queue on main.- No tests for generation discard or the in-flight token-refresh race (A, C, D) — Addressing/defer/401-park are covered; the load-bearing
userGenerationdiscard and the stuck-park ordering are not. Add a delayed-response login race and a “token replaced during in-flight then 401 → refetch with new token” case.
Consider
.unknownrequirement holds IAM for every app until params hydrate (A, D), including flag-off / first launch / params outage — intentional pertestAFetchHeldForAnUnknownRequirementGoesOutOnHydration, but it makes IAM availability depend on a second endpoint with no floor.- 403 parks like 401 via
OSResponseStatusUnauthorized(A, D); a forbidden user↔subscription pairing then waits forever for a token that cannot help. - Alias encode failure returns a request with nil
path(A, C, D) →stringByAppendingString:can raise; practically unreachable for Swift strings, but the defensive branch is worse than failing closed. - Login refetch / message clear is ungated (A, D) while other IV behavior is flag-scoped — may be a deliberate cross-user fix; call out and consider
disableLocalCachingon the shared legacy path. - Generation vs auth capture across
prepareForNewUser→setNewInternalUser(B) — generation bumps on the notification before_useris swapped; an overlapping fetch can pin new generation + old authorization. - Authorization pinned across the entire retry chain (D) instead of re-resolving like pipeline
prepareForExecution.
Noted
- Extracting an IAM fetch coordinator from the 1.4k-line
OSMessagingController(A, C, D). onUserWillChangecan overwrite a deferral with a nilpushSubscriptionId(A, D).- Bearer in
additionalHeadersis logged at verbose byOneSignalClient(A) — pre-existing pipeline pattern; redact centrally.
Dismissed
- “Park should ask for a token / invalidate on IAM 401” as a design alternative — intent explicitly forbids treating IAM 401 as invalidation (ambiguous user↔subscription pairing; matches Android). The Act On item is only about recovering when a replacement already exists or the wakeup was missed, not about inventing a new invalidation source.
- Broad “clear all impression/seen state on user change” (D) — device-scoped seen/redisplay may be intentional; only the display-queue leak is Act On.
Agreement map
Strong 4/4 consensus on stuck-park-after-token-refresh and incomplete user-change teardown. 3/4 on nil-path defense and missing generation tests. Split on whether ungated login refetch / unknown-requirement wait are bugs vs intentional product choices — called out under Consider.
Sent by Cursor Automation: Untitled
81c955f to
68e090c
Compare
68e090c to
f38ef06
Compare
1d08c49 to
b0aec18
Compare
f38ef06 to
18a103f
Compare
b0aec18 to
a642d1c
Compare
18a103f to
6238527
Compare
The in-app message fetch is user-scoped, so under Identity Verification it has to name the user by external_id and sign the call. It asks the user manager how to address the current user rather than assembling that itself, which keeps the alias and the token consistent with every other user-scoped call. The fetch deliberately does not invalidate a JWT. Getting this request right means getting both the push subscription ID and the user in the URL right, so a 401 here is at least as likely to mean the request was mismatched as it is to mean the token is bad. Treating it as proof would let a malformed fetch invalidate a token that works everywhere else. It handles the rejection and stops; the request pipeline remains the only thing that decides a token is no longer good. This follows Android. A fetch that cannot yet be addressed waits rather than going out unsigned, and is reattempted when the requirement hydrates or the app supplies a token. Co-authored-by: Cursor <cursoragent@cursor.com>
6238527 to
f74a573
Compare
…hsRun Login/logout must not clear or dismiss IAMs for apps with Identity Verification off. When the code paths are on, also drop queued non-preview messages and dismiss a showing one so they cannot stay up under the next user. Co-authored-by: Cursor <cursoragent@cursor.com>
OS_ON_USER_JWT_UPDATED is a no-op while a fetch is in flight, so parking the 401 would wait for a wakeup that already fired. Co-authored-by: Cursor <cursoragent@cursor.com>


Description
One Line Summary
Make the in-app message fetch address and sign the current user under Identity Verification via the user manager, wait when it cannot yet be addressed, and deliberately not treat IAM 401s as JWT invalidation.
Details
Motivation
The in-app message fetch is user-scoped, so under Identity Verification it must name the user by
external_idand sign the call. Assembling that in the messaging controller would risk alias and token drifting from every other user-scoped request. A 401 on this fetch is at least as likely to mean a mismatched push subscription ID / user in the URL as a bad token — treating it as proof would let a malformed fetch invalidate a token that works everywhere else (same as Android).Scope
OSMessagingController/OSInAppMessagingRequests— address + sign through the user manager; wait and reattempt when the requirement hydrates or the app supplies a tokenOneSignalUserManagerImpl(+ Jwt) — exposes what the IAM path needs to address the current user consistently with the request pipelineStacked on #1711 (
nan/jwt-pr6-request-pipeline).Testing
Unit testing
IamFetchIdentityVerificationTestsOSMessagingControllerUserStateTestsManual testing
Built in sequence as part of the local JWT stack against an iOS Simulator; this PR’s commit compiled on top of PR6.
Affected code checklist
Checklist
Overview
Testing
Final pass