From 4da002e29ba19040cd2713dabc5daddcbaca636a Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 22 May 2024 10:10:29 -0700 Subject: [PATCH 01/15] Session data and purchases become Deltas first, not Requests * session_count, session_time, and purchases are not model-driven. We were turning their data into `OSRequestUpdateProperties` instances immediately instead of enqueueing as Deltas first. This means they could not be combined with other User Updates in a single request. * Now, when they will send their data to the User Manager, it will manually enqueue a Delta onto the Operation Repo. * Previously, we used a flag to indicate if we should send these requests immediately (meaning the app is backgrounded), but that is no longer needed because the Deltas are enqueued to the Operation Repo, and then the Operation Repo has its own background task to flush. --- .../Source/OSOperationRepo.swift | 9 ++- .../OSPropertyOperationExecutor.swift | 30 -------- .../Source/OneSignalUserManagerImpl.swift | 70 +++++++------------ 3 files changed, 33 insertions(+), 76 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 5a9fd5d58..dd81de171 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -100,7 +100,14 @@ public class OSOperationRepo: NSObject { } } - func enqueueDelta(_ delta: OSDelta) { + /** + Enqueueing is driven by model changes and called manually by the User Manager to + add session time, session count and purchase data. + + // TODO: We can make this method internal once there is no manual adding of a Delta except through stores. + This can happen when session data and purchase data use the model / store / listener infrastructure. + */ + public func enqueueDelta(_ delta: OSDelta) { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 7de21b0df..b4a569a14 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -188,33 +188,3 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } } - -extension OSPropertyOperationExecutor { - // TODO: We can make this go through the operation repo - func updateProperties(propertiesDeltas: OSPropertiesDeltas, refreshDeviceMetadata: Bool, propertiesModel: OSPropertiesModel, identityModel: OSIdentityModel, sendImmediately: Bool = false, onSuccess: (() -> Void)? = nil, onFailure: (() -> Void)? = nil) { - - let request = OSRequestUpdateProperties( - properties: [:], - deltas: propertiesDeltas.jsonRepresentation(), - refreshDeviceMetadata: refreshDeviceMetadata, - identityModel: identityModel) - - if sendImmediately { - // Bypass the request queues - OneSignalCoreImpl.sharedClient().execute(request) { _ in - if let onSuccess = onSuccess { - onSuccess() - } - } onFailure: { _ in - if let onFailure = onFailure { - onFailure() - } - } - } else { - self.dispatchQueue.async { - self.updateRequestQueue.append(request) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) - } - } - } -} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index f2d971be4..c4bf7124e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -511,26 +511,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "sendPurchases") else { return } - guard let user = _user else { - OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_DEBUG, message: "Failed to send purchases because User is nil") - return - } - // Get the identity and properties model of the current user - let identityModel = user.identityModel - let propertiesModel = user.propertiesModel - let propertiesDeltas = OSPropertiesDeltas(sessionTime: nil, sessionCount: nil, amountSpent: nil, purchases: purchases) - - // propertyExecutor should exist as this should be called after `start()` has been called - if let propertyExecutor = self.propertyExecutor { - propertyExecutor.updateProperties( - propertiesDeltas: propertiesDeltas, - refreshDeviceMetadata: false, - propertiesModel: propertiesModel, - identityModel: identityModel - ) - } else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalUserManagerImpl.sendPurchases with purchases: \(purchases) cannot be executed due to missing property executor.") - } + updatePropertiesDeltas(property: "purchases", value: purchases) } private func fireJwtExpired() { @@ -559,7 +540,7 @@ extension OneSignalUserManagerImpl { OSUserExecutor.executePendingRequests() OSOperationRepo.sharedInstance.paused = false - updateSession(sessionCount: 1, sessionTime: nil, refreshDeviceMetadata: true) + updatePropertiesDeltas(property: "session_count", value: 1) // Fetch the user's data if there is a onesignal_id if let onesignalId = onesignalId { @@ -571,37 +552,36 @@ extension OneSignalUserManagerImpl { } } - @objc - public func updateSession(sessionCount: NSNumber?, sessionTime: NSNumber?, refreshDeviceMetadata: Bool, sendImmediately: Bool = false, onSuccess: (() -> Void)? = nil, onFailure: (() -> Void)? = nil) { - guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { - if let onFailure = onFailure { - onFailure() - } + /// This method accepts properties updates that not driven by model changes. + /// It enqueues an OSDelta to the Operation Repo. + /// + /// - Parameter property:Expected inputs are `"session_time"`, `"session_count"`, and `"purchases"`. + func updatePropertiesDeltas(property: String, value: Any) { + guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updatePropertiesDeltas") else { return } // Get the identity and properties model of the current user let identityModel = user.identityModel let propertiesModel = user.propertiesModel - let propertiesDeltas = OSPropertiesDeltas(sessionTime: sessionTime, sessionCount: sessionCount, amountSpent: nil, purchases: nil) - - // propertyExecutor should exist as this should be called after `start()` has been called - if let propertyExecutor = self.propertyExecutor { - propertyExecutor.updateProperties( - propertiesDeltas: propertiesDeltas, - refreshDeviceMetadata: refreshDeviceMetadata, - propertiesModel: propertiesModel, - identityModel: identityModel, - sendImmediately: sendImmediately, - onSuccess: onSuccess, - onFailure: onFailure - ) - } else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalUserManagerImpl.updateSession with sessionCount: \(String(describing: sessionCount)) sessionTime: \(String(describing: sessionTime)) cannot be executed due to missing property executor.") - if let onFailure = onFailure { - onFailure() - } + + let delta = OSDelta( + name: OS_UPDATE_PROPERTIES_DELTA, + identityModelId: identityModel.modelId, + model: propertiesModel, + property: property, + value: value + ) + OSOperationRepo.sharedInstance.enqueueDelta(delta) + } + + /// Time processors forward the session time to this method. + @objc + public func sendSessionTime(_ sessionTime: NSNumber) { + guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "sendSessionTime") else { + return } + updatePropertiesDeltas(property: "session_time", value: sessionTime.intValue) } /** From 58c8c79a2d05e745a812ec975076907a946e2932 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 23 May 2024 08:01:48 -0700 Subject: [PATCH 02/15] Update Unattributed Time Processor to use new methods * The time processor will not handle its own background task. * The ordering will be correct when the app is backgrounded. This time is sent to the user manager who enqueues a delta with the session time. Then the Operation Repo will start its background task and flush deltas. --- .../Source/OneSignalCommonDefines.h | 1 - .../Source/OSUnattributedFocusTimeProcessor.m | 19 +++---------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index d6ee6b1fa..8b9cccc30 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -192,7 +192,6 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState; // OneSignal Background Task Identifiers #define ATTRIBUTED_FOCUS_TASK @"ATTRIBUTED_FOCUS_TASK" -#define UNATTRIBUTED_FOCUS_TASK @"UNATTRIBUTED_FOCUS_TASK" #define SEND_SESSION_TIME_TO_USER_TASK @"SEND_SESSION_TIME_TO_USER_TASK" #define OPERATION_REPO_BACKGROUND_TASK @"OPERATION_REPO_BACKGROUND_TASK" #define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_" diff --git a/iOS_SDK/OneSignalSDK/Source/OSUnattributedFocusTimeProcessor.m b/iOS_SDK/OneSignalSDK/Source/OSUnattributedFocusTimeProcessor.m index e10da24eb..5f61ebc1f 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSUnattributedFocusTimeProcessor.m +++ b/iOS_SDK/OneSignalSDK/Source/OSUnattributedFocusTimeProcessor.m @@ -36,7 +36,6 @@ @implementation OSUnattributedFocusTimeProcessor - (instancetype)init { self = [super init]; - [OSBackgroundTaskManager setTaskInvalid:UNATTRIBUTED_FOCUS_TASK]; return self; } @@ -71,21 +70,9 @@ - (void)sendUnsentActiveTime:(OSFocusCallParams *)params { } - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(NSTimeInterval)totalTimeActive { - // should dispatch_async? - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OSBackgroundTaskManager beginBackgroundTask:UNATTRIBUTED_FOCUS_TASK]; - - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OSUnattributedFocusTimeProcessor:sendOnFocusCallWithParams start"]; - - [OneSignalUserManagerImpl.sharedInstance updateSessionWithSessionCount:nil sessionTime:@(totalTimeActive) refreshDeviceMetadata:false sendImmediately:true onSuccess:^{ - [super saveUnsentActiveTime:0]; - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"sendOnFocusCallWithParams unattributed succeed, saveUnsentActiveTime with 0"]; - [OSBackgroundTaskManager endBackgroundTask:UNATTRIBUTED_FOCUS_TASK]; - } onFailure:^{ - [OneSignalLog onesignalLog:ONE_S_LL_WARN message:@"sendOnFocusCallWithParams unattributed failed, will retry on next open"]; - [OSBackgroundTaskManager endBackgroundTask:UNATTRIBUTED_FOCUS_TASK]; - }]; - }); + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"OSUnattributedFocusTimeProcessor:sendSessionTime of %@", @(totalTimeActive)]]; + [OneSignalUserManagerImpl.sharedInstance sendSessionTime:@(totalTimeActive)]; + [super saveUnsentActiveTime:0]; } - (void)cancelDelayedJob { From 394381ef22c0fddb882249461493b715304051f4 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 23 May 2024 09:25:57 -0700 Subject: [PATCH 03/15] Update Attributed Time Processor to use new methods * The attributed time processor needs to send data to two separate places: session_time for Update User and session time for Outcomes. * Before sending data to outcomes, it needs to wait 30 seconds to see if the app re-opens in case the SDK will continue on the same session, this is for the outcomes endpoint to know session count for influences. * It will now send session_time to the user manager immediately as this does not need to wait for 30 seconds. The ordering will be correct when the app is backgrounded. This time is sent to the user manager who enqueues a delta with the session time. Then the Operation Repo will start its background task and flush deltas. * It will only send the elapsed time and does not need to handle unsent active time. This is because previous session times will be their own Delta instances that the operation repo / executor handles with retrying. These previous deltas already encompass the unsent active time. * The Attributed Time Processor still has a separate background task that handles sending the outcome, manages the 30 seconds wait time, and maintains a background task for it. It uses unsent active time and resets to 0 when the request is successful. --- .../Source/OneSignalCommonDefines.h | 1 - .../Source/OSAttributedFocusTimeProcessor.m | 21 +++++-------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 8b9cccc30..b0578fc11 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -192,7 +192,6 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState; // OneSignal Background Task Identifiers #define ATTRIBUTED_FOCUS_TASK @"ATTRIBUTED_FOCUS_TASK" -#define SEND_SESSION_TIME_TO_USER_TASK @"SEND_SESSION_TIME_TO_USER_TASK" #define OPERATION_REPO_BACKGROUND_TASK @"OPERATION_REPO_BACKGROUND_TASK" #define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_" #define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_" diff --git a/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m b/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m index 1efd0e931..0bd16b3e9 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m +++ b/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m @@ -44,8 +44,6 @@ @implementation OSAttributedFocusTimeProcessor { - (instancetype)init { self = [super init]; [OSBackgroundTaskManager setTaskInvalid:ATTRIBUTED_FOCUS_TASK]; - [OSBackgroundTaskManager setTaskInvalid:SEND_SESSION_TIME_TO_USER_TASK]; - return self; } @@ -64,6 +62,10 @@ - (void)sendOnFocusCall:(OSFocusCallParams *)params { message:[NSString stringWithFormat:@"sendOnFocusCall attributed with totalTimeActive %f", totalTimeActive]]; [super saveUnsentActiveTime:totalTimeActive]; + + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"OSAttributedFocusTimeProcessor:sendSessionTime of %@", @(params.timeElapsed)]]; + [OneSignalUserManagerImpl.sharedInstance sendSessionTime:@(params.timeElapsed)]; + [self sendOnFocusCallWithParams:params totalTimeActive:totalTimeActive]; } @@ -83,7 +85,6 @@ - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(N } [OSBackgroundTaskManager beginBackgroundTask:ATTRIBUTED_FOCUS_TASK]; - [OSBackgroundTaskManager beginBackgroundTask:SEND_SESSION_TIME_TO_USER_TASK]; if (params.onSessionEnded) { [self sendBackgroundAttributedSessionTimeWithParams:params withTotalTimeActive:@(totalTimeActive)]; @@ -108,20 +109,10 @@ - (void)sendBackgroundAttributedSessionTimeWithNSTimer:(NSTimer*)timer { - (void)sendBackgroundAttributedSessionTimeWithParams:(OSFocusCallParams *)params withTotalTimeActive:(NSNumber*)totalTimeActive { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OSAttributedFocusTimeProcessor:sendBackgroundAttributedSessionTimeWithParams start"]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OneSignalUserManagerImpl.sharedInstance updateSessionWithSessionCount:nil sessionTime:totalTimeActive refreshDeviceMetadata:false sendImmediately:true onSuccess:^{ - [super saveUnsentActiveTime:0]; - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"sendBackgroundAttributed session time succeed, saveUnsentActiveTime with 0"]; - [OSBackgroundTaskManager endBackgroundTask:SEND_SESSION_TIME_TO_USER_TASK]; - } onFailure:^{ - [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendBackgroundAttributed session time failed, will retry on next open"]; - [OSBackgroundTaskManager endBackgroundTask:SEND_SESSION_TIME_TO_USER_TASK]; - }]; - }); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [OneSignal sendSessionEndOutcomes:totalTimeActive params:params onSuccess:^(NSDictionary *result) { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"sendBackgroundAttributed succeed"]; + [super saveUnsentActiveTime:0]; [OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK]; } onFailure:^(NSError *error) { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendBackgroundAttributed failed, will retry on next open"]; @@ -137,8 +128,6 @@ - (void)cancelDelayedJob { [restCallTimer invalidate]; restCallTimer = nil; [OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK]; - [OSBackgroundTaskManager endBackgroundTask:SEND_SESSION_TIME_TO_USER_TASK]; - } @end From 6ddd65ffe438f0bfbbadda837dba48ee9705dd48 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 22 May 2024 11:13:38 -0700 Subject: [PATCH 04/15] Add concept of supported fields for user updates * OSDeltas can be created by Model changes and now manually created by the User Manager to enqueue updates not driven by model changes. These are for session time, session count, and purchases. In the latter scenario, the User Manager can set a value of "session_time" for example, but could potentially set any random string. * Introduce an enum OSPropertiesSupportedProperty, that restricts the properties we allow for updating a user. --- .../OneSignal.xcodeproj/project.pbxproj | 12 +++++ .../Executors/Support/SupportedProperty.swift | 44 +++++++++++++++++++ .../OSPropertiesModelStoreListener.swift | 5 +++ 3 files changed, 61 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 7865e8019..420a370d1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -153,6 +153,7 @@ 3CE8CC582911B2B2000DB0D3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE8CC572911B2B2000DB0D3 /* SystemConfiguration.framework */; }; 3CE8CC5B29143F4B000DB0D3 /* NSDateFormatter+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE98772A2591655800DE07D5 /* NSDateFormatter+OneSignal.m */; }; 3CE9227A289FA88B001B1062 /* OSIdentityModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */; }; + 3CEE90A72BFE6ABD00B0FB5B /* SupportedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */; }; 3CEE93422B7C4174008440BD /* OneSignalUserMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; }; 3CEE93432B7C4174008440BD /* OneSignalUserMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3CEE93462B7C73AB008440BD /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; @@ -1184,6 +1185,7 @@ 3CE8CC552911B1E0000DB0D3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 3CE8CC572911B2B2000DB0D3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelStoreListener.swift; sourceTree = ""; }; + 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedProperty.swift; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -1914,6 +1916,7 @@ 3C9AD6BA2B2284AB00BC1540 /* Executors */ = { isa = PBXGroup; children = ( + 3CEE90A52BFE6A7700B0FB5B /* Support */, 3C8E6E0028AC0BA10031E48A /* OSIdentityOperationExecutor.swift */, 3C8E6DFE28AB09AE0031E48A /* OSPropertyOperationExecutor.swift */, 3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */, @@ -1983,6 +1986,14 @@ path = OneSignalUserTests; sourceTree = ""; }; + 3CEE90A52BFE6A7700B0FB5B /* Support */ = { + isa = PBXGroup; + children = ( + 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */, + ); + path = Support; + sourceTree = ""; + }; 3E2400391D4FFC31008BDE70 /* OneSignalFramework */ = { isa = PBXGroup; children = ( @@ -3926,6 +3937,7 @@ 3C8E6E0128AC0BA10031E48A /* OSIdentityOperationExecutor.swift in Sources */, 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */, 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */, + 3CEE90A72BFE6ABD00B0FB5B /* SupportedProperty.swift in Sources */, 3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */, 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift new file mode 100644 index 000000000..ecaa24777 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift @@ -0,0 +1,44 @@ +/* + Modified MIT License + + Copyright 2024 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. + */ + +/** + These are supported properties for updating a user's properties. + The `OSDelta` `property` field for user updates must be one of the following. + The `OSPropertyOperationExecutor` will only process the following updates. + */ +// swiftlint:disable identifier_name +enum OSPropertiesSupportedProperty: String { + // Driven by Properties Model changes + case language + case location + case tags + // Created manually by User Manager, not through Models + case session_count + case session_time + case purchases +} +// swiftlint:enable identifier_name diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift index 7db8b6d39..611228801 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift @@ -45,6 +45,11 @@ class OSPropertiesModelStoreListener: OSModelStoreListener { } func getUpdateModelDelta(_ args: OSModelChangedArgs) -> OSDelta? { + guard let _ = OSPropertiesSupportedProperty(rawValue: args.property) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertiesModelStoreListener.getUpdateModelDelta encountered unsupported property: \(args.property)") + return nil + } + return OSDelta( name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: OneSignalUserManagerImpl.sharedInstance.user.identityModel.modelId, From ce80822c4c29c7b17de99817a344c2a21dc9a485 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 22 May 2024 11:55:25 -0700 Subject: [PATCH 05/15] Combine user update deltas * When processing the Delta queue and turning them into requests, we now combine deltas into one User Update request. --- .../OSPropertyOperationExecutor.swift | 115 ++++++++++++++++-- .../Requests/OSRequestUpdateProperties.swift | 17 +-- 2 files changed, 106 insertions(+), 26 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index b4a569a14..ae1d1fb7f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -28,6 +28,38 @@ import OneSignalOSCore import OneSignalCore +/// Helper struct to process and combine OSDeltas into one payload +private struct OSCombinedProperties { + var properties: [String: Any] = [:] + var tags: [String: String] = [:] + var location: OSLocationPoint? + var refreshDeviceMetadata = false + + // Items of Properties Deltas + var sessionTime: Int = 0 + var sessionCount: Int = 0 + var purchases: [[String: AnyObject]] = [] + + func jsonRepresentation() -> [String: Any] { + var propertiesObject = properties + propertiesObject["tags"] = tags.isEmpty ? nil : tags + propertiesObject["lat"] = location?.lat + propertiesObject["long"] = location?.long + + var deltas = [String: Any]() + deltas["session_count"] = (sessionCount > 0) ? sessionCount : nil + deltas["session_time"] = (sessionTime > 0) ? sessionTime : nil + deltas["purchases"] = purchases.isEmpty ? nil : purchases + + var params: [String: Any] = [:] + params["properties"] = propertiesObject.isEmpty ? nil : propertiesObject + params["refresh_device_metadata"] = refreshDeviceMetadata + params["deltas"] = deltas.isEmpty ? nil : deltas + + return params + } +} + class OSPropertyOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA] var deltaQueue: [OSDelta] = [] @@ -78,7 +110,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { func enqueueDelta(_ delta: OSDelta) { self.dispatchQueue.async { - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor enqueue delta\(delta)") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor enqueue delta \(delta)") self.deltaQueue.append(delta) } } @@ -89,38 +121,99 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } + /// The `deltaQueue` should only contain updates for one user. + /// Even when login -> addTag -> login -> addTag are called in immediate succession. func processDeltaQueue(inBackground: Bool) { self.dispatchQueue.async { - if !self.deltaQueue.isEmpty { - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)") + if self.deltaQueue.isEmpty { + // Delta queue is empty but there may be pending requests + self.processRequestQueue(inBackground: inBackground) + return } + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)") + + // Holds mapping of identity model ID to the updates for it; there should only be one user + var combinedProperties: [String: OSCombinedProperties] = [:] + + // 1. Combined deltas into a single OSCombinedProperties for every user for delta in self.deltaQueue { guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { - // drop this delta OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)") continue } + var combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] + combinedSoFar = self.combineProperties(existing: combinedSoFar, delta: delta) + combinedProperties[identityModel.modelId] = combinedSoFar + } + if combinedProperties.count > 1 { + OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.combinedProperties contains \(combinedProperties.count) users") + } + + // 2. Turn each OSCombinedProperties' data into a Request + for (modelId, properties) in combinedProperties { + guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(modelId) + else { + // This should never happen as we already checked this during Deltas processing above + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(properties)") + continue + } let request = OSRequestUpdateProperties( - properties: [delta.property: delta.value], - deltas: nil, - refreshDeviceMetadata: false, // Sort this out. + params: properties.jsonRepresentation(), identityModel: identityModel ) self.updateRequestQueue.append(request) } - self.deltaQueue = [] // TODO: Check that we can simply clear all the deltas in the deltaQueue - // persist executor's requests (including new request) to storage + self.deltaQueue.removeAll() + + // Persist executor's requests (including new request) to storage OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, withValue: []) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) // This should be empty, can remove instead? self.processRequestQueue(inBackground: inBackground) } } - // This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. + /// Helper method to combine the information in an `OSDelta` to the existing `OSCombinedProperties` so far. + private func combineProperties(existing: OSCombinedProperties?, delta: OSDelta) -> OSCombinedProperties { + var combinedProperties = existing ?? OSCombinedProperties() + + guard let property = OSPropertiesSupportedProperty(rawValue: delta.property) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.combineProperties dropped unsupported property: \(delta.property)") + return combinedProperties + } + + switch property { + case .tags: + if let tags = delta.value as? [String: String] { + for (tag, value) in tags { + combinedProperties.tags[tag] = value + } + } + case .location: + // Use the most recent location point + combinedProperties.location = delta.value as? OSLocationPoint + case .session_time: + combinedProperties.sessionTime += (delta.value as? Int ?? 0) + case .session_count: + combinedProperties.refreshDeviceMetadata = true + combinedProperties.sessionCount += (delta.value as? Int ?? 0) + case .purchases: + if let purchases = delta.value as? [[String: AnyObject]] { + for purchase in purchases { + combinedProperties.purchases.append(purchase) + } + } + default: + // First-level, un-nested properties as "language" + combinedProperties.properties[delta.property] = delta.value + } + return combinedProperties + } + + /// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue. func processRequestQueue(inBackground: Bool) { if updateRequestQueue.isEmpty { return diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift index 5348fbbee..307967111 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift @@ -53,23 +53,10 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { } } - init(properties: [String: Any], deltas: [String: Any]?, refreshDeviceMetadata: Bool?, identityModel: OSIdentityModel) { + init(params: [String: Any], identityModel: OSIdentityModel) { self.identityModel = identityModel - self.stringDescription = "" + self.stringDescription = "" super.init() - - var propertiesObject = properties - if let location = propertiesObject["location"] as? OSLocationPoint { - propertiesObject["lat"] = location.lat - propertiesObject["long"] = location.long - propertiesObject.removeValue(forKey: "location") - } - var params: [String: Any] = [:] - params["properties"] = propertiesObject - params["refresh_device_metadata"] = refreshDeviceMetadata - if let deltas = deltas { - params["deltas"] = deltas - } self.parameters = params self.method = PATCH _ = prepareForExecution() // sets the path property From 73ce8219d6759925e4a3ee871a6cdcf6fa864417 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 22 May 2024 11:57:42 -0700 Subject: [PATCH 06/15] Remove no longer used struct OSPropertiesDeltas * Also noted that we enqueue only one Properties Delta change at a time - for example, one of session_time, session_count, or purchases - so the struct OSPropertiesDeltas is not needed. It was always being created with only 1 property and the rest being nil. * Now that these properties deltas are going to be combined together, we are not using this struct anymore anyways. --- .../OneSignalUser/Source/OSPropertiesModel.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift index 3a44f6e73..e9575dddf 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift @@ -29,22 +29,6 @@ import Foundation import OneSignalOSCore import OneSignalCore -struct OSPropertiesDeltas { - let sessionTime: NSNumber? - let sessionCount: NSNumber? - let amountSpent: NSNumber? - let purchases: [[String: AnyObject]]? - - func jsonRepresentation() -> [String: Any] { - var deltas = [String: Any]() - deltas["session_count"] = sessionCount - deltas["session_time"] = sessionTime?.intValue // server expects an int - deltas["amountSpent"] = amountSpent - deltas["purchases"] = purchases - return deltas - } -} - // Both lat and long must exist to be accepted by the server class OSLocationPoint: NSObject, NSCoding { let lat: Float From 5d39587c8d23297cd276999d86ce6dad9a654495 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 23 May 2024 10:59:50 -0700 Subject: [PATCH 07/15] remove dead code in OneSignalTracker --- iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m index 76f794d01..453186ed4 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m @@ -50,13 +50,11 @@ + (NSString *)mExternalIdAuthToken; @implementation OneSignalTracker -static UIBackgroundTaskIdentifier focusBackgroundTask; static NSTimeInterval lastOpenedTime; static BOOL lastOnFocusWasToBackground = YES; + (void)resetLocals { [OSFocusTimeProcessorFactory resetUnsentActiveTime]; - focusBackgroundTask = 0; lastOpenedTime = 0; lastOnFocusWasToBackground = YES; } @@ -65,17 +63,6 @@ + (void)setLastOpenedTime:(NSTimeInterval)lastOpened { lastOpenedTime = lastOpened; } -+ (void)beginBackgroundFocusTask { - focusBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ - [OneSignalTracker endBackgroundFocusTask]; - }]; -} - -+ (void)endBackgroundFocusTask { - [[UIApplication sharedApplication] endBackgroundTask: focusBackgroundTask]; - focusBackgroundTask = UIBackgroundTaskInvalid; -} - + (void)onFocus:(BOOL)toBackground { // return if the user has not granted privacy permissions if ([OSPrivacyConsentController requiresUserPrivacyConsent]) From 08b84ba2eb4c64cb2751963917b5157baddf3796 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 23 May 2024 10:27:56 -0700 Subject: [PATCH 08/15] Purchase tracking fix variable type to be string * SKProduct.price is an NSDecimalNumber and we had been sending this as is, but the backend expects a String * This can be reproduced manually by a creating purchase array and calling `OneSignalUserManagerImpl sendPurchases` * It returns a 400 with: "Field deltas.purchases.amount was expecting value of type 'string', received value of type 'number' instead" --- iOS_SDK/OneSignalSDK/Source/OneSignalTrackIAP.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTrackIAP.m b/iOS_SDK/OneSignalSDK/Source/OneSignalTrackIAP.m index 8d3f5ad70..12c13936d 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTrackIAP.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTrackIAP.m @@ -124,8 +124,14 @@ - (void)productsRequest:(id)request didReceiveResponse:(id)response { NSString* productSku = [skProduct performSelector:@selector(productIdentifier)]; NSMutableDictionary* purchase = skusToTrack[productSku]; if (purchase) { // In rare cases this can be nil when there wasn't a connection to Apple when opening the app but there was when buying an IAP item. + + // SKProduct.price is an NSDecimalNumber, but the backend expects a String + NSNumberFormatter *formatter = [NSNumberFormatter new]; + [formatter setMinimumFractionDigits:2]; + NSString *formattedPrice = [formatter stringFromNumber:[skProduct performSelector:@selector(price)]]; + purchase[@"sku"] = productSku; - purchase[@"amount"] = [skProduct performSelector:@selector(price)]; + purchase[@"amount"] = formattedPrice; purchase[@"iso"] = [[skProduct performSelector:@selector(priceLocale)] objectForKey:NSLocaleCurrencyCode]; if ([purchase[@"count"] intValue] == 1) [purchase removeObjectForKey:@"count"]; From 3b47509777dd9e33d9988a6e129c4180d289ada3 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 21 May 2024 09:15:10 -0700 Subject: [PATCH 09/15] [tests] add Objective-C user module tests * Purchases are tracked in Objective-C and data is passed to the user manager to send purchases. * It is more accurate to keep purchase tracking tests in Objective-C as well as passing data can be a source of errors --- .../OneSignal.xcodeproj/project.pbxproj | 10 +++ .../MockOneSignalClient.swift | 1 + .../OneSignalCoreMocks.swift | 3 +- .../OneSignalUserMocks/MockUserRequests.swift | 4 +- .../OneSignalUserMocks.swift | 2 +- .../OneSignalUserObjcTests.m | 78 +++++++++++++++++++ .../OneSignalUserTests-Bridging-Header.h | 4 + 7 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests-Bridging-Header.h diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 420a370d1..327e02f30 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -141,6 +141,7 @@ 3CC9A6362AFA26E7008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */; }; 3CCF44BE299B17290021964D /* OneSignalWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CCF44BC299B17290021964D /* OneSignalWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3CCF44BF299B17290021964D /* OneSignalWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF44BD299B17290021964D /* OneSignalWrapper.m */; }; + 3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */; }; 3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE5F9E2289D88DC004A156E /* OSModelStoreChangedHandler.swift */; }; 3CE795F928DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE795F828DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift */; }; 3CE795FB28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */; }; @@ -1175,6 +1176,8 @@ 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3CCF44BC299B17290021964D /* OneSignalWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalWrapper.h; sourceTree = ""; }; 3CCF44BD299B17290021964D /* OneSignalWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalWrapper.m; sourceTree = ""; }; + 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalUserTests-Bridging-Header.h"; sourceTree = ""; }; + 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalUserObjcTests.m; sourceTree = ""; }; 3CE5F9E2289D88DC004A156E /* OSModelStoreChangedHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSModelStoreChangedHandler.swift; sourceTree = ""; }; 3CE795F828DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSSubscriptionModelStoreListener.swift; sourceTree = ""; }; 3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSSubscriptionOperationExecutor.swift; sourceTree = ""; }; @@ -1980,8 +1983,10 @@ 3CC063EC2B6D7FE8002BB07F /* OneSignalUserTests */ = { isa = PBXGroup; children = ( + 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */, 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */, 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */, + 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */, ); path = OneSignalUserTests; sourceTree = ""; @@ -3395,6 +3400,7 @@ 3CC063EA2B6D7FE8002BB07F = { CreatedOnToolsVersion = 15.2; DevelopmentTeam = 99SW8E36CT; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; TestTargetID = DEF5CCF02539321A0003E9CC; }; @@ -3783,6 +3789,7 @@ files = ( 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, + 3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5114,6 +5121,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalUserTests/OneSignalUserTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; @@ -5167,6 +5175,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalUserTests/OneSignalUserTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5215,6 +5224,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalUserTests/OneSignalUserTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift index 183df51b8..fbc5747f1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift @@ -154,6 +154,7 @@ extension MockOneSignalClient { /** Checks if there is only one executed request that contains the payload provided, and the url matches the path provided. */ + @objc public func onlyOneRequest(contains path: String, contains payload: [String: Any]) -> Bool { var found = false diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift index 176ca3a94..757b53d93 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift @@ -26,7 +26,7 @@ import XCTest @objc public class OneSignalCoreMocks: NSObject { - + @objc public static func clearUserDefaults() { guard let userDefaults = OneSignalUserDefaults.initStandard().userDefaults else { return @@ -46,6 +46,7 @@ public class OneSignalCoreMocks: NSObject { } /** Wait specified number of seconds for any async methods to run */ + @objc public static func waitForBackgroundThreads(seconds: Double) { let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds") _ = XCTWaiter.wait(for: [expectation], timeout: seconds) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 6c39b4ffc..042bb4d08 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -1,7 +1,8 @@ import OneSignalCore import OneSignalCoreMocks -public class MockUserRequests { +@objc +public class MockUserRequests: NSObject { public static func testIdentityPayload(onesignalId: String, externalId: String?) -> [String: [String: String]] { var aliases = [OS_ONESIGNAL_ID: onesignalId] @@ -74,6 +75,7 @@ extension MockUserRequests { } } + @objc public static func setDefaultCreateAnonUserResponses(with client: MockOneSignalClient) { let anonCreateResponse = testDefaultFullCreateUserResponse(onesignalId: anonUserOSID, externalId: nil, subscriptionId: testPushSubId) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift index cedfb6868..72782423a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift @@ -33,7 +33,7 @@ import OneSignalOSCore public class OneSignalUserMocks: NSObject { // TODO: create mocked server responses to user requests - + @objc public static func reset() { resetStaticUserExecutor() // TODO: Reset Operation Repo first diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m new file mode 100644 index 000000000..5d52fd7fe --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m @@ -0,0 +1,78 @@ +#import +#import +#import +#import +#import +#import + +@interface OneSignalUserObjcTests : XCTestCase + +@end + +@implementation OneSignalUserObjcTests + +- (void)setUp { + // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // App ID is set because User Manager has guards against nil App ID + [OneSignalConfigManager setAppId:@"test-app-id"]; + // Temp. logging to help debug during testing + [OneSignalLog setLogLevel:ONE_S_LL_VERBOSE]; +} + +- (void)tearDown { + // TODO: Need to clear all data between tests for client, user manager, models, etc. + [OneSignalCoreMocks clearUserDefaults]; + [OneSignalUserMocks reset]; +} + +/** + Tests passing purchase data to the User Manager to process and send. + It is written in Objective-C as the data comes from Objective-C code. + */ +- (void)testSendPurchases { + /* Setup */ + + MockOneSignalClient* client = [MockOneSignalClient new]; + + // 1. Set up mock responses for the anonymous user + [MockUserRequests setDefaultCreateAnonUserResponsesWith:client]; + [OneSignalCoreImpl setSharedClient:client]; + + /* When */ + + NSMutableArray* arrayOfPurchases = [NSMutableArray new]; + // SKProduct.price is an NSDecimalNumber, but the backend expects a String + NSNumberFormatter *formatter = [NSNumberFormatter new]; + [formatter setMinimumFractionDigits:2]; + + NSString *formattedPrice1 = [formatter stringFromNumber:[NSDecimalNumber numberWithFloat:3.0]]; + NSString *formattedPrice2 = [formatter stringFromNumber:[NSDecimalNumber numberWithFloat:4.05]]; + + NSDictionary* purchase1 = @{ + @"sku": @"productSku1", + @"amount": formattedPrice1, + @"iso": @"EUR" + }; + [arrayOfPurchases addObject:purchase1]; + + NSDictionary* purchase2 = @{ + @"sku": @"productSku2", + @"amount": formattedPrice2, + @"iso": @"USD" + }; + [arrayOfPurchases addObject:purchase2]; + + [OneSignalUserManagerImpl.sharedInstance sendPurchases:arrayOfPurchases]; + + // Run background threads + [OneSignalCoreMocks waitForBackgroundThreadsWithSeconds:0.5]; + + /* Then */ + + NSString* path = [NSString stringWithFormat:@"apps/test-app-id/users/by/onesignal_id/%@", @"test_anon_user_onesignal_id"]; + NSDictionary *payload = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:arrayOfPurchases forKey:@"purchases"] forKey:@"deltas"]; + + XCTAssertTrue([client onlyOneRequestWithContains:path contains:payload]); +} + +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests-Bridging-Header.h new file mode 100644 index 000000000..1b2cb5d6d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + From 434cfd682a225db781bb10ba46cc4854b8ce697f Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 28 May 2024 10:50:33 -0700 Subject: [PATCH 10/15] [tests] Add dictionary sorting to request's parameters * There is a need to compare two User Update Requests, with limitations of comparing Objc dictionaries to Swift dictionaries, and the Mock Client not knowing about product-specific OneSignalRequest implementations like OSRequestUpdateUser. * So, we can do comparison via their parameters payload. * Add logic to stringify the params in alphabetical order to allow streamlined comparison --- .../OneSignal.xcodeproj/project.pbxproj | 4 +++ .../Extensions/NSDictionary+UnitTests.swift | 24 +++++++++++++++++ .../OneSignalRequest+UnitTests.swift | 11 ++++++++ .../MockOneSignalClient.swift | 26 +++++++++++++++---- 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/OneSignalRequest+UnitTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 327e02f30..7785f414c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 3CE8CC5B29143F4B000DB0D3 /* NSDateFormatter+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE98772A2591655800DE07D5 /* NSDateFormatter+OneSignal.m */; }; 3CE9227A289FA88B001B1062 /* OSIdentityModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */; }; 3CEE90A72BFE6ABD00B0FB5B /* SupportedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */; }; + 3CEE90A92C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */; }; 3CEE93422B7C4174008440BD /* OneSignalUserMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; }; 3CEE93432B7C4174008440BD /* OneSignalUserMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3CEE93462B7C73AB008440BD /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; @@ -1189,6 +1190,7 @@ 3CE8CC572911B2B2000DB0D3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelStoreListener.swift; sourceTree = ""; }; 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedProperty.swift; sourceTree = ""; }; + 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalRequest+UnitTests.swift"; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -1912,6 +1914,7 @@ isa = PBXGroup; children = ( 3C8706752BDEED75000D8CD2 /* NSDictionary+UnitTests.swift */, + 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */, ); path = Extensions; sourceTree = ""; @@ -3760,6 +3763,7 @@ 4710EA552B8FD04400435356 /* MockOSDispatchQueue.swift in Sources */, 3CC063B22B6D7AD8002BB07F /* MockOneSignalClient.swift in Sources */, 3C8706762BDEED75000D8CD2 /* NSDictionary+UnitTests.swift in Sources */, + 3CEE90A92C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift in Sources */, 3CC063B42B6D7BA2002BB07F /* OneSignalCoreMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift index 9e30d1dbd..4549dec3c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift @@ -70,4 +70,28 @@ extension NSDictionary { guard y is AnyHashable else { return false } return (x as! AnyHashable) == (y as! AnyHashable) } + + /** + Returns a string representation of a dictionary in alphabetical order by key. + If there are dictionaries within this dictionary, those will also be stringified in alphabetical order by key. + This method is motivated by the need to compare two requests whose payloads may be unordered dictionaries. + */ + public func toSortedString() -> String { + guard let dict = self as? [String: Any] else { + return "[:]" + } + var result = "[" + let sortedKeys = Array(dict.keys).sorted(by: <) + for key in sortedKeys { + if let value = dict[key] as? NSDictionary { + result += " \(key): \(value.toSortedString())," + } else { + result += " \(key): \(String(describing: dict[key]))," + } + } + // drop the last comma within a dictionary's items + result = String(result.dropLast()) + result += "]" + return result + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/OneSignalRequest+UnitTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/OneSignalRequest+UnitTests.swift new file mode 100644 index 000000000..4ecc5f4eb --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/OneSignalRequest+UnitTests.swift @@ -0,0 +1,11 @@ +import OneSignalCore + +extension OneSignalRequest { + /// Returns alphabetically ordered string representation of request's parameters + public func stringifyParams() -> String { + guard let dict = self.parameters as? NSDictionary else { + return "[:]" + } + return dict.toSortedString() + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift index fbc5747f1..f2f7ce65e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift @@ -101,6 +101,21 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { } } + /// Helper method to stringify the name of a request for identification and comparison + private func stringify(_ request: OneSignalRequest) -> String { + var stringified = request.description + + switch request.description { + case let str where str.contains("OSRequestUpdateProperties"): + // Return an ordered representation of the request parameters + stringified = "" + default: + break + } + + return stringified + } + func finishExecutingRequest(_ request: OneSignalRequest, onSuccess successBlock: OSResultSuccessBlock, onFailure failureBlock: OSFailureBlock) { // TODO: This entire method needs to contained within the equivalent of @synchronized ❗️ @@ -110,18 +125,19 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { self.didCompleteRequest(request) + let stringifiedRequest = stringify(request) // Switch between types of requests with mock responses if request.isKind(of: OSRequestGetIosParams.self) { // send a mock remote params response successBlock(["mockTodo": "responseTodo"]) } - if (mockResponses[String(describing: request)]) != nil { - successBlock(mockResponses[String(describing: request)]) - } else if (mockFailureResponses[String(describing: request)]) != nil { - failureBlock(mockFailureResponses[String(describing: request)]) + if (mockResponses[stringifiedRequest]) != nil { + successBlock(mockResponses[stringifiedRequest]) + } else if (mockFailureResponses[stringifiedRequest]) != nil { + failureBlock(mockFailureResponses[stringifiedRequest]) } else { allRequestsHandled = false - print("🧪 cannot find a mock response for request: \(request)") + print("🧪 cannot find a mock response for request: \(stringifiedRequest)") } } From a8a4033d7745421d56e75b120c527b3c0e86e8c9 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 28 May 2024 11:04:24 -0700 Subject: [PATCH 11/15] [tests] update tests to show combined operations * When a tag and a language are added, they are now set in 1 request containing both updates instead of 2 requests --- .../OneSignalUserMocks/MockUserRequests.swift | 26 +++++- .../SwitchUserIntegrationTests.swift | 88 +++++-------------- 2 files changed, 46 insertions(+), 68 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 042bb4d08..10b38f4e8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -153,18 +153,36 @@ extension MockUserRequests { } public static func setAddTagsResponse(with client: MockOneSignalClient, tags: [String: String]) { + let params: NSDictionary = [ + "properties": [ + "tags": tags + ], + "refresh_device_metadata": false + ] + let tagsResponse = MockUserRequests.testPropertiesPayload(properties: ["tags": tags]) client.setMockResponseForRequest( - request: "", + request: "", response: tagsResponse ) } - public static func setSetLanguageResponse(with client: MockOneSignalClient, language: String) { + /// Sets the mock response when tags and language are added, which will be sent in one request + public static func setAddTagsAndLanguageResponse(with client: MockOneSignalClient, tags: [String: String], language: String) { + let params: NSDictionary = [ + "properties": [ + "language": Optional(language), // to match the stringify of the actual request + "tags": tags + ], + "refresh_device_metadata": false + ] + + let tagsResponse = testPropertiesPayload(properties: ["tags": tags]) + client.setMockResponseForRequest( - request: "", - response: [:] // The SDK does not use the response in any way + request: "", + response: tagsResponse ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index bbf9d19f4..64073e7c2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -95,16 +95,14 @@ final class SwitchUserIntegrationTests: XCTestCase { // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] MockUserRequests.setDefaultCreateAnonUserResponses(with: client) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserAnon) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_anon") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserAnon, language: "lang_anon") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_anon": "id_anon"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_anon@example.com") // 2. Set up mock responses for User A with 409 conflict response let tagsUserA = ["tag_a": "value_a"] MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: userA_EUID, conflicted: true) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserA) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_a") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserA, language: "lang_a") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_a": "id_a"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_a@example.com") MockUserRequests.setTransferSubscriptionResponse(with: client, externalId: userA_EUID) @@ -135,13 +133,9 @@ final class SwitchUserIntegrationTests: XCTestCase { XCTAssertTrue(client.allRequestsHandled) // 1. Asserts for first Anonymous User - XCTAssertTrue(client.onlyOneRequest( // Tag - contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["tags": tagsUserAnon]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["language": "lang_anon"]]) + contains: ["properties": ["language": "lang_anon", "tags": tagsUserAnon]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)/identity", @@ -153,13 +147,9 @@ final class SwitchUserIntegrationTests: XCTestCase { ) // 2. Asserts for User A - expected requests are sent - XCTAssertTrue(client.onlyOneRequest( // Tag + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)", - contains: ["properties": ["tags": tagsUserA]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language - contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)", - contains: ["properties": ["language": "lang_a"]]) + contains: ["properties": ["language": "lang_a", "tags": tagsUserA]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)/identity", @@ -209,23 +199,20 @@ final class SwitchUserIntegrationTests: XCTestCase { // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] MockUserRequests.setDefaultCreateAnonUserResponses(with: client) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserAnon) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_anon") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserAnon, language: "lang_anon") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_anon": "id_anon"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_anon@example.com") // 2. Set up mock responses for User A with 409 conflict response let tagsUserA = ["tag_a": "value_a"] MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: userA_EUID, conflicted: true) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserA) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_a") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserA, language: "lang_a") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_a": "id_a"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_a@example.com") // 3. Set up mock responses for second Anonymous User let tagsUserB = ["tag_b": "value_b"] - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserB) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_b") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserB, language: "lang_b") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_b": "id_b"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_b@example.com") @@ -260,13 +247,9 @@ final class SwitchUserIntegrationTests: XCTestCase { XCTAssertTrue(client.allRequestsHandled) // 1. Asserts for first Anonymous User - XCTAssertTrue(client.onlyOneRequest( // Tag - contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["tags": tagsUserAnon]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["language": "lang_anon"]]) + contains: ["properties": ["language": "lang_anon", "tags": tagsUserAnon]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)/identity", @@ -278,13 +261,9 @@ final class SwitchUserIntegrationTests: XCTestCase { ) // 2. Asserts for User A - XCTAssertTrue(client.onlyOneRequest( // Tag - contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)", - contains: ["properties": ["tags": tagsUserA]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)", - contains: ["properties": ["language": "lang_a"]]) + contains: ["properties": ["language": "lang_a", "tags": tagsUserA]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)/identity", @@ -296,13 +275,9 @@ final class SwitchUserIntegrationTests: XCTestCase { ) // 3. Asserts for the second Anonymous User - XCTAssertTrue(client.onlyOneRequest( // Tag + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["tags": tagsUserB]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language - contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["language": "lang_b"]]) + contains: ["properties": ["language": "lang_b", "tags": tagsUserB]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)/identity", @@ -347,24 +322,21 @@ final class SwitchUserIntegrationTests: XCTestCase { // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] MockUserRequests.setDefaultCreateAnonUserResponses(with: client) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserAnon) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_anon") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserAnon, language: "lang_anon") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_anon": "id_anon"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_anon@example.com") // 2. Set up mock responses for User A with 409 conflict response let tagsUserA = ["tag_a": "value_a"] MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: userA_EUID, conflicted: true) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserA) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_a") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserA, language: "lang_a") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_a": "id_a"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_a@example.com") // 3. Set up mock responses for for User B let tagsUserB = ["tag_b": "value_b"] MockUserRequests.setDefaultCreateUserResponses(with: client, externalId: userB_EUID) - MockUserRequests.setAddTagsResponse(with: client, tags: tagsUserB) - MockUserRequests.setSetLanguageResponse(with: client, language: "lang_b") + MockUserRequests.setAddTagsAndLanguageResponse(with: client, tags: tagsUserB, language: "lang_b") MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_b": "id_b"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_b@example.com") // Returns mocked user data to test hydration @@ -401,13 +373,9 @@ final class SwitchUserIntegrationTests: XCTestCase { XCTAssertTrue(client.allRequestsHandled) // 1. Asserts for first Anonymous User - XCTAssertTrue(client.onlyOneRequest( // Tag - contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["tags": tagsUserAnon]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", - contains: ["properties": ["language": "lang_anon"]]) + contains: ["properties": ["language": "lang_anon", "tags": tagsUserAnon]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)/identity", @@ -419,13 +387,9 @@ final class SwitchUserIntegrationTests: XCTestCase { ) // 2. Asserts for User A - XCTAssertTrue(client.onlyOneRequest( // Tag + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)", - contains: ["properties": ["tags": tagsUserA]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language - contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)", - contains: ["properties": ["language": "lang_a"]]) + contains: ["properties": ["language": "lang_a", "tags": tagsUserA]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/external_id/\(userA_EUID)/identity", @@ -437,13 +401,9 @@ final class SwitchUserIntegrationTests: XCTestCase { ) // 3. Asserts for User B - expected requests sent - XCTAssertTrue(client.onlyOneRequest( // Tag - contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)", - contains: ["properties": ["tags": tagsUserB]]) - ) - XCTAssertTrue(client.onlyOneRequest( // Language + XCTAssertTrue(client.onlyOneRequest( // Tag + Language contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)", - contains: ["properties": ["language": "lang_b"]]) + contains: ["properties": ["language": "lang_b", "tags": tagsUserB]]) ) XCTAssertTrue(client.onlyOneRequest( // Alias contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)/identity", From 5c377342002ba5fe78ee94dfbe67ec4147b59c3b Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 28 May 2024 15:28:42 -0700 Subject: [PATCH 12/15] [tests] add testing combining user updates * Add a test where many user properties updates are made and check they are all sent in one payload correctly * Update an equality checker helper method to handle testing float equality, needed for checking lat and long in the payload. --- .../Extensions/NSDictionary+UnitTests.swift | 12 ++- .../OneSignalUserTests.swift | 81 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift index 4549dec3c..a8cea9cf0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift @@ -66,9 +66,15 @@ extension NSDictionary { } private func equals(_ x: Any, _ y: Any) -> Bool { - guard x is AnyHashable else { return false } - guard y is AnyHashable else { return false } - return (x as! AnyHashable) == (y as! AnyHashable) + switch (x, y) { + case let (x as NSNumber, y as NSNumber): + // Handle float equality imprecision + return abs(x.floatValue - y.floatValue) <= .ulpOfOne + default: + guard x is AnyHashable else { return false } + guard y is AnyHashable else { return false } + return (x as! AnyHashable) == (y as! AnyHashable) + } } /** diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index e0dc80c9c..6ac2e80e5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -137,4 +137,85 @@ final class OneSignalUserTests: XCTestCase { identityModel.clearData() } } + + /** + Tests multiple user updates should be combined and sent together. + Multiple session times should be added. + Adding and removing multiple tags should be combined correctly. + Language uses the last language that is set. + Location uses the last point that is set. + */ + func testBasicCombiningUserUpdateDeltas_resultsInOneRequest() throws { + /* Setup */ + + let client = MockOneSignalClient() + MockUserRequests.setDefaultCreateAnonUserResponses(with: client) + OneSignalCoreImpl.setSharedClient(client) + + /* When */ + + OneSignalUserManagerImpl.sharedInstance.sendSessionTime(100) + + // This adds a `session_count` property with value of 1 + // It also sets `refresh_device_metadata` to `true` + OneSignalUserManagerImpl.sharedInstance.startNewSession() + + OneSignalUserManagerImpl.sharedInstance.setLanguage("lang_1") + + OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_1", value: "value_1") + + OneSignalUserManagerImpl.sharedInstance.setLanguage("lang_2") + + OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_2", value: "value_2") + + OneSignalUserManagerImpl.sharedInstance.sendSessionTime(50) + + OneSignalUserManagerImpl.sharedInstance.setLocation(latitude: 123.123, longitude: 145.145) + + OneSignalUserManagerImpl.sharedInstance.removeTag("tag_1") + + OneSignalUserManagerImpl.sharedInstance.addTags(["a": "a", "b": "b", "c": "c"]) + + OneSignalUserManagerImpl.sharedInstance.startNewSession() + + let purchases = [ + ["sku": "sku1", "amount": "1.25", "iso": "USD"], + ["sku": "sku2", "amount": "3.99", "iso": "USD"] + ] + + OneSignalUserManagerImpl.sharedInstance.sendPurchases(purchases as [[String: AnyObject]]) + + OneSignalUserManagerImpl.sharedInstance.setLocation(latitude: 111.111, longitude: 222.222) + + /* Then */ + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + let expectedPayload: [String: Any] = [ + "deltas": [ + "session_time": 150, // addition of 2 session times + "session_count": 2, // addition of 2 session counts + "purchases": purchases + ], + "properties": [ + "lat": 111.111, + "long": 222.222, + "language": "lang_2", + "tags": [ + "tag_1": "", + "tag_2": "value_2", + "a": "a", + "b": "b", + "c": "c" + ] + ], + "refresh_device_metadata": true + ] + + // Assert there is an update user request with the expected payload + XCTAssertTrue(client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(anonUserOSID)", + contains: expectedPayload) + ) + } } From f1a986d010e6ac1920167c29225261572f779695 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 28 May 2024 18:45:40 -0700 Subject: [PATCH 13/15] [tests] Operation Repo polling is too quick for test * Some tests require more time for the user updates to enqueue before the flush --- .../OneSignalUserTests/OneSignalUserTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 6ac2e80e5..c37b3d22b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -29,7 +29,8 @@ import XCTest import OneSignalCore import OneSignalCoreMocks import OneSignalUserMocks -import OneSignalOSCore +// Testable import OSCore to allow setting a different poll flush interval +@testable import OneSignalOSCore @testable import OneSignalUser final class OneSignalUserTests: XCTestCase { @@ -152,6 +153,9 @@ final class OneSignalUserTests: XCTestCase { MockUserRequests.setDefaultCreateAnonUserResponses(with: client) OneSignalCoreImpl.setSharedClient(client) + // Increase flush interval to allow all the updates to batch + OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + /* When */ OneSignalUserManagerImpl.sharedInstance.sendSessionTime(100) From e37e40b84de1741f4b5c0f0b6283f000d0d4c3b4 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 11 Jun 2024 08:17:52 -0700 Subject: [PATCH 14/15] address review comments * Rename `SupportedProperty.swift` to `OSPropertiesSupportedProperty.swift` and move to a higher-up folder in the user module * Simplify a piece of logic for combining properties by removing unnecessary intermediate step * Change a parameter type to an enum for more clarity and control --- .../OneSignalSDK/OneSignal.xcodeproj/project.pbxproj | 10 +++++----- .../Executors/OSPropertyOperationExecutor.swift | 5 ++--- .../Source/OneSignalUserManagerImpl.swift | 12 ++++++------ .../OSPropertiesSupportedProperty.swift} | 0 4 files changed, 13 insertions(+), 14 deletions(-) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{Executors/Support/SupportedProperty.swift => Support/OSPropertiesSupportedProperty.swift} (100%) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 7785f414c..61ace2bb6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -154,7 +154,7 @@ 3CE8CC582911B2B2000DB0D3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE8CC572911B2B2000DB0D3 /* SystemConfiguration.framework */; }; 3CE8CC5B29143F4B000DB0D3 /* NSDateFormatter+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE98772A2591655800DE07D5 /* NSDateFormatter+OneSignal.m */; }; 3CE9227A289FA88B001B1062 /* OSIdentityModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */; }; - 3CEE90A72BFE6ABD00B0FB5B /* SupportedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */; }; + 3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */; }; 3CEE90A92C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */; }; 3CEE93422B7C4174008440BD /* OneSignalUserMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; }; 3CEE93432B7C4174008440BD /* OneSignalUserMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC063DD2B6D7F2A002BB07F /* OneSignalUserMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1189,7 +1189,7 @@ 3CE8CC552911B1E0000DB0D3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 3CE8CC572911B2B2000DB0D3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelStoreListener.swift; sourceTree = ""; }; - 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedProperty.swift; sourceTree = ""; }; + 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesSupportedProperty.swift; sourceTree = ""; }; 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalRequest+UnitTests.swift"; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; @@ -1922,7 +1922,6 @@ 3C9AD6BA2B2284AB00BC1540 /* Executors */ = { isa = PBXGroup; children = ( - 3CEE90A52BFE6A7700B0FB5B /* Support */, 3C8E6E0028AC0BA10031E48A /* OSIdentityOperationExecutor.swift */, 3C8E6DFE28AB09AE0031E48A /* OSPropertyOperationExecutor.swift */, 3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */, @@ -1997,7 +1996,7 @@ 3CEE90A52BFE6A7700B0FB5B /* Support */ = { isa = PBXGroup; children = ( - 3CEE90A62BFE6ABD00B0FB5B /* SupportedProperty.swift */, + 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */, ); path = Support; sourceTree = ""; @@ -2277,6 +2276,7 @@ DE69E1A8282ED8360090BB3D /* Source */ = { isa = PBXGroup; children = ( + 3CEE90A52BFE6A7700B0FB5B /* Support */, 3C9AD6BA2B2284AB00BC1540 /* Executors */, 3C9AD6BD2B22877600BC1540 /* Requests */, DE69E1A9282ED8790090BB3D /* UnitTestApp-Bridging-Header.h */, @@ -3948,7 +3948,7 @@ 3C8E6E0128AC0BA10031E48A /* OSIdentityOperationExecutor.swift in Sources */, 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */, 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */, - 3CEE90A72BFE6ABD00B0FB5B /* SupportedProperty.swift in Sources */, + 3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */, 3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */, 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index ae1d1fb7f..27dec7298 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -142,9 +142,8 @@ class OSPropertyOperationExecutor: OSOperationExecutor { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)") continue } - var combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] - combinedSoFar = self.combineProperties(existing: combinedSoFar, delta: delta) - combinedProperties[identityModel.modelId] = combinedSoFar + let combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] + combinedProperties[identityModel.modelId] = self.combineProperties(existing: combinedSoFar, delta: delta) } if combinedProperties.count > 1 { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index c4bf7124e..d06b8493c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -511,7 +511,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "sendPurchases") else { return } - updatePropertiesDeltas(property: "purchases", value: purchases) + updatePropertiesDeltas(property: .purchases, value: purchases) } private func fireJwtExpired() { @@ -540,7 +540,7 @@ extension OneSignalUserManagerImpl { OSUserExecutor.executePendingRequests() OSOperationRepo.sharedInstance.paused = false - updatePropertiesDeltas(property: "session_count", value: 1) + updatePropertiesDeltas(property: .session_count, value: 1) // Fetch the user's data if there is a onesignal_id if let onesignalId = onesignalId { @@ -555,8 +555,8 @@ extension OneSignalUserManagerImpl { /// This method accepts properties updates that not driven by model changes. /// It enqueues an OSDelta to the Operation Repo. /// - /// - Parameter property:Expected inputs are `"session_time"`, `"session_count"`, and `"purchases"`. - func updatePropertiesDeltas(property: String, value: Any) { + /// - Parameter property:Expected inputs are `.session_time"`, `.session_count"`, and `.purchases"`. + func updatePropertiesDeltas(property: OSPropertiesSupportedProperty, value: Any) { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updatePropertiesDeltas") else { return } @@ -569,7 +569,7 @@ extension OneSignalUserManagerImpl { name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: propertiesModel, - property: property, + property: property.rawValue, value: value ) OSOperationRepo.sharedInstance.enqueueDelta(delta) @@ -581,7 +581,7 @@ extension OneSignalUserManagerImpl { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "sendSessionTime") else { return } - updatePropertiesDeltas(property: "session_time", value: sessionTime.intValue) + updatePropertiesDeltas(property: .session_time, value: sessionTime.intValue) } /** diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSPropertiesSupportedProperty.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/Support/SupportedProperty.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSPropertiesSupportedProperty.swift From f88eb7d62539ff5aac6f0c9c3f7bd43e95fb0a39 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Tue, 11 Jun 2024 15:00:21 -0700 Subject: [PATCH 15/15] Add OneSignalCoreMocks as a dependency of OneSignalUserMocks --- .../OneSignal.xcodeproj/project.pbxproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 61ace2bb6..417a9ef65 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -464,6 +464,8 @@ DEA4B4652888C59100E9FE12 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; }; DEA4B4662888C59E00E9FE12 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DEA4B4672888C5F200E9FE12 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; }; + DEA69F452C190045009BB128 /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; + DEA69F462C190045009BB128 /* OneSignalCoreMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DEA98C1928C90EE5000C6856 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; DEA98C1C28C90EE6000C6856 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; }; DEA98C1E28C90EE9000C6856 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; @@ -871,6 +873,13 @@ remoteGlobalIDString = DE7D187F27037F43002D3A5D; remoteInfo = OneSignalOutcomes; }; + DEA69F472C190045009BB128 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3CC063992B6D7A8C002BB07F; + remoteInfo = OneSignalCoreMocks; + }; DEBAAE062A420C9800BF2C1C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37747F8B19147D6400558FAD /* Project object */; @@ -1035,6 +1044,7 @@ files = ( 3CEE93542B7C78EC008440BD /* OneSignalUser.framework in Embed Frameworks */, 3CA8B8832BEC2FCB0010ADA1 /* XCTest.framework in Embed Frameworks */, + DEA69F462C190045009BB128 /* OneSignalCoreMocks.framework in Embed Frameworks */, 3CEE934F2B7C787B008440BD /* OneSignalOSCore.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1594,6 +1604,7 @@ files = ( 3CEE93532B7C78EC008440BD /* OneSignalUser.framework in Frameworks */, 3CA8B8822BEC2FCB0010ADA1 /* XCTest.framework in Frameworks */, + DEA69F452C190045009BB128 /* OneSignalCoreMocks.framework in Frameworks */, 3CEE934E2B7C787B008440BD /* OneSignalOSCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3074,6 +3085,7 @@ dependencies = ( 3CEE93512B7C787C008440BD /* PBXTargetDependency */, 3CEE93562B7C78EC008440BD /* PBXTargetDependency */, + DEA69F482C190045009BB128 /* PBXTargetDependency */, ); name = OneSignalUserMocks; productName = OneSignalUserMocks; @@ -4324,6 +4336,11 @@ target = DE7D187F27037F43002D3A5D /* OneSignalOutcomes */; targetProxy = DE7D18D42703ADE0002D3A5D /* PBXContainerItemProxy */; }; + DEA69F482C190045009BB128 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */; + targetProxy = DEA69F472C190045009BB128 /* PBXContainerItemProxy */; + }; DEBAAE072A420C9800BF2C1C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DE7D17E527026B95002D3A5D /* OneSignalCore */;