From 506ef06ee9269c365d8a9014fdd01910fc0ce0fa Mon Sep 17 00:00:00 2001 From: Nan Date: Sat, 4 Oct 2025 11:33:12 -0700 Subject: [PATCH 1/8] Add request for Click Events This is the skeleton, we still need to finalize the path and parameters --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../Source/OneSignalCommonDefines.h | 1 + .../Executors/OSLiveActivitiesExecutor.swift | 13 +++ .../OSRequestLiveActivityClicked.swift | 104 ++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 48f489b10..061b546d5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; + 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; 3C24B0EC2BD09D7A0052E771 /* OneSignalCoreObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C24B0EB2BD09D7A0052E771 /* OneSignalCoreObjCTests.m */; }; 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */; }; 3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */; }; @@ -1296,6 +1297,7 @@ 3C11518C289AF5E800565C41 /* OSModelChangedHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSModelChangedHandler.swift; sourceTree = ""; }; 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestLiveActivityClicked.swift; sourceTree = ""; }; 3C24B0EA2BD09D790052E771 /* OneSignalCoreTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalCoreTests-Bridging-Header.h"; sourceTree = ""; }; 3C24B0EB2BD09D7A0052E771 /* OneSignalCoreObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalCoreObjCTests.m; sourceTree = ""; }; 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelRepo.swift; sourceTree = ""; }; @@ -2335,6 +2337,7 @@ 3CFA8F472E9087DB00201FE5 /* OSRequestSetUpdateToken.swift */, 3CFA8F452E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift */, 3CFA8F5A2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift */, + 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */, ); path = Requests; sourceTree = ""; @@ -4443,6 +4446,7 @@ 3CFA8F542E9087DB00201FE5 /* OSLiveActivitiesExtension.swift in Sources */, 3CFA8F552E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift in Sources */, 3CFA8F562E9087DB00201FE5 /* OSRequestSetUpdateToken.swift in Sources */, + 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */, 3CFA8F572E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift in Sources */, 3CFA8F582E9087DB00201FE5 /* OSLiveActivityRequest.swift in Sources */, 3CFA8F592E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 8ed318877..964b522e4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -365,5 +365,6 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY" #define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY" #define OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY" +#define OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY" #endif /* OneSignalCommonDefines_h */ diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift index 038a58cd2..23c7b3082 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift @@ -123,11 +123,21 @@ class ReceiveReceiptsRequestCache: RequestCache { } } +class ClickedRequestCache: RequestCache { + // Keep click event requests for up to 30 days. + static let OneMonthInSeconds = TimeInterval(60 * 60 * 24 * 30) + + init() { + super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY, ttl: ClickedRequestCache.OneMonthInSeconds) + } +} + class OSLiveActivitiesExecutor: OSPushSubscriptionObserver { // The currently tracked update and start tokens (key) and their associated request (value). THESE ARE NOT THREAD SAFE let updateTokens: UpdateRequestCache = UpdateRequestCache() let startTokens: StartRequestCache = StartRequestCache() let receiveReceipts: ReceiveReceiptsRequestCache = ReceiveReceiptsRequestCache() + let clickEvents: ClickedRequestCache = ClickedRequestCache() // The live activities request dispatch queue, serial. This synchronizes access to `updateTokens` and `startTokens`. private var requestDispatch: OSDispatchQueue @@ -193,6 +203,7 @@ class OSLiveActivitiesExecutor: OSPushSubscriptionObserver { block(self.startTokens) block(self.updateTokens) block(self.receiveReceipts) + block(self.clickEvents) } private func getCache(_ request: OSLiveActivityRequest) -> RequestCache { @@ -200,6 +211,8 @@ class OSLiveActivitiesExecutor: OSPushSubscriptionObserver { return self.updateTokens } else if request is OSLiveActivityStartTokenRequest { return self.startTokens + } else if request is OSRequestLiveActivityClicked { + return self.clickEvents } return self.receiveReceipts diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift new file mode 100644 index 000000000..df29e2548 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift @@ -0,0 +1,104 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalUser + +class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { + override var description: String { return "(OSRequestLiveActivityClicked) key:\(key) requestSuccessful:\(requestSuccessful) activityType:\(activityType) activityId:\(activityId)" } + + var key: String // UUID representing this unique click + var activityType: String + var activityId: String + var requestSuccessful: Bool + var shouldForgetWhenSuccessful: Bool = true + + func prepareForExecution() -> Bool { + guard let appId = OneSignalConfigManager.getAppId() else { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the OSRequestLiveActivityClicked due to null app ID.") + return false + } + + guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the OSRequestLiveActivityClicked due to null subscription ID.") + return false + } + + // TODO: ⚠️ What is the path, method, and parameters + // TODO: ⚠️ Need to guard for encoding activity strings if in path + // TODO: ⚠️ Timestamp since we are caching? Same for received event. + self.path = "foo/bar/\(activityId)/click" + self.parameters = [ + "app_id": appId, + "player_id": subscriptionId, + "device_type": 0, + "live_activity_id": activityId, + "live_activity_type": activityType, + "click_id": key + ] + self.method = POST + + return true + } + + func supersedes(_ existing: any OSLiveActivityRequest) -> Bool { + return false + } + + init(key: String, activityType: String, activityId: String) { + self.key = key + self.activityType = activityType + self.activityId = activityId + self.requestSuccessful = false + super.init() + } + + func encode(with coder: NSCoder) { + coder.encode(key, forKey: "key") + coder.encode(activityType, forKey: "activityType") + coder.encode(activityId, forKey: "activityId") + coder.encode(requestSuccessful, forKey: "requestSuccessful") + coder.encode(timestamp, forKey: "timestamp") + } + + required init?(coder: NSCoder) { + guard + let key = coder.decodeObject(forKey: "key") as? String, + let activityType = coder.decodeObject(forKey: "activityType") as? String, + let activityId = coder.decodeObject(forKey: "activityId") as? String, + let timestamp = coder.decodeObject(forKey: "timestamp") as? Date + else { + return nil + } + self.key = key + self.activityType = activityType + self.activityId = activityId + self.requestSuccessful = coder.decodeBool(forKey: "requestSuccessful") + super.init() + self.timestamp = timestamp + } +} From a548de5692ae2d3f3e8197d7629acf11fa2d78c3 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 6 Oct 2025 17:39:47 -0700 Subject: [PATCH 2/8] Add widget extension methods for click tracking Add extension method `onesignalWidgetURL` to View and DynamicIsland that is modeled after the corresponding `widgetURL` methods in WidgetKit. --- .../OneSignal.xcodeproj/project.pbxproj | 8 ++ .../Source/LiveActivityConstants.swift | 40 +++++++ .../Source/OSLiveActivityViewExtensions.swift | 112 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 061b546d5..c6732dd46 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -76,6 +76,8 @@ 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */; }; 3C2DB2F12DE6CB5E0006B905 /* OneSignalBadgeHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C2DB2F22DE6CB5E0006B905 /* OneSignalBadgeHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */; }; + 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; }; + 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.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 */; }; @@ -1306,6 +1308,8 @@ 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDelta.swift; sourceTree = ""; }; 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalBadgeHelpers.h; sourceTree = ""; }; 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalBadgeHelpers.m; 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 = ""; }; 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 = ""; }; @@ -2349,8 +2353,10 @@ 3CFA8F482E9087DB00201FE5 /* Requests */, 3CFA8F4B2E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift */, 3CFA8F4C2E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift */, + 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */, 3CFA8F4D2E9087DB00201FE5 /* OSLiveActivitiesExtension.swift */, 3CFA8F492E9087DB00201FE5 /* AnyCodable.swift */, + 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */, 3CFA8F4A2E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift */, ); path = Source; @@ -4439,6 +4445,8 @@ buildActionMask = 2147483647; files = ( 3CFA8F4F2E9087DB00201FE5 /* AnyCodable.swift in Sources */, + 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */, + 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */, 3CFA8F502E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift in Sources */, 3CFA8F512E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift in Sources */, 3CFA8F522E9087DB00201FE5 /* OSRequestSetStartToken.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift new file mode 100644 index 000000000..bb2517add --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift @@ -0,0 +1,40 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/// Constants used throughout the OneSignalLiveActivities module +enum LiveActivityConstants { + /// URL components for OneSignal click tracking + enum Tracking { + static let scheme = "onesignal-liveactivity" + static let host = "track" + static let clickPath = "/click" + static let clickId = "clickId" + static let activityId = "activityId" + static let activityType = "activityType" + static let redirect = "redirect" + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift new file mode 100644 index 000000000..f828e1b5f --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift @@ -0,0 +1,112 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import WidgetKit +import ActivityKit +import SwiftUI + +@available(iOS 16.1, *) +extension DynamicIsland { + /// Sets the URL that opens the corresponding app of a Live Activity when a user taps on the Live Activity. + /// Sets OneSignal activity metadata. See Important callout below on usage. + /// + /// By setting the URL with this function, it becomes the default URL for deep linking into the app + /// for each view of the Live Activity. However, if you include a + /// in the Live Activity, + /// the link takes priority over the default URL. When a person taps on the `Link`, it takes them to the + /// place in the app that corresponds to the URL of the `Link`. + /// + /// - Parameters: + /// - url: The URL that opens the app. + /// - context: The activity view context. + /// + /// - Returns: The configuration object for the Dynamic Island with the specified URL. + /// + /// > Important: Use instead of`.widgetURL`. Requires handling from your app's URL handling code + /// (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI) using the + /// `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method. + public func onesignalWidgetURL( + _ url: URL?, + context: ActivityViewContext + ) -> DynamicIsland { + return self.widgetURL(generateTrackingDeepLink(originalURL: url, context: context)) + } +} + +@available(iOS 16.1, *) +extension View { + /// Sets the URL to open in the containing app when the user clicks the widget. + /// Sets OneSignal activity metadata. See Important callout below on usage. + /// + /// - Parameters: + /// - url: The URL to open in the containing app. + /// - context: The activity view context. + /// - Returns: A view that opens the specified URL when the user clicks + /// the widget. + /// + /// Widgets support one `onesignalWidgetURL` modifier in their view hierarchy. + /// If multiple views have `onesignalWidgetURL` modifiers, the behavior is undefined. + /// + /// > Important: Use instead of`.widgetURL`. Requires handling from your app's URL handling code + /// (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI) using the + /// `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method. + @MainActor @preconcurrency public func onesignalWidgetURL(_ url: URL?, context: ActivityViewContext) -> some View { + return self.widgetURL(generateTrackingDeepLink(originalURL: url, context: context)) + } +} + +// MARK: - Helper Function + +@available(iOS 16.1, *) +private func generateTrackingDeepLink(originalURL: URL?, context: ActivityViewContext) -> URL? { + // Generate a unique click ID + let clickId = UUID().uuidString + + // Get activity metadata + let activityId = context.attributes.onesignal.activityId + let activityType = String(describing: T.self) + + // Build OneSignal tracking URL + var components = URLComponents() + components.scheme = LiveActivityConstants.Tracking.scheme + components.host = LiveActivityConstants.Tracking.host + components.path = LiveActivityConstants.Tracking.clickPath + + var queryItems = [ + URLQueryItem(name: LiveActivityConstants.Tracking.clickId, value: clickId), + URLQueryItem(name: LiveActivityConstants.Tracking.activityId, value: activityId), + URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType) + ] + + if let originalURL = originalURL { + queryItems.append(URLQueryItem(name: LiveActivityConstants.Tracking.redirect, value: originalURL.absoluteString)) + } + + components.queryItems = queryItems + + return components.url +} From ac0684a11b1a96830185a91b3060b58f78ff5ec4 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 7 Oct 2025 13:53:32 -0700 Subject: [PATCH 3/8] Add trackClickAndReturnOriginal method Developers call the `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method to provide OneSignal the click tracking metadata. This method returns the intended original URL, with which they use to navigate their users. --- .../OneSignalLiveActivitiesManagerImpl.swift | 48 +++++++++++++++++++ .../Source/OSLiveActivities.swift | 18 +++++-- .../Source/OSStubLiveActivities.swift | 4 ++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift index d1beb7ed8..86ab9dded 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift @@ -175,6 +175,54 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities { } } + @objc + public static func trackClickAndReturnOriginal(_ url: URL) -> URL? { + // Check if this is a OneSignal click tracking URL + guard url.scheme == LiveActivityConstants.Tracking.scheme, + url.host == LiveActivityConstants.Tracking.host, + url.path == LiveActivityConstants.Tracking.clickPath, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let queryItems = components.queryItems else + { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "trackClickAndReturnOriginal:\(url) is not a tracking URL") + return url + } + + /// Helper function to extract redirect URL + func getRedirectURL() -> URL? { + guard let redirectString = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.redirect })?.value, + let redirectURL = URL(string: redirectString) + else { + return nil + } + return redirectURL + } + + // Extract metadata + guard let clickId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.clickId })?.value, + let activityId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.activityId })?.value, + let activityType = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.activityType })?.value else + { + OneSignalLog.onesignalLog(.LL_ERROR, message: "Missing required parameters in tracking URL: \(url)") + return getRedirectURL() + } + + trackClick(clickId: clickId, activityType: activityType, activityId: activityId) + + return getRedirectURL() + } + + /** + Track the click event. + - Parameters: + - clickId: UUID representing the unique click event, as it is possible for this click to be tracked multiple times. + */ + private static func trackClick(clickId: String, activityType: String, activityId: String) { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.LiveActivities trackClick called with clickId: \(clickId), activityType: \(activityType), activityId: \(activityId)") + let req = OSRequestLiveActivityClicked(key: clickId, activityType: activityType, activityId: activityId) + _executor.append(req) + } + @available(iOS 17.2, *) private static func listenForPushToStart(_ activityType: Attributes.Type, options: LiveActivitySetupOptions? = nil) { if options == nil || options!.enablePushToStart { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSLiveActivities.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSLiveActivities.swift index e35af78fd..9491cc474 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSLiveActivities.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSLiveActivities.swift @@ -34,7 +34,7 @@ import OneSignalCore public protocol OSLiveActivities { /** Indicate this device has entered a live activity, identified within OneSignal by the `activityId`. - - Parameters + - Parameters: - activityId: The activity identifier the live activity on this device will receive updates for. - withToken: The live activity's update token to receive the updates. */ @@ -43,7 +43,7 @@ public protocol OSLiveActivities { /** Indicate this device has entered a live activity, identified within OneSignal by the `activityId`. This method is deprecated since the request to enter a live activity will always succeed. - - Parameters + - Parameters: - activityId: The activity identifier the live activity on this device will receive updates for. - withToken: The live activity's update token to receive the updates. - withSuccess: A success callback that will be called when the live activity enter request has been queued. @@ -54,7 +54,7 @@ public protocol OSLiveActivities { /** Indicate this device has exited a live activity, identified within OneSignal by the `activityId`. - - Parameters + - Parameters: - activityId: The activity identifier the live activity on this device will no longer receive updates for. */ static func exit(_ activityId: String) @@ -62,11 +62,21 @@ public protocol OSLiveActivities { /** Indicate this device has exited a live activity, identified within OneSignal by the `activityId`. This method is deprecated since the request to enter a live activity will always succeed. - - Parameters + - Parameters: - activityId: The activity identifier the live activity on this device will no longer receive updates for. - withSuccess: A success callback that will be called when the live activity exit request has been queued. - withFailure: A failure callback that will be called when the live activity enter exit was not successfully queued. */ @available(*, deprecated) static func exit(_ activityId: String, withSuccess: OSResultSuccessBlock?, withFailure: OSFailureBlock?) + + /** + Use in conjunction with the `onesignalWidgetURL` modifier. Handle a URL opened in the app to track Live Activity clicks. Call this method from your app's URL handling code + (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI). + + - Parameters: + - url: The URL that was opened, which may be a OneSignal Live Activity click tracking URL. + - Returns: The intended original nullable URL. + */ + static func trackClickAndReturnOriginal(_ url: URL) -> URL? } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift index ea91df7f9..761875502 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift @@ -31,4 +31,8 @@ public class OSStubLiveActivities: NSObject, OSLiveActivities { OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") } + public static func trackClickAndReturnOriginal(_ url: URL) -> URL? { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + return url + } } From 57a14ae68f7318042c151839b8eb5435b371e89b Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 6 Oct 2025 17:41:40 -0700 Subject: [PATCH 4/8] Dev app updates to use new functionality --- iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m | 10 ++++++++++ .../OneSignalWidgetExtensionLiveActivity.swift | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index 3e96aad1e..635b87d19 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -46,6 +46,16 @@ @implementation AppDelegate OneSignalNotificationCenterDelegate *_notificationDelegate; +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + // Log the full tracking URL and the original extracted URL + // Also trigger trackClickAndReturnOriginal twice to confirm this click event is only sent once + NSLog(@"Dev App: application openURL FULL URL is %@", url); + NSURL *originalURL1 = [OneSignal.LiveActivities trackClickAndReturnOriginal:url]; + NSURL *originalURL2 = [OneSignal.LiveActivities trackClickAndReturnOriginal:url]; + NSLog(@"Dev App: application openURL processed, original URL is %@", originalURL1); + return YES; +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // [FIRApp configure]; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift index 24e60b8e1..0f9b772f0 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift +++ b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift @@ -52,9 +52,11 @@ import OneSignalLiveActivities } Spacer() } + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + // .widgetURL(URL(string: "myapp://product/12345")) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) - } dynamicIsland: { _ in + } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through // various regions, like leading/trailing/center/bottom @@ -75,7 +77,8 @@ import OneSignalLiveActivities } minimal: { Text("Min") } - .widgetURL(URL(string: "http://www.apple.com")) + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + // .widgetURL(URL(string: "myapp://product/12345")) .keylineTint(Color.red) } } From 94403e2fb8148c13ab29dcd6895c6567e9667287 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 12 Nov 2025 15:55:44 -0800 Subject: [PATCH 5/8] live activity: add notification ID to click event notification ID may be nullable if the live activity is started in-app --- .../Source/LiveActivityConstants.swift | 1 + .../Source/OSLiveActivityViewExtensions.swift | 4 +++- .../Source/OneSignalLiveActivitiesManagerImpl.swift | 8 +++++--- .../Source/Requests/OSRequestLiveActivityClicked.swift | 6 +++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift index bb2517add..0d57ff67f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift @@ -35,6 +35,7 @@ enum LiveActivityConstants { static let clickId = "clickId" static let activityId = "activityId" static let activityType = "activityType" + static let notificationId = "notificationId" static let redirect = "redirect" } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift index f828e1b5f..df503aeff 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift @@ -89,6 +89,7 @@ private func generateTrackingDeepLink(origin // Get activity metadata let activityId = context.attributes.onesignal.activityId let activityType = String(describing: T.self) + let notificationId = context.state.onesignal?.notificationId // Build OneSignal tracking URL var components = URLComponents() @@ -99,7 +100,8 @@ private func generateTrackingDeepLink(origin var queryItems = [ URLQueryItem(name: LiveActivityConstants.Tracking.clickId, value: clickId), URLQueryItem(name: LiveActivityConstants.Tracking.activityId, value: activityId), - URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType) + URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType), + URLQueryItem(name: LiveActivityConstants.Tracking.notificationId, value: notificationId) ] if let originalURL = originalURL { diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift index 86ab9dded..64ee3d0d1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift @@ -207,7 +207,9 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities { return getRedirectURL() } - trackClick(clickId: clickId, activityType: activityType, activityId: activityId) + let notificationId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.notificationId })?.value + + trackClick(clickId: clickId, activityType: activityType, activityId: activityId, notificationId: notificationId) return getRedirectURL() } @@ -217,9 +219,9 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities { - Parameters: - clickId: UUID representing the unique click event, as it is possible for this click to be tracked multiple times. */ - private static func trackClick(clickId: String, activityType: String, activityId: String) { + private static func trackClick(clickId: String, activityType: String, activityId: String, notificationId: String?) { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.LiveActivities trackClick called with clickId: \(clickId), activityType: \(activityType), activityId: \(activityId)") - let req = OSRequestLiveActivityClicked(key: clickId, activityType: activityType, activityId: activityId) + let req = OSRequestLiveActivityClicked(key: clickId, activityType: activityType, activityId: activityId, notificationId: notificationId) _executor.append(req) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift index df29e2548..d14c965c7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift @@ -34,6 +34,7 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { var key: String // UUID representing this unique click var activityType: String var activityId: String + var notificationId: String? var requestSuccessful: Bool var shouldForgetWhenSuccessful: Bool = true @@ -69,10 +70,11 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { return false } - init(key: String, activityType: String, activityId: String) { + init(key: String, activityType: String, activityId: String, notificationId: String?) { self.key = key self.activityType = activityType self.activityId = activityId + self.notificationId = notificationId self.requestSuccessful = false super.init() } @@ -81,6 +83,7 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { coder.encode(key, forKey: "key") coder.encode(activityType, forKey: "activityType") coder.encode(activityId, forKey: "activityId") + coder.encode(notificationId, forKey: "notificationId") coder.encode(requestSuccessful, forKey: "requestSuccessful") coder.encode(timestamp, forKey: "timestamp") } @@ -97,6 +100,7 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { self.key = key self.activityType = activityType self.activityId = activityId + self.notificationId = coder.decodeObject(forKey: "notificationId") as? String self.requestSuccessful = coder.decodeBool(forKey: "requestSuccessful") super.init() self.timestamp = timestamp From be40b40b5cd8ec9c1e3961055ac8371ed0d83257 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 13 Nov 2025 09:34:46 -0800 Subject: [PATCH 6/8] update click event request path and params --- .../OSRequestLiveActivityClicked.swift | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift index d14c965c7..18bd9604f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift @@ -49,19 +49,21 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest { return false } - // TODO: ⚠️ What is the path, method, and parameters - // TODO: ⚠️ Need to guard for encoding activity strings if in path - // TODO: ⚠️ Timestamp since we are caching? Same for received event. - self.path = "foo/bar/\(activityId)/click" - self.parameters = [ - "app_id": appId, - "player_id": subscriptionId, + guard let activityType = self.key.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlUserAllowed) else { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot translate activity type to url encoded string.") + return false + } + + var params: [String: Any] = [ "device_type": 0, - "live_activity_id": activityId, - "live_activity_type": activityType, - "click_id": key + "activity_id": activityId ] - self.method = POST + if let notificationId = notificationId { + params["notification_id"] = notificationId + } + self.parameters = params + self.path = "apps/\(appId)/activities/clicks/track/\(activityType)/subscriptions/\(subscriptionId)" + self.method = PUT return true } From e3a8df0a1b1ba1499be063884352a8041c8d9566 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 16 Dec 2025 13:56:43 -0800 Subject: [PATCH 7/8] update example app to track clicks * Use OneSignal-aware activity in the demo app * Add onesignalWidgetURL to all OneSignal-aware activities in the demo app --- iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist | 11 +++++++++++ .../OneSignalDevApp/OneSignalDevApp/ViewController.m | 8 ++++++-- .../OneSignalWidgetExtensionLiveActivity.swift | 8 ++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist index 7ac3fdb50..1bd38fe74 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist @@ -24,6 +24,17 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleURLName + com.onesignal.example + CFBundleURLSchemes + + myapp + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m index 0034b13d2..642caeb79 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m @@ -242,8 +242,12 @@ - (IBAction)startAndEnterLiveActivity:(id)sender { NSString *activityId = [self.activityId text]; // Will not make a live activity if activityId is empty if (activityId && activityId.length) { -// [LiveActivityController createDefaultActivityWithActivityId:activityId ]; - [LiveActivityController createActivityWithActivityId:activityId completionHandler:^(void) {} ]; + // 1. Create a Default activity + // [LiveActivityController createDefaultActivityWithActivityId:activityId ]; + // 2. Create non-OneSignal-aware activity + // [LiveActivityController createActivityWithActivityId:activityId completionHandler:^(void) {} ]; + // 3. Create OneSignal-aware activity + [LiveActivityController createOneSignalAwareActivityWithActivityId:activityId]; } } else { NSLog(@"Must use iOS 13 or later for swift concurrency which is required for [LiveActivityController createActivityWithCompletionHandler..."); diff --git a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift index 0f9b772f0..aa3e92a01 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift +++ b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift @@ -121,7 +121,8 @@ import OneSignalLiveActivities .padding([.all], 20) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) - } dynamicIsland: { _ in + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through // various regions, like leading/trailing/center/bottom @@ -143,6 +144,7 @@ import OneSignalLiveActivities Text("Min") } .keylineTint(Color.red) + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) } } } @@ -236,7 +238,8 @@ struct DefaultOneSignalLiveActivityWidget: Widget { .padding([.all], 20) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) - } dynamicIsland: { _ in + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through // various regions, like leading/trailing/center/bottom @@ -258,6 +261,7 @@ struct DefaultOneSignalLiveActivityWidget: Widget { Text("Min") } .keylineTint(Color.red) + .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) } } } From 029fe4941cd32ebecac9fb5d2070b63098e3bf58 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 8 Jan 2026 14:27:43 -0800 Subject: [PATCH 8/8] Add tests for live activity click events * Add tests for live activity click events * Refactor generateTrackingDeepLink helper method to be testable, and move to enum namespace (internal by default) * Update the URL in the example app to be more complex --- ...OneSignalWidgetExtensionLiveActivity.swift | 12 +- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../Source/OSLiveActivityViewExtensions.swift | 93 +++++++---- .../LiveActivitiesManagerTests.swift | 154 ++++++++++++++++++ .../OSLiveActivitiesExecutorTests.swift | 32 +++- 5 files changed, 257 insertions(+), 38 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/LiveActivitiesManagerTests.swift diff --git a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift index aa3e92a01..991048c14 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift +++ b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift @@ -52,7 +52,7 @@ import OneSignalLiveActivities } Spacer() } - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) // .widgetURL(URL(string: "myapp://product/12345")) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) @@ -77,7 +77,7 @@ import OneSignalLiveActivities } minimal: { Text("Min") } - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) // .widgetURL(URL(string: "myapp://product/12345")) .keylineTint(Color.red) } @@ -121,7 +121,7 @@ import OneSignalLiveActivities .padding([.all], 20) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through @@ -144,7 +144,7 @@ import OneSignalLiveActivities Text("Min") } .keylineTint(Color.red) - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) } } } @@ -238,7 +238,7 @@ struct DefaultOneSignalLiveActivityWidget: Widget { .padding([.all], 20) .activitySystemActionForegroundColor(.black) .activityBackgroundTint(.white) - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through @@ -261,7 +261,7 @@ struct DefaultOneSignalLiveActivityWidget: Widget { Text("Min") } .keylineTint(Color.red) - .onesignalWidgetURL(URL(string: "myapp://product/12345"), context: context) + .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context) } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index c6732dd46..0e1929681 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ 3C6299A72BEEA41900649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A62BEEA40100649187 /* PrivacyInfo.xcprivacy */; }; 3C6299A92BEEA46C00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */; }; 3C6299AB2BEEA4C000649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */; }; + 3C64C3322F1066D700693230 /* LiveActivitiesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */; }; 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */; }; 3C7021E32ECF0821001768C6 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E2400381D4FFC31008BDE70 /* OneSignalFramework.framework */; }; 3C7021E42ECF0821001768C6 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E2400381D4FFC31008BDE70 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1326,6 +1327,7 @@ 3C6299A62BEEA40100649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitiesManagerTests.swift; sourceTree = ""; }; 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = ""; }; 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalInAppMessagesTests-Bridging-Header.h"; sourceTree = ""; }; 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAMIntegrationTests.swift; sourceTree = ""; }; @@ -2434,6 +2436,7 @@ isa = PBXGroup; children = ( 4735424C2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift */, + 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */, 47278E462BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift */, ); path = OneSignalLiveActivitiesTests; @@ -4436,6 +4439,7 @@ buildActionMask = 2147483647; files = ( 4735424D2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift in Sources */, + 3C64C3322F1066D700693230 /* LiveActivitiesManagerTests.swift in Sources */, 47278E472BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift index df503aeff..7cbf99707 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift @@ -53,7 +53,7 @@ extension DynamicIsland { _ url: URL?, context: ActivityViewContext ) -> DynamicIsland { - return self.widgetURL(generateTrackingDeepLink(originalURL: url, context: context)) + return self.widgetURL(LiveActivityTrackingUtils.generateTrackingDeepLink(originalURL: url, context: context)) } } @@ -75,40 +75,71 @@ extension View { /// (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI) using the /// `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method. @MainActor @preconcurrency public func onesignalWidgetURL(_ url: URL?, context: ActivityViewContext) -> some View { - return self.widgetURL(generateTrackingDeepLink(originalURL: url, context: context)) + return self.widgetURL(LiveActivityTrackingUtils.generateTrackingDeepLink(originalURL: url, context: context)) } } -// MARK: - Helper Function +// MARK: - Tracking Utilities -@available(iOS 16.1, *) -private func generateTrackingDeepLink(originalURL: URL?, context: ActivityViewContext) -> URL? { - // Generate a unique click ID - let clickId = UUID().uuidString - - // Get activity metadata - let activityId = context.attributes.onesignal.activityId - let activityType = String(describing: T.self) - let notificationId = context.state.onesignal?.notificationId - - // Build OneSignal tracking URL - var components = URLComponents() - components.scheme = LiveActivityConstants.Tracking.scheme - components.host = LiveActivityConstants.Tracking.host - components.path = LiveActivityConstants.Tracking.clickPath - - var queryItems = [ - URLQueryItem(name: LiveActivityConstants.Tracking.clickId, value: clickId), - URLQueryItem(name: LiveActivityConstants.Tracking.activityId, value: activityId), - URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType), - URLQueryItem(name: LiveActivityConstants.Tracking.notificationId, value: notificationId) - ] - - if let originalURL = originalURL { - queryItems.append(URLQueryItem(name: LiveActivityConstants.Tracking.redirect, value: originalURL.absoluteString)) +/// Utilities for building and managing Live Activity tracking URLs +enum LiveActivityTrackingUtils { + /// Generates a tracking deep link from an original URL and activity context + /// - Parameters: + /// - originalURL: The original URL to track clicks for + /// - context: The activity view context containing metadata + /// - Returns: The tracking URL, or nil if construction failed + @available(iOS 16.1, *) + static func generateTrackingDeepLink(originalURL: URL?, context: ActivityViewContext) -> URL? { + // Get activity metadata from context + let activityId = context.attributes.onesignal.activityId + let activityType = String(describing: T.self) + let notificationId = context.state.onesignal?.notificationId + + return buildTrackingURL( + originalURL: originalURL, + activityId: activityId, + activityType: activityType, + notificationId: notificationId + ) } - components.queryItems = queryItems - - return components.url + /// Builds a tracking URL that wraps the original URL with OneSignal tracking parameters + /// - Parameters: + /// - originalURL: The original URL to track clicks for + /// - activityId: The activity identifier + /// - activityType: The activity type name + /// - notificationId: Optional notification ID + /// - Returns: The tracking URL, or nil if construction failed + static func buildTrackingURL( + originalURL: URL?, + activityId: String, + activityType: String, + notificationId: String? + ) -> URL? { + // Generate a unique click ID + let clickId = UUID().uuidString + + // Build OneSignal tracking URL + var components = URLComponents() + components.scheme = LiveActivityConstants.Tracking.scheme + components.host = LiveActivityConstants.Tracking.host + components.path = LiveActivityConstants.Tracking.clickPath + + var queryItems = [ + URLQueryItem(name: LiveActivityConstants.Tracking.clickId, value: clickId), + URLQueryItem(name: LiveActivityConstants.Tracking.activityId, value: activityId), + URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType), + URLQueryItem(name: LiveActivityConstants.Tracking.notificationId, value: notificationId) + ] + + if let originalURL = originalURL { + // URLQueryItem automatically percent-encodes the value when URLComponents constructs the URL + // This ensures special characters like &, #, ?, etc. in the redirect URL are properly encoded + queryItems.append(URLQueryItem(name: LiveActivityConstants.Tracking.redirect, value: originalURL.absoluteString)) + } + + components.queryItems = queryItems + + return components.url + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/LiveActivitiesManagerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/LiveActivitiesManagerTests.swift new file mode 100644 index 000000000..298334194 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/LiveActivitiesManagerTests.swift @@ -0,0 +1,154 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +@testable import OneSignalLiveActivities + +final class LiveActivitiesManagerTests: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + // MARK: - Helper Methods + + private func createTrackingURL( + from clientURL: URL?, + activityId: String = "test-activity-id", + activityType: String = "TestActivityType", + notificationId: String? = "test-notification-id" + ) -> URL? { + return LiveActivityTrackingUtils.buildTrackingURL( + originalURL: clientURL, + activityId: activityId, + activityType: activityType, + notificationId: notificationId + ) + } + + // MARK: - Tests + + func testTrackClickAndReturnOriginal_nonTrackingURL_returnsOriginalURL() throws { + /* Setup */ + let originalURL = URL(string: "https://example.com/path")! + + /* Then */ + XCTAssertEqual(OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(originalURL), originalURL) + } + + func testTrackClickAndReturnOriginal_validTrackingURLWithAllParameters_tracksClickAndReturnsRedirectURL() throws { + /* Setup */ + let originalURL = URL(string: "https://example.com/destination")! + let trackingURL = createTrackingURL(from: originalURL) + XCTAssertNotNil(trackingURL) + + /* Verify tracking URL structure */ + let trackingURLString = trackingURL!.absoluteString + XCTAssertTrue(trackingURLString.starts(with: "onesignal-liveactivity://track/click?")) + XCTAssertTrue(trackingURLString.contains("clickId=")) + XCTAssertTrue(trackingURLString.contains("activityId=test-activity-id")) + XCTAssertTrue(trackingURLString.contains("activityType=TestActivityType")) + XCTAssertTrue(trackingURLString.contains("notificationId=test-notification-id")) + XCTAssertTrue(trackingURLString.contains("redirect=https://example.com/destination")) + + XCTAssertEqual(trackingURL!.scheme, "onesignal-liveactivity") + XCTAssertEqual(trackingURL!.host, "track") + XCTAssertEqual(trackingURL!.path, "/click") + + let components = URLComponents(url: trackingURL!, resolvingAgainstBaseURL: false) + let queryItems = components?.queryItems ?? [] + + XCTAssertNotNil(queryItems.first(where: { $0.name == "clickId" })?.value) + XCTAssertEqual(queryItems.first(where: { $0.name == "activityId" })?.value, "test-activity-id") + XCTAssertEqual(queryItems.first(where: { $0.name == "activityType" })?.value, "TestActivityType") + XCTAssertEqual(queryItems.first(where: { $0.name == "notificationId" })?.value, "test-notification-id") + XCTAssertEqual(queryItems.first(where: { $0.name == "redirect" })?.value, "https://example.com/destination") + + /* Then */ + XCTAssertEqual(OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(trackingURL!), originalURL) + } + + func testTrackClickAndReturnOriginal_validTrackingURLWithoutNotificationId_tracksClickAndReturnsRedirectURL() throws { + /* Setup */ + let clientURL = URL(string: "https://example.com/destination")! + let trackingURL = createTrackingURL(from: clientURL, notificationId: nil) + XCTAssertNotNil(trackingURL) + + /* Then */ + XCTAssertEqual(OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(trackingURL!), clientURL) + } + + func testTrackClickAndReturnOriginal_trackingURLMissingRequiredParameters_returnsRedirectURLWithoutTracking() throws { + /* Setup */ + let redirectURL = "https://example.com/destination" + let trackingURLString = "onesignal-liveactivity://track/click?activityId=test-activity-id&redirect=\(redirectURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)" + let trackingURL = URL(string: trackingURLString)! + + /* When */ + let result = OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(trackingURL) + + /* Then */ + XCTAssertEqual(result!.absoluteString, redirectURL) + } + + func testTrackClickAndReturnOriginal_trackingURLWithNoRedirectParameter_returnsNil() throws { + /* Setup */ + let trackingURL = createTrackingURL(from: nil) + XCTAssertNotNil(trackingURL) + + /* Then */ + XCTAssertNil(OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(trackingURL!)) + } + + func testTrackClickAndReturnOriginal_malformedTrackingURL_returnsOriginalURL() throws { + /* Setup */ + let malformedURL = URL(string: "liveactivity://foo/wrong-path?clickId=test-click-id")! + + /* Then */ + XCTAssertEqual(OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(malformedURL), malformedURL) + } + + func testTrackClickAndReturnOriginal_complexURLWithQueryParamsAndFragment_preservesAllComponents() throws { + /* Setup */ + let clientURL = URL(string: "https://example.com/page?param1=value1¶m2=value2#section")! + let trackingURL = createTrackingURL(from: clientURL) + XCTAssertNotNil(trackingURL) + + /* When */ + let result = OneSignalLiveActivitiesManagerImpl.trackClickAndReturnOriginal(trackingURL!) + + /* Then */ + XCTAssertEqual(result!, clientURL) + XCTAssertEqual(result!.scheme, "https") + XCTAssertEqual(result!.host, "example.com") + XCTAssertEqual(result!.path, "/page") + XCTAssertEqual(result!.query, "param1=value1¶m2=value2") + XCTAssertEqual(result!.fragment, "section") + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift index 635a515f5..5bc7fa84c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift @@ -177,6 +177,31 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { XCTAssertTrue(mockClient.executedRequests[0] == request) } + func testClickEventWithSuccessfulRequest() throws { + /* Setup */ + let mockDispatchQueue = MockDispatchQueue() + let mockClient = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(mockClient) + OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") + OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) + mockClient.reset() + + let request = OSRequestLiveActivityClicked(key: "unique-click-id", activityType: "my-activity-type", activityId: "my-activity-id", notificationId: "my-notif-id") + mockClient.setMockResponseForRequest(request: String(describing: request), response: [String: Any]()) + + /* When */ + let executor = OSLiveActivitiesExecutor(requestDispatch: mockDispatchQueue) + executor.append(request) + mockDispatchQueue.waitForDispatches(2) + + /* Then */ + XCTAssertEqual(executor.clickEvents.items.count, 0) + XCTAssertEqual(mockClient.executedRequests.count, 1) + XCTAssertTrue(mockClient.executedRequests[0] == request) + } + func testRequestWillNotExecuteWhenNoSubscription() throws { /* Setup */ let mockDispatchQueue = MockDispatchQueue() @@ -261,13 +286,15 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { let setUpdateToken = OSRequestSetUpdateToken(key: "key-setUpdateToken", token: "my-token") let removeUpdateToken = OSRequestRemoveUpdateToken(key: "key-removeUpdateToken") let receiveReceipt = OSRequestLiveActivityReceiveReceipts(key: "key-receiveReceipt", activityType: "my-activity-type", activityId: "my-activity-id") + let clickEvent = OSRequestLiveActivityClicked(key: "key-clickEvent", activityType: "my-activity-type", activityId: "my-activity-id", notificationId: "notification-id") executor1.append(setStartToken) executor1.append(removeStartToken) executor1.append(setUpdateToken) executor1.append(removeUpdateToken) executor1.append(receiveReceipt) - mockDispatchQueue.waitForDispatches(5) + executor1.append(clickEvent) + mockDispatchQueue.waitForDispatches(6) // create a new executor which will uncache requests let executor2 = OSLiveActivitiesExecutor(requestDispatch: MockDispatchQueue()) @@ -283,6 +310,9 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { XCTAssertEqual(executor2.receiveReceipts.items.count, 1) XCTAssertTrue(executor2.receiveReceipts.items["key-receiveReceipt"] is OSRequestLiveActivityReceiveReceipts) + + XCTAssertEqual(executor2.clickEvents.items.count, 1) + XCTAssertTrue(executor2.clickEvents.items["key-clickEvent"] is OSRequestLiveActivityClicked) } func testSetStartRequestNotExecutedWithSameActivityTypeAndToken() throws {