Skip to content
Merged
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
4 changes: 4 additions & 0 deletions examples/demo/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
5B959D44AB09CB821C00AFBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F726E64F9B6817F917227C /* ContentView.swift */; };
5F4B7EC8437D1A8D80DF7674 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20B46F63592FC67B655BEB8 /* NotificationService.swift */; };
638B81D9DA5FD8636BB038B0 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF49509A218369322ECFA3B9 /* OneSignalUser.framework */; };
673E0146B4B27EB81FCF50DA /* LoginUserDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD26AED9EA84B0308C1BFE8 /* LoginUserDialog.swift */; };
674995A7A55C13341317E19B /* OSDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D261D46C404E325CBA87A9E0 /* OSDialog.swift */; };
68BC99D15FDCB26EB35EBB07 /* OneSignalLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; };
6E3E040FD8A750248E70E46F /* AppModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA9D80191548D49F09D30B3 /* AppModels.swift */; };
Expand Down Expand Up @@ -324,6 +325,7 @@
38138523A8A81A60A77800CA /* NotificationSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSender.swift; sourceTree = "<group>"; };
3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMultiDialog.swift; sourceTree = "<group>"; };
3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPushSection.swift; sourceTree = "<group>"; };
3DD26AED9EA84B0308C1BFE8 /* LoginUserDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUserDialog.swift; sourceTree = "<group>"; };
3FD8258E807E6672642A32E6 /* UserFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchService.swift; sourceTree = "<group>"; };
4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPairInputDialog.swift; sourceTree = "<group>"; };
4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesSection.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -512,6 +514,7 @@
2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */,
5984B93007C6B85AFE09045A /* KeyValueRow.swift */,
280B23B41935EAB89C8C6FCB /* ListWidgets.swift */,
3DD26AED9EA84B0308C1BFE8 /* LoginUserDialog.swift */,
4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */,
D261D46C404E325CBA87A9E0 /* OSDialog.swift */,
D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */,
Expand Down Expand Up @@ -828,6 +831,7 @@
7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */,
DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */,
B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */,
673E0146B4B27EB81FCF50DA /* LoginUserDialog.swift in Sources */,
E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */,
4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */,
674995A7A55C13341317E19B /* OSDialog.swift in Sources */,
Expand Down
9 changes: 8 additions & 1 deletion examples/demo/App/Models/AppModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ enum AddItemType {
case tag
case trigger
case externalUserId
case updateUserJwt

var title: String {
switch self {
Expand All @@ -89,12 +90,13 @@ 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: return true
case .alias, .tag, .trigger, .updateUserJwt: return true
case .email, .sms, .externalUserId: return false
}
}
Expand All @@ -103,6 +105,7 @@ enum AddItemType {
switch self {
case .alias: return "Label"
case .tag, .trigger: return "Key"
case .updateUserJwt: return "External User Id"
default: return "Key"
}
}
Expand All @@ -114,6 +117,7 @@ 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 @@ -141,6 +145,7 @@ enum AddItemType {
case .tag: return "tag"
case .trigger: return "trigger"
case .externalUserId: return "login_user_id"
case .updateUserJwt: return "update_jwt"
}
}

Expand All @@ -152,6 +157,7 @@ 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 @@ -165,6 +171,7 @@ 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
62 changes: 52 additions & 10 deletions examples/demo/App/Services/OneSignalService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ final class OneSignalService {
// MARK: - Initialization

/// Mirrors the Capacitor demo's `useOneSignal` init order: feed cached
/// consent into the SDK BEFORE `initialize`, then restore IAM-paused,
/// location-shared, and a previously-logged-in external user id once the
/// SDK is ready. Without this, toggles flip back to defaults on every
/// cold launch.
/// consent into the SDK BEFORE `initialize`, then restore IAM-paused and
/// 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`
/// stays observable on cold start.
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
OneSignal.Debug.setLogLevel(.LL_VERBOSE)

Expand All @@ -63,10 +66,6 @@ final class OneSignalService {

OneSignal.InAppMessages.paused = prefs.getIamPaused()
OneSignal.Location.isShared = prefs.getLocationShared()

if let storedExternalId = prefs.getExternalUserId() {
OneSignal.login(storedExternalId)
}
}

// MARK: - Identity
Expand Down Expand Up @@ -96,16 +95,51 @@ final class OneSignalService {

// MARK: - User

func login(externalId: String) {
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)
}
prefs.setExternalUserId(externalId)
OneSignal.login(externalId)
if let jwtToken = jwtToken {
OneSignal.login(externalId: externalId, token: jwtToken)
} else {
OneSignal.login(externalId)
}
}

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)
}
OneSignal.updateUserJwt(externalId: externalId, token: token)
}

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

/// 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.
var useIdentityVerification: Bool {
get { prefs.getUseIdentityVerification() }
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() }

// MARK: - Aliases

func addAlias(label: String, id: String) { OneSignal.User.addAlias(label: label, id: id) }
Expand Down Expand Up @@ -222,6 +256,14 @@ final class OneSignalService {
OneSignal.Notifications.addPermissionObserver(observer)
}

func addUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) {
OneSignal.addUserJwtInvalidatedListener(listener)
}

func removeUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) {
OneSignal.removeUserJwtInvalidatedListener(listener)
}

func addNotificationClickListener(_ listener: OSNotificationClickListener) {
OneSignal.Notifications.addClickListener(listener)
}
Expand Down
28 changes: 25 additions & 3 deletions examples/demo/App/Services/PreferencesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import Foundation

/// `UserDefaults`-backed cache for state the demo restores across cold launches:
/// consent flags, IAM paused, location shared, and the last-logged-in external
/// user id. Mirrors the Capacitor demo's `PreferencesService` so the iOS demo
/// re-feeds these into the SDK during initialization.
/// 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`.
final class PreferencesService {

static let shared = PreferencesService()
Expand All @@ -47,6 +47,8 @@ final class PreferencesService {
static let iamPaused = "onesignal.demo.iamPaused"
static let locationShared = "onesignal.demo.locationShared"
static let externalUserId = "onesignal.demo.externalUserId"
static let useIdentityVerification = "onesignal.demo.useIdentityVerification"
static let sessionJwtToken = "onesignal.demo.sessionJwtToken"
}

// MARK: - Consent
Expand Down Expand Up @@ -83,4 +85,24 @@ final class PreferencesService {
defaults.removeObject(forKey: Key.externalUserId)
}
}

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

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 {
return nil
}
return value
}

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.

} else {
defaults.removeObject(forKey: Key.sessionJwtToken)
}
}
}
53 changes: 45 additions & 8 deletions examples/demo/App/Services/UserFetchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,66 @@

import Foundation

/// Reads the OneSignal /users API to hydrate aliases / tags / channels in the demo
/// Why a demo /users fetch returned nothing, shown in the User section so a 401 under
/// Identity Verification is not mistaken for an empty user.
enum UserFetchError: Error, CustomStringConvertible {
case badRequest
case http(Int)
case badBody
case transport(Error)

var description: String {
switch self {
case .badRequest: return "bad request"
case .http(let status): return "HTTP \(status)"
case .badBody: return "unreadable body"
case .transport(let error): return error.localizedDescription
}
}
}

/// Reads the OneSignal /users API to hydrate aliases / tags / channels in the demo.
final class UserFetchService {
static let shared = UserFetchService()
private init() {}

func fetchUser(appId: String, onesignalId: String) async -> UserData? {
let urlString = "https://api.onesignal.com/apps/\(appId)/users/by/onesignal_id/\(onesignalId)"
guard let url = URL(string: urlString) else { return nil }
/// Fetches by `onesignal_id` (unsigned) or `external_id` (Bearer when `jwt` is set).
func fetchUser(
appId: String,
aliasLabel: String,
aliasValue: String,
jwt: String? = nil
) async -> Result<UserData, UserFetchError> {
guard !aliasValue.isEmpty else { return .failure(.badRequest) }

// Path-encode so external_ids with reserved characters don't misroute the GET.
var allowed = CharacterSet.urlPathAllowed
allowed.remove(charactersIn: "/")
guard
let encodedAlias = aliasValue.addingPercentEncoding(withAllowedCharacters: allowed),
let url = URL(string: "https://api.onesignal.com/apps/\(appId)/users/by/\(aliasLabel)/\(encodedAlias)")
else {
return .failure(.badRequest)
}

var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let jwt = jwt {
request.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
}

do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
return nil
return .failure(.http((response as? HTTPURLResponse)?.statusCode ?? -1))
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
return .failure(.badBody)
}
return parse(json)
return .success(parse(json))
} catch {
return nil
return .failure(.transport(error))
}
}

Expand Down
Loading