diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index c7977d3d2e74..e424aa4fdc0c 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -247,7 +247,7 @@ class BrowserCoordinator: BaseCoordinator, } switch route { - case .searchQuery, .search, .searchURL, .glean, .homepanel, .action, .fxaSignIn, .defaultBrowser: + case .searchQuery, .search, .searchURL, .glean, .homepanel, .action, .fxaSignIn, .defaultBrowser, .sharesheet: return true case let .settings(section): return canHandleSettings(with: section) @@ -273,6 +273,9 @@ class BrowserCoordinator: BaseCoordinator, case let .searchURL(url, tabId): handle(searchURL: url, tabId: tabId) + case let .sharesheet(url, title): + showShareSheet(with: url, title: title) + case let .glean(url): glean.handleDeeplinkUrl(url: url) @@ -514,11 +517,16 @@ class BrowserCoordinator: BaseCoordinator, } func showShareSheet(with url: URL?) { + showShareSheet(with: url, title: nil) + } + + func showShareSheet(with url: URL?, title: String?) { guard let url else { return } let showShareSheet = { url in self.showShareExtension( url: url, + title: title, sourceView: self.browserViewController.addressToolbarContainer, toastContainer: self.browserViewController.contentContainer, popoverArrowDirection: .any @@ -692,6 +700,7 @@ class BrowserCoordinator: BaseCoordinator, func showShareExtension( url: URL, + title: String?, sourceView: UIView, sourceRect: CGRect?, toastContainer: UIView, @@ -713,6 +722,7 @@ class BrowserCoordinator: BaseCoordinator, add(child: shareExtensionCoordinator) shareExtensionCoordinator.start( url: url, + title: title, sourceView: sourceView, sourceRect: sourceRect, popoverArrowDirection: popoverArrowDirection @@ -897,7 +907,7 @@ class BrowserCoordinator: BaseCoordinator, router.present(viewController) } -// MARK: - Password Generator + // MARK: - Password Generator func showPasswordGenerator(tab: Tab, frame: WKFrameInfo) { let passwordGenVC = PasswordGeneratorViewController(windowUUID: windowUUID, currentTab: tab, currentFrame: frame) diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift index 081f8b780ae9..19cbb7526589 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift @@ -34,6 +34,7 @@ protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler { /// from actions in the share extension /// - Parameter popoverArrowDirection: The arrow direction for the view controller presented as popover. func showShareExtension(url: URL, + title: String?, sourceView: UIView, sourceRect: CGRect?, toastContainer: UIView, @@ -108,6 +109,7 @@ protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler { extension BrowserNavigationHandler { func showShareExtension( url: URL, + title: String? = nil, sourceView: UIView, sourceRect: CGRect? = nil, toastContainer: UIView, @@ -115,6 +117,7 @@ extension BrowserNavigationHandler { ) { showShareExtension( url: url, + title: title, sourceView: sourceView, sourceRect: sourceRect, toastContainer: toastContainer, diff --git a/firefox-ios/Client/Coordinators/Router/DeeplinkInput.swift b/firefox-ios/Client/Coordinators/Router/DeeplinkInput.swift index 15967261a0f3..d4feacb1a8d1 100644 --- a/firefox-ios/Client/Coordinators/Router/DeeplinkInput.swift +++ b/firefox-ios/Client/Coordinators/Router/DeeplinkInput.swift @@ -12,6 +12,7 @@ enum DeeplinkInput { case fxaSignIn = "fxa-signin" case openUrl = "open-url" case openText = "open-text" + case sharesheet = "share-sheet" case glean case widgetMediumTopSitesOpenUrl = "widget-medium-topsites-open-url" case widgetSmallQuickLinkOpenUrl = "widget-small-quicklink-open-url" diff --git a/firefox-ios/Client/Coordinators/Router/Route.swift b/firefox-ios/Client/Coordinators/Router/Route.swift index 68a24be3d23d..5632f987f21a 100644 --- a/firefox-ios/Client/Coordinators/Router/Route.swift +++ b/firefox-ios/Client/Coordinators/Router/Route.swift @@ -66,6 +66,13 @@ enum Route: Equatable { /// settings to be displayed. case defaultBrowser(section: DefaultBrowserSection) + /// A route for opening a share sheet with a URL and an optional message. + /// + /// - Parameters: + /// - url: The `URL` object to be shared from the share sheet. + /// - title: An optional string to be used as the message in the share sheet. + case sharesheet(url: URL, title: String?) + /// An enumeration representing different sections of the home panel. enum HomepanelSection: String, CaseIterable, Equatable { case bookmarks diff --git a/firefox-ios/Client/Coordinators/Router/RouteBuilder.swift b/firefox-ios/Client/Coordinators/Router/RouteBuilder.swift index b64076ee9c08..92368046099f 100644 --- a/firefox-ios/Client/Coordinators/Router/RouteBuilder.swift +++ b/firefox-ios/Client/Coordinators/Router/RouteBuilder.swift @@ -121,6 +121,15 @@ final class RouteBuilder { case .fxaSignIn: return nil + + case .sharesheet: + let linkString = urlScanner.value(query: "url") + let titleText = urlScanner.value(query: "title") + if let link = linkString, let url = URL(string: link) { + return .sharesheet(url: url, title: titleText) + } else { + return nil + } } } else if urlScanner.isHTTPScheme { TelemetryWrapper.gleanRecordEvent(category: .action, method: .open, object: .asDefaultBrowser) @@ -192,7 +201,7 @@ final class RouteBuilder { private func recordTelemetry(input: DeeplinkInput.Host, isPrivate: Bool) { switch input { - case .deepLink, .fxaSignIn, .glean: + case .deepLink, .fxaSignIn, .glean, .sharesheet: return case .widgetMediumTopSitesOpenUrl: TelemetryWrapper.recordEvent(category: .action, method: .open, object: .mediumTopSitesWidget) diff --git a/firefox-ios/Client/Coordinators/ShareExtensionCoordinator.swift b/firefox-ios/Client/Coordinators/ShareExtensionCoordinator.swift index 9f75fbb439b1..34eb14726070 100644 --- a/firefox-ios/Client/Coordinators/ShareExtensionCoordinator.swift +++ b/firefox-ios/Client/Coordinators/ShareExtensionCoordinator.swift @@ -43,11 +43,15 @@ class ShareExtensionCoordinator: BaseCoordinator, /// Presents the Share extension from the source view func start( url: URL, + title: String? = nil, sourceView: UIView, sourceRect: CGRect? = nil, popoverArrowDirection: UIPopoverArrowDirection = .up ) { - let shareExtension = ShareExtensionHelper(url: url, tab: tabManager.selectedTab) + let shareExtension = ShareExtensionHelper( + url: url, + title: title ?? tabManager.selectedTab?.title, + tab: tabManager.selectedTab) let controller = shareExtension.createActivityViewController( tabManager.selectedTab?.webView ) { [weak self] completed, activityType in diff --git a/firefox-ios/Client/Frontend/Share/ShareExtensionHelper.swift b/firefox-ios/Client/Frontend/Share/ShareExtensionHelper.swift index 15b9cc74be6e..cb608414fe39 100644 --- a/firefox-ios/Client/Frontend/Share/ShareExtensionHelper.swift +++ b/firefox-ios/Client/Frontend/Share/ShareExtensionHelper.swift @@ -12,6 +12,7 @@ class ShareExtensionHelper: NSObject, FeatureFlaggable { private weak var selectedTab: Tab? private let url: URL + private let title: String? private var onePasswordExtensionItem: NSExtensionItem! private let browserFillIdentifier = "org.appextension.fill-browser-action" private let pocketIconExtension = "com.ideashower.ReadItLaterPro.AddToPocketExtension" @@ -22,8 +23,9 @@ class ShareExtensionHelper: NSObject, FeatureFlaggable { } // Can be a file:// or http(s):// url - init(url: URL, tab: Tab?) { + init(url: URL, title: String? = nil, tab: Tab?) { self.url = url + self.title = title self.selectedTab = tab } @@ -83,6 +85,12 @@ class ShareExtensionHelper: NSObject, FeatureFlaggable { guard !url.isFileURL else { return [url] } var activityItems = [Any]() + + // Add the title (if it exists) + if let title = self.title { + activityItems.append(title) + } + let printInfo = UIPrintInfo(dictionary: nil) printInfo.jobName = (url.absoluteString as NSString).lastPathComponent printInfo.outputType = .general diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift index e9c79cffe2bd..8ee28c8108ee 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift @@ -255,6 +255,24 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { XCTAssertTrue(mockRouter.presentedViewController is UIActivityViewController) } + func testShowShareExtension_addsShareExtensionCoordinatorWithTitle() { + let subject = createSubject() + + subject.showShareExtension( + url: URL( + string: "https://www.google.com" + )!, + title: "TEST TITLE", + sourceView: UIView(), + toastContainer: UIView() + ) + + XCTAssertEqual(subject.childCoordinators.count, 1) + XCTAssertTrue(subject.childCoordinators.first is ShareExtensionCoordinator) + XCTAssertEqual(mockRouter.presentCalled, 1) + XCTAssertTrue(mockRouter.presentedViewController is UIActivityViewController) + } + func testShowCreditCardAutofill_addsCredentialAutofillCoordinator() { let subject = createSubject() diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/RouteTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/RouteTests.swift index cdf33a493de4..9a41431fe8eb 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/RouteTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/RouteTests.swift @@ -347,6 +347,24 @@ class RouteTests: XCTestCase { XCTAssertEqual(route, .searchQuery(query: "google", isPrivate: false)) } + func testShareSheetRouteUrlOnly() { + let subject = createSubject() + let url = URL(string: "firefox://share-sheet?url=https://www.google.com")! + + let route = subject.makeRoute(url: url) + + XCTAssertEqual(route, .sharesheet(url: URL(string: "https://www.google.com")!, title: nil)) + } + + func testShareSheetRouteUrlAndTitle() { + let subject = createSubject() + let url = URL(string: "firefox://share-sheet?url=https://www.google.com&title=TEST TITLE")! + + let route = subject.makeRoute(url: url) + + XCTAssertEqual(route, .sharesheet(url: URL(string: "https://www.google.com")!, title: "TEST TITLE")) + } + // MARK: - AppAction func testAppAction_showIntroOnboarding() { diff --git a/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml b/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml index 6fd2f6527f62..7f9d75f5a9ea 100644 --- a/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml +++ b/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml @@ -61,6 +61,7 @@ import: OPEN_NEW_TAB: ://deep-link?url=homepanel/new-tab MAKE_DEFAULT_BROWSER: ://deep-link?url=default-browser/system-settings MAKE_DEFAULT_BROWSER_WITH_TUTORIAL: ://deep-link?url=default-browser/tutorial + OPEN_SHARE_SHEET: ://share-sheet styles: FALLBACK: priority: 40