Skip to content

chore: [SDK-5283] give the demo the same JWT behavior as Android - #1774

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 Android demo (OneSignal/OneSignal-Android-SDK#2784).

Details

Motivation

A review of both demos found that their JWT controls behaved differently, and found bugs. The REST fetch row kept the previous user's OK after a switch or logout. A late fetch could bring back the previous user. The demo stored a single token, so any invalidation or some user switches lost it. The Update User JWT dialog squeezed a long token into a half-width field.

Scope

❗️Demo app only. No SDK changes.

  • The JWT invalidated listener registers in AppDelegate at launch and feeds an app-wide JwtAskStore. The banner and toast are unchanged.
  • The demo stores one token per user. Logout clears them. An invalidation event doesn't, because the SDK also fires it when requests are only waiting for a token.
  • The REST fetch row resets on a user switch or logout, and a late fetch for the previous user is dropped.
  • A new UpdateJwtDialog stacks its fields and confirms with "Update".
  • build.md documents the controls and the remaining SDK differences.

No new Swift files, so project.pbxproj is unchanged.

Testing

Unit testing

None. Demo UI only.

Manual testing

Ran 13 scenarios on an iPhone 17 Pro simulator against a test app with Identity Verification on. They covered login with and without a token, user switches, expired tokens, PROVIDE JWT, and logout. 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

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

- The JWT invalidated listener lived on the view model. AppDelegate now
  registers it right after init, since the SDK doesn't replay the
  event. It hands each event to an app-wide JwtAskStore that the view
  model mirrors into jwtAskExternalId. The stored token stays, since
  the SDK fires the same event when requests park without a token.
- An invalidation for any user cleared the one stored token, and an A
  to B to A switch lost A's. The demo now keeps a token per external
  id. A login with a token or updateUserJwt stores it, and logout
  clears them all.
- The REST fetch row kept the previous user's result after a switch or
  logout, and a fetch for the old user could land afterwards and write
  the old external id back. A switch or logout now bumps
  requestSequence and resets the row, and a skipped fetch resets it
  too.
- The Update User JWT dialog laid the id and a several-hundred-character
  token side by side and confirmed with "Add". UpdateJwtDialog stacks
  them full width and confirms with "Update". AddItemType loses its
  updateUserJwt case and AddItemDialog its initialKey.
- build.md gains an Identity Verification controls section, including
  the behavior that still differs because the SDKs differ.
@nan-li
nan-li requested a review from a team October 29, 2026 16:16
@fadi-george

Copy link
Copy Markdown
Collaborator

Please adds screenshots

@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 token map and not clearing a token on invalidation match Android. The simulator failure is FileLogStoreRecordDatingTests, which this diff does not touch.


if useIdentityVerification {
guard let externalId = externalUserId ?? service.externalId, !externalId.isEmpty else {
userFetchStatus = nil

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 missing-id returns clear userFetchStatus and return before requestSequence increments, so an in-flight fetch still writes the row. Bump the sequence first, the way Android does. Same at the onesignal-id guard below.

Comment thread examples/demo/build.md
- **Stored tokens.** `PreferencesService` keeps every JWT the tester hands the SDK, keyed by external id, as a dictionary under `onesignal.demo.jwtTokens`. A login with a token or an `updateUserJwt` stores it for that id, whoever it is for. A login with the field blank changes nothing, and neither does an invalidation event. Logout clears them all. Plaintext UserDefaults is fine for a demo and not a pattern for real apps.
- **JWT field in `LoginUserDialog`** (`login_user_jwt_input`). Optional. A same-user login refetches, because the SDK fires no user-state event for it.
- **UPDATE USER JWT button** (`update_user_jwt_button`). Always enabled. Opens `UpdateJwtDialog`, which stacks the External User Id and JWT Token fields full width, prefills the id from the pending ask and then the current user, and confirms with `Update`. Calls `OneSignal.updateUserJwt` and refetches when the id is the current user's.
- **JWT ask banner** (`jwt_ask_banner`, with `jwt_ask_external_id_value` and `jwt_ask_provide_button`). `AppDelegate` registers `JwtInvalidatedHandler` right after init, because the SDK doesn't replay the event. The handler hands the asked id to `JwtAskStore`. It leaves the stored token alone, because the SDK also fires this event when requests park without a token, so the event doesn't prove the token is bad. A bad token shows up as a 401 in the REST fetch row. `OneSignalViewModel.jwtAskExternalId` mirrors it, and `UserSection` shows the banner plus a toast once per ask. A token for that id, from `updateUserJwt` or a login, clears the ask, and so does logout. The demo never answers the ask with a stored token on its own. PROVIDE JWT is always enabled.

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.

UserSection only toasts from onChange(of: jwtAskExternalId), which does not run for a value already set when the section appears. A launch ask that lands in JwtAskStore first shows the banner and skips the toast.

// Initialize OneSignal
OneSignalService.shared.initialize(launchOptions: launchOptions)

// Here rather than in the view model. The SDK doesn't replay this event, so a

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 SDK does replay pending JWT asks from addUserJwtInvalidatedListener. The same claim is on JwtAskStore and in build.md. Registering in AppDelegate is still right. Drop the "does not replay" sentence.

@fadi-george fadi-george left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

reviewing

@fadi-george

fadi-george commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

can we remove fetch row, doesn't seem that useful
the warning always comes up if theres an error right?

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