diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 7865e8019..417a9ef65 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 */; }; @@ -153,6 +154,8 @@ 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 /* 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, ); }; }; 3CEE93462B7C73AB008440BD /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; @@ -461,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 */; }; @@ -868,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 */; @@ -1032,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"; @@ -1174,6 +1187,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 = ""; }; @@ -1184,6 +1199,8 @@ 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 /* 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 = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -1587,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; @@ -1907,6 +1925,7 @@ isa = PBXGroup; children = ( 3C8706752BDEED75000D8CD2 /* NSDictionary+UnitTests.swift */, + 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */, ); path = Extensions; sourceTree = ""; @@ -1977,12 +1996,22 @@ 3CC063EC2B6D7FE8002BB07F /* OneSignalUserTests */ = { isa = PBXGroup; children = ( + 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */, 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */, 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */, + 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */, ); path = OneSignalUserTests; sourceTree = ""; }; + 3CEE90A52BFE6A7700B0FB5B /* Support */ = { + isa = PBXGroup; + children = ( + 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */, + ); + path = Support; + sourceTree = ""; + }; 3E2400391D4FFC31008BDE70 /* OneSignalFramework */ = { isa = PBXGroup; children = ( @@ -2258,6 +2287,7 @@ DE69E1A8282ED8360090BB3D /* Source */ = { isa = PBXGroup; children = ( + 3CEE90A52BFE6A7700B0FB5B /* Support */, 3C9AD6BA2B2284AB00BC1540 /* Executors */, 3C9AD6BD2B22877600BC1540 /* Requests */, DE69E1A9282ED8790090BB3D /* UnitTestApp-Bridging-Header.h */, @@ -3055,6 +3085,7 @@ dependencies = ( 3CEE93512B7C787C008440BD /* PBXTargetDependency */, 3CEE93562B7C78EC008440BD /* PBXTargetDependency */, + DEA69F482C190045009BB128 /* PBXTargetDependency */, ); name = OneSignalUserMocks; productName = OneSignalUserMocks; @@ -3384,6 +3415,7 @@ 3CC063EA2B6D7FE8002BB07F = { CreatedOnToolsVersion = 15.2; DevelopmentTeam = 99SW8E36CT; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; TestTargetID = DEF5CCF02539321A0003E9CC; }; @@ -3743,6 +3775,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; @@ -3772,6 +3805,7 @@ files = ( 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, + 3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3926,6 +3960,7 @@ 3C8E6E0128AC0BA10031E48A /* OSIdentityOperationExecutor.swift in Sources */, 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */, 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */, + 3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */, 3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */, 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, @@ -4301,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 */; @@ -5102,6 +5142,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"; @@ -5155,6 +5196,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"; @@ -5203,6 +5245,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/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index d6ee6b1fa..b0578fc11 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -192,8 +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_" #define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_" diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift index 9e30d1dbd..a8cea9cf0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/Extensions/NSDictionary+UnitTests.swift @@ -66,8 +66,38 @@ 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) + } + } + + /** + 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 183df51b8..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)") } } @@ -154,6 +170,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/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..27dec7298 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,98 @@ 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 } + let combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] + combinedProperties[identityModel.modelId] = self.combineProperties(existing: combinedSoFar, delta: delta) + } + + 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 @@ -188,33 +280,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/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 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, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index f2d971be4..d06b8493c 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: OSPropertiesSupportedProperty, 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.rawValue, + 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) } /** 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 diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSPropertiesSupportedProperty.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSPropertiesSupportedProperty.swift new file mode 100644 index 000000000..ecaa24777 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSPropertiesSupportedProperty.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/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 6c39b4ffc..10b38f4e8 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) @@ -151,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/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. +// + diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index e0dc80c9c..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 { @@ -137,4 +138,88 @@ 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) + + // Increase flush interval to allow all the updates to batch + OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + + /* 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) + ) + } } 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", 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 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 { 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"]; 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])