Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
02b56a0
feat: [JWT-3] feature flags, JWT config, and the Identity Verificatio…
nan-li Sep 10, 2026
272bb2c
feat: [JWT-4] identity model JWT, the JWT repo, and the public API (#…
nan-li Sep 10, 2026
b4e8aff
feat: [JWT-5] bind each Delta to the user that owns it (#1710)
nan-li Sep 10, 2026
addf79a
feat: [JWT-6] Identity Verification for the request pipeline (#1711)
nan-li Sep 10, 2026
1bf0c81
feat: [JWT-7] in-app messages under Identity Verification (#1712)
nan-li Sep 10, 2026
5f5336d
feat: [SDK-5156] wire named events into the log pipeline on iOS (#1732)
nan-li Sep 11, 2026
251eedf
feat: [SDK-5088] add device gesture that copies the push subscription…
nan-li Sep 11, 2026
6b202a5
feat: [SDK-5193] let an app opt out of subscription ID copy in Info.p…
nan-li Sep 11, 2026
57e62e5
feat: [JWT-8] demo app Identity Verification UI (#1708)
nan-li Sep 18, 2026
9ac6fdd
fix: [SDK-5099] respect REST API-disabled push subscriptions (#1731)
nan-li Sep 14, 2026
809d6ad
fix: [SDK-5202] canonicalize Chinese locales and make setLanguage("")…
fadi-george Sep 18, 2026
7df6890
fix: [SDK-5181] accept a create-subscription success with no subscrip…
nan-li Sep 18, 2026
ceb077a
fix: [SDK-5279] recreate the push subscription when the fetched user …
nan-li Sep 21, 2026
836b340
chore: [SDK-5282] keep the JWT out of the log pipeline (#1747)
nan-li Sep 21, 2026
c2fb272
chore: move OSSubscriptionModel device metadata properties into an ex…
nan-li Sep 21, 2026
a317781
fix: [SDK-5137] Identity Verification config and token request follow…
nan-li Sep 22, 2026
1a67200
fix: [SDK-5137] in-app message fetch wait follow-ups from the JWT rev…
nan-li Sep 22, 2026
56af6f6
fix: [SDK-5137] user request queue and identity model follow-ups from…
nan-li Sep 22, 2026
65ae0d3
chore: [SDK-5280] raise SDK deployment target to iOS 15 (#1749)
nan-li Sep 22, 2026
a260de0
chore: [SDK-5346] raise Mac Catalyst deployment target to 15 (#1757)
nan-li Sep 28, 2026
8f412f6
chore: [SDK-5307] remove stale compatibility code for iOS below 15 (#…
nan-li Sep 28, 2026
775f27f
fix: [SDK-5313] run in-app message trigger and pause changes on the m…
nan-li Oct 5, 2026
630b355
fix: [SDK-5426] reject blank identity strings (#1760)
abdulraqeeb33 Oct 6, 2026
f4e0153
fix: [SDK-5314] JWT - drop the Identity Verification beta queues and …
nan-li Oct 7, 2026
11849c9
feat: [SDK-5345] age out stale queued Requests to prevent bloat (#1759)
nan-li Oct 7, 2026
e286978
Merge main into the Identity Verification branch for the 5.8.0 pull r…
nan-li Oct 7, 2026
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,13 +95,32 @@ 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()
}

Expand All @@ -111,6 +129,22 @@ 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.
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 @@ -227,6 +261,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"
static let language = "onesignal.demo.language"
}

Expand Down Expand Up @@ -85,6 +87,26 @@ final class PreferencesService {
}
}

// 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)
} else {
defaults.removeObject(forKey: Key.sessionJwtToken)
}
}

// MARK: - Language

func getLanguage() -> String { defaults.string(forKey: Key.language) ?? "" }
Expand Down
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
Loading