From 1a76916e00482520fa9d90f044524097aaff237e Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 27 Mar 2025 19:50:02 -0700 Subject: [PATCH 1/5] Add custom events feature --- .../OneSignal.xcodeproj/project.pbxproj | 8 ++ .../Source/OneSignalCommonDefines.h | 7 ++ .../Source/OneSignalUserManagerImpl.swift | 39 +++++++++ .../Requests/OSRequestCustomEvents.swift | 87 +++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCustomEvents.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index cabab70a0..aabb06eb9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -91,6 +91,8 @@ 3C6299A92BEEA46C00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */; }; 3C6299AB2BEEA4C000649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */; }; 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */; }; + 3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */; }; + 3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */; }; 3C70FA672D0B68A100031066 /* OneSignalClientError.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C70FA652D0B68A100031066 /* OneSignalClientError.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C70FA682D0B68A100031066 /* OneSignalClientError.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C70FA662D0B68A100031066 /* OneSignalClientError.m */; }; 3C789DBD293C2206004CF83D /* OSFocusInfluenceParam.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A600B432453790700514A53 /* OSFocusInfluenceParam.m */; }; @@ -1265,6 +1267,8 @@ 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = ""; }; + 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCustomEventsExecutor.swift; sourceTree = ""; }; + 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestCustomEvents.swift; sourceTree = ""; }; 3C70FA652D0B68A100031066 /* OneSignalClientError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalClientError.h; sourceTree = ""; }; 3C70FA662D0B68A100031066 /* OneSignalClientError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalClientError.m; sourceTree = ""; }; 3C7A39D42B7C18EE0082665E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; @@ -2140,6 +2144,7 @@ isa = PBXGroup; children = ( 3C8E6E0028AC0BA10031E48A /* OSIdentityOperationExecutor.swift */, + 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */, 3C8E6DFE28AB09AE0031E48A /* OSPropertyOperationExecutor.swift */, 3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */, 3C9AD6BB2B2285FB00BC1540 /* OSUserExecutor.swift */, @@ -2162,6 +2167,7 @@ 3C9AD6C62B228A9800BC1540 /* OSRequestTransferSubscription.swift */, 3C9AD6C02B22886600BC1540 /* OSRequestUpdateSubscription.swift */, 3C9AD6C42B228A7300BC1540 /* OSRequestDeleteSubscription.swift */, + 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */, ); path = Requests; sourceTree = ""; @@ -4394,8 +4400,10 @@ 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */, 3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */, 3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */, + 3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */, 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, + 3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */, 3C9AD6CB2B228B5200BC1540 /* OSRequestIdentifyUser.swift in Sources */, 3C9AD6BC2B2285FB00BC1540 /* OSUserExecutor.swift in Sources */, 3C9AD6C32B22887700BC1540 /* OSRequestCreateUser.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 7fcc69057..02a4058e3 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -203,6 +203,7 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState; #define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_" #define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_" #define SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK @"SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK_" +#define CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK @"CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK_" // OneSignal constants #define OS_PUSH @"push" @@ -337,6 +338,8 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_REMOVE_SUBSCRIPTION_DELTA @"OS_REMOVE_SUBSCRIPTION_DELTA" #define OS_UPDATE_SUBSCRIPTION_DELTA @"OS_UPDATE_SUBSCRIPTION_DELTA" +#define OS_CUSTOM_EVENT_DELTA @"OS_CUSTOM_EVENT_DELTA" + // Operation Repo #define OS_OPERATION_REPO_DELTA_QUEUE_KEY @"OS_OPERATION_REPO_DELTA_QUEUE_KEY" @@ -359,6 +362,10 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY" +// Custom Events Executor +#define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY" +#define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY" + // Live Activies Executor #define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY" #define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 5d22dc858..462ce7c4b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -75,6 +75,15 @@ import OneSignalNotifications func removeSms(_ number: String) // Language func setLanguage(_ language: String) + // Events + /** + Track an event performed by the current user. + - Parameters: + - name: Name of the event, e.g., 'Started Free Trial' + - properties: Optional properties specific to the event. For example, an event with the name 'Started Free Trial' might have properties like promo code used or expiration date. + */ + func trackEvent(name: String, properties: [String: Any]?) + // ^ TODO: After alpha feedback, confirm value type for properties dict // JWT Token Expire typealias OSJwtCompletionBlock = (_ newJwtToken: String) -> Void typealias OSJwtExpiredHandler = (_ externalId: String, _ completion: OSJwtCompletionBlock) -> Void @@ -183,6 +192,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var propertyExecutor: OSPropertyOperationExecutor? var identityExecutor: OSIdentityOperationExecutor? var subscriptionExecutor: OSSubscriptionOperationExecutor? + var customEventsExecutor: OSCustomEventsExecutor? private override init() { self.identityModelStoreListener = OSIdentityModelStoreListener(store: identityModelStore) @@ -231,12 +241,15 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState) let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState) let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState) + let customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState) self.propertyExecutor = propertyExecutor self.identityExecutor = identityExecutor self.subscriptionExecutor = subscriptionExecutor + self.customEventsExecutor = customEventsExecutor OSOperationRepo.sharedInstance.addExecutor(identityExecutor) OSOperationRepo.sharedInstance.addExecutor(propertyExecutor) OSOperationRepo.sharedInstance.addExecutor(subscriptionExecutor) + OSOperationRepo.sharedInstance.addExecutor(customEventsExecutor) // Path 2. There is a legacy player to migrate if let legacyPlayerId = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_LEGACY_PLAYER_ID, defaultValue: nil) { @@ -795,6 +808,32 @@ extension OneSignalUserManagerImpl: OSUser { user.setLanguage(language) } + + public func trackEvent(name: String, properties: [String: Any]?) { + guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "trackEvent") else { + return + } + + let processedProperties = properties ?? [:] + + // Make sure the properties are serializable as JSON object + guard JSONSerialization.isValidJSONObject(processedProperties) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "trackEvent called with invalid properties \(processedProperties), dropping this event.") + return + } + + // Get the identity model of the current user + let identityModel = user.identityModel + + let delta = OSDelta( + name: OS_CUSTOM_EVENT_DELTA, + identityModelId: identityModel.modelId, + model: identityModel, + property: name, + value: processedProperties + ) + OSOperationRepo.sharedInstance.enqueueDelta(delta) + } } extension OneSignalUserManagerImpl { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCustomEvents.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCustomEvents.swift new file mode 100644 index 000000000..345e0e284 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCustomEvents.swift @@ -0,0 +1,87 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalOSCore + +class OSRequestCustomEvents: OneSignalRequest, OSUserRequest { + var sentToClient = false + let stringDescription: String + override var description: String { + return stringDescription + } + + var identityModel: OSIdentityModel + + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { + _ = self.addPushSubscriptionIdToAdditionalHeaders() + self.path = "apps/\(appId)/integrations/custom_events" + return true + } else { + return false + } + } + + init(events: [[String: Any]], identityModel: OSIdentityModel) { + self.identityModel = identityModel + self.stringDescription = "" + super.init() + self.parameters = [ + "events": events + ] + self.method = POST + } + + func encode(with coder: NSCoder) { + coder.encode(identityModel, forKey: "identityModel") + coder.encode(parameters, forKey: "parameters") + coder.encode(method.rawValue, forKey: "method") // Encodes as String + coder.encode(timestamp, forKey: "timestamp") + } + + required init?(coder: NSCoder) { + guard + let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel, + let rawMethod = coder.decodeObject(forKey: "method") as? UInt32, + let parameters = coder.decodeObject(forKey: "parameters") as? [String: Any], + let timestamp = coder.decodeObject(forKey: "timestamp") as? Date + else { + // Log error + return nil + } + self.identityModel = identityModel + self.stringDescription = "" + super.init() + self.parameters = parameters + self.method = HTTPMethod(rawValue: rawMethod) + self.timestamp = timestamp + } +} From 7aa7ad3909e3583fbf9ac1c64ebfbcd83c5c235b Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 31 Mar 2025 12:47:10 -0700 Subject: [PATCH 2/5] [nits] improve string description of OneSignalClientError Motivation: - previously it would log like this: "request failed with error: - now it will log like this: "request failed with error: " --- .../OneSignalCore/Source/API/OneSignalClientError.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m index e096eab13..fd0968407 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m @@ -46,4 +46,8 @@ - (instancetype)initWithCode:(NSInteger)code message:(NSString* _Nonnull)message return self; } +- (NSString *)description { + return [NSString stringWithFormat:@"", (long)_code, _message, _response, _underlyingError]; +} + @end From b59b360dbe995c30ae0b7fe042877bb0c57d1271 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 7 Apr 2025 08:36:35 -0700 Subject: [PATCH 3/5] [nits] privatize internal methods * No need to be on protocol nor exposed --- .../Source/OSOperationExecutor.swift | 3 --- .../OSIdentityOperationExecutor.swift | 10 +++++----- .../OSPropertyOperationExecutor.swift | 8 ++++---- .../OSSubscriptionOperationExecutor.swift | 20 +++++++++---------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationExecutor.swift index 63c5e7a5d..4afcf0ec7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationExecutor.swift @@ -32,11 +32,8 @@ import OneSignalCore */ public protocol OSOperationExecutor { var supportedDeltas: [String] { get } - var deltaQueue: [OSDelta] { get } func enqueueDelta(_ delta: OSDelta) func cacheDeltaQueue() func processDeltaQueue(inBackground: Bool) - - func processRequestQueue(inBackground: Bool) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 2af8ea812..1a3c3e839 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -30,11 +30,11 @@ import OneSignalCore class OSIdentityOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_ADD_ALIAS_DELTA, OS_REMOVE_ALIAS_DELTA] - var deltaQueue: [OSDelta] = [] + private var deltaQueue: [OSDelta] = [] // To simplify uncaching, we maintain separate request queues for each type - var addRequestQueue: [OSRequestAddAliases] = [] - var removeRequestQueue: [OSRequestRemoveAlias] = [] - let newRecordsState: OSNewRecordsState + private var addRequestQueue: [OSRequestAddAliases] = [] + private var removeRequestQueue: [OSRequestRemoveAlias] = [] + private let newRecordsState: OSNewRecordsState // The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global()) @@ -168,7 +168,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } /// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. - func processRequestQueue(inBackground: Bool) { + private func processRequestQueue(inBackground: Bool) { let requestQueue: [OneSignalRequest] = addRequestQueue + removeRequestQueue if requestQueue.isEmpty { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 427a930dc..e4220e0be 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -62,9 +62,9 @@ private struct OSCombinedProperties { class OSPropertyOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA] - var deltaQueue: [OSDelta] = [] - var updateRequestQueue: [OSRequestUpdateProperties] = [] - let newRecordsState: OSNewRecordsState + private var deltaQueue: [OSDelta] = [] + private var updateRequestQueue: [OSRequestUpdateProperties] = [] + private let newRecordsState: OSNewRecordsState // The property executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `updateRequestQueue`. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSPropertyOperationExecutor", target: .global()) @@ -221,7 +221,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } /// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. - func processRequestQueue(inBackground: Bool) { + private func processRequestQueue(inBackground: Bool) { if updateRequestQueue.isEmpty { return } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index f44d84157..18e66de80 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -30,13 +30,13 @@ import OneSignalCore class OSSubscriptionOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_ADD_SUBSCRIPTION_DELTA, OS_REMOVE_SUBSCRIPTION_DELTA, OS_UPDATE_SUBSCRIPTION_DELTA] - var deltaQueue: [OSDelta] = [] + private var deltaQueue: [OSDelta] = [] // To simplify uncaching, we maintain separate request queues for each type - var addRequestQueue: [OSRequestCreateSubscription] = [] - var removeRequestQueue: [OSRequestDeleteSubscription] = [] - var updateRequestQueue: [OSRequestUpdateSubscription] = [] - var subscriptionModels: [String: OSSubscriptionModel] = [:] - let newRecordsState: OSNewRecordsState + private var addRequestQueue: [OSRequestCreateSubscription] = [] + private var removeRequestQueue: [OSRequestDeleteSubscription] = [] + private var updateRequestQueue: [OSRequestUpdateSubscription] = [] + private var subscriptionModels: [String: OSSubscriptionModel] = [:] + private let newRecordsState: OSNewRecordsState // The Subscription executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSSubscriptionOperationExecutor", target: .global()) @@ -157,7 +157,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { /** Since there are 2 subscription stores, we need to check both stores for the model with a particular `modelId`. */ - func getSubscriptionModelFromStores(modelId: String) -> OSSubscriptionModel? { + private func getSubscriptionModelFromStores(modelId: String) -> OSSubscriptionModel? { if let modelInStore = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStore.getModel(modelId: modelId) { return modelInStore } @@ -246,7 +246,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } /// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. - func processRequestQueue(inBackground: Bool) { + private func processRequestQueue(inBackground: Bool) { let requestQueue: [OneSignalRequest] = addRequestQueue + removeRequestQueue + updateRequestQueue if requestQueue.isEmpty { @@ -269,7 +269,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } - func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) { + private func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) { guard !request.sentToClient else { return } @@ -391,7 +391,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } - func executeUpdateSubscriptionRequest(_ request: OSRequestUpdateSubscription, inBackground: Bool) { + private func executeUpdateSubscriptionRequest(_ request: OSRequestUpdateSubscription, inBackground: Bool) { guard !request.sentToClient else { return } From 713bdf21cbad3760f79985364c9da90c6e11ba54 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 7 Apr 2025 08:43:42 -0700 Subject: [PATCH 4/5] Create executor to handle custom events --- .../Executors/OSCustomEventsExecutor.swift | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift new file mode 100644 index 000000000..8168809f1 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift @@ -0,0 +1,249 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalOSCore +import OneSignalCore + +class OSCustomEventsExecutor: OSOperationExecutor { + private enum EventConstants { + static let name = "name" + static let onesignalId = "onesignal_id" + static let timestamp = "timestamp" + static let payload = "payload" + static let deviceType = "device_type" + static let sdk = "sdk" + static let appVersion = "app_version" + static let type = "type" + static let deviceModel = "device_model" + static let deviceOs = "device_os" + static let osSdk = "os_sdk" + static let ios = "ios" + static let iOSPush = "iOSPush" + } + + var supportedDeltas: [String] = [OS_CUSTOM_EVENT_DELTA] + private var deltaQueue: [OSDelta] = [] + private var requestQueue: [OSRequestCustomEvents] = [] + private let newRecordsState: OSNewRecordsState + + // The executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `requestQueue`. + private let dispatchQueue = DispatchQueue(label: "OneSignal.OSCustomEventsExecutor", target: .global()) + + init(newRecordsState: OSNewRecordsState) { + self.newRecordsState = newRecordsState + // Read unfinished deltas and requests from cache, if any... + uncacheDeltas() + uncacheRequests() + } + + private func uncacheDeltas() { + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { + for (index, delta) in deltaQueue.enumerated().reversed() { + if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil { + // The identity model does not exist, drop this Delta + OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(delta)") + deltaQueue.remove(at: index) + } + } + self.deltaQueue = deltaQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + } else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor error encountered reading from cache for \(OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY)") + self.deltaQueue = [] + } + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor successfully uncached Deltas: \(deltaQueue)") + } + + private func uncacheRequests() { + if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] { + // Hook each uncached Request to the model in the store + for (index, request) in requestQueue.enumerated().reversed() { + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // 1. The identity model exist in the repo, set it to be the Request's model + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // 2. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // 3. The identitymodel do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(request)") + requestQueue.remove(at: index) + } + } + self.requestQueue = requestQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue) + } else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor error encountered reading from cache for \(OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY)") + self.requestQueue = [] + } + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor successfully uncached Requests: \(requestQueue)") + } + + func enqueueDelta(_ delta: OSDelta) { + self.dispatchQueue.async { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor enqueue delta \(delta)") + self.deltaQueue.append(delta) + } + } + + func cacheDeltaQueue() { + self.dispatchQueue.async { + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + } + } + + /// The `deltaQueue` can contain events for multiple users. They will remain as Deltas if there is no onesignal ID yet for its user. + func processDeltaQueue(inBackground: Bool) { + self.dispatchQueue.async { + if self.deltaQueue.isEmpty { + // Delta queue is empty but there may be pending requests + self.processRequestQueue(inBackground: inBackground) + return + } + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor processDeltaQueue with queue: \(self.deltaQueue)") + + // Holds mapping of identity model ID to the events for it + var combinedEvents: [String: [[String: Any]]] = [:] + + // 1. Combine the events for every distinct user + for (index, delta) in self.deltaQueue.enumerated().reversed() { + guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId), + let onesignalId = identityModel.onesignalId + else { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor.processDeltaQueue skipping: \(delta)") + // keep this Delta in the queue, as it is not yet ready to be processed + continue + } + + guard let properties = delta.value as? [String: Any] else { + // This should not happen as there are preventative typing measures before this step + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor.processDeltaQueue dropped due to invalid properties: \(delta)") + self.deltaQueue.remove(at: index) + continue + } + + let event: [String: Any] = [ + EventConstants.name: delta.property, + EventConstants.onesignalId: onesignalId, + EventConstants.timestamp: ISO8601DateFormatter().string(from: delta.timestamp), + EventConstants.payload: self.addSdkMetadata(properties: properties) + ] + + combinedEvents[identityModel.modelId, default: []].append(event) + self.deltaQueue.remove(at: index) + } + + // 2. Turn each user's events into a Request + for (modelId, events) in combinedEvents { + guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(modelId) + else { + // This should never happen as we already checked this during Deltas processing above + continue + } + let request = OSRequestCustomEvents( + events: events, + identityModel: identityModel + ) + self.requestQueue.append(request) + } + + // Persist executor's requests (including new request) to storage + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + + self.processRequestQueue(inBackground: inBackground) + } + } + + /** + Adds additional data about the SDK to the event payload. + */ + private func addSdkMetadata(properties: [String: Any]) -> [String: Any] { + // TODO: Exact information contained in payload should be confirmed before the custom events GA release + let metadata = [ + EventConstants.deviceType: EventConstants.ios, + EventConstants.sdk: ONESIGNAL_VERSION, + EventConstants.appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + EventConstants.type: EventConstants.iOSPush, + EventConstants.deviceModel: OSDeviceUtils.getDeviceVariant(), + EventConstants.deviceOs: UIDevice.current.systemVersion + ] + var payload = properties + payload[EventConstants.osSdk] = metadata + return payload + } + + /// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. + private func processRequestQueue(inBackground: Bool) { + if requestQueue.isEmpty { + return + } + + for request in requestQueue { + executeRequest(request, inBackground: inBackground) + } + } + + private func executeRequest(_ request: OSRequestCustomEvents, inBackground: Bool) { + guard !request.sentToClient else { + return + } + guard request.prepareForExecution(newRecordsState: newRecordsState) else { + return + } + request.sentToClient = true + + let backgroundTaskIdentifier = CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK + UUID().uuidString + if inBackground { + OSBackgroundTaskManager.beginBackgroundTask(backgroundTaskIdentifier) + } + + OneSignalCoreImpl.sharedClient().execute(request) { _ in + self.dispatchQueue.async { + self.requestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue) + if inBackground { + OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) + } + } + } onFailure: { error in + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor request failed with error: \(error.debugDescription)") + self.dispatchQueue.async { + let responseType = OSNetworkingUtils.getResponseStatusType(error.code) + if responseType != .retryable { + // Fail, no retry, remove from cache and queue + self.requestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue) + } + // TODO: Handle payload too large (not necessary for alpha release) + if inBackground { + OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) + } + } + } + } +} From d4b6c8b6424bf840adf642bfc3d244ccb04bfa84 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 7 Apr 2025 10:45:32 -0700 Subject: [PATCH 5/5] [dev app] Add custom events examples * Add button at the bottom of Dev App called "Track Custom Events" * Add example custom events in Objective-C and Swift --- .../Base.lproj/Main.storyboard | 15 +++++-- .../OneSignalDevApp/SwiftTest.swift | 40 +++++++++++++++++++ .../OneSignalDevApp/ViewController.m | 11 +++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard index 8eb04046a..099f03a01 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard @@ -501,8 +501,8 @@ - + @@ -760,7 +769,7 @@ - + diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift index eae4a119f..2db90a09c 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift @@ -28,9 +28,49 @@ import Foundation import OneSignalFramework +@objc class SwiftTest: NSObject { func testSwiftUserModel() { let token1 = OneSignal.User.pushSubscription.token let token = OneSignal.User.pushSubscription.token } + + /** + Track multiple events with different properties. + Properties must pass `JSONSerialization.isValidJSONObject` to be accepted. + */ + @objc + static func trackCustomEvents() { + print("Dev App: track an event with nil properties") + OneSignal.User.trackEvent(name: "null properties", properties: nil) + + print("Dev App: track an event with empty properties") + OneSignal.User.trackEvent(name: "empty properties", properties: [:]) + + let formatter = DateFormatter() + formatter.dateStyle = .short + + let mixedTypes = [ + "string": "somestring", + "number": 5, + "bool": false, + "dateStr": formatter.string(from: Date()) + ] as [String: Any] + + let nestedDict = [ + "someDict": mixedTypes, + "anotherDict": [ + "foo": "bar", + "booleanVal": true, + "float": Float("3.14")! + ] + ] + let invalidProperties = ["date": Date()] + + print("Dev App: track an event with a valid nested dictionary") + OneSignal.User.trackEvent(name: "nested dictionary", properties: nestedDict) + + print("Dev App: track an event with invalid dictionary types") + OneSignal.User.trackEvent(name: "invalid dictionary", properties: invalidProperties) + } } diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m index 0034b13d2..923fd9384 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m @@ -277,4 +277,15 @@ - (IBAction)dontRequireConsent:(id)sender { [OneSignal setConsentRequired:false]; } +- (IBAction)trackCustomEvents:(id)sender { + NSLog(@"Dev App: adding custom events"); + [OneSignal.User trackEventWithName:@"simple event" properties:@{@"foobarbaz": @"foobarbaz"}]; + NSMutableDictionary *dict = [NSMutableDictionary new]; + dict[@"dict"] = @{@"abc" : @"def"}; + dict[@"false"] = false; + dict[@"int"] = @99; + [OneSignal.User trackEventWithName:@"complex event" properties:dict]; + [SwiftTest trackCustomEvents]; +} + @end