Skip to content
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 @@ -1320,7 +1320,12 @@ - (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state {

#pragma mark OSUserJwtConfigListener Methods
- (void)onRequiresUserAuthChangedFrom:(enum OSRequiresUserAuth)from to:(enum OSRequiresUserAuth)to {
// This callback is unused, the controller will fetch when subscription ID changes
// Identity Verification was turned off: a fetch deferred waiting for a JWT may never be
// retried via onJwtUpdated once auth is off, so release it here
if (to == OSRequiresUserAuthOff && shouldRetryGetInAppMessagesOnJwtUpdated) {
shouldRetryGetInAppMessagesOnJwtUpdated = false;
[self getInAppMessagesFromServer];
}
Comment thread
abdulraqeeb33 marked this conversation as resolved.
}

- (void)onJwtUpdatedWithExternalId:(NSString *)externalId token:(NSString *)token {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,35 @@ extension OSCustomEventsExecutor: OSUserJwtConfigListener {
// If auth changed from false or unknown to true, drop invalid items
if to == .on {
removeInvalidDeltasAndRequests()
} else if to == .off {
// Identity Verification was turned off: release requests parked awaiting a JWT.
reQueueAllPendingRequests()
}
}

func onJwtUpdated(externalId: String, token: String?) {
reQueuePendingRequestsForExternalId(externalId: externalId)
}

/// Identity Verification was turned off: move every auth-pended request, across all
/// external IDs, back into the request queue and flush.
private func reQueueAllPendingRequests() {

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.

so even though the calls originated with JWT and now JWT is off, we want to reQueue the requests?

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.

Good point, we could decide to drop but this would come from something like:

  1. login(userA) - addTag(userA)
  2. SDK tries to send it but the token for userA has since expired
  3. login(userB) - addTag(userB)
  4. SDK tries to send but the token for userB has expires
  5. All these requests go into a pending queue until an updated token is provided for one or both of them
  6. When IV resolves to false, we can send these requests without a token

self.dispatchQueue.async {
guard !self.pendingAuthRequests.isEmpty else {
return
}
for (_, requests) in self.pendingAuthRequests {
for request in requests {
self.requestQueue.append(request)
}
}
self.pendingAuthRequests = [:]
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
self.processRequestQueue(inBackground: false)
}
}

private func reQueuePendingRequestsForExternalId(externalId: String) {
self.dispatchQueue.async {
guard let requests = self.pendingAuthRequests[externalId] else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,16 +397,43 @@ class OSIdentityOperationExecutor: OSOperationExecutor {

extension OSIdentityOperationExecutor: OSUserJwtConfigListener {
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
// If auth changed from false or unknown to true, process requests
// If auth changed from false or unknown to true, drop now-invalid requests
if to == .on {
removeInvalidDeltasAndRequests()
} else if to == .off {
// Identity Verification was turned off: release requests parked awaiting a JWT.
reQueueAllPendingRequests()
}
}

func onJwtUpdated(externalId: String, token: String?) {
reQueuePendingRequestsForExternalId(externalId: externalId)
}

/// Identity Verification was turned off: move every auth-pended request, across all
/// external IDs, back into the request queue and flush
private func reQueueAllPendingRequests() {
self.dispatchQueue.async {
guard !self.pendingAuthRequests.isEmpty else {
return
}
for (_, requests) in self.pendingAuthRequests {
for request in requests {
if let addRequest = request as? OSRequestAddAliases {
self.addRequestQueue.append(addRequest)
} else if let removeRequest = request as? OSRequestRemoveAlias {
self.removeRequestQueue.append(removeRequest)
}
}
}
self.pendingAuthRequests = [:]
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
self.processRequestQueue(inBackground: false)
}
}

private func reQueuePendingRequestsForExternalId(externalId: String) {
self.dispatchQueue.async {
guard let requests = self.pendingAuthRequests[externalId] else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,16 +380,38 @@ class OSPropertyOperationExecutor: OSOperationExecutor {

extension OSPropertyOperationExecutor: OSUserJwtConfigListener {
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
// If auth changed from false or unknown to true, process requests
// If auth changed from false or unknown to true, drop now-invalid requests
if to == .on {
removeInvalidDeltasAndRequests()
} else if to == .off {
// Identity Verification was turned off: release requests parked awaiting a JWT.
reQueueAllPendingRequests()
}
}

func onJwtUpdated(externalId: String, token: String?) {
reQueuePendingRequestsForExternalId(externalId: externalId)
}

/// Identity Verification was turned off: move every auth-pended request, across all
/// external IDs, back into the update queue and flush.
private func reQueueAllPendingRequests() {
self.dispatchQueue.async {
guard !self.pendingAuthRequests.isEmpty else {
return
}
for (_, requests) in self.pendingAuthRequests {
for request in requests {
self.updateRequestQueue.append(request)
}
}
self.pendingAuthRequests = [:]
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
self.processRequestQueue(inBackground: false)
}
}

private func reQueuePendingRequestsForExternalId(externalId: String) {
self.dispatchQueue.async {
guard let requests = self.pendingAuthRequests[externalId] else {
Expand Down
Loading
Loading