diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 467f60eac..026d1027c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */; }; 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; }; 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */; }; + 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */; }; 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; }; 3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; }; @@ -1318,6 +1319,7 @@ 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyTriggerTrackingTests.swift; sourceTree = ""; }; 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityConstants.swift; sourceTree = ""; }; 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityViewExtensions.swift; sourceTree = ""; }; + 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndOutcomesRequestTests.swift; sourceTree = ""; }; 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; }; 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = ""; }; 3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = ""; }; @@ -2586,6 +2588,7 @@ 3C2C7DC2288E007E0020F9AE /* UnitTests-Bridging-Header.h */, 4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */, 4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */, + 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */, ); path = UnitTests; sourceTree = ""; @@ -4531,6 +4534,7 @@ 4529DED21FA81EA800CEAB1D /* NSObjectOverrider.m in Sources */, CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */, 5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */, + 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */, CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */, CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */, 7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 0fd71c4f1..ebba1ebda 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -200,7 +200,7 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState; #define focusAttributionStateString(enum) [@[@"ATTRIBUTED", @"NOT_ATTRIBUTED"] objectAtIndex:enum] // OneSignal Background Task Identifiers -#define ATTRIBUTED_FOCUS_TASK @"ATTRIBUTED_FOCUS_TASK" +#define SESSION_OUTCOMES_TASK @"SESSION_OUTCOMES_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/OneSignalOutcomes/Source/OutcomeEvents/OneSignalOutcomeEventsController.m b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OneSignalOutcomeEventsController.m index b9f17231a..c8d849870 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OneSignalOutcomeEventsController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OneSignalOutcomeEventsController.m @@ -117,12 +117,12 @@ - (void)sendSessionEndOutcomes:(NSNumber * _Nonnull)timeElapsed pushSubscriptionId:pushSubscriptionId onesignalId:onesignalId influenceParams:influenceParams] onSuccess:^(NSDictionary *result) { - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes attributed succeed"]; + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes succeed"]; if (successBlock) { successBlock(result); } } onFailure:^(OneSignalClientError *error) { - [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes attributed failed"]; + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes failed"]; if (failureBlock) { failureBlock(error.underlyingError); } diff --git a/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m b/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m index 64267b76d..5beb0464a 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m +++ b/iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m @@ -44,7 +44,7 @@ @implementation OSAttributedFocusTimeProcessor { - (instancetype)init { self = [super init]; - [OSBackgroundTaskManager setTaskInvalid:ATTRIBUTED_FOCUS_TASK]; + [OSBackgroundTaskManager setTaskInvalid:SESSION_OUTCOMES_TASK]; return self; } @@ -85,7 +85,7 @@ - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(N return; } - [OSBackgroundTaskManager beginBackgroundTask:ATTRIBUTED_FOCUS_TASK]; + [OSBackgroundTaskManager beginBackgroundTask:SESSION_OUTCOMES_TASK]; if (params.onSessionEnded) { [self sendBackgroundAttributedSessionTimeWithParams:params withTotalTimeActive:@(totalTimeActive)]; @@ -114,10 +114,10 @@ - (void)sendBackgroundAttributedSessionTimeWithParams:(OSFocusCallParams *)param [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]; + [OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK]; } onFailure:^(NSError *error) { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendBackgroundAttributed failed, will retry on next open"]; - [OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK]; + [OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK]; }]; }); } @@ -128,7 +128,7 @@ - (void)cancelDelayedJob { [restCallTimer invalidate]; restCallTimer = nil; - [OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK]; + [OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK]; } @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSFocusTimeProcessorFactory.m b/iOS_SDK/OneSignalSDK/Source/OSFocusTimeProcessorFactory.m index e5c80b270..c13a83d3f 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSFocusTimeProcessorFactory.m +++ b/iOS_SDK/OneSignalSDK/Source/OSFocusTimeProcessorFactory.m @@ -76,9 +76,6 @@ + (OSBaseFocusTimeProcessor *)createTimeProcessorWithInfluences:(NSArray #import #import "OSMacros.h" +#import "OneSignalFramework.h" #import "OSUnattributedFocusTimeProcessor.h" #import +@interface OneSignal () ++ (void)sendSessionEndOutcomes:(NSNumber*)totalTimeActive params:(OSFocusCallParams *)params onSuccess:(OSResultSuccessBlock _Nonnull)successBlock onFailure:(OSFailureBlock _Nonnull)failureBlock; +@end + @implementation OSUnattributedFocusTimeProcessor static let UNATTRIBUTED_MIN_SESSION_TIME_SEC = 60; - (instancetype)init { self = [super init]; + [OSBackgroundTaskManager setTaskInvalid:SESSION_OUTCOMES_TASK]; return self; } @@ -74,6 +80,17 @@ - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(N [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"OSUnattributedFocusTimeProcessor:sendSessionTime of %@", @(totalTimeActive)]]; [OneSignalUserManagerImpl.sharedInstance sendSessionTime:@(totalTimeActive)]; [super saveUnsentActiveTime:0]; + + [OSBackgroundTaskManager beginBackgroundTask:SESSION_OUTCOMES_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:@"sendUnattributed session end outcomes succeed"]; + [OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK]; + } onFailure:^(NSError *error) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendUnattributed session end outcomes failed"]; + [OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK]; + }]; + }); } - (void)cancelDelayedJob { diff --git a/iOS_SDK/OneSignalSDK/UnitTests/.swiftlint.yml b/iOS_SDK/OneSignalSDK/UnitTests/.swiftlint.yml new file mode 100644 index 000000000..c6778fb1d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTests/.swiftlint.yml @@ -0,0 +1,3 @@ +# in tests, we may want to force cast and throw any errors +disabled_rules: + - force_cast diff --git a/iOS_SDK/OneSignalSDK/UnitTests/SessionEndOutcomesRequestTests.swift b/iOS_SDK/OneSignalSDK/UnitTests/SessionEndOutcomesRequestTests.swift new file mode 100644 index 000000000..2b0edd8ee --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTests/SessionEndOutcomesRequestTests.swift @@ -0,0 +1,110 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalOutcomes + +class SessionEndOutcomesRequestTests: XCTestCase { + + func testUnattributedInfluence() { + let influenceParam = OSFocusInfluenceParam( + paramsInfluenceIds: nil, + influenceKey: "notification_ids", + directInfluence: false, + influenceDirectKey: "direct" + )! + + let request = OSRequestSendSessionEndOutcomes.withActiveTime( + 120, + appId: "test-app-id", + pushSubscriptionId: "test-push-sub-id", + onesignalId: "test-onesignal-id", + influenceParams: [influenceParam] + ) + + XCTAssertEqual(request.path, "outcomes/measure") + XCTAssertEqual(request.method, POST) + + let params = request.parameters as! [String: Any] + XCTAssertEqual(params["app_id"] as? String, "test-app-id") + XCTAssertEqual(params["id"] as? String, "os__session_duration") + XCTAssertEqual(params["session_time"] as? Int, 120) + XCTAssertEqual(params["onesignal_id"] as? String, "test-onesignal-id") + + let subscription = params["subscription"] as! [String: Any] + XCTAssertEqual(subscription["id"] as? String, "test-push-sub-id") + XCTAssertEqual(subscription["type"] as? String, "iOSPush") + + XCTAssertEqual(params["direct"] as? Bool, false) + XCTAssertNil(params["notification_ids"]) + } + + func testAttributedDirectInfluence() { + let notificationIds = ["notif-1", "notif-2"] + let influenceParam = OSFocusInfluenceParam( + paramsInfluenceIds: notificationIds, + influenceKey: "notification_ids", + directInfluence: true, + influenceDirectKey: "direct" + )! + + let request = OSRequestSendSessionEndOutcomes.withActiveTime( + 60, + appId: "test-app-id", + pushSubscriptionId: "test-push-sub-id", + onesignalId: "test-onesignal-id", + influenceParams: [influenceParam] + ) + + let params = request.parameters as! [String: Any] + XCTAssertEqual(params["direct"] as? Bool, true) + XCTAssertEqual(params["notification_ids"] as? [String], notificationIds) + XCTAssertEqual(params["session_time"] as? Int, 60) + } + + func testAttributedIndirectInfluence() { + let notificationIds = ["notif-1", "notif-2", "notif-3"] + let influenceParam = OSFocusInfluenceParam( + paramsInfluenceIds: notificationIds, + influenceKey: "notification_ids", + directInfluence: false, + influenceDirectKey: "direct" + )! + + let request = OSRequestSendSessionEndOutcomes.withActiveTime( + 90, + appId: "test-app-id", + pushSubscriptionId: "test-push-sub-id", + onesignalId: "test-onesignal-id", + influenceParams: [influenceParam] + ) + + let params = request.parameters as! [String: Any] + XCTAssertEqual(params["direct"] as? Bool, false) + XCTAssertEqual(params["notification_ids"] as? [String], notificationIds) + } +} diff --git a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h index 878f77fa5..05458d465 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h @@ -3,3 +3,4 @@ // #import "OneSignalFramework.h" +#import "OSOutcomesRequests.h"