diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift index e5cd30b65d6..9564acdfef0 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift @@ -19,7 +19,7 @@ protocol TabTrayDelegate: AnyObject { func tabOrderChanged() } -class TabTrayController: LoadingViewController { +class TabTrayController: AuthenticationController { typealias DataSource = UICollectionViewDiffableDataSource typealias Snapshot = NSDiffableDataSourceSnapshot @@ -47,7 +47,6 @@ class TabTrayController: LoadingViewController { let tabManager: TabManager let braveCore: BraveCoreMain - private let windowProtection: WindowProtection? private var openTabsSessionServiceListener: OpenTabsSessionStateListener? private var syncServicStateListener: AnyObject? @@ -99,6 +98,7 @@ class TabTrayController: LoadingViewController { var tabTrayMode: TabTrayMode = .local private var privateModeCancellable: AnyCancellable? private var initialScrollCompleted = false + private var localAuthObservers = Set() // MARK: User Interface Elements @@ -188,9 +188,8 @@ class TabTrayController: LoadingViewController { init(tabManager: TabManager, braveCore: BraveCoreMain, windowProtection: WindowProtection?) { self.tabManager = tabManager self.braveCore = braveCore - self.windowProtection = windowProtection - super.init(nibName: nil, bundle: nil) + super.init(windowProtection: windowProtection, isCancellable: true, unlockScreentitle: "Private Browsing is Locked") if !UIAccessibility.isReduceMotionEnabled { transitioningDelegate = self @@ -304,6 +303,19 @@ class TabTrayController: LoadingViewController { .sink(receiveValue: { [weak self] isPrivateBrowsing in self?.updateColors(isPrivateBrowsing) }) + + windowProtection?.cancelPressed + .sink { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + }.store(in: &localAuthObservers) + + windowProtection?.finalizedAuthentication + .sink { [weak self] success in + if success { + self?.toggleModeChanger() + } + self?.navigationController?.popViewController(animated: true) + }.store(in: &localAuthObservers) reloadOpenTabsSession() @@ -587,6 +599,14 @@ class TabTrayController: LoadingViewController { } @objc func togglePrivateModeAction() { + if !privateMode, Preferences.Privacy.privateBrowsingLock.value { + askForAuthentication(viewType: .tabTray) + } else { + toggleModeChanger() + } + } + + func toggleModeChanger() { tabTraySearchController.isActive = false // Mode Change action disabled while drap-drop is active @@ -624,7 +644,6 @@ class TabTrayController: LoadingViewController { navigationController?.setNavigationBarHidden(privateMode, animated: false) tabTypeSelector.isHidden = privateMode - } func remove(tab: Tab) { diff --git a/Sources/Brave/Frontend/ClientPreferences.swift b/Sources/Brave/Frontend/ClientPreferences.swift index a32cbeeec4a..d845634dbc1 100644 --- a/Sources/Brave/Frontend/ClientPreferences.swift +++ b/Sources/Brave/Frontend/ClientPreferences.swift @@ -123,6 +123,7 @@ extension Preferences { final public class Privacy { static let lockWithPasscode = Option(key: "privacy.lock-with-passcode", default: false) + static let privateBrowsingLock = Option(key: "privacy.private-browsing-lock", default: false) /// Forces all private tabs public static let privateBrowsingOnly = Option(key: "privacy.private-only", default: false) /// Blocks all cookies and access to local storage diff --git a/Sources/Brave/Frontend/Passcode/WindowProtection.swift b/Sources/Brave/Frontend/Passcode/WindowProtection.swift index 95b9c5c7763..19ffcd5764c 100644 --- a/Sources/Brave/Frontend/Passcode/WindowProtection.swift +++ b/Sources/Brave/Frontend/Passcode/WindowProtection.swift @@ -18,6 +18,13 @@ public class WindowProtection { private class LockedViewController: UIViewController { let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) let lockImageView = UIImageView(image: UIImage(named: "browser-lock-icon", in: .module, compatibleWith: nil)!) + let titleLabel = UILabel().then { + $0.font = .preferredFont(for: .title3, weight: .semibold) + $0.adjustsFontForContentSizeCategory = true + $0.textColor = .bravePrimary + $0.numberOfLines = 0 + $0.textAlignment = .center + } let unlockButton = FilledActionButton(type: .system).then { $0.setTitle(Strings.unlockButtonTitle, for: .normal) $0.titleLabel?.font = .preferredFont(forTextStyle: .headline) @@ -25,7 +32,6 @@ public class WindowProtection { $0.backgroundColor = .braveBlurpleTint $0.isHidden = true } - let cancelButton = ActionButton(type: .system).then { $0.setTitle(Strings.cancelButtonTitle, for: .normal) $0.titleLabel?.font = .preferredFont(forTextStyle: .headline) @@ -39,12 +45,19 @@ public class WindowProtection { super.viewDidLoad() view.addSubview(backgroundView) + view.addSubview(titleLabel) view.addSubview(lockImageView) view.addSubview(unlockButton) view.addSubview(cancelButton) backgroundView.snp.makeConstraints { $0.edges.equalTo(view) } + titleLabel.snp.makeConstraints { + $0.leading.greaterThanOrEqualToSuperview().offset(20) + $0.trailing.lessThanOrEqualToSuperview().offset(-20) + $0.centerX.equalToSuperview() + $0.bottom.equalTo(lockImageView.snp.top).offset(-40) + } lockImageView.snp.makeConstraints { $0.center.equalTo(view) } @@ -103,12 +116,24 @@ public class WindowProtection { } } - private let onCancelPressed = PassthroughSubject() + var unlockScreentitle: String = "" { + didSet { + lockedViewController.titleLabel.isHidden = unlockScreentitle.isEmpty + lockedViewController.titleLabel.text = unlockScreentitle + } + } + private let onCancelPressed = PassthroughSubject() + private let didFinalizeAuthentication = PassthroughSubject() + var cancelPressed: AnyPublisher { onCancelPressed.eraseToAnyPublisher() } + var finalizedAuthentication: AnyPublisher { + didFinalizeAuthentication.eraseToAnyPublisher() + } + public init?(window: UIWindow) { guard let scene = window.windowScene else { return nil } protectedWindow = window @@ -208,6 +233,8 @@ public class WindowProtection { Logger.module.error("Failed to unlock browser using local authentication: \(error.localizedDescription)") } } + + self.didFinalizeAuthentication.send(success) } } } diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 80897fd7af7..870923e15b7 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -589,7 +589,16 @@ class SettingsViewController: TableViewController { return Section( header: .title(Strings.security), rows: [ - .boolRow(title: Strings.browserLock, detailText: Strings.browserLockDescription, option: Preferences.Privacy.lockWithPasscode, image: UIImage(braveSystemNamed: "leo.biometric.login")), + .boolRow( + title: Strings.Privacy.browserLock, + detailText: Strings.Privacy.browserLockDescription, + option: Preferences.Privacy.lockWithPasscode, + image: UIImage(braveSystemNamed: "leo.biometric.login")), + .boolRow( + title: Strings.Privacy.privateBrowsingLock, + detailText: Strings.Privacy.privateBrowsingLockDescription, + option: Preferences.Privacy.privateBrowsingLock, + image: UIImage(braveSystemNamed: "leo.lock")), Row( text: Strings.Login.loginListNavigationTitle, selection: { [unowned self] in diff --git a/Sources/Brave/Frontend/Sync/SyncViewController.swift b/Sources/Brave/Frontend/Sync/SyncViewController.swift index 8b0aff38fa9..f7f186b035d 100644 --- a/Sources/Brave/Frontend/Sync/SyncViewController.swift +++ b/Sources/Brave/Frontend/Sync/SyncViewController.swift @@ -7,10 +7,8 @@ import Data import LocalAuthentication import Combine -class SyncViewController: UIViewController { +class SyncViewController: AuthenticationController { - let windowProtection: WindowProtection? - private let requiresAuthentication: Bool private let isModallyPresented: Bool private var localAuthObservers = Set() @@ -20,11 +18,8 @@ class SyncViewController: UIViewController { requiresAuthentication: Bool = false, isAuthenticationCancellable: Bool = true, isModallyPresented: Bool = false) { - self.windowProtection = windowProtection - self.requiresAuthentication = requiresAuthentication self.isModallyPresented = isModallyPresented - - super.init(nibName: nil, bundle: nil) + super.init(windowProtection: windowProtection, requiresAuthentication: requiresAuthentication) windowProtection?.isCancellable = isAuthenticationCancellable @@ -67,26 +62,6 @@ class SyncViewController: UIViewController { code() } - /// A method to ask biometric authentication to user - /// - Parameter completion: block returning authentication status - func askForAuthentication(completion: ((Bool, LAError.Code?) -> Void)? = nil) { - guard let windowProtection = windowProtection else { - completion?(false, nil) - return - } - - if !windowProtection.isPassCodeAvailable { - showSetPasscodeError() { - completion?(false, LAError.passcodeNotSet) - } - } else { - windowProtection.presentAuthenticationForViewController( - determineLockWithPasscode: false) { status, error in - completion?(status, error) - } - } - } - private func dismissSyncController() { if isModallyPresented { self.dismiss(animated: true) @@ -94,21 +69,4 @@ class SyncViewController: UIViewController { self.navigationController?.popViewController(animated: true) } } - - /// An alert presenter for passcode error to warn user to setup passcode to use feature - /// - Parameter completion: block after Ok button is pressed - private func showSetPasscodeError(completion: @escaping (() -> Void)) { - let alert = UIAlertController( - title: Strings.Sync.syncSetPasscodeAlertTitle, - message: Strings.Sync.syncSetPasscodeAlertDescription, - preferredStyle: .alert) - - alert.addAction( - UIAlertAction(title: Strings.OKString, style: .default, handler: { _ in - completion() - }) - ) - - present(alert, animated: true, completion: nil) - } } diff --git a/Sources/Brave/Frontend/Sync/SyncWelcomeViewController.swift b/Sources/Brave/Frontend/Sync/SyncWelcomeViewController.swift index fbd298121de..8b3d80302f0 100644 --- a/Sources/Brave/Frontend/Sync/SyncWelcomeViewController.swift +++ b/Sources/Brave/Frontend/Sync/SyncWelcomeViewController.swift @@ -21,7 +21,7 @@ class SyncWelcomeViewController: SyncViewController { private var overlayView: UIView? - private var isLoading: Bool = false { + override var isLoading: Bool { didSet { overlayView?.removeFromSuperview() diff --git a/Sources/Brave/Frontend/Widgets/LoadingViewController.swift b/Sources/Brave/Frontend/Widgets/LoadingViewController.swift new file mode 100644 index 00000000000..a49e6a3860c --- /dev/null +++ b/Sources/Brave/Frontend/Widgets/LoadingViewController.swift @@ -0,0 +1,100 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveShared +import Shared +import LocalAuthentication + +public class LoadingViewController: UIViewController { + + let spinner = UIActivityIndicatorView().then { + $0.snp.makeConstraints { make in + make.size.equalTo(24) + } + $0.hidesWhenStopped = true + $0.isHidden = true + } + + var isLoading: Bool = false { + didSet { + if isLoading { + view.addSubview(spinner) + spinner.snp.makeConstraints { + $0.center.equalTo(view.snp.center) + } + spinner.startAnimating() + } else { + spinner.stopAnimating() + spinner.removeFromSuperview() + } + } + } +} + +public class AuthenticationController: LoadingViewController { + enum AuthViewType { + case sync, tabTray + } + + let windowProtection: WindowProtection? + let requiresAuthentication: Bool + + // MARK: Lifecycle + + init(windowProtection: WindowProtection? = nil, + requiresAuthentication: Bool = false, + isCancellable: Bool = false, + unlockScreentitle: String = "") { + self.windowProtection = windowProtection + self.requiresAuthentication = requiresAuthentication + + super.init(nibName: nil, bundle: nil) + + self.windowProtection?.isCancellable = isCancellable + self.windowProtection?.unlockScreentitle = unlockScreentitle + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// A method to ask biometric authentication to user + /// - Parameter completion: block returning authentication status + func askForAuthentication(viewType: AuthViewType = .sync, completion: ((Bool, LAError.Code?) -> Void)? = nil) { + guard let windowProtection = windowProtection else { + completion?(false, nil) + return + } + + if !windowProtection.isPassCodeAvailable { + showSetPasscodeError(viewType: viewType) { + completion?(false, LAError.passcodeNotSet) + } + } else { + windowProtection.presentAuthenticationForViewController( + determineLockWithPasscode: false) { status, error in + completion?(status, error) + } + } + } + + /// An alert presenter for passcode error to warn user to setup passcode to use feature + /// - Parameter completion: block after Ok button is pressed + func showSetPasscodeError(viewType: AuthViewType, completion: @escaping (() -> Void)) { + let alert = UIAlertController( + title: Strings.Sync.syncSetPasscodeAlertTitle, + message: viewType == .sync ? Strings.Sync.syncSetPasscodeAlertDescription : Strings.Privacy.tabTraySetPasscodeAlertDescription, + preferredStyle: .alert) + + alert.addAction( + UIAlertAction(title: Strings.OKString, style: .default, handler: { _ in + completion() + }) + ) + + present(alert, animated: true, completion: nil) + } +} diff --git a/Sources/Brave/Frontend/Widgets/SiteTableViewController.swift b/Sources/Brave/Frontend/Widgets/SiteTableViewController.swift index ee649ca484c..824030e3508 100644 --- a/Sources/Brave/Frontend/Widgets/SiteTableViewController.swift +++ b/Sources/Brave/Frontend/Widgets/SiteTableViewController.swift @@ -130,29 +130,3 @@ public class SiteTableViewController: LoadingViewController, UITableViewDelegate true } } - -public class LoadingViewController: UIViewController { - - let spinner = UIActivityIndicatorView().then { - $0.snp.makeConstraints { make in - make.size.equalTo(24) - } - $0.hidesWhenStopped = true - $0.isHidden = true - } - - var isLoading: Bool = false { - didSet { - if isLoading { - view.addSubview(spinner) - spinner.snp.makeConstraints { - $0.center.equalTo(view.snp.center) - } - spinner.startAnimating() - } else { - spinner.stopAnimating() - spinner.removeFromSuperview() - } - } - } -} diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 14f90fc5045..e4283b3566f 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -1036,8 +1036,6 @@ extension Strings { public static let confirm = NSLocalizedString("Confirm", tableName: "BraveShared", bundle: .module, value: "Confirm", comment: "") public static let privacy = NSLocalizedString("Privacy", tableName: "BraveShared", bundle: .module, value: "Privacy", comment: "Settings privacy section title") public static let security = NSLocalizedString("Security", tableName: "BraveShared", bundle: .module, value: "Security", comment: "Settings security section title") - public static let browserLock = NSLocalizedString("BrowserLock", tableName: "BraveShared", bundle: .module, value: "Browser Lock", comment: "Setting to enable the browser lock privacy feature") - public static let browserLockDescription = NSLocalizedString("BrowserLockDescription", tableName: "BraveShared", bundle: .module, value: "Unlock Brave with Touch ID, Face ID or system passcode.", comment: "") public static let saveLogins = NSLocalizedString("SaveLogins", tableName: "BraveShared", bundle: .module, value: "Save Logins", comment: "Setting to enable the built-in password manager") public static let showBookmarkButtonInTopToolbar = NSLocalizedString("ShowBookmarkButtonInTopToolbar", tableName: "BraveShared", bundle: .module, value: "Show Bookmarks Shortcut", comment: "Setting to show a bookmark button on the top level menu that triggers a panel of the user's bookmarks.") public static let alwaysRequestDesktopSite = NSLocalizedString("AlwaysRequestDesktopSite", tableName: "BraveShared", bundle: .module, value: "Always Request Desktop Site", comment: "Setting to always request the desktop version of a website.") @@ -3256,7 +3254,7 @@ extension Strings { "login.syncSetPasscodeAlertDescription", tableName: "BraveShared", bundle: .module, - value: "To setup sync chain or see settings, you must first set a passcode on your device..", + value: "To setup sync chain or see settings, you must first set a passcode on your device.", comment: "The message displayed in alert when a user needs to set a passcode") } } @@ -3298,6 +3296,38 @@ extension Strings { } } +extension Strings { + public struct Privacy { + public static let browserLock = + NSLocalizedString( + "BrowserLock", tableName: "BraveShared", bundle: .module, + value: "Browser Lock", + comment: "Title for setting to enable the browser lock privacy feature") + public static let browserLockDescription = + NSLocalizedString( + "BrowserLockDescription", tableName: "BraveShared", bundle: .module, + value: "Unlock Brave with Touch ID, Face ID or system passcode.", + comment: "Description for setting to enable the browser lock privacy feature") + public static let privateBrowsingLock = + NSLocalizedString( + "privacy.private.browsing.lock.title", tableName: "BraveShared", bundle: .module, + value: "Private Browsing Lock", + comment: "Title for setting to enable the private browsing lock privacy feature") + public static let privateBrowsingLockDescription = + NSLocalizedString( + "privacy.private.browsing.lock.description", tableName: "BraveShared", bundle: .module, + value: "Require Passcode to Unlock Private Browsing", + comment: "Description for setting to enable the browser lock privacy feature") + public static let tabTraySetPasscodeAlertDescription = + NSLocalizedString( + "privacy.tab.tray.passcode.alert", + tableName: "BraveShared", + bundle: .module, + value: "To switch private browsing mode, you must first set a passcode on your device.", + comment: "The message displayed in alert when a user needs to set a passcode") + } +} + extension Strings { public struct Login { public static let loginListEmptyScreenTitle =