diff --git a/firefox-ios/Account/FxAPushMessageHandler.swift b/firefox-ios/Account/FxAPushMessageHandler.swift index e0252f85ab2e..5cff5ba3d809 100644 --- a/firefox-ios/Account/FxAPushMessageHandler.swift +++ b/firefox-ios/Account/FxAPushMessageHandler.swift @@ -5,6 +5,7 @@ import Shared import Account import Common +import enum MozillaAppServices.IncomingDeviceCommand let PendingAccountDisconnectedKey = "PendingAccountDisconnect" @@ -57,9 +58,11 @@ extension FxAPushMessageHandler { case .tabReceived(_, let tabData): let title = tabData.entries.last?.title ?? "" let url = tabData.entries.last?.url ?? "" - completion(.success(PushMessage.commandReceived(tab: ["title": title, "url": url]))) - default: - break + let command = CommandReceived.tabReceived(tab: ["title": title, "url": url]) + completion(.success(PushMessage.commandReceived(command: command))) + case .tabsClosed(_, let payload): + let command = CommandReceived.tabsClosed(urls: payload.urls) + completion(.success(PushMessage.commandReceived(command: command))) } case .deviceConnected(let deviceName): completion(.success(PushMessage.deviceConnected(deviceName))) @@ -92,7 +95,7 @@ enum PushMessageType: String { } enum PushMessage: Equatable { - case commandReceived(tab: [String: String]) + case commandReceived(command: CommandReceived) case deviceConnected(String) case deviceDisconnected case profileUpdated @@ -158,3 +161,8 @@ enum PushMessageError: MaybeErrorType { } } } + +enum CommandReceived: Equatable { + case tabReceived(tab: [String: String]) + case tabsClosed(urls: [String]) +} diff --git a/firefox-ios/Client/Application/AppDelegate+PushNotifications.swift b/firefox-ios/Client/Application/AppDelegate+PushNotifications.swift index f2f0364bd022..1012551c80e8 100644 --- a/firefox-ios/Client/Application/AppDelegate+PushNotifications.swift +++ b/firefox-ios/Client/Application/AppDelegate+PushNotifications.swift @@ -85,12 +85,23 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let content = response.notification.request.content if content.categoryIdentifier == NotificationSurfaceManager.Constant.notificationCategoryId { - switch response.actionIdentifier { - case UNNotificationDismissActionIdentifier: - notificationSurfaceManager.didDismissNotification(content.userInfo) - default: - notificationSurfaceManager.didTapNotification(content.userInfo) - } + switch response.actionIdentifier { + case UNNotificationDismissActionIdentifier: + notificationSurfaceManager.didDismissNotification(content.userInfo) + default: + notificationSurfaceManager.didTapNotification(content.userInfo) + } + } else if content.categoryIdentifier == NotificationCloseTabs.notificationCategoryId { + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + // Since the notification is coming from the background, we should give a little + // time to ensure we can show the recently closed tabs panel + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + NotificationCenter.default.post(name: .RemoteTabNotificationTapped, object: nil) + } + default: + break + } } // We don't poll for commands here because we do that once the application wakes up // The notification service ensures that when the application wakes up, the application will check diff --git a/firefox-ios/Client/Application/AppLaunchUtil.swift b/firefox-ios/Client/Application/AppLaunchUtil.swift index fefd5d5df2a4..0455c68e91be 100644 --- a/firefox-ios/Client/Application/AppLaunchUtil.swift +++ b/firefox-ios/Client/Application/AppLaunchUtil.swift @@ -78,13 +78,7 @@ class AppLaunchUtil { } } - // RustFirefoxAccounts doesn't have access to the feature flags - // So we check the nimbus flag here before sending it to the startup - var fxaFeatures: RustFxAFeatures = [] - if LegacyFeatureFlagsManager.shared.isFeatureEnabled(.closeRemoteTabs, checking: .buildAndUser) { - fxaFeatures.insert(.closeRemoteTabs) - } - RustFirefoxAccounts.startup(prefs: profile.prefs, features: fxaFeatures) { _ in + RustFirefoxAccounts.startup(prefs: profile.prefs) { _ in self.logger.log("RustFirefoxAccounts started", level: .info, category: .sync) AppEventQueue.signal(event: .accountManagerInitialized) } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index d71b65a789c8..81ac6a41bc87 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -848,6 +848,12 @@ class BrowserViewController: UIViewController, selector: #selector(handlePageZoomLevelUpdated), name: .PageZoomLevelUpdated, object: nil) + notificationCenter.addObserver( + self, + selector: #selector(openRecentlyClosedTabs), + name: .RemoteTabNotificationTapped, + object: nil + ) } private func switchToolbarIfNeeded() { @@ -3587,6 +3593,14 @@ extension BrowserViewController: HomePanelDelegate { func homePanelDidRequestBookmarkToast(url: URL?, action: BookmarkAction) { showBookmarkToast(bookmarkURL: url, action: action) } + + @objc + func openRecentlyClosedTabs() { + DispatchQueue.main.async { + self.navigationHandler?.show(homepanelSection: .history) + self.notificationCenter.post(name: .OpenRecentlyClosedTabs) + } + } } // MARK: - SearchViewController diff --git a/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanel.swift b/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanel.swift index 70f80714428d..8ee71b4eab3c 100644 --- a/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanel.swift +++ b/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanel.swift @@ -350,6 +350,11 @@ class HistoryPanel: UIViewController, } showClearRecentHistory() + break + case .OpenRecentlyClosedTabs: + historyCoordinatorDelegate?.showRecentlyClosedTab() + applySnapshot(animatingDifferences: true) + break default: // no need to do anything at all break diff --git a/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanelViewModel.swift b/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanelViewModel.swift index fab990a94669..0c434ca1f9ea 100644 --- a/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanelViewModel.swift +++ b/firefox-ios/Client/Frontend/Library/HistoryPanel/HistoryPanelViewModel.swift @@ -83,7 +83,8 @@ class HistoryPanelViewModel: FeatureFlaggable { Notification.Name.PrivateDataClearedHistory, Notification.Name.DynamicFontChanged, Notification.Name.DatabaseWasReopened, - Notification.Name.OpenClearRecentHistory] + Notification.Name.OpenClearRecentHistory, + Notification.Name.OpenRecentlyClosedTabs] // MARK: - Inits diff --git a/firefox-ios/Client/Frontend/Strings.swift b/firefox-ios/Client/Frontend/Strings.swift index 4954c9ffa095..9f05057158d7 100644 --- a/firefox-ios/Client/Frontend/Strings.swift +++ b/firefox-ios/Client/Frontend/Strings.swift @@ -3725,6 +3725,20 @@ extension String { tableName: nil, value: "View", comment: "Label for an action used to view one or more tabs from a notification.") + +// MARK: - Close tab notifications + public static let CloseTab_ArrivingNotification_title = MZLocalizedString( + key: "CloseTab_ArrivingNotification_title", + tableName: "FxANotification", + value: "Firefox tabs closed: %1$@", + comment: "Title of notification shown when a remote device has requested to close a number of tabs defined by ($1)") + + // Notification Actions + public static let CloseTabViewActionTitle = MZLocalizedString( + key: "CloseTab.ViewAction.title", + tableName: "FxANotification", + value: "View recently closed tabs", + comment: "Label for an action used to view recently closed tabs.") } // MARK: - Engagement notification diff --git a/firefox-ios/Extensions/NotificationService/NotificationPayloads.swift b/firefox-ios/Extensions/NotificationService/NotificationPayloads.swift index 36ffce41d63d..75243af4cf67 100644 --- a/firefox-ios/Extensions/NotificationService/NotificationPayloads.swift +++ b/firefox-ios/Extensions/NotificationService/NotificationPayloads.swift @@ -14,3 +14,9 @@ struct NotificationSentTabs { static let deviceNameKey = "deviceName" } } + +struct NotificationCloseTabs { + static let closeTabsKey = "closeRemoteTabs" + static let notificationCategoryId: String = "org.mozilla.ios.fxa.notification.category" + static let messageIdKey: String = "messageId" +} diff --git a/firefox-ios/Extensions/NotificationService/NotificationService.swift b/firefox-ios/Extensions/NotificationService/NotificationService.swift index 80f6dffa692b..28ca4ebe6e1e 100644 --- a/firefox-ios/Extensions/NotificationService/NotificationService.swift +++ b/firefox-ios/Extensions/NotificationService/NotificationService.swift @@ -133,8 +133,13 @@ class SyncDataDisplay { } switch message { - case .commandReceived(let tab): - displayNewSentTabNotification(tab: tab) + case .commandReceived(let command): + switch command { + case .tabReceived(let tab): + displayNewSentTabNotification(tab: tab) + case .tabsClosed(let urls): + displayClosedTabNotification(urls: urls) + } case .deviceConnected(let deviceName): displayDeviceConnectedNotification(deviceName) case .deviceDisconnected: @@ -214,6 +219,18 @@ class SyncDataDisplay { } } + func displayClosedTabNotification(urls: [String]) { + notificationContent.userInfo[NotificationCloseTabs.closeTabsKey] + = [NotificationCloseTabs.messageIdKey: "closeRemoteTab"] + + notificationContent.categoryIdentifier = NotificationCloseTabs.notificationCategoryId + + presentNotification( + title: String(format: .CloseTab_ArrivingNotification_title, "\(urls.count)"), + body: .CloseTabViewActionTitle + ) + } + func presentNotification(title: String, body: String, titleArg: String? = nil, bodyArg: String? = nil) { func stringWithOptionalArg(_ s: String, _ a: String?) -> String { if let a = a { diff --git a/firefox-ios/RustFxA/RustFirefoxAccounts.swift b/firefox-ios/RustFxA/RustFirefoxAccounts.swift index 15e81426fe70..51532a3e3b3e 100644 --- a/firefox-ios/RustFxA/RustFirefoxAccounts.swift +++ b/firefox-ios/RustFxA/RustFirefoxAccounts.swift @@ -29,8 +29,6 @@ final class Unknown: NSObject, NSCoding { public struct RustFxAFeatures: OptionSet { public let rawValue: Int - public static let closeRemoteTabs = RustFxAFeatures(rawValue: 1 << 0) - public init(rawValue: Int) { self.rawValue = rawValue } @@ -177,10 +175,7 @@ open class RustFirefoxAccounts { let type = UIDevice.current.userInterfaceIdiom == .pad ? DeviceType.tablet : DeviceType.mobile - var capabilities: [DeviceCapability] = [.sendTab] - if features.contains(.closeRemoteTabs) { - capabilities.append(.closeTabs) - } + let capabilities: [DeviceCapability] = [.sendTab, .closeTabs] let deviceConfig = DeviceConfig( name: DeviceInfo.defaultClientName(), deviceType: type, diff --git a/firefox-ios/Shared/NotificationConstants.swift b/firefox-ios/Shared/NotificationConstants.swift index 34fd71a67a66..4d3f612280f1 100644 --- a/firefox-ios/Shared/NotificationConstants.swift +++ b/firefox-ios/Shared/NotificationConstants.swift @@ -73,6 +73,10 @@ extension Notification.Name { public static let OpenClearRecentHistory = Notification.Name("OpenClearRecentHistory") + public static let RemoteTabNotificationTapped = Notification.Name("RemoteTabNotificationTapped") + + public static let OpenRecentlyClosedTabs = Notification.Name("OpenRecentlyClosedTabs") + public static let LibraryPanelStateDidChange = Notification.Name("LibraryPanelStateDidChange") public static let SearchSettingsChanged = Notification.Name("SearchSettingsChanged") diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Helpers/NotificationManagerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Helpers/NotificationManagerTests.swift index f75f58bcb988..d221171195b8 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Helpers/NotificationManagerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Helpers/NotificationManagerTests.swift @@ -85,6 +85,21 @@ class NotificationManagerTests: XCTestCase { XCTAssertEqual(self.center.pendingRequests.count, 1) } + func testCloseRemoteTabNotification() { + let notificationContent = UNMutableNotificationContent() + // Test with the categoryIdentify being the close remote tab identifier + notificationContent.categoryIdentifier = NotificationCloseTabs.notificationCategoryId + let request = UNNotificationRequest(identifier: "id1", + content: notificationContent, + trigger: nil) + + UNUserNotificationCenter.current().add(request) { (error) in + if let error = error { + XCTFail("Error adding notification request: \(error)") + } + } + } + // MARK: - Helpers private func buildRequest(title: String, body: String, id: String) -> UNNotificationRequest { diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/HistoryPanelTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/HistoryPanelTests.swift index a7359f5de152..f6edcc58f3cb 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/HistoryPanelTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/HistoryPanelTests.swift @@ -9,15 +9,18 @@ import Common class HistoryPanelTests: XCTestCase { let windowUUID: WindowUUID = .XCTestDefaultUUID + private var notificationCenter: MockNotificationCenter! override func setUp() { super.setUp() LegacyFeatureFlagsManager.shared.initializeDeveloperFeatures(with: MockProfile()) DependencyHelperMock().bootstrapDependencies() + notificationCenter = MockNotificationCenter() } override func tearDown() { super.tearDown() DependencyHelperMock().reset() + notificationCenter = nil } func testHistoryButtons() { @@ -88,6 +91,14 @@ class HistoryPanelTests: XCTestCase { XCTAssertTrue(panel.shouldDismissOnDone()) } + func testHistoryPanel_ShouldReceiveClosedTabNotification() { + let panel = createSubject() + panel.loadView() + notificationCenter.post(name: .OpenRecentlyClosedTabs) + + XCTAssertEqual(notificationCenter.postCallCount, 1) + } + private func createSubject() -> HistoryPanel { let profile = MockProfile() let subject = HistoryPanel(profile: profile, windowUUID: windowUUID)