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
25 changes: 22 additions & 3 deletions 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 updateJwt

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

var requiresKeyValue: Bool {
switch self {
case .alias, .tag, .trigger: return true
case .email, .sms, .externalUserId: return false
case .alias, .tag, .trigger, .externalUserId, .updateJwt: return true
case .email, .sms: return false
}
}

/// When true the second field may be left blank (confirm stays enabled).
/// Used by Login, where the JWT token is optional.
var optionalValue: Bool {
switch self {
case .externalUserId: return true
default: return false
}
}

var keyPlaceholder: String {
switch self {
case .alias: return "Label"
case .tag, .trigger: return "Key"
case .externalUserId, .updateJwt: return "External User Id"
default: return "Key"
}
}
Expand All @@ -113,7 +125,8 @@ enum AddItemType {
case .email: return "Email Address"
case .sms: return "Phone Number"
case .tag, .trigger: return "Value"
case .externalUserId: return "External User Id"
case .externalUserId: return "JWT Token (optional)"
case .updateJwt: return "JWT Token"
}
}

Expand All @@ -128,6 +141,7 @@ enum AddItemType {
var confirmLabel: String {
switch self {
case .externalUserId: return "Login"
case .updateJwt: return "Update"
default: return "Add"
}
}
Expand All @@ -141,6 +155,7 @@ enum AddItemType {
case .tag: return "tag"
case .trigger: return "trigger"
case .externalUserId: return "login_user_id"
case .updateJwt: return "update_jwt"
}
}

Expand All @@ -152,6 +167,8 @@ enum AddItemType {
case .alias: return "alias_label_input"
case .tag: return "tag_key_input"
case .trigger: return "trigger_key_input"
case .externalUserId: return "login_user_id_input"
case .updateJwt: return "update_jwt_external_id_input"
default: return "\(accessibilityKey)_key_input"
}
}
Expand All @@ -165,6 +182,8 @@ enum AddItemType {
case .alias: return "alias_id_input"
case .tag: return "tag_value_input"
case .trigger: return "trigger_value_input"
case .externalUserId: return "login_user_jwt_input"
case .updateJwt: return "update_jwt_token_input"
default: return "\(accessibilityKey)_input"
}
}
Expand Down
8 changes: 6 additions & 2 deletions examples/demo/App/Services/OneSignalService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ final class OneSignalService {

// MARK: - User

func login(externalId: String) {
func login(externalId: String, token: String? = nil) {
prefs.setExternalUserId(externalId)
OneSignal.login(externalId)
OneSignal.login(externalId: externalId, token: token)
}

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

func logout() {
Expand Down
17 changes: 10 additions & 7 deletions examples/demo/App/ViewModels/OneSignalViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ final class OneSignalViewModel: ObservableObject {

// MARK: - UI State

@Published var isLoading: Bool = false

@Published var activeTooltip: TooltipData?

// MARK: - Computed
Expand Down Expand Up @@ -129,7 +127,6 @@ final class OneSignalViewModel: ObservableObject {
guard let onesignalId = service.onesignalId else { return }
requestSequence &+= 1
let captured = requestSequence
isLoading = true

let userData = await UserFetchService.shared.fetchUser(appId: appId, onesignalId: onesignalId)

Expand All @@ -145,7 +142,6 @@ final class OneSignalViewModel: ObservableObject {
externalUserId = extId
}
}
isLoading = false
}

// MARK: - Consent
Expand All @@ -166,15 +162,22 @@ final class OneSignalViewModel: ObservableObject {

// MARK: - User

func login(externalId: String) {
func login(externalId: String, token: String? = nil) {
let trimmed = externalId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
isLoading = true
service.login(externalId: trimmed)
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines)
service.login(externalId: trimmed, token: (trimmedToken?.isEmpty ?? true) ? nil : trimmedToken)
externalUserId = trimmed
clearUserData()
}

func updateUserJwt(externalId: String, token: String) {
let trimmedId = externalId.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedId.isEmpty, !trimmedToken.isEmpty else { return }
service.updateUserJwt(externalId: trimmedId, token: trimmedToken)
}

func logout() {
service.logout()
externalUserId = nil
Expand Down
5 changes: 3 additions & 2 deletions examples/demo/App/Views/Components/AddItemDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ struct AddItemDialog: View {

private var isValid: Bool {
if itemType.requiresKeyValue {
return !keyText.trimmingCharacters(in: .whitespaces).isEmpty &&
!valueText.trimmingCharacters(in: .whitespaces).isEmpty
let keyOK = !keyText.trimmingCharacters(in: .whitespaces).isEmpty
if itemType.optionalValue { return keyOK }
return keyOK && !valueText.trimmingCharacters(in: .whitespaces).isEmpty
}
return !valueText.trimmingCharacters(in: .whitespaces).isEmpty
}
Expand Down
23 changes: 21 additions & 2 deletions examples/demo/App/Views/Sections/UserSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import SwiftUI
struct UserSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
@State private var loginOpen = false
@State private var updateJwtOpen = false

var body: some View {
SectionCard(title: "USER", sectionKey: "user") {
Expand All @@ -55,6 +56,14 @@ struct UserSection: View {
loginOpen = true
}

ActionButton(
"UPDATE JWT",
style: .outline,
accessibilityID: "update_jwt_button"
) {
updateJwtOpen = true
}

if viewModel.isLoggedIn {
ActionButton(
"LOGOUT USER",
Expand All @@ -68,12 +77,22 @@ struct UserSection: View {
.osCenteredDialog(isPresented: $loginOpen) {
AddItemDialog(
itemType: .externalUserId,
onAdd: { _, value in
viewModel.login(externalId: value)
onAdd: { externalId, token in
viewModel.login(externalId: externalId, token: token.isEmpty ? nil : token)
loginOpen = false
},
onCancel: { loginOpen = false }
)
}
.osCenteredDialog(isPresented: $updateJwtOpen) {
AddItemDialog(
itemType: .updateJwt,
onAdd: { externalId, token in
viewModel.updateUserJwt(externalId: externalId, token: token)
updateJwtOpen = false
},
onCancel: { updateJwtOpen = false }
)
}
}
}
4 changes: 4 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
3CC063E02B6D7F2A002BB07F /* OneSignalUserMocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CC063DF2B6D7F2A002BB07F /* OneSignalUserMocks.h */; settings = {ATTRIBUTES = (Public, ); }; };
3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */; };
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */; };
B91A66287DEA4026A4DC5952 /* OSIdentityModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1399651D1A401EB888DA77 /* OSIdentityModelTests.swift */; };
3CC063EF2B6D7FE8002BB07F /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */; };
3CC9A6342AFA1FDE008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */; };
Expand Down Expand Up @@ -1439,6 +1440,7 @@
3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserMocks.swift; sourceTree = "<group>"; };
3CC063EB2B6D7FE8002BB07F /* OneSignalUserTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalUserTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserTests.swift; sourceTree = "<group>"; };
6C1399651D1A401EB888DA77 /* OSIdentityModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelTests.swift; sourceTree = "<group>"; };
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConcurrencyTests.swift; sourceTree = "<group>"; };
3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2422,6 +2424,7 @@
3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */,
3CF11E3E2C6D61AC002856F5 /* Executors */,
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */,
6C1399651D1A401EB888DA77 /* OSIdentityModelTests.swift */,
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */,
3CB331672F281679000E1801 /* CustomEventsIntegrationTests.swift */,
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */,
Expand Down Expand Up @@ -4539,6 +4542,7 @@
DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */,
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
B91A66287DEA4026A4DC5952 /* OSIdentityModelTests.swift in Sources */,
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,
DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */,
3CB3316A2F281692000E1801 /* OSCustomEventsExecutorTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,55 @@ class OSIdentityModel: OSModel {
return internalGetAlias(OS_EXTERNAL_ID)
}

// All access to aliases should go through helper methods with locking
// All access to aliases and jwtBearerToken must go through the lock
var aliases: [String: String] = [:]
private let aliasesLock = NSRecursiveLock()
private let lock = NSRecursiveLock()

// MARK: - JWT

private var jwtBearerTokenLocked: String? // only read/write under self.lock
public var jwtBearerToken: String? {
didSet {
guard jwtBearerToken != oldValue else {
return
get {
lock.withLock { jwtBearerTokenLocked }
}
set {
// Lock only the storage write. The change notifier fires synchronously
// to listeners that may take other locks
let changed = lock.withLock {
guard newValue != jwtBearerTokenLocked else { return false }
jwtBearerTokenLocked = newValue
return true
}
if changed {
self.set(property: OS_JWT_BEARER_TOKEN, newValue: newValue)
}
self.set(property: OS_JWT_BEARER_TOKEN, newValue: jwtBearerToken)
}
}

func isJwtValid() -> Bool {
return jwtBearerToken != nil && jwtBearerToken != "" && jwtBearerToken != OS_JWT_TOKEN_INVALID
/// Returns the bearer token if it is valid, otherwise nil, snapshots once
func getValidJwt() -> String? {
let token = jwtBearerToken
guard let token = token, !token.isEmpty, token != OS_JWT_TOKEN_INVALID else {
return nil
}
return token
}

/**
Atomically transition the JWT token to `OS_JWT_TOKEN_INVALID`. Returns
`true` if the transition occurred, `false` if the token was already invalid.
*/
@discardableResult
func invalidateJwtBearerToken() -> Bool {
let changed = lock.withLock {
guard jwtBearerTokenLocked != OS_JWT_TOKEN_INVALID else { return false }
jwtBearerTokenLocked = OS_JWT_TOKEN_INVALID
return true
}
if changed {
self.set(property: OS_JWT_BEARER_TOKEN, newValue: OS_JWT_TOKEN_INVALID)
}
return changed
}

// MARK: - Initialization
Expand All @@ -66,10 +98,10 @@ class OSIdentityModel: OSModel {
}

override func encode(with coder: NSCoder) {
aliasesLock.withLock {
lock.withLock {
super.encode(with: coder)
coder.encode(aliases, forKey: "aliases")
coder.encode(jwtBearerToken, forKey: OS_JWT_BEARER_TOKEN)
coder.encode(jwtBearerTokenLocked, forKey: OS_JWT_BEARER_TOKEN)
}
}

Expand All @@ -79,20 +111,20 @@ class OSIdentityModel: OSModel {
// log error
return nil
}
self.jwtBearerToken = coder.decodeObject(forKey: OS_JWT_BEARER_TOKEN) as? String
self.jwtBearerTokenLocked = coder.decodeObject(forKey: OS_JWT_BEARER_TOKEN) as? String
self.aliases = aliases
}

/** Threadsafe getter for an alias */
private func internalGetAlias(_ label: String) -> String? {
aliasesLock.withLock {
lock.withLock {
return self.aliases[label]
}
}

/** Threadsafe setter or removal for aliases */
private func internalAddAliases(_ aliases: [String: String]) {
aliasesLock.withLock {
lock.withLock {
for (label, id) in aliases {
// Remove the alias if the ID field is ""
self.aliases[label] = id.isEmpty ? nil : id
Expand All @@ -105,7 +137,7 @@ class OSIdentityModel: OSModel {
Called to clear the model's data in preparation for hydration via a fetch user call.
*/
func clearData() {
aliasesLock.withLock {
lock.withLock {
self.aliases = [:]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,19 @@ class OSIdentityModelRepo {
This can be optimized in the future to re-use an Identity Model if multiple logins are made for the same user.
*/
func updateJwtToken(externalId: String, token: String) {
var found = false
lock.withLock {
for model in models.values {
if model.externalId == externalId {
model.jwtBearerToken = token
found = true
}
}
// Snapshot matching models under the repo lock, then mutate outside.
// Writing the token fires the model's change notifier synchronously
// (→ onModelUpdated → onJwtTokenChanged); doing that while holding the
// repo lock leaves a trap for future listeners to deadlock on.
let matchingModels: [OSIdentityModel] = lock.withLock {
models.values.filter { $0.externalId == externalId }
}
if !found {
guard !matchingModels.isEmpty else {
OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_ERROR, message: "Update User JWT called for external ID \(externalId) that does not exist")
return
}
for model in matchingModels {
model.jwtBearerToken = token
}
}
}
Expand Down
Loading
Loading