diff --git a/ios/AirshipFrameworkProxy/AirshipDelegate.swift b/ios/AirshipFrameworkProxy/AirshipDelegate.swift index 2a1c276..5396ea0 100644 --- a/ios/AirshipFrameworkProxy/AirshipDelegate.swift +++ b/ios/AirshipFrameworkProxy/AirshipDelegate.swift @@ -97,7 +97,8 @@ extension AirshipDelegate: PushNotificationDelegate { func receivedNotificationResponse(_ notificationResponse: UNNotificationResponse) async { LaunchDeepLinkTracker.shared.onNotificationResponse( userInfo: notificationResponse.notification.request.content.userInfo, - isDefaultAction: notificationResponse.actionIdentifier == UNNotificationDefaultActionIdentifier + isDefaultAction: notificationResponse.actionIdentifier == UNNotificationDefaultActionIdentifier, + isAppForegrounded: AppStateTracker.shared.isForegrounded ) do { diff --git a/ios/AirshipFrameworkProxy/LaunchDeepLinkTracker.swift b/ios/AirshipFrameworkProxy/LaunchDeepLinkTracker.swift index b66ec47..3944eed 100644 --- a/ios/AirshipFrameworkProxy/LaunchDeepLinkTracker.swift +++ b/ios/AirshipFrameworkProxy/LaunchDeepLinkTracker.swift @@ -32,17 +32,21 @@ final class LaunchDeepLinkTracker { /// Called from the notification response handler with the tapped /// notification's payload. A default-action tap resolves the launch, - /// stashing the payload's deep link if present. + /// stashing the payload's deep link when the app was not already + /// foregrounded. func onNotificationResponse( userInfo: [AnyHashable: Any], - isDefaultAction: Bool + isDefaultAction: Bool, + isAppForegrounded: Bool ) { guard isDefaultAction else { return } - let deepLink = Self.deepLinkActionKeys - .compactMap { userInfo[$0] as? String } - .first - if let deepLink { - stash = (deepLink, dateProvider()) + if !isAppForegrounded { + let deepLink = Self.deepLinkActionKeys + .compactMap { userInfo[$0] as? String } + .first + if let deepLink { + stash = (deepLink, dateProvider()) + } } launchResolved = true resolveWaiters() diff --git a/ios/AirshipFrameworkProxy/Proxies/AirshipProxy.swift b/ios/AirshipFrameworkProxy/Proxies/AirshipProxy.swift index 8700c6a..a910510 100644 --- a/ios/AirshipFrameworkProxy/Proxies/AirshipProxy.swift +++ b/ios/AirshipFrameworkProxy/Proxies/AirshipProxy.swift @@ -151,6 +151,13 @@ public final class AirshipProxy: Sendable { return Airship.isFlying } + /// Returns the deep link that launched the app from a notification tap, + /// or nil if the app was not launched by a notification with a deep + /// link. One-shot: the value is consumed on read. + public func getLaunchDeepLink() async -> String? { + return await LaunchDeepLinkTracker.shared.takeLaunchDeepLink() + } + private static func ensureAirshipReady() throws { guard Airship.isFlying else { AirshipLogger.warn("Airship not ready. Ensure takeOff has completed before using proxy APIs.") @@ -221,6 +228,15 @@ public final class AirshipProxy: Sendable { } } + if AppStateTracker.shared.isForegrounded { + LaunchDeepLinkTracker.shared.onLaunchResolved() + } else { + Task { @MainActor in + await AppStateTracker.shared.waitForActive() + LaunchDeepLinkTracker.shared.onLaunchResolved() + } + } + Airship.push.defaultPresentationOptions = self.proxyStore.foregroundPresentationOptions if let categories = self.loadCategories() { diff --git a/ios/AirshipFrameworkProxyTests/LaunchDeepLinkTrackerTest.swift b/ios/AirshipFrameworkProxyTests/LaunchDeepLinkTrackerTest.swift index 2f683a1..6be8640 100644 --- a/ios/AirshipFrameworkProxyTests/LaunchDeepLinkTrackerTest.swift +++ b/ios/AirshipFrameworkProxyTests/LaunchDeepLinkTrackerTest.swift @@ -10,7 +10,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker() tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) let result = await tracker.takeLaunchDeepLink() XCTAssertEqual(result, "myapp://home") @@ -20,7 +21,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker() tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) _ = await tracker.takeLaunchDeepLink() let second = await tracker.takeLaunchDeepLink() @@ -31,15 +33,31 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker() tracker.onNotificationResponse( userInfo: ["deep_link_action": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) let result = await tracker.takeLaunchDeepLink() XCTAssertEqual(result, "myapp://home") } + func testForegroundTapSkipsStash() async { + let tracker = LaunchDeepLinkTracker() + tracker.onNotificationResponse( + userInfo: ["^d": "myapp://home"], + isDefaultAction: true, + isAppForegrounded: true + ) + let result = await tracker.takeLaunchDeepLink() + XCTAssertNil(result) + } + func testTapWithoutDeepLinkResolvesNil() async { let tracker = LaunchDeepLinkTracker() - tracker.onNotificationResponse(userInfo: [:], isDefaultAction: true) + tracker.onNotificationResponse( + userInfo: [:], + isDefaultAction: true, + isAppForegrounded: false + ) let result = await tracker.takeLaunchDeepLink() XCTAssertNil(result) } @@ -48,7 +66,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker() tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: false + isDefaultAction: false, + isAppForegrounded: false ) tracker.onLaunchResolved() let result = await tracker.takeLaunchDeepLink() @@ -61,7 +80,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { await Task.yield() tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) let result = await task.value XCTAssertEqual(result, "myapp://home") @@ -88,7 +108,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker(dateProvider: { now }) tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) now = now.addingTimeInterval(11.0) let result = await tracker.takeLaunchDeepLink() @@ -100,7 +121,8 @@ final class LaunchDeepLinkTrackerTest: XCTestCase { let tracker = LaunchDeepLinkTracker(dateProvider: { now }) tracker.onNotificationResponse( userInfo: ["^d": "myapp://home"], - isDefaultAction: true + isDefaultAction: true, + isAppForegrounded: false ) now = now.addingTimeInterval(9.0) let result = await tracker.takeLaunchDeepLink()