Skip to content

feat: [JWT-8] demo app Identity Verification UI - #1708

Open
nan-li wants to merge 7 commits into
5.7-mainfrom
nan/jwt-pr8-demo-ui
Open

feat: [JWT-8] demo app Identity Verification UI#1708
nan-li wants to merge 7 commits into
5.7-mainfrom
nan/jwt-pr8-demo-ui

Conversation

@nan-li

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

Copy link
Copy Markdown
Contributor

Summary

Final slice of the Identity Verification (JWT) stack: the demo app UI for logging in with a JWT, replacing a user's JWT, and pointing the demo's REST user fetch at external_id with a bearer token.

This PR was previously the umbrella review PR for the whole stack against main. Everything below it has since merged into 5.7-main, so the diff is now only the demo app, 9 files and +382/-36, all under examples/demo/. No SDK source changes.

Already merged into 5.7-main: #1706 read-your-write consistency, #1707 feature flags, JWT config and the IV gate, #1709 identity model JWT and public API, #1710 delta ownership, #1711 request pipeline, #1712 in-app messages.

What this adds

  • LoginUserDialog replaces the generic add-item dialog for login. External id is required, JWT is optional. With a token it calls OneSignal.login(externalId:token:), without one the single-argument login. A same-user login keeps the stored bearer unless a new one is supplied, and refetches directly since no user-state event fires for the same user.
  • An UPDATE USER JWT button and dialog calling OneSignal.updateUserJwt(externalId:token:), so token replacement can be exercised without a fresh login. The external id field is prefilled with the id the SDK asked for, or the current user. The demo only saves the token as its own REST bearer when the id is the current user; the SDK call goes through either way.
  • The SDK's token ask is visible. When OSUserJwtInvalidatedListener fires, the User section shows a banner with the external id and a PROVIDE JWT button, a toast announces it, and the demo drops its stored copy of the rejected token.
  • A "Fetch by external_id (JWT)" toggle, persisted across launches. Off, the demo hydrates by onesignal_id unsigned. On, it hydrates by external_id with an Authorization: Bearer header. Flipping it refetches. It only changes the demo's own REST fetch, not anything in the SDK.
  • A "REST fetch" status row showing OK or the failure reason, so a 401 under Identity Verification is not mistaken for an empty user.
  • UserFetchService.fetchUser takes an alias label and value, percent-encodes the alias so external ids containing reserved characters do not misroute the GET, sends Accept alongside the optional bearer, and returns a Result with the HTTP status on failure.
  • PreferencesService stores the toggle and the demo session JWT. logout() clears the token.
  • Passthroughs for addUserJwtInvalidatedListener and removeUserJwtInvalidatedListener.

Cold-start behavior

The demo no longer calls login at launch. The SDK restores the user from its own cache and keeps the JWT it archived. If that token is missing, was rejected before the app was killed, or has expired, the new session's first signed request parks and the SDK fires OSUserJwtInvalidatedListener. The demo shows that ask as the banner above and clears its own stored copy, so the path a customer's app hits on relaunch is visible instead of being papered over by re-supplying the token at launch.

Test plan

  • Login with an external id and no JWT, confirm the unsigned onesignal_id fetch still hydrates
  • Login with a JWT, turn the fetch toggle on, confirm the fetch switches to external_id with a bearer and the status row reads OK
  • Login with an external id containing reserved characters (/, #, space), confirm the GET does not misroute
  • UPDATE USER JWT with a replacement token, confirm later fetches use it; with a different external id, confirm the status row does not change
  • Login again as the same user with an empty JWT field, confirm the stored bearer survives and the display refetches
  • Logout, confirm the stored token is cleared and the signed fetch stops
  • Login with no token, relaunch, confirm the banner and toast appear and PROVIDE JWT releases the parked requests
  • Flip the fetch toggle with an expired or wrong token, confirm the status row shows HTTP 401

@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)

Skill: Cursor interrogate (pstack). Reviewers: claude-fable-5-thinking-xhigh, gpt-5.6-sol-xhigh, cursor-grok-4.5-high-fast, claude-opus-5-thinking-high. Lead judgment applied below — feed findings into the stacked PRs (#1705#1707+), not as a merge-to-main checklist.

Intent

External-ID–scoped JWTs so user-mutating and related SDK traffic can be Identity-Verification gated, with ownership tracking so switches / races don’t apply the wrong user’s token or unblock the wrong waiter. Umbrella stack: gate + JWT repo/public API + delta ownership + IV request pipeline + IAM under IV + RYW fixes + demo UI.

Reviewers

  • Reviewer A: claude-fable-5-thinking-xhigh, 10 findings
  • Reviewer B: gpt-5.6-sol-xhigh, 7 findings
  • Reviewer C: cursor-grok-4.5-high-fast, 9 findings
  • Reviewer D: claude-opus-5-thinking-high, 10 findings

Act On

  1. Bearer JWT logged at verbose (and headers logger now prints it) — A/B/C/D. updateUserJwt still logs the full token (// TODO: omit… before shipping). Separately, OSRequestAuth.setBearer populates Authorization into additionalHeaders, and OneSignalClient already logs that dictionary verbatim — pre-IV that path was inert; it is now a credential sink whenever verbose is on. Redact both before any IV GA.
  2. Parked Create User can complete after a user switch and report the wrong current user — A/D. _executePendingRequests steps over awaitsToken so another user’s work can run first; when the parked create later succeeds, parseFetchUserResponseidentityModel.hydrateOSUserStateSnapshot.fireUserStateChanged is not gated by currentUser(matching:), while later hydration in the same function is. Add a regression test for login(A) (no token) → login(B)updateUserJwt(A).
  3. App-id change clears IV disk keys / retries without resetting in-memory gate — C (also flagged on #1707). handleAppIdChange removes OSUD_USE_IDENTITY_VERIFICATION / feature flags but leaves OSUserJwtConfig / OSFeatureManager memory; refreshIfUnknown / refreshIfEmpty then no-op. Stale scheduleDownloadIOSParamsRetryWithAppId: can still hydrate the previous app’s jwt_required into the shared singleton.
  4. JWT persisted into shared UserDefaults archives — A/B/C/D. Encoding jwtBearerToken on OSIdentityModel (and thus model/request caches) expands exposure vs the prior in-memory-only + “make this token secure” TODO (removed). Persist via Keychain (or drop cross-launch persistence and re-ask), and stop embedding the secret in every archived request identity copy.
  5. IAM deferred-fetch slot can be wiped by onUserWillChange with a nil subscription id — D. Single deferredFetchSubscriptionId; login while Create User is parked (no push sub id yet) overwrites a prior 401 park with nil, and retryDeferredFetch then no-ops for the rest of the session.

Consider

  • Feature-flag layer has no production writer — A/B/C/D. setEnabledFeatureKeys is test-only; newCodePathsRun collapses to ivBehaviorActive. Land remote-params delivery with the flag, or delete the dead branch until then.
  • Deltas stamped from live identityModel.externalId during clearUserData — D. Fetch clears aliases before hydrate; tags/aliases in that window stamp nil and are purged under IV.
  • Cached .off + async params hydrate — B. Unsigned mutations can 401 and be dropped before the session learns IV turned on.
  • currentUser(matching:) then mutate/logout is not atomic — B. Check-then-act across executors can still clear/logout the wrong user under a tight switch.
  • JWT-invalidated observer lazy init race — D. Lost listener + askedForToken can strand asks for the session.
  • RYW subscription bar uses sticky prior tokens / shared condition clear-on-timeout — B/A/D. Later subscription writes can look “ready” too early; timeout can lower the bar for a concurrent waiter.
  • IAM 401 never invalidates JWT — A. Documented ambiguity vs mid-session expiry with no user traffic; consider bounded invalidate after repeated signed 401s.
  • Logout unsubscribe vs immediate re-login create ordering — A. Concurrent executors can leave the device unsubscribed server-side.
  • Create/Fetch ownerExternalId still live-read — C. Dual ownership convention vs stamped requests; weak under clearData.

Noted

  • Pre-ownership / pre-addsNewRecords cache decode → purge or skip cool-down on upgrade (release note / decode default).
  • OSRequestGetInAppMessages encode-failure returns path-less request.
  • OneSignalUserManagerImpl.swift crossed ~1k lines despite +Jwt extraction.
  • Listener replay after remove / possible duplicate ask — B.

Dismissed

  • Rewrites that only prefer a different structure without a broken path (general “code judo” without concrete failure).
  • Re-litigating three-state jwt_required / hold-until-known design (intent of the stack).
  • Treating umbrella-vs-stacked-PR process as a code defect.

Agreement Map

Strong consensus on credential handling (verbose token log + UserDefaults persistence) and on dead OSFeatureManager producer. Two independent models (A/D) reconstructed the same parked-create → wrong OSUserStateObserver path after step-over. C resurfaced the #1707 app-id / stale-retry gate bugs still present in the tip. B uniquely stressed rollout-transition silent drops and RYW generation mismatch; D uniquely stressed IAM defer-nil cancel and observer lazy-init. Net: IV ownership/gating story is real, but shipping blockers are credential leakage/persistence, wrong-user observer after park-step-over, and app-id gate memory/retry consistency.

Inline comments below map to Act On / high-signal Consider items.

Open in Web View Automation 

Sent by Cursor Automation: Untitled

return
}
// TODO: omit the token from this log before shipping — keep for testing.
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.updateUserJwt called for externalId: \(externalId) with token: \(token)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — credential in logs (consensus A/B/C/D)

This still logs the full bearer token at verbose, with an explicit pre-ship TODO. Separately, OSRequestAuth.setBearer puts Authorization: Bearer … into additionalHeaders, and OneSignalClient logs that dictionary verbatim — so every signed request becomes a second leak once verbose is on.

Omit the token here (externalId / presence only) and redact Authorization in the client logger before IV GA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not addressing.* Same answer as on #1709 and #1711. The token is valid for at most 1 hour, and we can remove later before release.

lock.withLock {
super.encode(with: coder)
coder.encode(aliases, forKey: "aliases")
coder.encode(jwtBearerTokenLocked, forKey: OS_JWT_BEARER_TOKEN)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — JWT archived to shared UserDefaults (consensus A/B/C/D)

Encoding jwtBearerTokenLocked (and set(property:) → model-store save) persists the bearer into App Group UserDefaults / request archives. Pre-stack this was in-memory with a “make this token secure” TODO; the TODO was removed while exposure grew.

Prefer Keychain (external-id keyed) or session-only memory + re-ask; do not keep N copies of the secret in model/request plists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not addressing. Same answer as on #1709, product decision. The token is valid for at most 1 hour, and a user who kills and reopens the app inside that window should keep working without another ask, keep its token across a relaunch.


internalAddAliases(remoteAliases)
fireUserStateChanged(newOnesignalId: newOnesignalId, newExternalId: newExternalId)
OSUserStateSnapshot.fireUserStateChanged(newOnesignalId: newOnesignalId, newExternalId: newExternalId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — companion to parked-create race

fireUserStateChanged runs unconditionally from hydration. Callers that hydrate non-current models (see OSUserExecutor step-over of token-parked creates) will notify the app of the wrong current user.

Move this fire to call sites that know the model is current, or gate on currentUser(matching:).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

same as above, filed together, one fix will address both

Comment thread iOS_SDK/OneSignalSDK/Source/OneSignal.m Outdated
[sharedUserDefaults removeValueForKey:OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY];

[sharedUserDefaults removeValueForKey:OSUD_USE_IDENTITY_VERIFICATION];
[sharedUserDefaults removeValueForKey:OSUD_SDK_FEATURE_FLAGS];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — disk clear without in-memory reset (C; also #1707)

App-id change removes OSUD_USE_IDENTITY_VERIFICATION / OSUD_SDK_FEATURE_FLAGS but does not reset OSUserJwtConfig.shared / OSFeatureManager.shared. refreshIfUnknown / refreshIfEmpty no-op when memory is already set, so the previous app’s gate can keep driving IV behavior until (or instead of) the new params hydrate.

Reset requirement to .unknown and clear feature keys here (same moment as the disk clear).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not addressing, tracked in SDK-5142. Same answer as on #1707. App ID changes are not supported since v5. A change between launches is covered because the config is rebuilt from the cleared disk key on the next process. A mid-process change is what SDK-5142 scopes.

if (_downloadedParameters || _didCallDownloadParameters)
return;

[self downloadIOSParamsWithAppId:appId];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — stale appId on params retry (C; also #1707)

This block captures the failing appId. handleAppIdChange resets download flags but does not cancel the delayed retry, so it can still call downloadIOSParamsWithAppId: for the old app and hydrate jwt_required into the shared config used by the new app.

Compare against OneSignalIdentifiers.currentAppId (or a generation stamp) before retrying.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same answer as above and on #1707. App ID changes are not supported, tracked in SDK-5142.

@synchronized (self) {
self.userGeneration += 1;
}
[self deferFetchWithSubscriptionId:OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act On — deferred IAM fetch erased by nil (D)

One slot: deferFetchWithSubscriptionId: overwrites unconditionally. Under IV, login while Create User is parked often has no push subscription id yet, so this writes nil and drops a fetch parked by handleUnauthorizedFetch: / “no onesignal id”. retryDeferredFetch then no-ops for the rest of the session.

Ignore nil here, or separate “fetch owed” from “subscription id to use”.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not addressing. A parked fetch always has a subscription id, because the fetch returns before parking when there is none, and onUserWillChange reads the same push model. The two only differ if a 404 cleared the id first, and the next Create User brings a new id that starts its own fetch through the push subscription observer.

name: OS_UPDATE_PROPERTIES_DELTA,
identityModelId: userInstance.identityModel.modelId,
identityModelId: identityModel.modelId,
externalId: identityModel.externalId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider — live externalId stamp during clear/hydrate window (D)

Deltas take identityModel.externalId from the live model. clearUserData blanks aliases before fetch hydrate; work in that window stamps nil and OSOperationRepo drops it under IV (silent loss for an identified user).

Preserve external_id across clearData, or hydrate aliases atomically so an identified model never reads anonymous.

Comment thread iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSFeatureManager.swift Outdated
@nan-li
nan-li force-pushed the nan/jwt-pr8-demo-ui branch 7 times, most recently from 2d7dfa9 to 3f0ca5e Compare August 13, 2026 17:17
@nan-li
nan-li force-pushed the nan/jwt-pr8-demo-ui branch 2 times, most recently from 6280471 to 4df957f Compare August 21, 2026 17:00
@nan-li
nan-li force-pushed the nan/jwt-pr8-demo-ui branch from 4df957f to 3db98a5 Compare August 28, 2026 17:35

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Umbrella-only notes. The stacked PRs have the product comments; these two are fixture / demo hygiene on the tip.

Not a merge vehicle, I know. Flagging here because this is where CI is red and where the demo JWT lands on disk.

- shouldFetchOnUserChangeWithSubscriptionID is cleared
- deferredFetchSubscriptionId is cleared
*/
func testRetriesFetchWhenUserStateChangesWithValidOneSignalID() throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test is red on the umbrella for a fixture mismatch, not because the retry path is wrong.

It seeds RYW for testOneSignalId, then setDefaultIdentifyUserResponses hydrates anonUserOSID. After 1712 the retry goes through authorizationForFetchOrDefer and the RYW wait. waitUntil is 5s, RYW timeout is 30s, so the test times out waiting for a token that will never arrive for that id.

IamFetchIdentityVerificationTests already seeds RYW for the post-login id. I’d do the same here, or pass testOneSignalId through the identify mock so the two ids match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for digging in. I don't think the ids are mismatched. executeIdentifyUserRequest ignores the mock response and hydrates from the anon model's own onesignal_id, which is testOneSignalId, the one the test seeds, so the fetch already waits on the id that has a token. It also passed on a re-run of the same commit with no code change (run 33195383982, attempt 2), and the earlier red runs on this branch failed on unrelated tests, so this is a flaky test rather than a fixture problem.


func setSessionJwtToken(_ value: String?) {
if let value = value, !value.isEmpty {
defaults.set(value, forKey: Key.sessionJwtToken)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dev-only, but this still writes a bearer to UserDefaults.standard (backups, anyone dumping defaults). It also teaches an unsafe pattern.

I’d keep it in memory for the process, or Keychain. The update path already prints Updated JWT for: without the token, which is the right instinct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not addressing. Same reasoning as on #1709. The token is valid for at most 1 hour, and the SDK itself keeps it across a relaunch on purpose, so the demo keeping it for its REST fetch matches that.

Exercises the Identity Verification surface end to end: log in with a
token, watch a token be rejected and supply a replacement through the
invalidated listener, and see how the SDK behaves while the requirement
is still unknown.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nan-li
nan-li force-pushed the nan/jwt-pr8-demo-ui branch from 3db98a5 to 20d6bd3 Compare September 10, 2026 15:05
@nan-li
nan-li changed the base branch from main to 5.7-main September 10, 2026 15:05
@nan-li nan-li changed the title Identity Verification (JWT) — full stack for adversarial review feat: [JWT-8] demo app Identity Verification UI Sep 10, 2026

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

JWT-contract gaps in the demo vs the APIs on 5.7-main. Head 20d6bd3.

  1. initialize() still login(storedExternalId) with no token. Identity-model persistence plus same-user no-op means the SDK does keep a valid JWT across relaunch, so the PR body is wrong. Cache miss or OS_JWT_TOKEN_INVALID then parks and asks, and the demo already has the token in prefs. Pass prefs.getSessionJwtToken() into login(externalId:token:), or skip the extra login.

  2. onUserJwtInvalidated only prints. Clear sessionJwtToken (REST keeps sending the rejected bearer), surface the ask, and prefill UPDATE USER JWT with event.externalId.

  3. UPDATE USER JWT takes any id and writes prefs before the SDK accepts it. Lock it to the current external id.

  4. Same-user login with an empty JWT field clears the demo bearer while the SDK keeps its token. Same-user login with a JWT wipes hydrate and never refetches, because user-state does not fire.

  5. The IV toggle does not refetch, and /users failures collapse to nil with no status. Relabel the toggle so it is not read as a client-side jwt_required switch.

…ing in at launch

The demo called login(storedExternalId) on every cold start. The SDK already
restores the user from its own cache, so that call was a no-op, and passing the
stored token there would have hidden the path a customer's app hits on relaunch,
where the SDK either signs with the token it archived or asks for one through
OSUserJwtInvalidatedListener.

Drop the cold-start login. When the SDK asks for a token, clear the demo's stale
copy so the REST fetch stops sending a rejected bearer, show a banner with the
external id and a PROVIDE JWT button, toast the ask, and prefill the UPDATE USER
JWT dialog with that id.
…t user

UPDATE USER JWT accepts any external id, and the SDK ignores a token for an id
it has no identity model for. The demo saved the token as its REST bearer before
the SDK decided, so a mismatched id left the demo signing the current user's
fetch with someone else's token. Only write prefs when the id is the current user.
A same-user login with an empty JWT field cleared the demo's stored token while
the SDK kept its own, so the REST fetch lost its bearer for no reason. Only
overwrite the stored token when one was supplied or the user changed.

A same-user login also wiped the hydrated display and waited for a user-state
event that never fires for the same user. Refetch directly in that case.
…/users result

The toggle read as a client-side jwt_required switch. It only changes how the
demo's own /users fetch is addressed, so name it that way and refetch when it
flips instead of waiting for the next login.

A failed fetch collapsed to nil and left the last hydrated state on screen with
no indication, so a 401 under Identity Verification looked like nothing
happened. The fetch now returns a Result, and the User section shows OK or the
failure reason.
@nan-li

nan-li commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, these are good ease of use points for the demo app

  1. Addressed in 11dd0d4, the other way round. You're right that the PR body was wrong; the SDK does keep the archived token across a relaunch. I dropped the cold-start login instead of feeding the stored token into it. That call was a no-op for a same user anyway, and passing the token there would hide the path a customer hits on relaunch: the SDK signs with what it archived, or asks. The PR body's cold-start section now says that.

  2. Addressed in 11dd0d4. onUserJwtInvalidated clears the stored bearer, shows a banner in the User section with the external id and a PROVIDE JWT button, and toasts. UPDATE USER JWT opens prefilled with the asked id.

  3. Addressed in 6db4e7d, with the field left editable. A token for a non-current user is a real SDK path (a parked create for A after switching to B), so I did not lock it. The demo only saves the token as its own REST bearer when the id is the current user; the SDK call goes through either way, and the SDK already ignores an id it has no model for.

  4. Addressed in 1f5f7a0. A same-user login only overwrites the stored bearer when a token was supplied, matching the SDK keeping its own. Same-user login now refetches directly instead of waiting for a user-state event that never fires.

  5. Addressed in 4ea6caf. The toggle is "Fetch by external_id (JWT)" with a note that it only changes the demo's own fetch, and flipping it refetches. fetchUser returns a Result, and a new "REST fetch" row shows OK or the reason, so a 401 reads as a 401.

IMG_3620IMG_3621

The JWT UI work pushed OneSignalViewModel's class body to 360 lines, past
SwiftLint's 350-line type_body_length error threshold, which failed the
Swift Lint job. Moved outcomes, in-app messages, triggers, custom events,
notifications and Live Activities into an extension in the same file. The
rule only counts the primary declaration, so the body drops to 264 lines
and leaves room for the next demo change. Pure code movement, no logic
change.

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The five from the last pass are closed at 70d5450.

One leftover: updateUserJwt does not refetch the demo /users call, so after PROVIDE JWT for the current user the REST row can stay on the previous 401 until a user-state event or a toggle flip. Not blocking.

updateUserJwt stored a fresh bearer but never refetched, so the REST fetch
row kept showing the 401 that prompted the ask until a user-state event or
a toggle flip happened to fire. Refetch when the token is for the current
user, the same condition the service uses to decide whether to store the
bearer, matching what a same-user login already does.
@nan-li

nan-li commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

The five from the last pass are closed at 70d5450.

One leftover: updateUserJwt does not refetch the demo /users call, so after PROVIDE JWT for the current user the REST row can stay on the previous 401 until a user-state event or a toggle flip. Not blocking.

This is a nice to have, so I will add it. Addressed in 5edbe37. updateUserJwt now refetches when the token is for the current user

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.

2 participants