Skip to content

feat: [JWT-7] in-app messages under Identity Verification - #1712

Open
nan-li wants to merge 3 commits into
nan/jwt-pr6-request-pipelinefrom
nan/jwt-pr7-iam
Open

feat: [JWT-7] in-app messages under Identity Verification#1712
nan-li wants to merge 3 commits into
nan/jwt-pr6-request-pipelinefrom
nan/jwt-pr7-iam

Conversation

@nan-li

@nan-li nan-li commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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_id and 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 token
  • OneSignalUserManagerImpl (+ Jwt) — exposes what the IAM path needs to address the current user consistently with the request pipeline
  • IAM fetch does not invalidate a JWT on 401; the request pipeline remains the only path that decides a token is no longer good
  • Unit coverage for IAM under Identity Verification and updated messaging controller user-state tests

Stacked on #1711 (nan/jwt-pr6-request-pipeline).

Testing

Unit testing

  • New IamFetchIdentityVerificationTests
  • Updated OSMessagingControllerUserStateTests

Manual 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

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Stale 401 / missed JWT wakeup can park the fetch for the rest of the session (A, B, C, D) — handleUnauthorizedFetch always defers on a signed 401 and never compares the rejected token to the current validJwt. If OS_ON_USER_JWT_UPDATED fired 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 concurrent storeJwt. Pipeline JWT invalidation already guards “replacement landed while rejected request was in flight”; this path needs the same.
  2. onUserWillChange does not drop presented/queued IAMs (A, B, C, D) — Only messages = @[]. messageDisplayQueue and the showing viewController are 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 by setInAppMessagingPaused: and clear the queue on main.
  3. No tests for generation discard or the in-flight token-refresh race (A, C, D) — Addressing/defer/401-park are covered; the load-bearing userGeneration discard 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

  • .unknown requirement holds IAM for every app until params hydrate (A, D), including flag-off / first launch / params outage — intentional per testAFetchHeldForAnUnknownRequirementGoesOutOnHydration, 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 disableLocalCaching on the shared legacy path.
  • Generation vs auth capture across prepareForNewUsersetNewInternalUser (B) — generation bumps on the notification before _user is 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).
  • onUserWillChange can overwrite a deferral with a nil pushSubscriptionId (A, D).
  • Bearer in additionalHeaders is logged at verbose by OneSignalClient (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.

Open in Web View Automation 

Sent by Cursor Automation: Untitled

@nan-li nan-li changed the title feat: [PR7] in-app messages under Identity Verification feat: [JWT-7] in-app messages under Identity Verification Aug 12, 2026
@nan-li
nan-li force-pushed the nan/jwt-pr6-request-pipeline branch 2 times, most recently from 1d08c49 to b0aec18 Compare August 12, 2026 17:35
@nan-li
nan-li force-pushed the nan/jwt-pr6-request-pipeline branch from b0aec18 to a642d1c Compare August 12, 2026 21:22
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>
nan-li and others added 2 commits August 13, 2026 09:28
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant