Skip to content

chore: [SDK-5283] give the demo the same JWT behavior as the iOS demo - #2784

Open
nan-li wants to merge 1 commit into
mainfrom
nan/sdk-5283
Open

nan-li wants to merge 1 commit into
mainfrom
nan/sdk-5283

Conversation

@nan-li

@nan-li nan-li commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Make the demo's Identity Verification controls behave the same as the iOS demo (OneSignal/OneSignal-iOS-SDK#1774).

Details

Motivation

A review of both demos found that their JWT controls behaved differently, and found bugs. The REST fetch row could show OK for a fetch that never ran. A late fetch could bring back the previous user after logout. The demo stored a single token, so some user switches lost it.

Scope

❗️Demo app only. No SDK changes.

  • The JWT invalidated listener registers in MainApplication, so events aren't missed while MainActivity is gone. The User section shows a banner with a PROVIDE JWT button, and a snackbar.
  • The demo stores one token per user. Logout clears them. An invalidation event doesn't, because SDKs also fire it when requests are only waiting for a token.
  • The REST fetch row shows OK or the error, and resets on a user switch or logout.
  • The toggle is renamed "Fetch by external_id (JWT)" and refetches when flipped.
  • The Update User JWT dialog stacks its fields and confirms with "Update".

The remaining differences from the iOS demo come from the SDKs (SDK-5474, SDK-5476) and are listed in examples/build.md.

Testing

Unit testing

None. Demo UI only.

Manual testing

Ran 13 scenarios on an Android 34 emulator against a test app with Identity Verification on. They covered login with and without a token, user switches, expired tokens, PROVIDE JWT, logout, and rotation. All behaved as expected.

Screenshots

iOS on the left, Android on the right, both demos on an app with Identity Verification on.

  1. Login as alice with an expired token. The REST fetch row shows the 401 and the banner names the user the SDK is waiting on. Android also shows the snackbar.
both-1-jwt-ask-banner
  1. PROVIDE JWT opens the Update User JWT dialog with the id prefilled and the token field at full width.
both-2-update-user-jwt-dialog
  1. After Update, the banner clears and the REST fetch row reads OK.
both-3-after-update

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
    • If it is hard to explain how any codes changes are related to each other then it most likely needs to be more than one PR
  • 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.
    • Simplify with less code, followed by splitting up code into well named functions and variables, followed by adding comments to the code.
  • I have reviewed this PR myself, ensuring it meets each checklist item
    • WIP (Work In Progress) is ok, but explain what is still in progress and what you would like feedback on. Start the PR title with "WIP" to indicate this.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

✓ Coverage check passed (no source files changed)

📥 View workflow run

@nan-li
nan-li marked this pull request as ready for review October 12, 2026 16:09
@nan-li
nan-li requested a review from a team as a code owner October 12, 2026 16:09

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

Multi-model review (Claude Opus 5.5, GPT 5.6 Sol, Grok 4.7) — demo-only JWT/IV parity with iOS. 10 files, +357/−84.

Act on (3/3): JwtAskStore.onInvalidated posts to the main queue, while answered/clear write immediately. A 401 that was already queued can run after logout or PROVIDE JWT and put the banner/snackbar back for a user whose tokens were just cleared. Stamp a generation on the posted runnable (or send every mutation through the handler) so clear/answered win.

Consider (Grok; related Opus): A different-user loginUser only calls resetUserFetch() and waits for onUserStateChange. That callback is onesignal_id hydrate, which does not fire while LoginUserOperation is parked for a JWT (Android still does not ask on park — SDK-5476). Launch already fetches by external_id without a onesignal_id; a switch does not, so the REST row stays — and a stored bearer is never sent. Calling fetchUserDataFromApi() after _externalUserId is updated would match launch.

Consider (3/3): readJwtTokens deletes JWT_TOKEN_PREF without copying it under the cached external id. Upgrading testers lose the old single token and get an unsigned 401 until they paste it again.

Noted: snackbar “once per ask” is per Activity (rememberSaveable), not process-wide. answered/clear assume the main thread.

Dismissed: listener not literally “right after init” — it is still after initWithContext and before any async 401. resetUserFetch racing a hydrate is unlikely: createAndSwitchToNewUser replaces the identity model (onModelReplaced is empty) and login() returns before enqueue.

Open in Web View Automation 

Sent by Cursor Automation: PR Reviews

Comment on lines +27 to +29
fun onInvalidated(externalId: String) {
mainHandler.post {
_externalId.value = 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.

Act on (3/3): this post is not invalidated by answered/clear. invalidateJwt already hops to launchOnDefault before this runs, so a logout or PROVIDE JWT can setValue(null) and then this runnable restores the old ask (banner + snackbar). Give the runnable a generation, or post answered/clear through the same handler.

Comment on lines +315 to +321
if (sameUser) {
// No user-state event fires for the same user, so refetch here instead of
// clearing lists that nothing would repopulate.
fetchUserDataFromApi()
return@withContext
}
resetUserFetch()

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 (Grok; related Opus): same-user refetches here because no user-state event fires. The other branch only resetUserFetch() and waits for onUserStateChange, which is onesignal_id hydrate — not replace, and not a parked LoginUserOperation (SDK-5476). Launch already fetches by external_id with no onesignal_id; a switch leaves the row at — and never sends a stored bearer. fetchUserDataFromApi() after _externalUserId is set would match launch.

Comment on lines +108 to +109
if (prefs.contains(LEGACY_JWT_TOKEN_PREF)) {
prefs.edit().remove(LEGACY_JWT_TOKEN_PREF).apply()

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 (3/3): this deletes the old single-token key and does not copy it under the cached external id. An upgraded demo with the toggle on will unsigned-401 until the tester pastes the JWT again. If that is acceptable, say so in examples/build.md; otherwise migrate then delete.

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

Approve. Demo-only, and the behavior in the description matches the code.

The JwtAskStore ordering note already on the thread is the one nit: post answered and clear through the same handler, and let a later invalidation still show.

A different-user login that succeeds still refetches when the server id arrives. A parked login leaving the REST row at the dash placeholder is what resetUserFetch describes. The legacy JWT_TOKEN_PREF delete is intentional in SharedPreferenceUtil.

@fadi-george fadi-george 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.

I need to test it out.

The demo's Identity Verification controls now follow one contract
shared with the iOS demo.

- onUserJwtInvalidated only logged. MainApplication now registers the
  listener right after init, since the SDK doesn't replay the event
  and MainViewModel dies with MainActivity. The listener shows a
  banner with the asked id, a PROVIDE JWT button, and a snackbar once
  per ask. It leaves the stored token alone, since an SDK that asks
  when requests park without a token fires the same event.
- The demo kept one token, so an invalidation for another user or an
  A to B to A switch lost the right one. It now keeps a token per
  external id. A login with a token or updateUserJwt stores it, and
  logout clears them all.
- A failed /users fetch was invisible. A REST fetch row now reads OK
  or the reason, for example "external_id fetch failed: HTTP 401". A
  user switch, logout, or skipped fetch resets it, and a switch or
  logout drops any fetch still in flight for the previous user.
- The toggle read "Identity Verification" but only picks the alias for
  the demo's own REST fetch. It now reads "Fetch by external_id (JWT)"
  and refetches when flipped. The launch fetch no longer needs a
  onesignal_id.
- Update User JWT opens a dialog with the fields stacked full width,
  prefilled from the pending ask, confirming with "Update". UPDATE
  USER JWT and PROVIDE JWT are always enabled.
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.

3 participants