Skip to content

Commit ff7db27

Browse files
committed
chore: [SDK-5283] give the demo the same JWT behavior as Android
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.
1 parent 6a22dea commit ff7db27

9 files changed

Lines changed: 191 additions & 88 deletions

File tree

‎examples/demo/App/App.swift‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
5454
// Initialize OneSignal
5555
OneSignalService.shared.initialize(launchOptions: launchOptions)
5656

57+
// Here rather than in the view model. The SDK doesn't replay this event, so a
58+
// listener added later misses a 401 at launch.
59+
OneSignalService.shared.addUserJwtInvalidatedListener(JwtInvalidatedHandler.shared)
60+
5761
// Set up notification lifecycle listeners
5862
setupNotificationListeners()
5963

@@ -114,6 +118,20 @@ class NotificationClickHandler: NSObject, OSNotificationClickListener {
114118
}
115119
}
116120

121+
// MARK: - JWT Handler
122+
123+
class JwtInvalidatedHandler: NSObject, OSUserJwtInvalidatedListener {
124+
static let shared = JwtInvalidatedHandler()
125+
126+
func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) {
127+
let externalId = event.externalId
128+
LogService.info("JWT invalidated for externalId: \(externalId)")
129+
Task { @MainActor in
130+
JwtAskStore.shared.invalidated(externalId: externalId)
131+
}
132+
}
133+
}
134+
117135
// MARK: - In-App Message Handlers
118136

119137
class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener {

‎examples/demo/App/Models/AppModels.swift‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ enum AddItemType {
8080
case tag
8181
case trigger
8282
case externalUserId
83-
case updateUserJwt
8483

8584
var title: String {
8685
switch self {
@@ -90,13 +89,12 @@ enum AddItemType {
9089
case .tag: return "Add Tag"
9190
case .trigger: return "Add Trigger"
9291
case .externalUserId: return "Login User"
93-
case .updateUserJwt: return "Update User JWT"
9492
}
9593
}
9694

9795
var requiresKeyValue: Bool {
9896
switch self {
99-
case .alias, .tag, .trigger, .updateUserJwt: return true
97+
case .alias, .tag, .trigger: return true
10098
case .email, .sms, .externalUserId: return false
10199
}
102100
}
@@ -105,7 +103,6 @@ enum AddItemType {
105103
switch self {
106104
case .alias: return "Label"
107105
case .tag, .trigger: return "Key"
108-
case .updateUserJwt: return "External User Id"
109106
default: return "Key"
110107
}
111108
}
@@ -117,7 +114,6 @@ enum AddItemType {
117114
case .sms: return "Phone Number"
118115
case .tag, .trigger: return "Value"
119116
case .externalUserId: return "External User Id"
120-
case .updateUserJwt: return "JWT Token"
121117
}
122118
}
123119

@@ -145,7 +141,6 @@ enum AddItemType {
145141
case .tag: return "tag"
146142
case .trigger: return "trigger"
147143
case .externalUserId: return "login_user_id"
148-
case .updateUserJwt: return "update_jwt"
149144
}
150145
}
151146

@@ -157,7 +152,6 @@ enum AddItemType {
157152
case .alias: return "alias_label_input"
158153
case .tag: return "tag_key_input"
159154
case .trigger: return "trigger_key_input"
160-
case .updateUserJwt: return "update_jwt_external_id_input"
161155
default: return "\(accessibilityKey)_key_input"
162156
}
163157
}
@@ -171,7 +165,6 @@ enum AddItemType {
171165
case .alias: return "alias_id_input"
172166
case .tag: return "tag_value_input"
173167
case .trigger: return "trigger_value_input"
174-
case .updateUserJwt: return "update_jwt_token_input"
175168
default: return "\(accessibilityKey)_input"
176169
}
177170
}

‎examples/demo/App/Services/OneSignalService.swift‎

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ final class OneSignalService {
5353
/// location-shared once the SDK is ready. Without this, toggles flip back
5454
/// to defaults on every cold launch.
5555
///
56-
/// No login here. The SDK restores the user from its own cache, and the stored JWT
57-
/// stays with the demo so the SDK's own ask through `OSUserJwtInvalidatedListener`
56+
/// No login here. The SDK restores the user from its own cache, and the stored JWTs
57+
/// stay with the demo so the SDK's own ask through `OSUserJwtInvalidatedListener`
5858
/// stays observable on cold start.
5959
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
6060
OneSignal.Debug.setLogLevel(.LL_VERBOSE)
@@ -96,10 +96,9 @@ final class OneSignalService {
9696
// MARK: - User
9797

9898
func login(externalId: String, jwtToken: String? = nil) {
99-
// Demo REST fetch only. Not replayed to the SDK at launch. A same-user login
100-
// without a token keeps the stored one, matching the SDK, which keeps its own.
101-
if jwtToken != nil || externalId != OneSignal.User.externalId {
102-
prefs.setSessionJwtToken(jwtToken)
99+
// A blank token field leaves any token already stored for this user.
100+
if let jwtToken = jwtToken {
101+
prefs.setJwtToken(jwtToken, externalId: externalId)
103102
}
104103
prefs.setExternalUserId(externalId)
105104
if let jwtToken = jwtToken {
@@ -110,17 +109,15 @@ final class OneSignalService {
110109
}
111110

112111
func updateUserJwt(externalId: String, token: String) {
113-
// The demo's REST bearer belongs to the current user. A token for anyone else
114-
// still goes to the SDK, which owns the parked work for that id.
115-
if externalId == OneSignal.User.externalId {
116-
prefs.setSessionJwtToken(token)
117-
}
112+
// Stored whoever it's for. The SDK can hold parked requests for a user who isn't the
113+
// current one, including one who has logged out.
114+
prefs.setJwtToken(token, externalId: externalId)
118115
OneSignal.updateUserJwt(externalId: externalId, token: token)
119116
}
120117

121118
func logout() {
122119
prefs.setExternalUserId(nil)
123-
prefs.setSessionJwtToken(nil)
120+
prefs.clearJwtTokens()
124121
OneSignal.logout()
125122
}
126123

@@ -129,11 +126,6 @@ final class OneSignalService {
129126
OneSignal.User.setLanguage(language)
130127
}
131128

132-
/// Called when the SDK rejects the token, so the REST fetch stops sending it.
133-
func clearSessionJwtToken() {
134-
prefs.setSessionJwtToken(nil)
135-
}
136-
137129
// MARK: - Identity Verification (demo REST fetch)
138130

139131
/// Demo toggle for addressing the REST user fetch by `external_id`. Persisted across launches.
@@ -142,8 +134,8 @@ final class OneSignalService {
142134
set { prefs.setUseIdentityVerification(newValue) }
143135
}
144136

145-
/// Token from the last login / updateUserJwt, for the demo REST fetch only. Cleared when the SDK rejects it.
146-
var sessionJwtToken: String? { prefs.getSessionJwtToken() }
137+
/// The last token handed to the SDK for this user, for the demo REST fetch only.
138+
func jwtToken(for externalId: String) -> String? { prefs.getJwtToken(externalId: externalId) }
147139

148140
// MARK: - Aliases
149141

@@ -286,3 +278,37 @@ final class OneSignalService {
286278
OneSignal.InAppMessages.addLifecycleListener(listener)
287279
}
288280
}
281+
282+
// MARK: - JWT ask
283+
284+
/// The SDK's pending ask for a JWT. App-scoped so an event caught by the listener `AppDelegate`
285+
/// registers at launch still reaches the UI, however late the view model starts. The SDK doesn't
286+
/// replay the event.
287+
@MainActor
288+
final class JwtAskStore: ObservableObject {
289+
290+
static let shared = JwtAskStore()
291+
292+
/// External id the SDK is waiting on a JWT for, or nil.
293+
@Published private(set) var externalId: String?
294+
295+
private init() {}
296+
297+
/// Replaces any earlier ask. The stored token stays. The SDK also fires this event when
298+
/// requests park without a token, so it doesn't prove the stored token is bad. A token that
299+
/// is bad shows up as a 401 in the REST fetch row.
300+
func invalidated(externalId: String) {
301+
self.externalId = externalId
302+
}
303+
304+
/// A token for `externalId` answers the ask if it is for that user.
305+
func answered(externalId: String) {
306+
if self.externalId == externalId {
307+
self.externalId = nil
308+
}
309+
}
310+
311+
func clear() {
312+
externalId = nil
313+
}
314+
}

‎examples/demo/App/Services/PreferencesService.swift‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import Foundation
2929

3030
/// `UserDefaults`-backed cache for state the demo restores across cold launches:
3131
/// consent flags, IAM paused, location shared, the last-logged-in external user id,
32-
/// the Identity Verification toggle, and the demo REST JWT. The JWT is only for the
33-
/// demo's `/users` fetch — it is not automatically passed to `login` / `updateUserJwt`.
32+
/// the Identity Verification toggle, and the demo's JWTs. The JWTs are only for the
33+
/// demo's `/users` fetch. The demo never passes them to `login` or `updateUserJwt` on its own.
3434
final class PreferencesService {
3535

3636
static let shared = PreferencesService()
@@ -48,7 +48,9 @@ final class PreferencesService {
4848
static let locationShared = "onesignal.demo.locationShared"
4949
static let externalUserId = "onesignal.demo.externalUserId"
5050
static let useIdentityVerification = "onesignal.demo.useIdentityVerification"
51-
static let sessionJwtToken = "onesignal.demo.sessionJwtToken"
51+
static let jwtTokens = "onesignal.demo.jwtTokens"
52+
/// The single token the demo kept before `jwtTokens`. Deleted on first read.
53+
static let legacySessionJwtToken = "onesignal.demo.sessionJwtToken"
5254
static let language = "onesignal.demo.language"
5355
}
5456

@@ -92,19 +94,29 @@ final class PreferencesService {
9294
func getUseIdentityVerification() -> Bool { defaults.bool(forKey: Key.useIdentityVerification) }
9395
func setUseIdentityVerification(_ value: Bool) { defaults.set(value, forKey: Key.useIdentityVerification) }
9496

95-
func getSessionJwtToken() -> String? {
96-
guard let value = defaults.string(forKey: Key.sessionJwtToken), !value.isEmpty else {
97+
// Every JWT the tester handed the SDK, keyed by external id. Plaintext UserDefaults is fine
98+
// for a demo and not a pattern for real apps.
99+
100+
func getJwtToken(externalId: String) -> String? {
101+
guard let value = jwtTokens()[externalId], !value.isEmpty else {
97102
return nil
98103
}
99104
return value
100105
}
101106

102-
func setSessionJwtToken(_ value: String?) {
103-
if let value = value, !value.isEmpty {
104-
defaults.set(value, forKey: Key.sessionJwtToken)
105-
} else {
106-
defaults.removeObject(forKey: Key.sessionJwtToken)
107-
}
107+
func setJwtToken(_ token: String, externalId: String) {
108+
var tokens = jwtTokens()
109+
tokens[externalId] = token
110+
defaults.set(tokens, forKey: Key.jwtTokens)
111+
}
112+
113+
func clearJwtTokens() {
114+
defaults.removeObject(forKey: Key.jwtTokens)
115+
}
116+
117+
private func jwtTokens() -> [String: String] {
118+
defaults.removeObject(forKey: Key.legacySessionJwtToken)
119+
return defaults.dictionary(forKey: Key.jwtTokens) as? [String: String] ?? [:]
108120
}
109121

110122
// MARK: - Language

0 commit comments

Comments
 (0)