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) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 2b3289e0a..5ab6c84dc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,12 +69,15 @@ 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 */; }; 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 */; }; @@ -1251,6 +1254,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 = ""; }; @@ -1259,6 +1263,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 = ""; }; @@ -2258,6 +2264,7 @@ 3CFA8F472E9087DB00201FE5 /* OSRequestSetUpdateToken.swift */, 3CFA8F452E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift */, 3CFA8F5A2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift */, + 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */, ); path = Requests; sourceTree = ""; @@ -2269,8 +2276,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; @@ -4304,6 +4313,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 */, @@ -4311,6 +4322,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 db1841fd9..4b496dfad 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/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 +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift index bc59449fa..6be4bfe93 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/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 + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityReceiveReceipts.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityReceiveReceipts.swift index 6bae8f371..027acf49a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityReceiveReceipts.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityReceiveReceipts.swift @@ -54,7 +54,8 @@ class OSRequestLiveActivityReceiveReceipts: OneSignalRequest, OSLiveActivityRequ "player_id": subscriptionId, "device_type": 0, "live_activity_id": activityId, - "live_activity_type": activityType + "live_activity_type": activityType, + "timestamp": ISO8601DateFormatter().string(from: timestamp) ] self.method = PUT 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 + } }