diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 36e5b3745e..db71e63d65 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -238,6 +238,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel startupSync() +#if SUBSCRIPTION + let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default + + let currentEnvironment = UserDefaultsWrapper(key: .subscriptionEnvironment, + defaultValue: defaultEnvironment).wrappedValue + SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment + + #if APPSTORE || !STRIPE + SubscriptionPurchaseEnvironment.current = .appStore + #else + SubscriptionPurchaseEnvironment.current = .stripe + #endif + + Task { + let accountManager = AccountManager() + do { + try accountManager.migrateAccessTokenToNewStore() + } catch { + if let error = error as? AccountManager.MigrationError { + switch error { + case AccountManager.MigrationError.migrationFailed: + os_log(.default, log: .subscription, "Access token migration failed") + case AccountManager.MigrationError.noMigrationNeeded: + os_log(.default, log: .subscription, "No access token migration needed") + } + } + } + await accountManager.checkSubscriptionState() + } +#endif + if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() } @@ -280,33 +311,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #endif #if SUBSCRIPTION - Task { - let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default - let currentEnvironment = UserDefaultsWrapper(key: .subscriptionEnvironment, - defaultValue: defaultEnvironment).wrappedValue - SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment - - #if APPSTORE || !STRIPE - SubscriptionPurchaseEnvironment.current = .appStore - #else - SubscriptionPurchaseEnvironment.current = .stripe - #endif - let accountManager = AccountManager() - do { - try accountManager.migrateAccessTokenToNewStore() - } catch { - if let error = error as? AccountManager.MigrationError { - switch error { - case AccountManager.MigrationError.migrationFailed: - os_log(.default, log: .subscription, "Access token migration failed") - case AccountManager.MigrationError.noMigrationNeeded: - os_log(.default, log: .subscription, "No access token migration needed") - } - } - } - await accountManager.checkSubscriptionState() - } #endif } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 1405f73892..8da191ebce 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -142,6 +142,9 @@ protocol NewWindowPolicyDecisionMaker { #if SUBSCRIPTION if let url { if url.isChild(of: URL.subscriptionBaseURL) { + if SubscriptionPurchaseEnvironment.currentServiceEnvironment == .staging, url.getParameter(named: "environment") == nil { + return .subscription(url.appendingParameter(name: "environment", value: "staging")) + } return .subscription(url) } else if url.isChild(of: URL.identityTheftRestoration) { return .identityTheftRestoration(url) @@ -215,6 +218,7 @@ protocol NewWindowPolicyDecisionMaker { var url: URL? { userEditableUrl } + var userEditableUrl: URL? { switch self { case .url(let url, credential: _, source: _) where !(url.isDuckPlayer || url.isDuckURLScheme): @@ -291,6 +295,33 @@ protocol NewWindowPolicyDecisionMaker { isUrl } + var canBeDuplicated: Bool { + switch self { + case .settings, .subscription, .identityTheftRestoration, .dataBrokerProtection: + return false + default: + return true + } + } + + var canBePinned: Bool { + switch self { + case .subscription, .identityTheftRestoration, .dataBrokerProtection: + return false + default: + return isUrl + } + } + + var canBeBookmarked: Bool { + switch self { + case .subscription, .identityTheftRestoration, .dataBrokerProtection: + return false + default: + return isUrl + } + } + } private struct ExtensionDependencies: TabExtensionDependencies { let privacyFeatures: PrivacyFeaturesProtocol diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 585bf62961..a804545717 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -397,7 +397,7 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionNotFoundAlert(), firstButtonAction: { - WindowControllersManager.shared.show(url: .subscriptionPurchase, source: .ui, newTab: true) + WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) }) } @@ -406,7 +406,7 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionInactiveAlert(), firstButtonAction: { - WindowControllersManager.shared.show(url: .subscriptionPurchase, source: .ui, newTab: true) + WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) }) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 09bed8e360..37b06c15d5 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -23,6 +23,7 @@ import Common import SwiftUI import WebKit +// swiftlint:disable file_length // swiftlint:disable:next type_body_length final class BrowserTabViewController: NSViewController { @@ -157,6 +158,10 @@ final class BrowserTabViewController: NSViewController { selector: #selector(onCloseSubscriptionPage), name: .subscriptionPageCloseAndOpenPreferences, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(onSubscriptionAccountDidSignOut), + name: .accountDidSignOut, + object: nil) #endif } @@ -230,6 +235,22 @@ final class BrowserTabViewController: NSViewController { openNewTab(with: .settings(pane: .subscription)) } + + @objc + private func onSubscriptionAccountDidSignOut(_ notification: Notification) { + Task { @MainActor in + tabCollectionViewModel.removeAll { tabContent in + if case .subscription = tabContent { + return true + } else if case .identityTheftRestoration = tabContent { + return true + } else { + return false + } + } + } + } + #endif private func subscribeToSelectedTabViewModel() { @@ -1190,3 +1211,5 @@ extension BrowserTabViewController { #Preview { BrowserTabViewController(tabCollectionViewModel: TabCollectionViewModel(tabCollection: TabCollection(tabs: [.init(content: .url(.duckDuckGo, source: .ui))]))) } + +// swiftlint:enable file_length diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 8d5133d4b5..313a517b24 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -991,6 +991,15 @@ extension TabBarViewController: TabBarViewItemDelegate { } } + func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool { + guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") + return false + } + + return tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.content.canBeDuplicated ?? false + } + func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") @@ -1006,7 +1015,7 @@ extension TabBarViewController: TabBarViewItemDelegate { return false } - return tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.isUrl ?? false + return tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.content.canBePinned ?? false } func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) { @@ -1019,6 +1028,15 @@ extension TabBarViewController: TabBarViewItemDelegate { tabCollectionViewModel.pinTab(at: indexPath.item) } + func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") + return false + } + + return tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.content.canBeBookmarked ?? false + } + func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 4c75c69bdf..b4a1bbca64 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -30,7 +30,9 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, isMouseOver: Bool) + func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool + func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemTogglePermissionAction(_ tabBarViewItem: TabBarViewItem) @@ -505,6 +507,7 @@ extension TabBarViewItem: NSMenuDelegate { private func addDuplicateMenuItem(to menu: NSMenu) { let duplicateMenuItem = NSMenuItem(title: UserText.duplicateTab, action: #selector(duplicateAction(_:)), keyEquivalent: "") duplicateMenuItem.target = self + duplicateMenuItem.isEnabled = delegate?.tabBarViewItemCanBeDuplicated(self) ?? false menu.addItem(duplicateMenuItem) } @@ -518,6 +521,7 @@ extension TabBarViewItem: NSMenuDelegate { private func addBookmarkMenuItem(to menu: NSMenu) { let bookmarkMenuItem = NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(bookmarkThisPageAction(_:)), keyEquivalent: "") bookmarkMenuItem.target = self + bookmarkMenuItem.isEnabled = delegate?.tabBarViewItemCanBeBookmarked(self) ?? false menu.addItem(bookmarkMenuItem) } diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index e0319a25e8..e1fd1751c0 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -374,6 +374,16 @@ final class TabCollectionViewModel: NSObject { } } + func removeAll(matching condition: (Tab.TabContent) -> Bool) { + let tabs = tabCollection.tabs.filter { condition($0.content) } + + for tab in tabs { + if let index = indexInAllTabs(of: tab) { + remove(at: index) + } + } + } + func remove(at index: TabIndex, published: Bool = true, forceChange: Bool = false) { switch index { case .unpinned(let i): diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index ab577650a2..c09b956f23 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -21,6 +21,8 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { + var mockedCurrentTab: Tab? + var hasItemsToTheRight = false var audioState: WKWebView.AudioState = .notSupported @@ -28,10 +30,6 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } - func tabBarViewItemCanBePinned(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { - return true - } - func tabBarViewItemCloseAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } @@ -48,14 +46,26 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + mockedCurrentTab?.content.canBeDuplicated ?? true + } + func tabBarViewItemDuplicateAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } + func tabBarViewItemCanBePinned(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + mockedCurrentTab?.content.canBePinned ?? true + } + func tabBarViewItemPinAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } + func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + mockedCurrentTab?.content.canBeBookmarked ?? true + } + func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 71ee8f1b9e..14f3d089c1 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -18,6 +18,11 @@ import Macros import XCTest + +#if SUBSCRIPTION +import Subscription +#endif + @testable import DuckDuckGo_Privacy_Browser @MainActor @@ -141,11 +146,57 @@ final class TabBarViewItemTests: XCTestCase { // Update url let tab = Tab() tab.url = #URL("https://www.apple.com") + delegate.mockedCurrentTab = tab let vm = TabViewModel(tab: tab) tabBarViewItem.subscribe(to: vm, tabCollectionViewModel: TabCollectionViewModel()) // update menu tabBarViewItem.menuNeedsUpdate(menu) let item = menu.items .first { $0.title == UserText.fireproofSite } XCTAssertTrue(item?.isEnabled ?? false) + + let duplicateItem = menu.items.first { $0.title == UserText.duplicateTab } + XCTAssertTrue(duplicateItem?.isEnabled ?? false) + + let pinItem = menu.items.first { $0.title == UserText.pinTab } + XCTAssertTrue(pinItem?.isEnabled ?? false) + + let bookmarkItem = menu.items.first { $0.title == UserText.bookmarkThisPage } + XCTAssertTrue(bookmarkItem?.isEnabled ?? false) + } + +#if SUBSCRIPTION + func testSubscriptionTabDisabledItems() { + // Set up fake views for the TabBarViewItems + let textField = NSTextField() + let imageView = NSImageView() + let constraints = NSLayoutConstraint() + let button = NSButton() + let mouseButton = MouseOverButton() + tabBarViewItem.titleTextField = textField + tabBarViewItem.faviconImageView = imageView + tabBarViewItem.faviconWrapperView = imageView + tabBarViewItem.titleTextFieldLeadingConstraint = constraints + tabBarViewItem.permissionButton = button + tabBarViewItem.tabLoadingPermissionLeadingConstraint = constraints + tabBarViewItem.closeButton = mouseButton + + // Update url + let tab = Tab(content: .subscription(.subscriptionPurchase)) + delegate.mockedCurrentTab = tab + let vm = TabViewModel(tab: tab) + tabBarViewItem.subscribe(to: vm, tabCollectionViewModel: TabCollectionViewModel()) + // update menu + tabBarViewItem.menuNeedsUpdate(menu) + + let duplicateItem = menu.items.first { $0.title == UserText.duplicateTab } + XCTAssertFalse(duplicateItem?.isEnabled ?? true) + + let pinItem = menu.items.first { $0.title == UserText.pinTab } + XCTAssertFalse(pinItem?.isEnabled ?? true) + + let bookmarkItem = menu.items.first { $0.title == UserText.bookmarkThisPage } + XCTAssertFalse(bookmarkItem?.isEnabled ?? true) } +#endif + }