diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift
index 8bf183dafd..272e4ea794 100644
--- a/Core/FeatureFlag.swift
+++ b/Core/FeatureFlag.swift
@@ -47,6 +47,7 @@ public enum FeatureFlag: String {
case autcompleteTabs
case textZoom
case adAttributionReporting
+ case handoff
/// https://app.asana.com/0/72649045549333/1208231259093710/f
case networkProtectionUserTips
@@ -127,6 +128,8 @@ extension FeatureFlag: FeatureFlagDescribing {
return .remoteReleasable(.feature(.autofillSurveys))
case .autcompleteTabs:
return .remoteReleasable(.feature(.autocompleteTabs))
+ case .handoff:
+ return .internalOnly()
case .networkProtectionUserTips:
return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips))
case .textZoom:
diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift
index 0ec3c93bf1..da568f09af 100644
--- a/DuckDuckGo/AppDelegate.swift
+++ b/DuckDuckGo/AppDelegate.swift
@@ -721,6 +721,8 @@ import os.log
}
AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in }
+
+ mainViewController?.currentTab?.becomeCurrentActivity()
}
private func stopAndRemoveVPNIfNotAuthenticated() async {
@@ -921,6 +923,17 @@ import os.log
return true
}
+ func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
+ guard userActivity.activityType == "com.duckduckgo.mobile.ios.web-browsing",
+ let mainViewController else {
+ return false
+ }
+
+ restorationHandler([mainViewController])
+
+ return true
+ }
+
// MARK: private
private func sendAppLaunchPostback() {
diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist
index 6d3941b0ca..3df47d72d9 100644
--- a/DuckDuckGo/Info.plist
+++ b/DuckDuckGo/Info.plist
@@ -192,6 +192,7 @@
NSUserActivityTypes
ConfigurationIntent
+ com.duckduckgo.mobile.ios.web-browsing
UIApplicationShortcutItems
diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift
index e5b54dca00..094b4eb5c7 100644
--- a/DuckDuckGo/MainViewController.swift
+++ b/DuckDuckGo/MainViewController.swift
@@ -1055,6 +1055,7 @@ class MainViewController: UIViewController {
hideNotificationBarIfBrokenSitePromptShown()
if tab.link == nil {
attachHomeScreen()
+ invalidateCurrentActivity()
} else {
attachTab(tab: tab)
refreshControls()
@@ -1072,7 +1073,7 @@ class MainViewController: UIViewController {
hideNotificationBarIfBrokenSitePromptShown()
currentTab?.progressWorker.progressBar = nil
currentTab?.chromeDelegate = nil
-
+
addToContentContainer(controller: tab)
viewCoordinator.logoContainer.isHidden = true
@@ -1080,6 +1081,7 @@ class MainViewController: UIViewController {
tab.progressWorker.progressBar = viewCoordinator.progress
chromeManager.attach(to: tab.webView.scrollView)
tab.chromeDelegate = self
+ tab.becomeCurrentActivity()
}
private func addToContentContainer(controller: UIViewController) {
@@ -1464,6 +1466,8 @@ class MainViewController: UIViewController {
tabsBarController?.refresh(tabsModel: tabManager.model)
swipeTabsCoordinator?.refresh(tabsModel: tabManager.model, scrollToSelected: true)
newTabPageViewController?.openedAsNewTab(allowingKeyboard: allowingKeyboard)
+
+ invalidateCurrentActivity()
}
func updateFindInPage() {
@@ -2962,3 +2966,27 @@ extension MainViewController: AIChatViewControllerDelegate {
loadUrlInNewTab(url, inheritedAttribution: nil)
}
}
+
+// NSUserActivity-related
+extension MainViewController {
+ private func invalidateCurrentActivity() {
+ guard supportsHandoff() else { return }
+
+ userActivity?.invalidate()
+ userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
+ userActivity?.webpageURL = nil
+ userActivity?.becomeCurrent()
+ }
+
+ override func restoreUserActivityState(_ activity: NSUserActivity) {
+ guard supportsHandoff(), activity.activityType == "com.duckduckgo.mobile.ios.web-browsing", let url = activity.webpageURL else {
+ return
+ }
+
+ loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil)
+ }
+
+ private func supportsHandoff() -> Bool {
+ featureFlagger.isFeatureOn(.handoff)
+ }
+}
diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift
index 24dddd1628..8d59725d76 100644
--- a/DuckDuckGo/TabViewController.swift
+++ b/DuckDuckGo/TabViewController.swift
@@ -211,6 +211,7 @@ class TabViewController: UIViewController {
updateTabModel()
delegate?.tabLoadingStateDidChange(tab: self)
checkLoginDetectionAfterNavigation()
+ updateCurrentActivity(url: url)
}
}
@@ -274,7 +275,7 @@ class TabViewController: UIViewController {
manager.delegate = self
return manager
}()
-
+
private static let debugEvents = EventMapping { event, _, _, onComplete in
let domainEvent: Pixel.Event
switch event {
@@ -3182,3 +3183,42 @@ extension TabViewController: DuckPlayerTabNavigationHandling {
}
}
+
+// NSUserActivity-related
+extension TabViewController {
+ func becomeCurrentActivity() {
+ guard supportsHandoff() else { return }
+
+ if userActivity?.webpageURL == nil {
+ userActivity?.invalidate()
+ userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
+ userActivity?.webpageURL = nil
+ }
+
+ userActivity?.becomeCurrent()
+ }
+
+ private func updateCurrentActivity(url: URL?) {
+ guard supportsHandoff() else { return }
+
+ let newURL: URL? = {
+ guard let url, let scheme = url.scheme, ["http", "https"].contains(scheme) else { return nil }
+ return url.isDuckDuckGo ? url.removingInternalSearchParameters() : url
+ }()
+ guard newURL != userActivity?.webpageURL else { return }
+
+ userActivity?.invalidate()
+ if newURL != nil {
+ userActivity = NSUserActivity(activityType: "com.duckduckgo.mobile.ios.web-browsing")
+ } else {
+ userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
+ }
+ userActivity?.webpageURL = newURL
+
+ userActivity?.becomeCurrent()
+ }
+
+ private func supportsHandoff() -> Bool {
+ featureFlagger.isFeatureOn(.handoff)
+ }
+}