Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/demo/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
// 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.

// listener added later misses a 401 at launch.
OneSignalService.shared.addUserJwtInvalidatedListener(JwtInvalidatedHandler.shared)

// Set up notification lifecycle listeners
setupNotificationListeners()

Expand Down Expand Up @@ -114,6 +118,20 @@ class NotificationClickHandler: NSObject, OSNotificationClickListener {
}
}

// MARK: - JWT Handler

class JwtInvalidatedHandler: NSObject, OSUserJwtInvalidatedListener {
static let shared = JwtInvalidatedHandler()

func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) {
let externalId = event.externalId
LogService.info("JWT invalidated for externalId: \(externalId)")
Task { @MainActor in
JwtAskStore.shared.invalidated(externalId: externalId)
}
}
}

// MARK: - In-App Message Handlers

class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener {
Expand Down
9 changes: 1 addition & 8 deletions examples/demo/App/Models/AppModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ enum AddItemType {
case tag
case trigger
case externalUserId
case updateUserJwt

var title: String {
switch self {
Expand All @@ -90,13 +89,12 @@ enum AddItemType {
case .tag: return "Add Tag"
case .trigger: return "Add Trigger"
case .externalUserId: return "Login User"
case .updateUserJwt: return "Update User JWT"
}
}

var requiresKeyValue: Bool {
switch self {
case .alias, .tag, .trigger, .updateUserJwt: return true
case .alias, .tag, .trigger: return true
case .email, .sms, .externalUserId: return false
}
}
Expand All @@ -105,7 +103,6 @@ enum AddItemType {
switch self {
case .alias: return "Label"
case .tag, .trigger: return "Key"
case .updateUserJwt: return "External User Id"
default: return "Key"
}
}
Expand All @@ -117,7 +114,6 @@ enum AddItemType {
case .sms: return "Phone Number"
case .tag, .trigger: return "Value"
case .externalUserId: return "External User Id"
case .updateUserJwt: return "JWT Token"
}
}

Expand Down Expand Up @@ -145,7 +141,6 @@ enum AddItemType {
case .tag: return "tag"
case .trigger: return "trigger"
case .externalUserId: return "login_user_id"
case .updateUserJwt: return "update_jwt"
}
}

Expand All @@ -157,7 +152,6 @@ enum AddItemType {
case .alias: return "alias_label_input"
case .tag: return "tag_key_input"
case .trigger: return "trigger_key_input"
case .updateUserJwt: return "update_jwt_external_id_input"
default: return "\(accessibilityKey)_key_input"
}
}
Expand All @@ -171,7 +165,6 @@ enum AddItemType {
case .alias: return "alias_id_input"
case .tag: return "tag_value_input"
case .trigger: return "trigger_value_input"
case .updateUserJwt: return "update_jwt_token_input"
default: return "\(accessibilityKey)_input"
}
}
Expand Down
64 changes: 45 additions & 19 deletions examples/demo/App/Services/OneSignalService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ final class OneSignalService {
/// location-shared once the SDK is ready. Without this, toggles flip back
/// to defaults on every cold launch.
///
/// No login here. The SDK restores the user from its own cache, and the stored JWT
/// stays with the demo so the SDK's own ask through `OSUserJwtInvalidatedListener`
/// No login here. The SDK restores the user from its own cache, and the stored JWTs
/// stay with the demo so the SDK's own ask through `OSUserJwtInvalidatedListener`
/// stays observable on cold start.
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
OneSignal.Debug.setLogLevel(.LL_VERBOSE)
Expand Down Expand Up @@ -96,10 +96,9 @@ final class OneSignalService {
// MARK: - User

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

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

func logout() {
prefs.setExternalUserId(nil)
prefs.setSessionJwtToken(nil)
prefs.clearJwtTokens()
OneSignal.logout()
}

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

/// Called when the SDK rejects the token, so the REST fetch stops sending it.
func clearSessionJwtToken() {
prefs.setSessionJwtToken(nil)
}

// MARK: - Identity Verification (demo REST fetch)

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

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

// MARK: - Aliases

Expand Down Expand Up @@ -286,3 +278,37 @@ final class OneSignalService {
OneSignal.InAppMessages.addLifecycleListener(listener)
}
}

// MARK: - JWT ask

/// The SDK's pending ask for a JWT. App-scoped so an event caught by the listener `AppDelegate`
/// registers at launch still reaches the UI, however late the view model starts. The SDK doesn't
/// replay the event.
@MainActor
final class JwtAskStore: ObservableObject {

static let shared = JwtAskStore()

/// External id the SDK is waiting on a JWT for, or nil.
@Published private(set) var externalId: String?

private init() {}

/// Replaces any earlier ask. The stored token stays. The SDK also fires this event when
/// requests park without a token, so it doesn't prove the stored token is bad. A token that
/// is bad shows up as a 401 in the REST fetch row.
func invalidated(externalId: String) {
self.externalId = externalId
}

/// A token for `externalId` answers the ask if it is for that user.
func answered(externalId: String) {
if self.externalId == externalId {
self.externalId = nil
}
}

func clear() {
externalId = nil
}
}
34 changes: 23 additions & 11 deletions examples/demo/App/Services/PreferencesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import Foundation

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

static let shared = PreferencesService()
Expand All @@ -48,7 +48,9 @@ final class PreferencesService {
static let locationShared = "onesignal.demo.locationShared"
static let externalUserId = "onesignal.demo.externalUserId"
static let useIdentityVerification = "onesignal.demo.useIdentityVerification"
static let sessionJwtToken = "onesignal.demo.sessionJwtToken"
static let jwtTokens = "onesignal.demo.jwtTokens"
/// The single token the demo kept before `jwtTokens`. Deleted on first read.
static let legacySessionJwtToken = "onesignal.demo.sessionJwtToken"
static let language = "onesignal.demo.language"
}

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

func getSessionJwtToken() -> String? {
guard let value = defaults.string(forKey: Key.sessionJwtToken), !value.isEmpty else {
// Every JWT the tester handed the SDK, keyed by external id. Plaintext UserDefaults is fine
// for a demo and not a pattern for real apps.

func getJwtToken(externalId: String) -> String? {
guard let value = jwtTokens()[externalId], !value.isEmpty else {
return nil
}
return value
}

func setSessionJwtToken(_ value: String?) {
if let value = value, !value.isEmpty {
defaults.set(value, forKey: Key.sessionJwtToken)
} else {
defaults.removeObject(forKey: Key.sessionJwtToken)
}
func setJwtToken(_ token: String, externalId: String) {
var tokens = jwtTokens()
tokens[externalId] = token
defaults.set(tokens, forKey: Key.jwtTokens)
}

func clearJwtTokens() {
defaults.removeObject(forKey: Key.jwtTokens)
}

private func jwtTokens() -> [String: String] {
defaults.removeObject(forKey: Key.legacySessionJwtToken)
return defaults.dictionary(forKey: Key.jwtTokens) as? [String: String] ?? [:]
}

// MARK: - Language
Expand Down
Loading
Loading