Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf7c24d
chore: normalize project.pbxproj ordering
nan-li Aug 11, 2026
3421d83
fix: [PR2] read-your-write consistency defects
nan-li Aug 11, 2026
953ff22
fix: scope RYW resolve to one user and lower the bar on timeout
nan-li Aug 12, 2026
c09d5a9
chore: tighten RYW comments to iOS comment style
nan-li Aug 12, 2026
1b80146
feat: [PR3] feature flags, JWT config, and the Identity Verification …
nan-li Aug 11, 2026
9ea39ab
fix: [PR3] tighten params retry class and hydration handler race
nan-li Aug 12, 2026
2f3cd34
feat: [PR4] identity model JWT, the JWT repo, and the public API
nan-li Aug 11, 2026
5aea671
fix: [PR4] mark JWT bearer updates as local-only
nan-li Aug 12, 2026
2c267bf
feat: [PR5] bind each Delta to the user that owns it
nan-li Aug 11, 2026
b2a3c44
fix: [PR5] drop stale properties/subscription deltas instead of mis-s…
nan-li Aug 12, 2026
5f40aae
feat: [PR6] Identity Verification for the request pipeline
nan-li Aug 11, 2026
3197590
fix: [PR6] hold new-record IDs in purge tests under TEST delay
nan-li Aug 12, 2026
a642d1c
chore: [PR6] split types to clear SwiftLint length errors
nan-li Aug 12, 2026
ebeb006
fix: [PR6] keep restored Identify under IV so reshape can promote it
nan-li Aug 13, 2026
b39bcff
fix: [PR6] default missing addsNewRecords to true on Create User decode
nan-li Aug 13, 2026
1a3a237
fix: [PR6] keep uncached owned requests only while Identity Verificat…
nan-li Aug 13, 2026
f74a573
feat: [PR7] in-app messages under Identity Verification
nan-li Aug 11, 2026
51718e0
fix: [PR7] reset in-app messages on user change only while newCodePat…
nan-li Aug 13, 2026
e5884a7
fix: [PR7] refetch IAMs on 401 when a replacement token already landed
nan-li Aug 13, 2026
1fbdef9
feat: [PR8] demo app Identity Verification UI
nan-li Aug 11, 2026
3f0ca5e
chore: [PR8] (will revert) add local feature-flag override for testing
nan-li Aug 13, 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
22 changes: 13 additions & 9 deletions examples/demo/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 @@ -321,6 +322,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 @@ -358,7 +360,7 @@
E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = "<group>"; };
ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = "<group>"; };
F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
"TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = "<group>"; };
"TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -504,6 +506,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 @@ -564,10 +567,10 @@
path = Views;
sourceTree = "<group>";
};
"TEMP_9E448277-C21D-48E5-98E2-992293BCF68A" /* demo */ = {
"TEMP_22A9BEEB-F02D-4CD8-B729-B06597DB45B7" /* demo */ = {
isa = PBXGroup;
children = (
"TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */,
"TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */,
);
path = demo;
sourceTree = "<group>";
Expand Down Expand Up @@ -810,6 +813,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 Expand Up @@ -936,7 +940,7 @@
/* Begin XCBuildConfiguration section */
0D2EF3911CA89837C30DB0D1 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
Expand All @@ -954,7 +958,7 @@
};
4A0C935808978B5A7673E412 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App.entitlements;
Expand Down Expand Up @@ -1034,7 +1038,7 @@
};
D0E56A85F1C385808720F94B /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
Expand All @@ -1056,7 +1060,7 @@
};
EB1CC3A930E09FEBECF9195D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App.entitlements;
Expand All @@ -1079,7 +1083,7 @@
};
F305A3E63851EE49DA2D190E /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
Expand All @@ -1102,7 +1106,7 @@
};
F5FD25168D9B32A08A468069 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
baseConfigurationReference = "TEMP_4292FBC8-5C5B-4FEC-8FC6-E00954AA81D6" /* Build.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
DEVELOPMENT_TEAM = 99SW8E36CT;
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
35 changes: 33 additions & 2 deletions examples/demo/App/Services/OneSignalService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,39 @@ final class OneSignalService {

// MARK: - User

func login(externalId: String) {
func login(externalId: String, jwtToken: String? = nil) {
prefs.setExternalUserId(externalId)
OneSignal.login(externalId)
// Persist for the demo REST fetch only; cold start does not call login/updateUserJwt with it.
prefs.setSessionJwtToken(jwtToken)
if let jwtToken = jwtToken {
OneSignal.login(externalId: externalId, token: jwtToken)
} else {
OneSignal.login(externalId)
}
}

func updateUserJwt(externalId: String, token: String) {
prefs.setSessionJwtToken(token)
OneSignal.updateUserJwt(externalId: externalId, token: token)
}

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

// 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) }
}

/// JWT from the last login / updateUserJwt. Used by the demo REST fetch only — not auto-fed to the SDK on cold start.
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 +245,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)
} else {
defaults.removeObject(forKey: Key.sessionJwtToken)
}
}
}
27 changes: 23 additions & 4 deletions examples/demo/App/Services/UserFetchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,36 @@

import Foundation

/// Reads the OneSignal /users API to hydrate aliases / tags / channels in the demo
/// 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 -> UserData? {
guard !aliasValue.isEmpty else { return nil }

// 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 nil
}

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)
Expand Down
Loading
Loading