Skip to content

Commit

Permalink
Handle subscription tabs state based on authentication events (#2372)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1199230911884351/1206726299874130/f

**Description**:
When reloading subscription purchase and welcome page should be
refreshed based on the authentication state
when signing out the subscription and ITR tabs should be closed.

Additional considerations to facilitate above:
- subscription and ITR tabs should disallow pin/bookmark/duplicate
actions
- when opening a new subscription tab the old one should be reused
preventing multiple tabs of this type

**Steps to test this PR**:
Opening tabs on closing them on sign out
1. Purchase or restore a subscription.
2. Open subscription type page (e.g. add or mana email)
3. Try repeating opening a new subscription type tab, the previous one
should be reused instead of opening a new one.
4. Open ITR page.
5. Try repeating opening a new ITR tab, the previous one should be
reused instead of opening a new one.
6. Leave subscription and ITR tabs open
7. Remove subscription
8. subscription and ITR tabs should be closed

Possible actions for subscription and ITR tabs:
1. Open subscription type page (e.g. add or mana email)
2. Right click on the tab. It should not be possible to
pin/bookmark/duplicate it.
3. Open ITR page.
4. Right click on the tab. It should not be possible to
pin/bookmark/duplicate it.

Maintaining welcome page state:
1. Have a subscription welcome page open
2. Close and reopen the browser.
3. While having the up to date auth token the welcome page should not
redirect elsewhere.

Proper purchase/welcome page state upon restoration:
1. Purchase subscription via App Store
2. Remove the subscription
3. Open purchase page
4. Open settings
5. Click "I have a subscription" -> "Restore" to restore subscription
6. Switch to a purchase page that should reload to welcome page


<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

---
###### Internal references:
[Pull Request Review
Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f)
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
[Pull Request
Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f)
  • Loading branch information
miasma13 authored Mar 11, 2024
1 parent a26f509 commit 62e1b3b
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 33 deletions.
57 changes: 31 additions & 26 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
}

Expand Down
31 changes: 31 additions & 0 deletions DuckDuckGo/Tab/Model/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}

Expand All @@ -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))
})
}

Expand Down
23 changes: 23 additions & 0 deletions DuckDuckGo/Tab/View/BrowserTabViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Common
import SwiftUI
import WebKit

// swiftlint:disable file_length
// swiftlint:disable:next type_body_length
final class BrowserTabViewController: NSViewController {

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -1190,3 +1211,5 @@ extension BrowserTabViewController {
#Preview {
BrowserTabViewController(tabCollectionViewModel: TabCollectionViewModel(tabCollection: TabCollection(tabs: [.init(content: .url(.duckDuckGo, source: .ui))])))
}

// swiftlint:enable file_length
20 changes: 19 additions & 1 deletion DuckDuckGo/TabBar/View/TabBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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) {
Expand All @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/TabBar/View/TabBarViewItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand Down
10 changes: 10 additions & 0 deletions DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
18 changes: 14 additions & 4 deletions UnitTests/TabBar/View/MockTabViewItemDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ import Foundation

class MockTabViewItemDelegate: TabBarViewItemDelegate {

var mockedCurrentTab: Tab?

var hasItemsToTheRight = false
var audioState: WKWebView.AudioState = .notSupported

func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) {

}

func tabBarViewItemCanBePinned(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool {
return true
}

func tabBarViewItemCloseAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) {

}
Expand All @@ -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) {

}
Expand Down
51 changes: 51 additions & 0 deletions UnitTests/TabBar/View/TabBarViewItemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

import Macros
import XCTest

#if SUBSCRIPTION
import Subscription
#endif

@testable import DuckDuckGo_Privacy_Browser

@MainActor
Expand Down Expand Up @@ -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

}

0 comments on commit 62e1b3b

Please sign in to comment.