From 5fe8c6850f28859d657041be3b107a68280f9f66 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 25 Feb 2026 22:16:08 +0200 Subject: [PATCH 01/27] Unlock by biometrics private tabs feature --- firefox-ios/Client.xcodeproj/project.pbxproj | 8 ++ .../Tabs/Action/PrivatePanelLockAction.swift | 18 +++ .../Browser/Tabs/Action/TabPanelAction.swift | 6 +- .../Middleware/TabManagerMiddleware.swift | 36 +++++ .../Browser/Tabs/State/TabsPanelState.swift | 37 ++++-- .../Views/PrivateTabsLockOverlayView.swift | 118 +++++++++++++++++ .../Views/TabDisplayPanelViewController.swift | 125 ++++++++++++++++++ 7 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift create mode 100644 firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 32353c3a07a16..4df8f7718f0ce 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -778,6 +778,8 @@ 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */; }; 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F80D332A0A52D700013C3D /* PrivacyPolicyViewController.swift */; }; + 767742A92F4F649500DEC06D /* PrivatePanelLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */; }; + 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; 781C19CF2A780BEC0000DF46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 781C19CE2A780BEC0000DF46 /* Common */; }; 787EDD852943EE75002B93AE /* JumpBackInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787EDD832943EE75002B93AE /* JumpBackInTests.swift */; }; 789A0B232E2E969D004547CE /* FxUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5423F62DF6EF17000AA578 /* FxUserState.swift */; }; @@ -8764,6 +8766,8 @@ 764643DCB449658AAA8ED829 /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/InfoPlist.strings; sourceTree = ""; }; 765243D790B967A69BD5DCF7 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/ErrorPages.strings; sourceTree = ""; }; 766F49788D099E2201B74791 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Intro.strings; sourceTree = ""; }; + 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivatePanelLockAction.swift; sourceTree = ""; }; + 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateTabsLockOverlayView.swift; sourceTree = ""; }; 76784C13A069B829DCA22A6C /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/LoginManager.strings; sourceTree = ""; }; 7681461D8ADDD3E9662A2A53 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Shared.strings; sourceTree = ""; }; 7693448FA2E2865EA28005EE /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/ErrorPages.strings; sourceTree = ""; }; @@ -12100,6 +12104,7 @@ 219914042AF963F900153598 /* TabTrayAction.swift */, 8A505BA42D9198900070EB44 /* TopTabsAction.swift */, 219935E82B070F9000E5966F /* TabPanelAction.swift */, + 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */, 5A9F833F2B2B4AE800272819 /* TabPeekAction.swift */, ); path = Action; @@ -12150,6 +12155,7 @@ 1DDE3DB22AC34E1E0039363B /* TabCell.swift */, F605DD572CC73469009A671B /* TabDisplayDiffableDataSource.swift */, 213BF7522AC21D1B00C53A64 /* TabDisplayPanelViewController.swift */, + 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */, 214EF4142AC5D5D0005BCCDA /* TabDisplayView.swift */, 5A679E4A2B239FAE004F2B0D /* TabPeekViewController.swift */, 8A06DF5E2DE0C0EB007B7E9D /* TabTitleSupplementaryView.swift */, @@ -18706,6 +18712,7 @@ 8AC6F2242D6E243F00D10A9F /* ExperimentEmptyPrivateTabsView.swift in Sources */, 1D0BA05C24F46A0400D731B5 /* TopSitesProvider.swift in Sources */, A093CD292F35408E0017774C /* MozAdsClientFactory.swift in Sources */, + 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */, DFACBF7F277B5F7B003D5F41 /* LegacyWallpaperBackgroundView.swift in Sources */, 8A7D08E32CAAF7C30035999C /* HomepageViewController.swift in Sources */, D01017F5219CB6BD009CBB5A /* DownloadContentScript.swift in Sources */, @@ -19511,6 +19518,7 @@ E13C072D2C2189B80087E404 /* ToolbarActionConfiguration.swift in Sources */, C24B31292E86B8470049134A /* GenericSelectableItemCellView.swift in Sources */, 43175DB826B87D2C00C41C31 /* AdsTelemetryHelper.swift in Sources */, + 767742A92F4F649500DEC06D /* PrivatePanelLockAction.swift in Sources */, E58368AA287D632F0087A449 /* StoryProvider.swift in Sources */, 8A3EF7FF2A2FCFBB00796E3A /* ChangeToChinaSetting.swift in Sources */, 1D558A5A2BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift in Sources */, diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift new file mode 100644 index 0000000000000..bd35e9f838cde --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift @@ -0,0 +1,18 @@ +// 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 Common +import Redux + +enum PrivatePanelLockActionType: ActionType { + case enteredPrivatePanel + case requestAuth + case authSucceeded + case authFailed +} + +struct PrivatePanelLockAction: Action { + let windowUUID: WindowUUID + let actionType: ActionType +} diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift index f4842a13e1e72..c7b3bf8051dde 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift @@ -72,17 +72,20 @@ struct TabPanelMiddlewareAction: Action { let tabDisplayModel: TabDisplayModel? let toastType: ToastType?? let scrollBehavior: TabScrollBehavior? + let privatePanelLockState: PrivatePanelLockState? init(tabDisplayModel: TabDisplayModel? = nil, toastType: ToastType? = nil, scrollBehavior: TabScrollBehavior? = nil, windowUUID: WindowUUID, - actionType: ActionType) { + actionType: ActionType, + privatePanelLockState: PrivatePanelLockState? = nil) { self.windowUUID = windowUUID self.actionType = actionType self.tabDisplayModel = tabDisplayModel self.toastType = toastType self.scrollBehavior = scrollBehavior + self.privatePanelLockState = privatePanelLockState } } @@ -93,6 +96,7 @@ enum TabPanelMiddlewareActionType: ActionType { case refreshTabs case showToast case scrollToTab + case setPrivatePanelLockState } struct ScreenshotAction: Action { diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift index 68d9da9bd44ca..fa915b5eec30a 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift @@ -67,6 +67,8 @@ final class TabManagerMiddleware: FeatureFlaggable, self.resolveTabTrayActions(action: action, state: state) } else if let action = action as? TabPanelViewAction { self.resolveTabPanelViewActions(action: action, state: state) + } else if let action = action as? PrivatePanelLockAction { + self.resolveTabPrivatePanelLockActions(action: action, state: state) } else if let action = action as? MainMenuAction { self.resolveMainMenuActions(with: action, appState: state) } else if let action = action as? ScreenshotAction { @@ -185,6 +187,40 @@ final class TabManagerMiddleware: FeatureFlaggable, } } + private func resolveTabPrivatePanelLockActions(action: PrivatePanelLockAction, state: AppState) { + guard let tabsState = state.screenState(TabsPanelState.self, for: .tabsPanel, window: action.windowUUID), + tabsState.isPrivateMode else { return } + + switch action.actionType { + case PrivatePanelLockActionType.enteredPrivatePanel: + store.dispatch(TabPanelMiddlewareAction( + windowUUID: action.windowUUID, + actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, + privatePanelLockState: .lockedPrompt + )) + case PrivatePanelLockActionType.requestAuth: + store.dispatch(TabPanelMiddlewareAction( + windowUUID: action.windowUUID, + actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, + privatePanelLockState: .authenticating + )) + case PrivatePanelLockActionType.authSucceeded: + store.dispatch(TabPanelMiddlewareAction( + windowUUID: action.windowUUID, + actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, + privatePanelLockState: .unlocked + )) + case PrivatePanelLockActionType.authFailed: + store.dispatch(TabPanelMiddlewareAction( + windowUUID: action.windowUUID, + actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, + privatePanelLockState: .failed + )) + default: + break + } + } + private func resolveTabPanelViewActions(action: TabPanelViewAction, state: AppState) { switch action.actionType { case TabPanelViewActionType.tabPanelDidLoad: diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index 03f19d22d982b..dbd24b6edd4a2 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -6,7 +6,15 @@ import Foundation import Redux import Common +enum PrivatePanelLockState: Equatable { + case unlocked + case lockedPrompt + case authenticating + case failed +} + struct TabsPanelState: ScreenState, Equatable { + struct ScrollState: Equatable { let toIndex: Int let withAnimation: Bool @@ -18,6 +26,7 @@ struct TabsPanelState: ScreenState, Equatable { var scrollState: ScrollState? var didTapAddTab: Bool var urlRequest: URLRequest? + var privateLockState: PrivatePanelLockState var isPrivateTabsEmpty: Bool { guard isPrivateMode else { return true } @@ -39,7 +48,8 @@ struct TabsPanelState: ScreenState, Equatable { tabs: panelState.tabs, scrollState: panelState.scrollState, didTapAddTab: panelState.didTapAddTab, - urlRequest: panelState.urlRequest) + urlRequest: panelState.urlRequest, + privateLockState: panelState.privateLockState) } init(windowUUID: WindowUUID, isPrivateMode: Bool = false) { @@ -59,13 +69,15 @@ struct TabsPanelState: ScreenState, Equatable { toastType: ToastType? = nil, scrollState: ScrollState? = nil, didTapAddTab: Bool = false, - urlRequest: URLRequest? = nil) { + urlRequest: URLRequest? = nil, + privateLockState: PrivatePanelLockState = .unlocked) { self.isPrivateMode = isPrivateMode self.tabs = tabs self.windowUUID = windowUUID self.scrollState = scrollState self.didTapAddTab = didTapAddTab self.urlRequest = urlRequest + self.privateLockState = privateLockState } static let reducer: Reducer = { state, action in @@ -90,7 +102,8 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: tabsModel.isPrivateMode, - tabs: tabsModel.tabs) + tabs: tabsModel.tabs, + privateLockState: state.privateLockState) case TabPanelMiddlewareActionType.willAppearTabPanel: let scrollModel = createTabScrollBehavior( @@ -100,13 +113,15 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, tabs: state.tabs, - scrollState: scrollModel) + scrollState: scrollModel, + privateLockState: state.privateLockState) case TabPanelMiddlewareActionType.refreshTabs: guard let tabModel = action.tabDisplayModel else { return defaultState(from: state) } return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, - tabs: tabModel.tabs) + tabs: tabModel.tabs, + privateLockState: state.privateLockState) case TabPanelMiddlewareActionType.scrollToTab: guard let scrollBehavior = action.scrollBehavior else { return defaultState(from: state) } @@ -114,8 +129,13 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, tabs: state.tabs, - scrollState: scrollModel) - + scrollState: scrollModel, + privateLockState: state.privateLockState) + case TabPanelMiddlewareActionType.setPrivatePanelLockState: + guard state.isPrivateMode, let lock = action.privatePanelLockState else { return state } + var state = state + state.privateLockState = lock + return state default: return defaultState(from: state) } @@ -124,7 +144,8 @@ struct TabsPanelState: ScreenState, Equatable { static func defaultState(from state: TabsPanelState) -> TabsPanelState { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, - tabs: state.tabs) + tabs: state.tabs, + privateLockState: state.privateLockState) } static func createTabScrollBehavior( diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift new file mode 100644 index 0000000000000..2f758cc214d9a --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -0,0 +1,118 @@ +// 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 + +final class PrivateTabsLockOverlayView: UIView { + + enum Mode: Equatable { + case prompt + case authenticating + case failed + } + + var onUnlockTapped: (() -> Void)? + var onRetryTapped: (() -> Void)? + + private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + private let spinner = UIActivityIndicatorView(style: .large) + private let unlockButton = UIButton(type: .system) + private let retryButton = UIButton(type: .system) + private let stack = UIStackView() + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + translatesAutoresizingMaskIntoConstraints = false + + blurView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurView) + NSLayoutConstraint.activate([ + blurView.topAnchor.constraint(equalTo: topAnchor), + blurView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: trailingAnchor), + blurView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + titleLabel.font = .preferredFont(forTextStyle: .title2) + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + titleLabel.textColor = .white + titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + titleLabel.text = "Private Tabs Locked" + + subtitleLabel.font = .preferredFont(forTextStyle: .body) + subtitleLabel.textAlignment = .center + subtitleLabel.numberOfLines = 0 + subtitleLabel.textColor = .white.withAlphaComponent(0.85) + subtitleLabel.text = "Unlock with Face ID to view private tabs." + + spinner.hidesWhenStopped = true + + unlockButton.setTitle("Unlock", for: .normal) + unlockButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + unlockButton.addTarget(self, action: #selector(unlockTapped), for: .touchUpInside) + + retryButton.setTitle("Try Again", for: .normal) + retryButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside) + + stack.axis = .vertical + stack.alignment = .fill + stack.distribution = .fill + stack.spacing = 12 + stack.translatesAutoresizingMaskIntoConstraints = false + + stack.addArrangedSubview(titleLabel) + stack.addArrangedSubview(subtitleLabel) + stack.addArrangedSubview(spinner) + stack.addArrangedSubview(unlockButton) + stack.addArrangedSubview(retryButton) + + addSubview(stack) + + NSLayoutConstraint.activate([ + stack.centerXAnchor.constraint(equalTo: centerXAnchor), + stack.centerYAnchor.constraint(equalTo: centerYAnchor), + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + apply(mode: .prompt) + } + + func apply(mode: Mode) { + switch mode { + case .prompt: + spinner.stopAnimating() + unlockButton.isHidden = false + retryButton.isHidden = true + subtitleLabel.text = "Unlock with Face ID to view private tabs" + + case .authenticating: + spinner.startAnimating() + unlockButton.isHidden = true + retryButton.isHidden = true + subtitleLabel.text = "Authenticating…" + + case .failed: + spinner.stopAnimating() + unlockButton.isHidden = true + retryButton.isHidden = false + subtitleLabel.text = "Face ID failed. Try again." + } + } + + @objc private func unlockTapped() { onUnlockTapped?() } + @objc private func retryTapped() { onRetryTapped?() } +} diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index cefbc2fb90b74..671fc2576c1ae 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -5,6 +5,7 @@ import Common import Redux import UIKit +import LocalAuthentication protocol TabTrayThemeable { @MainActor @@ -122,6 +123,9 @@ final class TabDisplayPanelViewController: UIViewController, actionType: TabPanelViewActionType.tabPanelWillAppear)) viewHasAppeared = true } + if panelType == .privateTabs { + store.dispatch(PrivatePanelLockAction(windowUUID: windowUUID, actionType: PrivatePanelLockActionType.enteredPrivatePanel)) + } updateInsets() } @@ -164,6 +168,14 @@ final class TabDisplayPanelViewController: UIViewController, backgroundPrivacyOverlay.isHidden = true setupEmptyView() setupFadeView() + + view.addSubview(privateLockOverlay) + NSLayoutConstraint.activate([ + privateLockOverlay.topAnchor.constraint(equalTo: view.topAnchor), + privateLockOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor), + privateLockOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor), + privateLockOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) } private func setupEmptyView() { @@ -194,6 +206,101 @@ final class TabDisplayPanelViewController: UIViewController, view.subviews.forEach { $0.removeFromSuperview() } view.removeFromSuperview() } + + private lazy var privateLockOverlay: PrivateTabsLockOverlayView = { + let overlay = PrivateTabsLockOverlayView() + overlay.isHidden = true + overlay.onUnlockTapped = { [weak self] in self?.startPrivateAuthFlow() } + overlay.onRetryTapped = { [weak self] in self?.startPrivateAuthFlow() } + return overlay + }() + + private func startPrivateAuthFlow() { + store.dispatch( + PrivatePanelLockAction( + windowUUID: windowUUID, + actionType: PrivatePanelLockActionType.requestAuth + ) + ) + + let context = LAContext() + context.localizedCancelTitle = "Cancel" + + var error: NSError? + + // ВАЖЛИВО: перевіряємо ту саму policy + guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { + store.dispatch( + PrivatePanelLockAction( + windowUUID: windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + return + } + + context.evaluatePolicy( + .deviceOwnerAuthentication, + localizedReason: "Unlock your private tabs." + ) { [weak self] success, authError in + guard let self else { return } + + Task { @MainActor in + if success { + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authSucceeded + ) + ) + } else { + if let laError = authError as? LAError { + switch laError.code { + + case .userCancel, .systemCancel, .appCancel: + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + + case .authenticationFailed: + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + + case .biometryLockout: + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + + default: + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + } + } else { + store.dispatch( + PrivatePanelLockAction( + windowUUID: self.windowUUID, + actionType: PrivatePanelLockActionType.authFailed + ) + ) + } + } + } + } + } // MARK: - Themeable @@ -344,6 +451,7 @@ final class TabDisplayPanelViewController: UIViewController, if panelType == .privateTabs, tabsState.isPrivateMode { // Only adjust the empty view if we are in private mode shouldShowEmptyView(tabsState.isPrivateTabsEmpty) + applyPrivateLockUI(tabsState.privateLockState) } shouldShowFadeView() @@ -351,6 +459,23 @@ final class TabDisplayPanelViewController: UIViewController, applyTheme() } } + + private func applyPrivateLockUI(_ lock: PrivatePanelLockState) { + switch lock { + case .unlocked: + privateLockOverlay.isHidden = true + privateLockOverlay.apply(mode: .prompt) + case .lockedPrompt: + privateLockOverlay.isHidden = false + privateLockOverlay.apply(mode: .prompt) + case .authenticating: + privateLockOverlay.isHidden = false + privateLockOverlay.apply(mode: .authenticating) + case .failed: + privateLockOverlay.isHidden = false + privateLockOverlay.apply(mode: .failed) + } + } // MARK: - EmptyPrivateTabsViewDelegate From 0e381b91ede593aea3434c9a48367baf2abc2c1b Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 25 Feb 2026 22:51:47 +0200 Subject: [PATCH 02/27] added settings --- BrowserKit/Sources/Shared/Prefs.swift | 1 + .../Tabs/Middleware/TabManagerMiddleware.swift | 3 ++- .../Main/AppSettingsTableViewController.swift | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/BrowserKit/Sources/Shared/Prefs.swift b/BrowserKit/Sources/Shared/Prefs.swift index a25071b4476ae..9522c104f7113 100644 --- a/BrowserKit/Sources/Shared/Prefs.swift +++ b/BrowserKit/Sources/Shared/Prefs.swift @@ -165,6 +165,7 @@ public struct PrefsKeys { // Firefox settings public struct Settings { public static let closePrivateTabs = "ClosePrivateTabs" + public static let lockPrivateTabs = "LockPrivateTabs" public static let sentFromFirefoxWhatsApp = "SentFromFirefoxWhatsApp" public static let navigationToolbarMiddleButton = "settings.navigationToolbarMiddleButton" public static let translationsFeature = "settings.translationFeature" diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift index fa915b5eec30a..9440f4e0de44a 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift @@ -188,7 +188,8 @@ final class TabManagerMiddleware: FeatureFlaggable, } private func resolveTabPrivatePanelLockActions(action: PrivatePanelLockAction, state: AppState) { - guard let tabsState = state.screenState(TabsPanelState.self, for: .tabsPanel, window: action.windowUUID), + let shouldLock = profile.prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false + guard shouldLock, let tabsState = state.screenState(TabsPanelState.self, for: .tabsPanel, window: action.windowUUID), tabsState.isPrivateMode else { return } switch action.actionType { diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index eec13a9e8d0eb..cf27af5948eb0 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -435,6 +435,19 @@ class AppSettingsTableViewController: SettingsTableViewController, store.dispatch(action) } ) + + privacySettings.append( + BoolSetting( + prefs: profile.prefs, + theme: themeManager.getCurrentTheme(for: windowUUID), + prefKey: PrefsKeys.Settings.lockPrivateTabs, + defaultValue: false, + titleText: "Lock Private Tabs", + statusText: "Use Biometrics or Passcode to see Private Tabs" + ) { _ in + + } + ) } privacySettings.append(ContentBlockerSetting(settings: self, settingsDelegate: parentCoordinator)) From a3fcfdead30bfc7107d4560c279521b4ae84b455 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 4 Mar 2026 22:40:40 +0200 Subject: [PATCH 03/27] moved to new source of truth BrowserViewControllerState --- firefox-ios/Client.xcodeproj/project.pbxproj | 20 ++- .../Client/Configuration/version.xcconfig | 2 +- .../Actions/PrivateLockAction.swift | 33 +++++ .../Middleware/PrivateLockMiddleware.swift | 88 ++++++++++++ .../State/BrowserViewControllerState.swift | 127 +++++++++++++----- .../Tabs/Action/PrivatePanelLockAction.swift | 18 --- .../Browser/Tabs/Action/TabPanelAction.swift | 6 +- .../Middleware/TabManagerMiddleware.swift | 37 ----- .../Browser/Tabs/State/TabsPanelState.swift | 37 ++--- .../Views/TabDisplayPanelViewController.swift | 89 +----------- .../Client/Redux/GlobalState/AppState.swift | 3 +- 11 files changed, 250 insertions(+), 210 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift create mode 100644 firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift delete mode 100644 firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 4df8f7718f0ce..69d711bfc35b7 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -778,8 +778,9 @@ 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */; }; 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F80D332A0A52D700013C3D /* PrivacyPolicyViewController.swift */; }; - 767742A92F4F649500DEC06D /* PrivatePanelLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */; }; + 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivateLockAction.swift */; }; 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; + 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */; }; 781C19CF2A780BEC0000DF46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 781C19CE2A780BEC0000DF46 /* Common */; }; 787EDD852943EE75002B93AE /* JumpBackInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787EDD832943EE75002B93AE /* JumpBackInTests.swift */; }; 789A0B232E2E969D004547CE /* FxUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5423F62DF6EF17000AA578 /* FxUserState.swift */; }; @@ -8766,11 +8767,12 @@ 764643DCB449658AAA8ED829 /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/InfoPlist.strings; sourceTree = ""; }; 765243D790B967A69BD5DCF7 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/ErrorPages.strings; sourceTree = ""; }; 766F49788D099E2201B74791 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Intro.strings; sourceTree = ""; }; - 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivatePanelLockAction.swift; sourceTree = ""; }; + 767742A82F4F649500DEC06D /* PrivateLockAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateLockAction.swift; sourceTree = ""; }; 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateTabsLockOverlayView.swift; sourceTree = ""; }; 76784C13A069B829DCA22A6C /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/LoginManager.strings; sourceTree = ""; }; 7681461D8ADDD3E9662A2A53 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Shared.strings; sourceTree = ""; }; 7693448FA2E2865EA28005EE /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/ErrorPages.strings; sourceTree = ""; }; + 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateLockMiddleware.swift; sourceTree = ""; }; 772744E9858179A908A070DB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ClearPrivateData.strings; sourceTree = ""; }; 77294827911067CD5994B5FD /* co */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = co; path = co.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; 779D4B8EAE580D7B2B55EC3F /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/HistoryPanel.strings; sourceTree = ""; }; @@ -12104,7 +12106,6 @@ 219914042AF963F900153598 /* TabTrayAction.swift */, 8A505BA42D9198900070EB44 /* TopTabsAction.swift */, 219935E82B070F9000E5966F /* TabPanelAction.swift */, - 767742A82F4F649500DEC06D /* PrivatePanelLockAction.swift */, 5A9F833F2B2B4AE800272819 /* TabPeekAction.swift */, ); path = Action; @@ -12869,6 +12870,7 @@ 5A2918CA2B522338002B197E /* GeneralBrowserAction.swift */, 7ADC1D182C27D35B003ED924 /* WebContextMenuActionsProvider.swift */, 8A8D277C2CC000BE0076AD3A /* NavigationBrowserAction.swift */, + 767742A82F4F649500DEC06D /* PrivateLockAction.swift */, ); path = Actions; sourceTree = ""; @@ -12992,6 +12994,14 @@ path = NativeErrorPage; sourceTree = ""; }; + 76AFB6612F58B1590064E6AC /* Middleware */ = { + isa = PBXGroup; + children = ( + 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */, + ); + path = Middleware; + sourceTree = ""; + }; 7AC7E04E2C160EB600051D4D /* Reader */ = { isa = PBXGroup; children = ( @@ -15272,6 +15282,7 @@ 21365E382F30FD580000C369 /* BrowserViewControllerLayoutManager.swift */, 0BBFBF232DFC201300160911 /* WebEngineIntegration */, 5A2918C92B522326002B197E /* Actions */, + 76AFB6612F58B1590064E6AC /* Middleware */, 81122E1F2B2219AA003DD9F8 /* Views */, 81122E1E2B2219A0003DD9F8 /* State */, 81122E1D2B221998003DD9F8 /* Extensions */, @@ -19518,7 +19529,7 @@ E13C072D2C2189B80087E404 /* ToolbarActionConfiguration.swift in Sources */, C24B31292E86B8470049134A /* GenericSelectableItemCellView.swift in Sources */, 43175DB826B87D2C00C41C31 /* AdsTelemetryHelper.swift in Sources */, - 767742A92F4F649500DEC06D /* PrivatePanelLockAction.swift in Sources */, + 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */, E58368AA287D632F0087A449 /* StoryProvider.swift in Sources */, 8A3EF7FF2A2FCFBB00796E3A /* ChangeToChinaSetting.swift in Sources */, 1D558A5A2BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift in Sources */, @@ -19536,6 +19547,7 @@ E13C07292C217D700087E404 /* NavigationBarState.swift in Sources */, 59A68FD5260B8D520F890F4A /* ReaderPanel.swift in Sources */, 0A93C8AA2C87070300BEA143 /* TrackingProtectionConnectionStatusView.swift in Sources */, + 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */, 39BD570E2E53F3ED00CA1317 /* MerinoFeedFetcher.swift in Sources */, C849E46126B9C39B00260F0B /* EnhancedTrackingProtectionVC.swift in Sources */, 8AA0A6632CAC40AA00AC7EB3 /* HomepageDiffableDataSource.swift in Sources */, diff --git a/firefox-ios/Client/Configuration/version.xcconfig b/firefox-ios/Client/Configuration/version.xcconfig index 0b98011590d5e..93280e46bf6cd 100644 --- a/firefox-ios/Client/Configuration/version.xcconfig +++ b/firefox-ios/Client/Configuration/version.xcconfig @@ -1 +1 @@ -APP_VERSION = 149.0 \ No newline at end of file +APP_VERSION = 148.3 diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift new file mode 100644 index 0000000000000..2222f559c8d2f --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -0,0 +1,33 @@ +// 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 Common +import Redux + +enum PrivateLockActionType: ActionType { + case enteredPrivatePanel + case requestAuth(String) +} + +struct PrivateLockAction: Action { + let windowUUID: WindowUUID + let actionType: ActionType +} + +enum PrivateLockMiddlewareActionType: ActionType { + case setPrivateLockState +} + +struct PrivateLockMiddlewareAction: Action { + let windowUUID: WindowUUID + let actionType: ActionType + let privatePanelLockState: PrivateLockState +} + +enum PrivateLockState: Equatable { + case unlocked + case lockedPrompt + case authenticating + case failed +} diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift new file mode 100644 index 0000000000000..0f2694d029aec --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -0,0 +1,88 @@ +// 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 Common +import Redux +import Shared +import LocalAuthentication + +@MainActor +final class PrivateLockMiddleware { + + private let prefs: Prefs + + init(profile: Profile = AppContainer.shared.resolve()) { + prefs = profile.prefs + } + + lazy var lockProvider: Middleware = { state, action in + if let action = action as? PrivateLockAction { + self.resolveTabPrivateLockActions(action: action, state: state) + } + } + + private func resolveTabPrivateLockActions(action: PrivateLockAction, state: AppState) { + let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false + guard shouldLock else { return } + + switch action.actionType { + case PrivateLockActionType.enteredPrivatePanel: + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: .lockedPrompt + )) + case PrivateLockActionType.requestAuth(let reason): + let browserState = state.screenState(BrowserViewControllerState.self, for: .browserViewController, window: action.windowUUID) + guard browserState?.privateLockState != PrivateLockState.authenticating else { return } + auth(reason: reason, windowUUID: action.windowUUID) + default: + break + } + } + + private func auth(reason: String, windowUUID: WindowUUID) { + + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: .authenticating + )) + + let context = LAContext() + context.localizedCancelTitle = "Cancel" + + var error: NSError? + + guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { + Self.authFailed(windowUUID: windowUUID) + return + } + + context.evaluatePolicy( + .deviceOwnerAuthentication, + localizedReason: reason + ) { success, authError in + Task { @MainActor in + if success { + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: .unlocked + )) + } else { + Self.authFailed(windowUUID: windowUUID) + } + } + } + } + + private static func authFailed(windowUUID: WindowUUID) { + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: .failed + )) + } +} diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index f7093ffc38e44..38c06f4867a39 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -51,6 +51,7 @@ struct BrowserViewControllerState: ScreenState { var frameContext: PasswordGeneratorFrameContext? var microsurveyState: MicrosurveyPromptState var navigationDestination: NavigationDestination? + var privateLockState: PrivateLockState init(appState: AppState, uuid: WindowUUID) { guard let bvcState = appState.screenState( @@ -74,7 +75,8 @@ struct BrowserViewControllerState: ScreenState { buttonTapped: bvcState.buttonTapped, frameContext: bvcState.frameContext, microsurveyState: bvcState.microsurveyState, - navigationDestination: bvcState.navigationDestination) + navigationDestination: bvcState.navigationDestination, + privateLockState: bvcState.privateLockState) } init(windowUUID: WindowUUID) { @@ -104,7 +106,8 @@ struct BrowserViewControllerState: ScreenState { buttonTapped: UIButton? = nil, frameContext: PasswordGeneratorFrameContext? = nil, microsurveyState: MicrosurveyPromptState, - navigationDestination: NavigationDestination? = nil + navigationDestination: NavigationDestination? = nil, + privateLockState: PrivateLockState = .unlocked ) { self.searchScreenState = searchScreenState self.toast = toast @@ -119,6 +122,7 @@ struct BrowserViewControllerState: ScreenState { self.frameContext = frameContext self.microsurveyState = microsurveyState self.navigationDestination = navigationDestination + self.privateLockState = privateLockState } static let reducer: Reducer = { state, action in @@ -141,6 +145,8 @@ struct BrowserViewControllerState: ScreenState { return reduceStateForToolbarAction(action: action, state: state) } else if let action = action as? SummarizeAction { return reduceStateForSummarizeAction(action: action, state: state) + } else if let action = action as? PrivateLockMiddlewareAction { + return reduceStateForPrivateLockAction(action: action, state: state) } else { return BrowserViewControllerState( searchScreenState: state.searchScreenState, @@ -149,7 +155,8 @@ struct BrowserViewControllerState: ScreenState { shouldStartAtHome: false, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - navigationDestination: nil) + navigationDestination: nil, + privateLockState: state.privateLockState) } } @@ -180,7 +187,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - navigationDestination: action.navigationDestination + navigationDestination: action.navigationDestination, + privateLockState: state.privateLockState ) default: return passthroughState(from: state, action: action) @@ -214,12 +222,29 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - navigationDestination: NavigationDestination(.summarizer(config: action.summarizerConfig)) + navigationDestination: NavigationDestination(.summarizer(config: action.summarizerConfig)), + privateLockState: state.privateLockState ) default: return passthroughState(from: state, action: action) } } + + @MainActor + static func reduceStateForPrivateLockAction( + action: PrivateLockMiddlewareAction, + state: BrowserViewControllerState + ) -> BrowserViewControllerState { + switch action.actionType { + case PrivateLockMiddlewareActionType.setPrivateLockState: +// guard state.pri + var newState = state + newState.privateLockState = action.privatePanelLockState + return newState + default: + return state + } + } // MARK: - Toolbar Action @@ -247,7 +272,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - navigationDestination: NavigationDestination(.homepageZeroSearch) + navigationDestination: NavigationDestination(.homepageZeroSearch), + privateLockState: state.privateLockState ) default: return passthroughState(from: state, action: action) @@ -282,7 +308,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - navigationDestination: NavigationDestination(.zeroSearch) + navigationDestination: NavigationDestination(.zeroSearch), + privateLockState: state.privateLockState ) } @@ -293,7 +320,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -365,7 +393,9 @@ struct BrowserViewControllerState: ScreenState { toast: toastType, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState + ) } @MainActor @@ -377,7 +407,8 @@ struct BrowserViewControllerState: ScreenState { showOverlay: showOverlay, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -389,7 +420,9 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .home, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState + ) } @MainActor @@ -401,7 +434,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .newTab, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -413,7 +447,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .backForwardList, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -427,7 +462,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .trackingProtectionDetails, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -440,7 +476,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .menu, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -452,7 +489,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .tabsLongPressActions, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -465,7 +503,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .reloadLongPressAction, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -478,7 +517,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .locationViewLongPressAction, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -490,7 +530,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .back, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -502,7 +543,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .forward, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -514,7 +556,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .tabTray, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -526,7 +569,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .reload, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -538,7 +582,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .reloadNoCache, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -550,7 +595,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, navigateTo: .stopLoading, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -563,7 +609,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .share, buttonTapped: action.buttonTapped, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -575,7 +622,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .readerMode, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -587,7 +635,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .newTabLongPressActions, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -599,7 +648,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .readerModeLongPressAction, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -610,7 +660,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .dataClearance, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -622,7 +673,8 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .passwordGenerator, frameContext: action.frameContext, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -633,7 +685,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, displayView: .summarizer(config: action.summarizerConfig), - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -647,7 +700,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: microsurveyState + microsurveyState: microsurveyState, + privateLockState: state.privateLockState ) } @@ -657,7 +711,8 @@ struct BrowserViewControllerState: ScreenState { searchScreenState: state.searchScreenState, windowUUID: state.windowUUID, browserViewType: state.browserViewType, - microsurveyState: microsurveyState + microsurveyState: microsurveyState, + privateLockState: state.privateLockState ) } @@ -679,7 +734,8 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, reloadWebView: true, browserViewType: browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } @MainActor @@ -692,6 +748,7 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, shouldStartAtHome: action.shouldStartAtHome ?? false, browserViewType: state.browserViewType, - microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action)) + microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), + privateLockState: state.privateLockState) } } diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift deleted file mode 100644 index bd35e9f838cde..0000000000000 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Action/PrivatePanelLockAction.swift +++ /dev/null @@ -1,18 +0,0 @@ -// 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 Common -import Redux - -enum PrivatePanelLockActionType: ActionType { - case enteredPrivatePanel - case requestAuth - case authSucceeded - case authFailed -} - -struct PrivatePanelLockAction: Action { - let windowUUID: WindowUUID - let actionType: ActionType -} diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift index c7b3bf8051dde..f4842a13e1e72 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Action/TabPanelAction.swift @@ -72,20 +72,17 @@ struct TabPanelMiddlewareAction: Action { let tabDisplayModel: TabDisplayModel? let toastType: ToastType?? let scrollBehavior: TabScrollBehavior? - let privatePanelLockState: PrivatePanelLockState? init(tabDisplayModel: TabDisplayModel? = nil, toastType: ToastType? = nil, scrollBehavior: TabScrollBehavior? = nil, windowUUID: WindowUUID, - actionType: ActionType, - privatePanelLockState: PrivatePanelLockState? = nil) { + actionType: ActionType) { self.windowUUID = windowUUID self.actionType = actionType self.tabDisplayModel = tabDisplayModel self.toastType = toastType self.scrollBehavior = scrollBehavior - self.privatePanelLockState = privatePanelLockState } } @@ -96,7 +93,6 @@ enum TabPanelMiddlewareActionType: ActionType { case refreshTabs case showToast case scrollToTab - case setPrivatePanelLockState } struct ScreenshotAction: Action { diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift index 9440f4e0de44a..68d9da9bd44ca 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Middleware/TabManagerMiddleware.swift @@ -67,8 +67,6 @@ final class TabManagerMiddleware: FeatureFlaggable, self.resolveTabTrayActions(action: action, state: state) } else if let action = action as? TabPanelViewAction { self.resolveTabPanelViewActions(action: action, state: state) - } else if let action = action as? PrivatePanelLockAction { - self.resolveTabPrivatePanelLockActions(action: action, state: state) } else if let action = action as? MainMenuAction { self.resolveMainMenuActions(with: action, appState: state) } else if let action = action as? ScreenshotAction { @@ -187,41 +185,6 @@ final class TabManagerMiddleware: FeatureFlaggable, } } - private func resolveTabPrivatePanelLockActions(action: PrivatePanelLockAction, state: AppState) { - let shouldLock = profile.prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false - guard shouldLock, let tabsState = state.screenState(TabsPanelState.self, for: .tabsPanel, window: action.windowUUID), - tabsState.isPrivateMode else { return } - - switch action.actionType { - case PrivatePanelLockActionType.enteredPrivatePanel: - store.dispatch(TabPanelMiddlewareAction( - windowUUID: action.windowUUID, - actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, - privatePanelLockState: .lockedPrompt - )) - case PrivatePanelLockActionType.requestAuth: - store.dispatch(TabPanelMiddlewareAction( - windowUUID: action.windowUUID, - actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, - privatePanelLockState: .authenticating - )) - case PrivatePanelLockActionType.authSucceeded: - store.dispatch(TabPanelMiddlewareAction( - windowUUID: action.windowUUID, - actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, - privatePanelLockState: .unlocked - )) - case PrivatePanelLockActionType.authFailed: - store.dispatch(TabPanelMiddlewareAction( - windowUUID: action.windowUUID, - actionType: TabPanelMiddlewareActionType.setPrivatePanelLockState, - privatePanelLockState: .failed - )) - default: - break - } - } - private func resolveTabPanelViewActions(action: TabPanelViewAction, state: AppState) { switch action.actionType { case TabPanelViewActionType.tabPanelDidLoad: diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index dbd24b6edd4a2..50b7ca1301992 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -6,13 +6,6 @@ import Foundation import Redux import Common -enum PrivatePanelLockState: Equatable { - case unlocked - case lockedPrompt - case authenticating - case failed -} - struct TabsPanelState: ScreenState, Equatable { struct ScrollState: Equatable { @@ -26,7 +19,7 @@ struct TabsPanelState: ScreenState, Equatable { var scrollState: ScrollState? var didTapAddTab: Bool var urlRequest: URLRequest? - var privateLockState: PrivatePanelLockState + var privateLockState: PrivateLockState var isPrivateTabsEmpty: Bool { guard isPrivateMode else { return true } @@ -42,14 +35,16 @@ struct TabsPanelState: ScreenState, Equatable { self.init(windowUUID: uuid) return } - + + let browserState = appState.screenState(BrowserViewControllerState.self, for: .browserViewController, window: uuid) + let privateLockState = browserState?.privateLockState ?? .unlocked self.init(windowUUID: panelState.windowUUID, isPrivateMode: panelState.isPrivateMode, tabs: panelState.tabs, scrollState: panelState.scrollState, didTapAddTab: panelState.didTapAddTab, urlRequest: panelState.urlRequest, - privateLockState: panelState.privateLockState) + privateLockState: privateLockState) } init(windowUUID: WindowUUID, isPrivateMode: Bool = false) { @@ -70,7 +65,7 @@ struct TabsPanelState: ScreenState, Equatable { scrollState: ScrollState? = nil, didTapAddTab: Bool = false, urlRequest: URLRequest? = nil, - privateLockState: PrivatePanelLockState = .unlocked) { + privateLockState: PrivateLockState = .unlocked) { self.isPrivateMode = isPrivateMode self.tabs = tabs self.windowUUID = windowUUID @@ -102,8 +97,7 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: tabsModel.isPrivateMode, - tabs: tabsModel.tabs, - privateLockState: state.privateLockState) + tabs: tabsModel.tabs) case TabPanelMiddlewareActionType.willAppearTabPanel: let scrollModel = createTabScrollBehavior( @@ -113,15 +107,13 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, tabs: state.tabs, - scrollState: scrollModel, - privateLockState: state.privateLockState) + scrollState: scrollModel) case TabPanelMiddlewareActionType.refreshTabs: guard let tabModel = action.tabDisplayModel else { return defaultState(from: state) } return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, - tabs: tabModel.tabs, - privateLockState: state.privateLockState) + tabs: tabModel.tabs) case TabPanelMiddlewareActionType.scrollToTab: guard let scrollBehavior = action.scrollBehavior else { return defaultState(from: state) } @@ -129,13 +121,7 @@ struct TabsPanelState: ScreenState, Equatable { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, tabs: state.tabs, - scrollState: scrollModel, - privateLockState: state.privateLockState) - case TabPanelMiddlewareActionType.setPrivatePanelLockState: - guard state.isPrivateMode, let lock = action.privatePanelLockState else { return state } - var state = state - state.privateLockState = lock - return state + scrollState: scrollModel) default: return defaultState(from: state) } @@ -144,8 +130,7 @@ struct TabsPanelState: ScreenState, Equatable { static func defaultState(from state: TabsPanelState) -> TabsPanelState { return TabsPanelState(windowUUID: state.windowUUID, isPrivateMode: state.isPrivateMode, - tabs: state.tabs, - privateLockState: state.privateLockState) + tabs: state.tabs) } static func createTabScrollBehavior( diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index 671fc2576c1ae..9de4ff007b990 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -5,7 +5,6 @@ import Common import Redux import UIKit -import LocalAuthentication protocol TabTrayThemeable { @MainActor @@ -124,7 +123,9 @@ final class TabDisplayPanelViewController: UIViewController, viewHasAppeared = true } if panelType == .privateTabs { - store.dispatch(PrivatePanelLockAction(windowUUID: windowUUID, actionType: PrivatePanelLockActionType.enteredPrivatePanel)) + store.dispatch( + PrivateLockAction(windowUUID: windowUUID, + actionType: PrivateLockActionType.enteredPrivatePanel)) } updateInsets() } @@ -217,89 +218,11 @@ final class TabDisplayPanelViewController: UIViewController, private func startPrivateAuthFlow() { store.dispatch( - PrivatePanelLockAction( + PrivateLockAction( windowUUID: windowUUID, - actionType: PrivatePanelLockActionType.requestAuth + actionType: PrivateLockActionType.requestAuth("Unlock your private tabs") ) ) - - let context = LAContext() - context.localizedCancelTitle = "Cancel" - - var error: NSError? - - // ВАЖЛИВО: перевіряємо ту саму policy - guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { - store.dispatch( - PrivatePanelLockAction( - windowUUID: windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - return - } - - context.evaluatePolicy( - .deviceOwnerAuthentication, - localizedReason: "Unlock your private tabs." - ) { [weak self] success, authError in - guard let self else { return } - - Task { @MainActor in - if success { - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authSucceeded - ) - ) - } else { - if let laError = authError as? LAError { - switch laError.code { - - case .userCancel, .systemCancel, .appCancel: - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - - case .authenticationFailed: - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - - case .biometryLockout: - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - - default: - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - } - } else { - store.dispatch( - PrivatePanelLockAction( - windowUUID: self.windowUUID, - actionType: PrivatePanelLockActionType.authFailed - ) - ) - } - } - } - } } // MARK: - Themeable @@ -460,7 +383,7 @@ final class TabDisplayPanelViewController: UIViewController, } } - private func applyPrivateLockUI(_ lock: PrivatePanelLockState) { + private func applyPrivateLockUI(_ lock: PrivateLockState) { switch lock { case .unlocked: privateLockOverlay.isHidden = true diff --git a/firefox-ios/Client/Redux/GlobalState/AppState.swift b/firefox-ios/Client/Redux/GlobalState/AppState.swift index 88dc2c0a8ca30..ebed9389355a2 100644 --- a/firefox-ios/Client/Redux/GlobalState/AppState.swift +++ b/firefox-ios/Client/Redux/GlobalState/AppState.swift @@ -86,7 +86,8 @@ let middlewares = [ ShortcutsLibraryMiddleware().shortcutsLibraryProvider, SummarizerMiddleware().summarizerProvider, TermsOfUseMiddleware().termsOfUseProvider, - TranslationsMiddleware().translationsProvider + TranslationsMiddleware().translationsProvider, + PrivateLockMiddleware().lockProvider ] // In order for us to mock and test the middlewares easier, From 44f7609347058118b5601015573538e2be9b974e Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 6 Mar 2026 22:28:50 +0200 Subject: [PATCH 04/27] added complete event logic, handling and processing --- .../Browser/BrowserCoordinator.swift | 9 + .../Actions/PrivateLockAction.swift | 46 +++- .../Middleware/PrivateLockMiddleware.swift | 86 +++++-- .../State/BrowserViewControllerState.swift | 212 ++++++++++++++---- .../Views/BrowserViewController.swift | 85 +++++++ .../Browser/Tabs/State/TabTrayPanelType.swift | 6 + .../Browser/Tabs/State/TabsPanelState.swift | 7 +- .../Views/PrivateTabsLockOverlayView.swift | 42 +++- .../Views/TabDisplayPanelViewController.swift | 28 +-- 9 files changed, 424 insertions(+), 97 deletions(-) diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index 16f83b6702d8b..bf9d781c21ea9 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -1286,6 +1286,15 @@ final class BrowserCoordinator: BaseCoordinator, // [FXIOS-10482] Initial bandaid for memory leaking during tab tray open/close. Needs further investigation. coordinator.dismissChildTabTrayPanels() remove(child: coordinator) + let panel = TabTrayPanelType.convert(from: tabManager.selectedTab) + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.setTrayDisplayContextAndPanelType, + trayDisplayContext: .page, + trayPanelType: panel + ) + ) } // MARK: - WindowEventCoordinator diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index 2222f559c8d2f..36dbd0b75b788 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -6,28 +6,56 @@ import Common import Redux enum PrivateLockActionType: ActionType { - case enteredPrivatePanel case requestAuth(String) + case setPrivateContext + case setTrayDisplayContext + case setTrayDisplayContextAndPanelType + case didEnterBackground + case willEnterForeground } struct PrivateLockAction: Action { let windowUUID: WindowUUID let actionType: ActionType + let trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? + let trayPanelType: TabTrayPanelType? + + init(windowUUID: WindowUUID, + actionType: ActionType, + trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? = nil, + trayPanelType: TabTrayPanelType? = nil) { + self.windowUUID = windowUUID + self.actionType = actionType + self.trayDisplayContext = trayDisplayContext + self.trayPanelType = trayPanelType + } } enum PrivateLockMiddlewareActionType: ActionType { case setPrivateLockState + case changedTabTrayPanelType + case setTrayDisplayContext } struct PrivateLockMiddlewareAction: Action { let windowUUID: WindowUUID let actionType: ActionType - let privatePanelLockState: PrivateLockState -} - -enum PrivateLockState: Equatable { - case unlocked - case lockedPrompt - case authenticating - case failed + let privateLockState: BrowserViewControllerState.PrivateLockDomainState? + let trayPanelType: TabTrayPanelType? + let trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? + let privateLockEnabled: Bool? + + init(windowUUID: WindowUUID, + actionType: ActionType, + privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState? = nil, + trayPanelType: TabTrayPanelType? = nil, + trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? = nil, + privateLockEnabled: Bool? = nil) { + self.windowUUID = windowUUID + self.actionType = actionType + self.privateLockState = privatePanelLockState + self.trayPanelType = trayPanelType + self.trayDisplayContext = trayDisplayContext + self.privateLockEnabled = privateLockEnabled + } } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 0f2694d029aec..f676797101818 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -19,24 +19,56 @@ final class PrivateLockMiddleware { lazy var lockProvider: Middleware = { state, action in if let action = action as? PrivateLockAction { self.resolveTabPrivateLockActions(action: action, state: state) + } else if let action = action as? TabTrayAction { + self.resolveTabChange(action: action, state: state) } } private func resolveTabPrivateLockActions(action: PrivateLockAction, state: AppState) { - let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false - guard shouldLock else { return } + guard isPrivateLockFeatureEnabled() else { + Self.unlock(windowUUID: action.windowUUID) + return + } switch action.actionType { - case PrivateLockActionType.enteredPrivatePanel: - store.dispatch(PrivateLockMiddlewareAction( - windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, - privatePanelLockState: .lockedPrompt - )) case PrivateLockActionType.requestAuth(let reason): let browserState = state.screenState(BrowserViewControllerState.self, for: .browserViewController, window: action.windowUUID) - guard browserState?.privateLockState != PrivateLockState.authenticating else { return } + guard browserState?.privateLockState.auth != .authenticating else { return } auth(reason: reason, windowUUID: action.windowUUID) + case PrivateLockActionType.setTrayDisplayContext: + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, + trayDisplayContext: action.trayDisplayContext) + ) + case PrivateLockActionType.setTrayDisplayContextAndPanelType: + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, + trayDisplayContext: action.trayDisplayContext) + ) + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, + trayPanelType: action.trayPanelType, + privateLockEnabled: isPrivateLockFeatureEnabled() + )) + case PrivateLockActionType.didEnterBackground, PrivateLockActionType.willEnterForeground: + Self.lock(triggeredByFailure: false, windowUUID: action.windowUUID) + default: + break + } + } + + private func resolveTabChange(action: TabTrayAction, state: AppState) { + switch action.actionType { + case TabTrayActionType.changePanel: + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, + trayPanelType: action.panelType, + privateLockEnabled: isPrivateLockFeatureEnabled() + )) default: break } @@ -47,7 +79,9 @@ final class PrivateLockMiddleware { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.setPrivateLockState, - privatePanelLockState: .authenticating + privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .locked, + auth: .authenticating, + lastUnlockedAt: nil) )) let context = LAContext() @@ -56,7 +90,7 @@ final class PrivateLockMiddleware { var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { - Self.authFailed(windowUUID: windowUUID) + Self.lock(triggeredByFailure: true, windowUUID: windowUUID) return } @@ -66,23 +100,37 @@ final class PrivateLockMiddleware { ) { success, authError in Task { @MainActor in if success { - store.dispatch(PrivateLockMiddlewareAction( - windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, - privatePanelLockState: .unlocked - )) + Self.unlock(windowUUID: windowUUID) } else { - Self.authFailed(windowUUID: windowUUID) + Self.lock(triggeredByFailure: true, windowUUID: windowUUID) } } } } - private static func authFailed(windowUUID: WindowUUID) { + private static func lock(triggeredByFailure: Bool, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.setPrivateLockState, - privatePanelLockState: .failed + privatePanelLockState: + BrowserViewControllerState.PrivateLockDomainState(access: .locked, + auth: triggeredByFailure ? .failed : .idle, + lastUnlockedAt: nil) )) } + + private static func unlock(windowUUID: WindowUUID) { + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .unlocked, + auth: .idle, + lastUnlockedAt: Date()) + )) + } + + private func isPrivateLockFeatureEnabled() -> Bool { + let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false + return shouldLock + } } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 38c06f4867a39..5343acb474cf7 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -37,6 +37,42 @@ struct BrowserViewControllerState: ScreenState { // TODO: FXIOS-13118 Clean up and remove as we should have one navigation entry point case summarizer(config: SummarizerConfig?) } + + enum PrivateAccessState: Equatable { + case locked + case unlocked + } + + enum PrivateAuthState: Equatable { + case idle + case authenticating + case failed + } + + enum TrayDisplayContext: Equatable { + case page + case tray + } + + struct PrivateLockDomainState: Equatable { + var access: PrivateAccessState = .locked + var auth: PrivateAuthState = .idle + var lastUnlockedAt: Date? + let relockInterval: TimeInterval = 20 + + var shouldRelockByTime: Bool { + guard let lastUnlockedAt else { return true } + return Date().timeIntervalSince(lastUnlockedAt) > relockInterval + } + + func copy(access: PrivateAccessState? = nil, + auth: PrivateAuthState? = nil, + lastUnlockedAt: Date? = nil) -> PrivateLockDomainState { + PrivateLockDomainState(access: access ?? self.access, + auth: auth ?? self.auth, + lastUnlockedAt: lastUnlockedAt ?? self.lastUnlockedAt) + } + } let windowUUID: WindowUUID var searchScreenState: SearchScreenState @@ -51,7 +87,9 @@ struct BrowserViewControllerState: ScreenState { var frameContext: PasswordGeneratorFrameContext? var microsurveyState: MicrosurveyPromptState var navigationDestination: NavigationDestination? - var privateLockState: PrivateLockState + var privateLockState: PrivateLockDomainState + var trayPanelType: TabTrayPanelType? + var trayDisplayContext: TrayDisplayContext = TrayDisplayContext.page init(appState: AppState, uuid: WindowUUID) { guard let bvcState = appState.screenState( @@ -76,7 +114,9 @@ struct BrowserViewControllerState: ScreenState { frameContext: bvcState.frameContext, microsurveyState: bvcState.microsurveyState, navigationDestination: bvcState.navigationDestination, - privateLockState: bvcState.privateLockState) + privateLockState: bvcState.privateLockState, + trayPanelType: bvcState.trayPanelType, + trayDisplayContext: bvcState.trayDisplayContext) } init(windowUUID: WindowUUID) { @@ -107,7 +147,10 @@ struct BrowserViewControllerState: ScreenState { frameContext: PasswordGeneratorFrameContext? = nil, microsurveyState: MicrosurveyPromptState, navigationDestination: NavigationDestination? = nil, - privateLockState: PrivateLockState = .unlocked + privateLockState: PrivateLockDomainState = + PrivateLockDomainState(access: .unlocked, auth: .idle, lastUnlockedAt: nil), + trayPanelType: TabTrayPanelType? = nil, + trayDisplayContext: TrayDisplayContext = TrayDisplayContext.page ) { self.searchScreenState = searchScreenState self.toast = toast @@ -123,6 +166,7 @@ struct BrowserViewControllerState: ScreenState { self.microsurveyState = microsurveyState self.navigationDestination = navigationDestination self.privateLockState = privateLockState + self.trayPanelType = trayPanelType } static let reducer: Reducer = { state, action in @@ -156,7 +200,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), navigationDestination: nil, - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } } @@ -188,7 +234,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), navigationDestination: action.navigationDestination, - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) default: return passthroughState(from: state, action: action) @@ -223,7 +271,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), navigationDestination: NavigationDestination(.summarizer(config: action.summarizerConfig)), - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) default: return passthroughState(from: state, action: action) @@ -237,14 +287,37 @@ struct BrowserViewControllerState: ScreenState { ) -> BrowserViewControllerState { switch action.actionType { case PrivateLockMiddlewareActionType.setPrivateLockState: -// guard state.pri + guard let privateLockState = action.privateLockState else { return state } var newState = state - newState.privateLockState = action.privatePanelLockState + newState.privateLockState = privateLockState + return newState + case PrivateLockMiddlewareActionType.changedTabTrayPanelType: + guard let panelType = action.trayPanelType else { return state } + var newState = state + newState.trayPanelType = panelType + let becamePrivateVisible = !state.isPrivateSurfaceVisible && newState.isPrivateSurfaceVisible + if becamePrivateVisible { + let privateLockFeatureEnabled = action.privateLockEnabled ?? false + if privateLockFeatureEnabled, + newState.privateLockState.auth != .authenticating, + newState.privateLockState.shouldRelockByTime == true { + newState.privateLockState = newState.privateLockState.copy(access: .locked) + } + } + return newState + case PrivateLockMiddlewareActionType.setTrayDisplayContext: + guard let trayDisplayContext = action.trayDisplayContext else { return state } + var newState = state + newState.trayDisplayContext = trayDisplayContext return newState default: return state } } + + private var isPrivateSurfaceVisible: Bool { + trayPanelType == .privateTabs + } // MARK: - Toolbar Action @@ -273,7 +346,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), navigationDestination: NavigationDestination(.homepageZeroSearch), - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) default: return passthroughState(from: state, action: action) @@ -309,7 +384,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), navigationDestination: NavigationDestination(.zeroSearch), - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) } @@ -321,7 +398,10 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext + ) } @MainActor @@ -394,7 +474,9 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) } @@ -408,7 +490,9 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -421,7 +505,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .home, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) } @@ -435,7 +521,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .newTab, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -448,7 +536,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .backForwardList, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -463,7 +553,9 @@ struct BrowserViewControllerState: ScreenState { displayView: .trackingProtectionDetails, buttonTapped: action.buttonTapped, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -477,7 +569,9 @@ struct BrowserViewControllerState: ScreenState { displayView: .menu, buttonTapped: action.buttonTapped, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -490,7 +584,10 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .tabsLongPressActions, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext + ) } @MainActor @@ -504,7 +601,9 @@ struct BrowserViewControllerState: ScreenState { displayView: .reloadLongPressAction, buttonTapped: action.buttonTapped, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -518,7 +617,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .locationViewLongPressAction, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -531,7 +632,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .back, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -544,7 +647,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .forward, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -557,7 +662,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .tabTray, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -570,7 +677,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .reload, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -583,7 +692,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .reloadNoCache, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -596,7 +707,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, navigateTo: .stopLoading, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -610,7 +723,9 @@ struct BrowserViewControllerState: ScreenState { displayView: .share, buttonTapped: action.buttonTapped, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -623,7 +738,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .readerMode, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -636,7 +753,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .newTabLongPressActions, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -649,7 +768,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .readerModeLongPressAction, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -661,7 +782,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .dataClearance, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -674,7 +797,9 @@ struct BrowserViewControllerState: ScreenState { displayView: .passwordGenerator, frameContext: action.frameContext, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -686,7 +811,9 @@ struct BrowserViewControllerState: ScreenState { browserViewType: state.browserViewType, displayView: .summarizer(config: action.summarizerConfig), microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } @MainActor @@ -701,7 +828,9 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: microsurveyState, - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) } @@ -712,7 +841,9 @@ struct BrowserViewControllerState: ScreenState { windowUUID: state.windowUUID, browserViewType: state.browserViewType, microsurveyState: microsurveyState, - privateLockState: state.privateLockState + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext ) } @@ -735,7 +866,10 @@ struct BrowserViewControllerState: ScreenState { reloadWebView: true, browserViewType: browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext + ) } @MainActor @@ -749,6 +883,8 @@ struct BrowserViewControllerState: ScreenState { shouldStartAtHome: action.shouldStartAtHome ?? false, browserViewType: state.browserViewType, microsurveyState: MicrosurveyPromptState.reducer(state.microsurveyState, action), - privateLockState: state.privateLockState) + privateLockState: state.privateLockState, + trayPanelType: state.trayPanelType, + trayDisplayContext: state.trayDisplayContext) } } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 18d093448abd9..4040cdbfbc535 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -966,11 +966,27 @@ class BrowserViewController: UIViewController, guard canShowPrivacyWindow else { return } privacyWindowHelper.showWindow(windowScene: currentWindowScene, withThemedColor: currentTheme().colors.layer3) + + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.didEnterBackground + ) + ) } func sceneDidActivateNotification() { privacyWindowHelper.removeWindow() } + + func sceneWillEnterForegroundNotification() { + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.willEnterForeground + ) + ) + } func appWillResignActiveNotification() { // Dismiss any popovers that might be visible @@ -1126,6 +1142,7 @@ class BrowserViewController: UIViewController, dismissModalsIfStartAtHome() shouldHideAddressToolbar() dismissToolbarCFRs(with: windowUUID) + applyPrivateLockUI(state.privateLockState) } private func showToastType(toast: ToastType) { @@ -1174,6 +1191,15 @@ class BrowserViewController: UIViewController, createMicrosurveyPrompt(with: state.microsurveyState) } } + + private func startPrivateAuthFlow() { + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.requestAuth("Unlock your private tabs") + ) + ) + } // MARK: - Lifecycle @@ -1294,6 +1320,29 @@ class BrowserViewController: UIViewController, title = .SettingsHomePageSectionName navigationItem.backButtonDisplayMode = .generic } + + private lazy var privateLockOverlay: PrivateTabsLockOverlayView = { + let overlay = PrivateTabsLockOverlayView() + overlay.isHidden = true + overlay.onUnlockTapped = { [weak self] in self?.startPrivateAuthFlow() } + overlay.onRetryTapped = { [weak self] in self?.startPrivateAuthFlow() } + return overlay + }() + + private func applyPrivateLockUI(_ lock: BrowserViewControllerState.PrivateLockDomainState?) { + guard let lock else { + privateLockOverlay.renderHidden() + return + } + + privateLockOverlay.render( + access: lock.access, + auth: lock.auth + ) + addressToolbarContainer.isHidden = lock.access == .locked + view.bringSubviewToFront(privateLockOverlay) + view.bringSubviewToFront(bottomContainer) + } // MARK: - Notifiable func handleNotifications(_ notification: Notification) { @@ -1320,6 +1369,8 @@ class BrowserViewController: UIViewController, sceneDidEnterBackgroundNotification(windowScene: windowScene) case UIScene.didActivateNotification: sceneDidActivateNotification() + case UIScene.willEnterForegroundNotification: + sceneWillEnterForegroundNotification() case UIAccessibility.announcementDidFinishNotification: didFinishAnnouncement(announcementText: announcementText) case UIAccessibility.reduceTransparencyStatusDidChangeNotification: @@ -1359,6 +1410,7 @@ class BrowserViewController: UIViewController, UIApplication.didEnterBackgroundNotification, UIApplication.willTerminateNotification, UIScene.didEnterBackgroundNotification, + UIScene.willEnterForegroundNotification, UIScene.didActivateNotification, UIAccessibility.announcementDidFinishNotification, UIAccessibility.reduceTransparencyStatusDidChangeNotification, @@ -1495,6 +1547,7 @@ class BrowserViewController: UIViewController, return self?.newTabSettings } } + view.addSubview(privateLockOverlay) } private func enqueueTabRestoration() { @@ -1791,6 +1844,13 @@ class BrowserViewController: UIViewController, updateHeaderConstraints() } setupBlurViews() + + NSLayoutConstraint.activate([ + privateLockOverlay.topAnchor.constraint(equalTo: view.topAnchor), + privateLockOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor), + privateLockOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor), + privateLockOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) } private func setupBlurViews() { @@ -2277,6 +2337,14 @@ class BrowserViewController: UIViewController, topTabsViewController?.refreshTabs() } setupMicrosurvey() + + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.setTrayDisplayContext, + trayDisplayContext: .page + ) + ) } func updateInContentHomePanel(_ url: URL?, focusUrlBar: Bool = false) { @@ -3391,6 +3459,14 @@ class BrowserViewController: UIViewController, let isPrivateTab = tabManager.selectedTab?.isPrivate ?? false let selectedSegment: TabTrayPanelType = focusedSegment ?? (isPrivateTab ? .privateTabs : .tabs) navigationHandler?.showTabTray(selectedPanel: selectedSegment) + + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.setTrayDisplayContext, + trayDisplayContext: .tray + ) + ) } func submitSearchText(_ text: String, forTab tab: Tab) { @@ -4917,6 +4993,15 @@ extension BrowserViewController: TabManagerDelegate { if needsReload { selectedTab.reloadPage() } + + let panel = TabTrayPanelType.convert(from: selectedTab) + store.dispatch( + PrivateLockAction( + windowUUID: windowUUID, + actionType: PrivateLockActionType.setTrayDisplayContextAndPanelType, + trayPanelType: panel + ) + ) } // TODO: FXIOS-14347 This function will be removed when toolbarTranslucencyRefactor is on for everyone diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayPanelType.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayPanelType.swift index 39ed233cadd69..759bd159335ba 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayPanelType.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayPanelType.swift @@ -64,4 +64,10 @@ enum TabTrayPanelType: Int, CaseIterable { } return panelType } + + @MainActor + static func convert(from tab: Tab?) -> TabTrayPanelType { + let isPrivateTab = tab?.isPrivate ?? false + return isPrivateTab ? TabTrayPanelType.privateTabs : TabTrayPanelType.tabs + } } diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index 50b7ca1301992..0030711228f73 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -19,7 +19,7 @@ struct TabsPanelState: ScreenState, Equatable { var scrollState: ScrollState? var didTapAddTab: Bool var urlRequest: URLRequest? - var privateLockState: PrivateLockState + var privateLockState: BrowserViewControllerState.PrivateLockDomainState? var isPrivateTabsEmpty: Bool { guard isPrivateMode else { return true } @@ -37,14 +37,13 @@ struct TabsPanelState: ScreenState, Equatable { } let browserState = appState.screenState(BrowserViewControllerState.self, for: .browserViewController, window: uuid) - let privateLockState = browserState?.privateLockState ?? .unlocked self.init(windowUUID: panelState.windowUUID, isPrivateMode: panelState.isPrivateMode, tabs: panelState.tabs, scrollState: panelState.scrollState, didTapAddTab: panelState.didTapAddTab, urlRequest: panelState.urlRequest, - privateLockState: privateLockState) + privateLockState: browserState?.privateLockState) } init(windowUUID: WindowUUID, isPrivateMode: Bool = false) { @@ -65,7 +64,7 @@ struct TabsPanelState: ScreenState, Equatable { scrollState: ScrollState? = nil, didTapAddTab: Bool = false, urlRequest: URLRequest? = nil, - privateLockState: PrivateLockState = .unlocked) { + privateLockState: BrowserViewControllerState.PrivateLockDomainState? = nil) { self.isPrivateMode = isPrivateMode self.tabs = tabs self.windowUUID = windowUUID diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index 2f758cc214d9a..3e5bed3b209fd 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -31,18 +31,44 @@ final class PrivateTabsLockOverlayView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func render(access: BrowserViewControllerState.PrivateAccessState, auth: BrowserViewControllerState.PrivateAuthState) { + switch access { + case .unlocked: + isHidden = true + apply(mode: .prompt) + + case .locked: + isHidden = false + switch auth { + case .idle: + apply(mode: .prompt) + case .authenticating: + apply(mode: .authenticating) + case .failed: + apply(mode: .failed) + } + } + } + + func renderHidden() { + isHidden = true + apply(mode: .prompt) + } private func setup() { translatesAutoresizingMaskIntoConstraints = false - blurView.translatesAutoresizingMaskIntoConstraints = false - addSubview(blurView) - NSLayoutConstraint.activate([ - blurView.topAnchor.constraint(equalTo: topAnchor), - blurView.leadingAnchor.constraint(equalTo: leadingAnchor), - blurView.trailingAnchor.constraint(equalTo: trailingAnchor), - blurView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) +// blurView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(blurView) +// NSLayoutConstraint.activate([ +// blurView.topAnchor.constraint(equalTo: topAnchor), +// blurView.leadingAnchor.constraint(equalTo: leadingAnchor), +// blurView.trailingAnchor.constraint(equalTo: trailingAnchor), +// blurView.bottomAnchor.constraint(equalTo: bottomAnchor) +// ]) + + backgroundColor = .blue titleLabel.font = .preferredFont(forTextStyle: .title2) titleLabel.textAlignment = .center diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index 9de4ff007b990..ffb7f8f0956bd 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -122,11 +122,6 @@ final class TabDisplayPanelViewController: UIViewController, actionType: TabPanelViewActionType.tabPanelWillAppear)) viewHasAppeared = true } - if panelType == .privateTabs { - store.dispatch( - PrivateLockAction(windowUUID: windowUUID, - actionType: PrivateLockActionType.enteredPrivatePanel)) - } updateInsets() } @@ -383,21 +378,16 @@ final class TabDisplayPanelViewController: UIViewController, } } - private func applyPrivateLockUI(_ lock: PrivateLockState) { - switch lock { - case .unlocked: - privateLockOverlay.isHidden = true - privateLockOverlay.apply(mode: .prompt) - case .lockedPrompt: - privateLockOverlay.isHidden = false - privateLockOverlay.apply(mode: .prompt) - case .authenticating: - privateLockOverlay.isHidden = false - privateLockOverlay.apply(mode: .authenticating) - case .failed: - privateLockOverlay.isHidden = false - privateLockOverlay.apply(mode: .failed) + private func applyPrivateLockUI(_ lock: BrowserViewControllerState.PrivateLockDomainState?) { + guard let lock else { + privateLockOverlay.renderHidden() + return } + + privateLockOverlay.render( + access: lock.access, + auth: lock.auth + ) } // MARK: - EmptyPrivateTabsViewDelegate From 9bd0e791d761ee3490023c5368b3e7503efadbca Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 7 Mar 2026 15:24:40 +0200 Subject: [PATCH 05/27] refactored state and middleware, moved orchestration into middleware and left state mutation for state --- .../Middleware/PrivateLockMiddleware.swift | 56 ++++++++++++++----- .../State/BrowserViewControllerState.swift | 17 +++--- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index f676797101818..53699c0c9801e 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -32,7 +32,7 @@ final class PrivateLockMiddleware { switch action.actionType { case PrivateLockActionType.requestAuth(let reason): - let browserState = state.screenState(BrowserViewControllerState.self, for: .browserViewController, window: action.windowUUID) + let browserState = self.browserState(from: state, windowUUID: action.windowUUID) guard browserState?.privateLockState.auth != .authenticating else { return } auth(reason: reason, windowUUID: action.windowUUID) case PrivateLockActionType.setTrayDisplayContext: @@ -47,12 +47,9 @@ final class PrivateLockMiddleware { actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, trayDisplayContext: action.trayDisplayContext) ) - store.dispatch(PrivateLockMiddlewareAction( - windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, - trayPanelType: action.trayPanelType, - privateLockEnabled: isPrivateLockFeatureEnabled() - )) + resolveTabTrayPanelTypeChange(panel: action.trayPanelType, + windowUUID: action.windowUUID, + state: state) case PrivateLockActionType.didEnterBackground, PrivateLockActionType.willEnterForeground: Self.lock(triggeredByFailure: false, windowUUID: action.windowUUID) default: @@ -63,17 +60,42 @@ final class PrivateLockMiddleware { private func resolveTabChange(action: TabTrayAction, state: AppState) { switch action.actionType { case TabTrayActionType.changePanel: - store.dispatch(PrivateLockMiddlewareAction( - windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, - trayPanelType: action.panelType, - privateLockEnabled: isPrivateLockFeatureEnabled() - )) + resolveTabTrayPanelTypeChange(panel: action.panelType, + windowUUID: action.windowUUID, + state: state) default: break } } + private func resolveTabTrayPanelTypeChange(panel: TabTrayPanelType?, + windowUUID: WindowUUID, + state: AppState) { + + guard let panelType = panel, + let state = browserState(from: state, windowUUID: windowUUID) + else { return } + + let privateLockEnabled = isPrivateLockFeatureEnabled() + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, + trayPanelType: panel, + privateLockEnabled: privateLockEnabled + )) + + guard state.didBecomePrivateVisible(afterChangingPanelTo: panelType) else { return } + guard privateLockEnabled else { return } + guard state.privateLockState.auth != .authenticating else { return } + guard state.privateLockState.shouldRelockByTime else { return } + + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: state.privateLockState.locked() + )) + } + private func auth(reason: String, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( @@ -133,4 +155,12 @@ final class PrivateLockMiddleware { let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false return shouldLock } + + private func browserState(from appState: AppState, windowUUID: WindowUUID) -> BrowserViewControllerState? { + appState.screenState( + BrowserViewControllerState.self, + for: .browserViewController, + window: windowUUID + ) + } } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 5343acb474cf7..5155d9fe2f840 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -72,6 +72,10 @@ struct BrowserViewControllerState: ScreenState { auth: auth ?? self.auth, lastUnlockedAt: lastUnlockedAt ?? self.lastUnlockedAt) } + + func locked() -> PrivateLockDomainState { + copy(access: .locked) + } } let windowUUID: WindowUUID @@ -295,15 +299,6 @@ struct BrowserViewControllerState: ScreenState { guard let panelType = action.trayPanelType else { return state } var newState = state newState.trayPanelType = panelType - let becamePrivateVisible = !state.isPrivateSurfaceVisible && newState.isPrivateSurfaceVisible - if becamePrivateVisible { - let privateLockFeatureEnabled = action.privateLockEnabled ?? false - if privateLockFeatureEnabled, - newState.privateLockState.auth != .authenticating, - newState.privateLockState.shouldRelockByTime == true { - newState.privateLockState = newState.privateLockState.copy(access: .locked) - } - } return newState case PrivateLockMiddlewareActionType.setTrayDisplayContext: guard let trayDisplayContext = action.trayDisplayContext else { return state } @@ -315,6 +310,10 @@ struct BrowserViewControllerState: ScreenState { } } + func didBecomePrivateVisible(afterChangingPanelTo panelType: TabTrayPanelType) -> Bool { + !isPrivateSurfaceVisible && panelType == .privateTabs + } + private var isPrivateSurfaceVisible: Bool { trayPanelType == .privateTabs } From 3eccf3dbe58e6ea0c5763f856e8bed958405be97 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 7 Mar 2026 16:29:14 +0200 Subject: [PATCH 06/27] fixed code alignment --- .../Middleware/PrivateLockMiddleware.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 53699c0c9801e..b75efe12753ff 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -85,9 +85,9 @@ final class PrivateLockMiddleware { )) guard state.didBecomePrivateVisible(afterChangingPanelTo: panelType) else { return } - guard privateLockEnabled else { return } - guard state.privateLockState.auth != .authenticating else { return } - guard state.privateLockState.shouldRelockByTime else { return } + guard privateLockEnabled else { return } + guard state.privateLockState.auth != .authenticating else { return } + guard state.privateLockState.shouldRelockByTime else { return } store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, From ca69c3b8cda8985836b126f695f1658c70f9b4f2 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 7 Mar 2026 19:18:30 +0200 Subject: [PATCH 07/27] fixed bugs with layer on background event --- .../Views/BrowserViewController.swift | 9 ++++++++- .../Browser/Tabs/State/TabTrayState.swift | 17 +++++++++++++---- .../Tabs/Views/TabTrayViewController.swift | 2 ++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 4040cdbfbc535..178861f816560 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -946,6 +946,13 @@ class BrowserViewController: UIViewController, // individual TabManager instances for each BVC, so we perform these here instead. tabManager.preserveTabs() logTelemetryForAppDidEnterBackground() + + if let state = browserViewControllerState { + let shouldLock = profile.prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false + if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && shouldLock { + focusOnTabSegment() + } + } } /// Remove KVO observers on terminate to prevent crashes during force-close. @@ -1142,7 +1149,7 @@ class BrowserViewController: UIViewController, dismissModalsIfStartAtHome() shouldHideAddressToolbar() dismissToolbarCFRs(with: windowUUID) - applyPrivateLockUI(state.privateLockState) +// applyPrivateLockUI(state.privateLockState) } private func showToastType(toast: ToastType) { diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayState.swift index e50e218cb0c00..2d74975c5e1f5 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabTrayState.swift @@ -22,6 +22,7 @@ struct TabTrayState: ScreenState, Equatable { var windowUUID: WindowUUID var showCloseConfirmation: Bool var enableDeleteTabsButton: Bool? + var hideToolbar: Bool? var navigationTitle: String { return selectedPanel.navTitle @@ -45,6 +46,9 @@ struct TabTrayState: ScreenState, Equatable { return } + let browserState = appState.screenState(BrowserViewControllerState.self, for: .browserViewController, window: uuid) + let privateLockState = browserState?.privateLockState + let hideToolbar = privateLockState?.access == .locked && browserState?.trayPanelType == .privateTabs self.init(windowUUID: panelState.windowUUID, isPrivateMode: panelState.isPrivateMode, selectedPanel: panelState.selectedPanel, @@ -54,7 +58,8 @@ struct TabTrayState: ScreenState, Equatable { shouldDismiss: panelState.shouldDismiss, toastType: panelState.toastType, showCloseConfirmation: panelState.showCloseConfirmation, - enableDeleteTabsButton: panelState.enableDeleteTabsButton) + enableDeleteTabsButton: panelState.enableDeleteTabsButton, + hideToolbar: hideToolbar) } init(windowUUID: WindowUUID) { @@ -64,7 +69,8 @@ struct TabTrayState: ScreenState, Equatable { normalTabsCount: "0", privateTabsCount: "0", hasSyncableAccount: false, - toastType: nil) + toastType: nil, + hideToolbar: nil) } init(windowUUID: WindowUUID, panelType: TabTrayPanelType) { @@ -73,7 +79,8 @@ struct TabTrayState: ScreenState, Equatable { selectedPanel: panelType, normalTabsCount: "0", privateTabsCount: "0", - hasSyncableAccount: false) + hasSyncableAccount: false, + hideToolbar: nil) } init(windowUUID: WindowUUID, @@ -85,7 +92,8 @@ struct TabTrayState: ScreenState, Equatable { shouldDismiss: Bool = false, toastType: ToastType? = nil, showCloseConfirmation: Bool = false, - enableDeleteTabsButton: Bool? = nil) { + enableDeleteTabsButton: Bool? = nil, + hideToolbar: Bool? = nil) { self.windowUUID = windowUUID self.isPrivateMode = isPrivateMode self.selectedPanel = selectedPanel @@ -96,6 +104,7 @@ struct TabTrayState: ScreenState, Equatable { self.toastType = toastType self.showCloseConfirmation = showCloseConfirmation self.enableDeleteTabsButton = enableDeleteTabsButton + self.hideToolbar = hideToolbar } static let reducer: Reducer = { state, action in diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift index 1eba86db65773..3419df69658ae 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift @@ -416,6 +416,8 @@ final class TabTrayViewController: UIViewController, if !themeAnimator.isAnimating && swipeFromIndex == nil { applyTheme() } + + navigationController?.setToolbarHidden(state.hideToolbar ?? false, animated: false) } func updateTabCountImage(count: String) { From 98adb6bd24ade24a11e083a8f5a4b4963f403caa Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 7 Mar 2026 20:10:41 +0200 Subject: [PATCH 08/27] fixed bugs, lock screen improvement --- .../Browser/BrowserCoordinator.swift | 22 +++-- .../Browser/BrowserNavigationHandler.swift | 2 +- .../Views/BrowserViewController.swift | 24 +++-- .../Views/FirefoxGradientBackgroundView.swift | 93 +++++++++++++++++++ .../Views/PrivateTabsLockOverlayView.swift | 45 ++++++--- .../BrowserCoordinatorTests.swift | 14 +-- .../Mocks/MockBrowserCoordinator.swift | 2 +- 7 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index bf9d781c21ea9..fc8bb4a20960d 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -498,7 +498,9 @@ final class BrowserCoordinator: BaseCoordinator, } func didFinishSettings(from coordinator: SettingsCoordinator) { - router.dismiss(animated: true, completion: nil) + router.dismiss(animated: true, completion: { [weak browserViewController] in + browserViewController?.settingsControllerDidHide() + }) remove(child: coordinator) } @@ -965,7 +967,7 @@ final class BrowserCoordinator: BaseCoordinator, coordinator.showQRCode(delegate: delegate) } - func showTabTray(selectedPanel: TabTrayPanelType) { + func showTabTray(selectedPanel: TabTrayPanelType, animated: Bool) { guard !childCoordinators.contains(where: { $0 is TabTrayCoordinator }) else { return // flow is already handled } @@ -1004,9 +1006,12 @@ final class BrowserCoordinator: BaseCoordinator, if featureFlags.isFeatureEnabled(.tabTrayUIExperiments, checking: .buildOnly) && UIDevice.current.userInterfaceIdiom != .pad && selectedPanel != .syncedTabs { guard let tabTrayVC = tabTrayCoordinator.tabTrayViewController else { return } - present(navigationController, customTransition: tabTrayVC, style: modalPresentationStyle) + present(navigationController, + customTransition: tabTrayVC, + style: modalPresentationStyle, + animated: animated) } else { - present(navigationController) + present(navigationController, animated: animated) } guard browserViewController.isAppStoreReviewTriggerEnabled else { return } browserViewController.ratingPromptManager.showRatingPromptIfNeeded() @@ -1015,12 +1020,13 @@ final class BrowserCoordinator: BaseCoordinator, // This implementation of present is specifically for the animation on .tabTrayUIExperiments private func present(_ viewController: UIViewController, customTransition: UIViewControllerTransitioningDelegate, - style: UIModalPresentationStyle) { + style: UIModalPresentationStyle, + animated: Bool = true) { browserViewController.willNavigateAway(from: tabManager.selectedTab) if !UIAccessibility.isReduceMotionEnabled { router.present( viewController, - animated: true, + animated: animated, customTransition: customTransition, presentationStyle: style ) @@ -1029,9 +1035,9 @@ final class BrowserCoordinator: BaseCoordinator, } } - private func present(_ viewController: UIViewController) { + private func present(_ viewController: UIViewController, animated: Bool = true) { browserViewController.willNavigateAway(from: tabManager.selectedTab) - router.present(viewController) + router.present(viewController, animated: animated) } func showBackForwardList() { diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift index 1df6560b8420d..849980f99ee9d 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift @@ -68,7 +68,7 @@ protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler { /// Shows the Tab Tray View Controller. @MainActor - func showTabTray(selectedPanel: TabTrayPanelType) + func showTabTray(selectedPanel: TabTrayPanelType, animated: Bool) /// Shows the Back Forward List View Controller. @MainActor diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 178861f816560..3aecf01cd6826 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -947,10 +947,17 @@ class BrowserViewController: UIViewController, tabManager.preserveTabs() logTelemetryForAppDidEnterBackground() + showPrivacyOverlayIfNeeded() + } + + private func showPrivacyOverlayIfNeeded(checkActualState: Bool = false) { if let state = browserViewControllerState { let shouldLock = profile.prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && shouldLock { - focusOnTabSegment() + focusOnTabSegment(animated: false) + if checkActualState { + sceneWillEnterForegroundNotification() + } } } } @@ -3004,7 +3011,7 @@ class BrowserViewController: UIViewController, case .summarizer(let config): navigationHandler?.showSummarizePanel(.shakeGesture, config: config) case .tabTray(let panelType): - navigationHandler?.showTabTray(selectedPanel: panelType) + navigationHandler?.showTabTray(selectedPanel: panelType, animated: true) case .homepageZeroSearch: store.dispatch( GeneralBrowserAction( @@ -3373,10 +3380,10 @@ class BrowserViewController: UIViewController, presentSheetWith(viewModel: viewModel, on: self, from: view) } - func focusOnTabSegment() { + func focusOnTabSegment(animated: Bool = true) { let isPrivateTab = tabManager.selectedTab?.isPrivate ?? false let segmentToFocus = isPrivateTab ? TabTrayPanelType.privateTabs : TabTrayPanelType.tabs - showTabTray(focusedSegment: segmentToFocus) + showTabTray(focusedSegment: segmentToFocus, animated: animated) } /// When the trait collection changes the top taps display might have to change @@ -3460,12 +3467,13 @@ class BrowserViewController: UIViewController, } func showTabTray(withFocusOnUnselectedTab tabToFocus: Tab? = nil, - focusedSegment: TabTrayPanelType? = nil) { + focusedSegment: TabTrayPanelType? = nil, + animated: Bool = true) { updateFindInPageVisibility(isVisible: false) let isPrivateTab = tabManager.selectedTab?.isPrivate ?? false let selectedSegment: TabTrayPanelType = focusedSegment ?? (isPrivateTab ? .privateTabs : .tabs) - navigationHandler?.showTabTray(selectedPanel: selectedSegment) + navigationHandler?.showTabTray(selectedPanel: selectedSegment, animated: animated) store.dispatch( PrivateLockAction( @@ -4769,6 +4777,10 @@ extension BrowserViewController: SearchViewControllerDelegate { let navController = ModalSettingsNavigationController(rootViewController: searchSettingsTableViewController) self.present(navController, animated: true, completion: nil) } + + func settingsControllerDidHide() { + showPrivacyOverlayIfNeeded(checkActualState: true) + } func updateForDefaultSearchEngineDidChange() { // Update search icon when the search engine changes diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift new file mode 100644 index 0000000000000..c23cbb9a4ed80 --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift @@ -0,0 +1,93 @@ +// 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 + +final class FirefoxGradientBackgroundView: UIView { + + private let gradientLayer = CAGradientLayer() + + private let glow1 = CALayer() + private let glow2 = CALayer() + private let glow3 = CALayer() + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + + gradientLayer.colors = [ + UIColor(red: 1.0, green: 0.44, blue: 0.22, alpha: 1).cgColor, // orange + UIColor(red: 0.83, green: 0.25, blue: 1.0, alpha: 1).cgColor, // purple + UIColor(red: 0.02, green: 0.1, blue: 0.3, alpha: 1).cgColor // dark blue + ] + + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 1) + + layer.addSublayer(gradientLayer) + + configureGlow(glow1, + color: UIColor(red: 1, green: 0.3, blue: 0.35, alpha: 0.6)) + + configureGlow(glow2, + color: UIColor(red: 0.8, green: 0.2, blue: 1.0, alpha: 0.6)) + + configureGlow(glow3, + color: UIColor(red: 0.35, green: 0.0, blue: 1.0, alpha: 0.6)) + } + + private func configureGlow(_ layerGlow: CALayer, color: UIColor) { + + layerGlow.backgroundColor = color.cgColor + layerGlow.opacity = 0.8 + + layerGlow.shadowColor = color.cgColor + layerGlow.shadowRadius = 120 + layerGlow.shadowOpacity = 1 + layerGlow.shadowOffset = .zero + + layer.addSublayer(layerGlow) + } + + override func layoutSubviews() { + super.layoutSubviews() + + gradientLayer.frame = bounds + + let size1: CGFloat = bounds.width * 0.9 + glow1.frame = CGRect( + x: bounds.width * 0.65, + y: bounds.height * 0.1, + width: size1, + height: size1 + ) + glow1.cornerRadius = size1 / 2 + + let size2: CGFloat = bounds.width * 0.8 + glow2.frame = CGRect( + x: bounds.width * -0.2, + y: bounds.height * 0.4, + width: size2, + height: size2 + ) + glow2.cornerRadius = size2 / 2 + + let size3: CGFloat = bounds.width * 0.9 + glow3.frame = CGRect( + x: bounds.width * 0.2, + y: bounds.height * 0.75, + width: size3, + height: size3 + ) + glow3.cornerRadius = size3 / 2 + } +} diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index 3e5bed3b209fd..7da14baf52265 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -19,7 +19,6 @@ final class PrivateTabsLockOverlayView: UIView { private let titleLabel = UILabel() private let subtitleLabel = UILabel() private let spinner = UIActivityIndicatorView(style: .large) - private let unlockButton = UIButton(type: .system) private let retryButton = UIButton(type: .system) private let stack = UIStackView() @@ -59,16 +58,17 @@ final class PrivateTabsLockOverlayView: UIView { private func setup() { translatesAutoresizingMaskIntoConstraints = false -// blurView.translatesAutoresizingMaskIntoConstraints = false -// addSubview(blurView) -// NSLayoutConstraint.activate([ -// blurView.topAnchor.constraint(equalTo: topAnchor), -// blurView.leadingAnchor.constraint(equalTo: leadingAnchor), -// blurView.trailingAnchor.constraint(equalTo: trailingAnchor), -// blurView.bottomAnchor.constraint(equalTo: bottomAnchor) -// ]) - - backgroundColor = .blue + let background = FirefoxGradientBackgroundView() + + addSubview(background) + background.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + background.topAnchor.constraint(equalTo: topAnchor), + background.bottomAnchor.constraint(equalTo: bottomAnchor), + background.leadingAnchor.constraint(equalTo: leadingAnchor), + background.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) titleLabel.font = .preferredFont(forTextStyle: .title2) titleLabel.textAlignment = .center @@ -94,7 +94,7 @@ final class PrivateTabsLockOverlayView: UIView { retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside) stack.axis = .vertical - stack.alignment = .fill + stack.alignment = .center stack.distribution = .fill stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false @@ -116,6 +116,27 @@ final class PrivateTabsLockOverlayView: UIView { apply(mode: .prompt) } + + private let unlockButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + + var config = UIButton.Configuration.filled() + config.title = "Unlock" + config.baseBackgroundColor = UIColor.systemBlue + config.baseForegroundColor = .white + config.cornerStyle = .capsule + config.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 28, bottom: 14, trailing: 28) + + button.configuration = config + button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.18 + button.layer.shadowRadius = 10 + button.layer.shadowOffset = CGSize(width: 0, height: 4) + + return button + }() func apply(mode: Mode) { switch mode { 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 4d8ee7a8dca9a..cf19412b94b67 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/BrowserCoordinatorTests.swift @@ -396,7 +396,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { func testShowTabTray() throws { setupNimbusTabTrayUIExperimentTesting(isEnabled: false) let subject = createSubject() - subject.showTabTray(selectedPanel: .tabs) + subject.showTabTray(selectedPanel: .tabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -409,7 +409,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { setupNimbusTabTrayUIExperimentTesting(isEnabled: true) let subject = createSubject() subject.browserViewController = browserViewController - subject.showTabTray(selectedPanel: .tabs) + subject.showTabTray(selectedPanel: .tabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -421,7 +421,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { func testShowTabTray_withPrivateTabs_withoutExperiment_showDefaultPresentation() throws { setupNimbusTabTrayUIExperimentTesting(isEnabled: false) let subject = createSubject() - subject.showTabTray(selectedPanel: .privateTabs) + subject.showTabTray(selectedPanel: .privateTabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -434,7 +434,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { setupNimbusTabTrayUIExperimentTesting(isEnabled: true) let subject = createSubject() subject.browserViewController = browserViewController - subject.showTabTray(selectedPanel: .privateTabs) + subject.showTabTray(selectedPanel: .privateTabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -447,7 +447,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { setupNimbusTabTrayUIExperimentTesting(isEnabled: false) let subject = createSubject() subject.browserViewController = browserViewController - subject.showTabTray(selectedPanel: .syncedTabs) + subject.showTabTray(selectedPanel: .syncedTabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -460,7 +460,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { setupNimbusTabTrayUIExperimentTesting(isEnabled: true) let subject = createSubject() subject.browserViewController = browserViewController - subject.showTabTray(selectedPanel: .syncedTabs) + subject.showTabTray(selectedPanel: .syncedTabs, animated: true) XCTAssertEqual(subject.childCoordinators.count, 1) XCTAssertNotNil(subject.childCoordinators[0] as? TabTrayCoordinator) @@ -471,7 +471,7 @@ final class BrowserCoordinatorTests: XCTestCase, FeatureFlaggable { func testDismissTabTray_removesChild() throws { let subject = createSubject() - subject.showTabTray(selectedPanel: .tabs) + subject.showTabTray(selectedPanel: .tabs, animated: true) guard let tabTrayCoordinator = subject.childCoordinators[0] as? TabTrayCoordinator else { XCTFail("Tab tray coordinator was expected to be resolved") return diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift index bef2217f2c801..b2767e6588c1c 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/Mocks/MockBrowserCoordinator.swift @@ -99,7 +99,7 @@ class MockBrowserCoordinator: BrowserNavigationHandler, showEnhancedTrackingProtectionCalled += 1 } - func showTabTray(selectedPanel: TabTrayPanelType) { + func showTabTray(selectedPanel: TabTrayPanelType, animated: Bool) { showTabTrayCalled += 1 } From 4c7b7b01666ffbe34b4c18732c517d026cb14822 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 7 Mar 2026 20:48:09 +0200 Subject: [PATCH 09/27] removed extra code --- .../State/BrowserViewControllerState.swift | 3 +- .../Views/BrowserViewController.swift | 32 ------------------- .../Views/TabDisplayPanelViewController.swift | 2 ++ 3 files changed, 3 insertions(+), 34 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 5155d9fe2f840..63a14830ab04d 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -61,8 +61,7 @@ struct BrowserViewControllerState: ScreenState { let relockInterval: TimeInterval = 20 var shouldRelockByTime: Bool { - guard let lastUnlockedAt else { return true } - return Date().timeIntervalSince(lastUnlockedAt) > relockInterval + return true } func copy(access: PrivateAccessState? = nil, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 3aecf01cd6826..d7915d1414fdd 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -1156,7 +1156,6 @@ class BrowserViewController: UIViewController, dismissModalsIfStartAtHome() shouldHideAddressToolbar() dismissToolbarCFRs(with: windowUUID) -// applyPrivateLockUI(state.privateLockState) } private func showToastType(toast: ToastType) { @@ -1334,29 +1333,6 @@ class BrowserViewController: UIViewController, title = .SettingsHomePageSectionName navigationItem.backButtonDisplayMode = .generic } - - private lazy var privateLockOverlay: PrivateTabsLockOverlayView = { - let overlay = PrivateTabsLockOverlayView() - overlay.isHidden = true - overlay.onUnlockTapped = { [weak self] in self?.startPrivateAuthFlow() } - overlay.onRetryTapped = { [weak self] in self?.startPrivateAuthFlow() } - return overlay - }() - - private func applyPrivateLockUI(_ lock: BrowserViewControllerState.PrivateLockDomainState?) { - guard let lock else { - privateLockOverlay.renderHidden() - return - } - - privateLockOverlay.render( - access: lock.access, - auth: lock.auth - ) - addressToolbarContainer.isHidden = lock.access == .locked - view.bringSubviewToFront(privateLockOverlay) - view.bringSubviewToFront(bottomContainer) - } // MARK: - Notifiable func handleNotifications(_ notification: Notification) { @@ -1561,7 +1537,6 @@ class BrowserViewController: UIViewController, return self?.newTabSettings } } - view.addSubview(privateLockOverlay) } private func enqueueTabRestoration() { @@ -1858,13 +1833,6 @@ class BrowserViewController: UIViewController, updateHeaderConstraints() } setupBlurViews() - - NSLayoutConstraint.activate([ - privateLockOverlay.topAnchor.constraint(equalTo: view.topAnchor), - privateLockOverlay.leadingAnchor.constraint(equalTo: view.leadingAnchor), - privateLockOverlay.trailingAnchor.constraint(equalTo: view.trailingAnchor), - privateLockOverlay.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) } private func setupBlurViews() { diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index ffb7f8f0956bd..d73a5eb6154ce 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -369,6 +369,8 @@ final class TabDisplayPanelViewController: UIViewController, if panelType == .privateTabs, tabsState.isPrivateMode { // Only adjust the empty view if we are in private mode shouldShowEmptyView(tabsState.isPrivateTabsEmpty) + } + if panelType == .privateTabs { applyPrivateLockUI(tabsState.privateLockState) } shouldShowFadeView() From 12261ab0b1a4f773f3dcdf6545bebaf167b36c6b Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sun, 8 Mar 2026 00:28:24 +0200 Subject: [PATCH 10/27] fixed bug with setting change, fixed retry button styling --- .../Actions/PrivateLockAction.swift | 1 + .../Middleware/PrivateLockMiddleware.swift | 7 ++++++ .../State/BrowserViewControllerState.swift | 9 ++++++-- .../Views/PrivateTabsLockOverlayView.swift | 22 ++++++++++++++++++- .../Main/AppSettingsTableViewController.swift | 4 +++- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index 36dbd0b75b788..280f423ea16f7 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -12,6 +12,7 @@ enum PrivateLockActionType: ActionType { case setTrayDisplayContextAndPanelType case didEnterBackground case willEnterForeground + case lockPrivateTabsSettingsDidChange } struct PrivateLockAction: Action { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index b75efe12753ff..0db18a4b25889 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -52,6 +52,13 @@ final class PrivateLockMiddleware { state: state) case PrivateLockActionType.didEnterBackground, PrivateLockActionType.willEnterForeground: Self.lock(triggeredByFailure: false, windowUUID: action.windowUUID) + case PrivateLockActionType.lockPrivateTabsSettingsDidChange: + let browserState = self.browserState(from: state, windowUUID: action.windowUUID) + store.dispatch(PrivateLockMiddlewareAction( + windowUUID: action.windowUUID, + actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + privatePanelLockState: browserState?.privateLockState.withLastUnlocked(at: nil) + )) default: break } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 63a14830ab04d..0be14234a63c1 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -58,10 +58,11 @@ struct BrowserViewControllerState: ScreenState { var access: PrivateAccessState = .locked var auth: PrivateAuthState = .idle var lastUnlockedAt: Date? - let relockInterval: TimeInterval = 20 + let relockInterval: TimeInterval = 120 var shouldRelockByTime: Bool { - return true + guard let lastUnlockedAt else { return true } + return Date().timeIntervalSince(lastUnlockedAt) > relockInterval } func copy(access: PrivateAccessState? = nil, @@ -75,6 +76,10 @@ struct BrowserViewControllerState: ScreenState { func locked() -> PrivateLockDomainState { copy(access: .locked) } + + func withLastUnlocked(at: Date?) -> PrivateLockDomainState { + PrivateLockDomainState(access: access, auth: auth, lastUnlockedAt: at) + } } let windowUUID: WindowUUID diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index 7da14baf52265..31b2f26f704d6 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -19,7 +19,6 @@ final class PrivateTabsLockOverlayView: UIView { private let titleLabel = UILabel() private let subtitleLabel = UILabel() private let spinner = UIActivityIndicatorView(style: .large) - private let retryButton = UIButton(type: .system) private let stack = UIStackView() override init(frame: CGRect) { @@ -137,6 +136,27 @@ final class PrivateTabsLockOverlayView: UIView { return button }() + + private let retryButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + + var config = UIButton.Configuration.filled() + config.title = "Try Again" + config.baseBackgroundColor = UIColor.systemBlue + config.baseForegroundColor = .white + config.cornerStyle = .capsule + config.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 28, bottom: 14, trailing: 28) + + button.configuration = config + button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.18 + button.layer.shadowRadius = 10 + button.layer.shadowOffset = CGSize(width: 0, height: 4) + + return button + }() func apply(mode: Mode) { switch mode { diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index cf27af5948eb0..4c6550b9810a5 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -445,7 +445,9 @@ class AppSettingsTableViewController: SettingsTableViewController, titleText: "Lock Private Tabs", statusText: "Use Biometrics or Passcode to see Private Tabs" ) { _ in - + let action = PrivateLockAction(windowUUID: self.windowUUID, + actionType: PrivateLockActionType.lockPrivateTabsSettingsDidChange) + store.dispatch(action) } ) } From 5908abeaa99c06435383f223edec1e03c8d5e96e Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sun, 8 Mar 2026 00:48:07 +0200 Subject: [PATCH 11/27] fixed 1 unit test --- .../BrowserViewController/BrowserViewControllerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/BrowserViewController/BrowserViewControllerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/BrowserViewController/BrowserViewControllerTests.swift index 3227f3e1e0298..474dbf0937254 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/BrowserViewController/BrowserViewControllerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/BrowserViewController/BrowserViewControllerTests.swift @@ -139,7 +139,7 @@ class BrowserViewControllerTests: XCTestCase, StoreTestUtility { let actionCalled = try XCTUnwrap(mockStore.dispatchedActions[2] as? GeneralBrowserAction) let actionType = try XCTUnwrap(actionCalled.actionType as? GeneralBrowserActionType) - XCTAssertEqual(mockStore.dispatchedActions.count, 5) + XCTAssertEqual(mockStore.dispatchedActions.count, 6) XCTAssertEqual(actionType, GeneralBrowserActionType.didSelectedTabChangeToHomepage) } From f8d4e71b505e67bb8d93391ad3611bbfd1aa4ff4 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sun, 8 Mar 2026 00:55:03 +0200 Subject: [PATCH 12/27] returned back version number --- firefox-ios/Client/Configuration/version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firefox-ios/Client/Configuration/version.xcconfig b/firefox-ios/Client/Configuration/version.xcconfig index 93280e46bf6cd..805f9900e6545 100644 --- a/firefox-ios/Client/Configuration/version.xcconfig +++ b/firefox-ios/Client/Configuration/version.xcconfig @@ -1 +1 @@ -APP_VERSION = 148.3 +APP_VERSION = 149.0 From ed9a9335a7814e94c9a7ab209b82365afb2006f1 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sun, 8 Mar 2026 01:07:41 +0200 Subject: [PATCH 13/27] fixed proj file --- firefox-ios/Client.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 69d711bfc35b7..2c861297fc329 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -781,6 +781,7 @@ 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivateLockAction.swift */; }; 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */; }; + 76F1091F2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F1091E2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift */; }; 781C19CF2A780BEC0000DF46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 781C19CE2A780BEC0000DF46 /* Common */; }; 787EDD852943EE75002B93AE /* JumpBackInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787EDD832943EE75002B93AE /* JumpBackInTests.swift */; }; 789A0B232E2E969D004547CE /* FxUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5423F62DF6EF17000AA578 /* FxUserState.swift */; }; @@ -8773,6 +8774,7 @@ 7681461D8ADDD3E9662A2A53 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Shared.strings; sourceTree = ""; }; 7693448FA2E2865EA28005EE /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/ErrorPages.strings; sourceTree = ""; }; 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateLockMiddleware.swift; sourceTree = ""; }; + 76F1091E2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxGradientBackgroundView.swift; sourceTree = ""; }; 772744E9858179A908A070DB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ClearPrivateData.strings; sourceTree = ""; }; 77294827911067CD5994B5FD /* co */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = co; path = co.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; 779D4B8EAE580D7B2B55EC3F /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/HistoryPanel.strings; sourceTree = ""; }; @@ -12157,6 +12159,7 @@ F605DD572CC73469009A671B /* TabDisplayDiffableDataSource.swift */, 213BF7522AC21D1B00C53A64 /* TabDisplayPanelViewController.swift */, 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */, + 76F1091E2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift */, 214EF4142AC5D5D0005BCCDA /* TabDisplayView.swift */, 5A679E4A2B239FAE004F2B0D /* TabPeekViewController.swift */, 8A06DF5E2DE0C0EB007B7E9D /* TabTitleSupplementaryView.swift */, @@ -19479,6 +19482,7 @@ 8A19ACB62A3290F9001C2147 /* NotificationsSetting.swift in Sources */, 21365E392F30FD700000C369 /* BrowserViewControllerLayoutManager.swift in Sources */, 43D16B8229831E6A009F8279 /* CreditCardInputField.swift in Sources */, + 76F1091F2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift in Sources */, 8187561A2BB4618500DCD1F3 /* OnboardingViewControllerState.swift in Sources */, F82F68B12E86EF5F002E42D1 /* DeleteAutofillKeysSetting.swift in Sources */, EB98550124226EF70040F24B /* AppDelegate+SyncSentTabs.swift in Sources */, From 7c92ad3758b3d611bcca83ed04a0b6caf9dfd6fb Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 11 Mar 2026 16:14:49 +0200 Subject: [PATCH 14/27] added feature flag privateTabsLockFeature and made use of it --- firefox-ios/Client.xcodeproj/project.pbxproj | 4 +++ .../FeatureFlags/NimbusFlaggableFeature.swift | 4 +++ .../Middleware/PrivateLockMiddleware.swift | 5 ++-- .../PrivateTabsLockFeatureGate.swift | 20 +++++++++++++ .../Views/BrowserViewController.swift | 10 +++++-- .../Main/AppSettingsTableViewController.swift | 30 ++++++++++--------- .../FeatureFlagsDebugViewController.swift | 7 +++++ .../Nimbus/NimbusFeatureFlagLayer.swift | 7 +++++ .../privateTabsLockFeature.yaml | 18 +++++++++++ firefox-ios/nimbus.fml.yaml | 1 + 10 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift create mode 100644 firefox-ios/nimbus-features/privateTabsLockFeature.yaml diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 2c861297fc329..1f7c341c3927c 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -778,6 +778,7 @@ 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */; }; 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F80D332A0A52D700013C3D /* PrivacyPolicyViewController.swift */; }; + 76206ED02F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */; }; 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivateLockAction.swift */; }; 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */; }; @@ -8765,6 +8766,7 @@ 75BA4E00A029A2AD26DF8960 /* anp */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = anp; path = anp.lproj/ClearPrivateData.strings; sourceTree = ""; }; 75BC41B2962DAC5B25A288B4 /* hsb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hsb; path = hsb.lproj/LoginManager.strings; sourceTree = ""; }; 75EF43EA84AEEBBE2C059310 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Menu.strings; sourceTree = ""; }; + 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateTabsLockFeatureGate.swift; sourceTree = ""; }; 764643DCB449658AAA8ED829 /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/InfoPlist.strings; sourceTree = ""; }; 765243D790B967A69BD5DCF7 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/ErrorPages.strings; sourceTree = ""; }; 766F49788D099E2201B74791 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Intro.strings; sourceTree = ""; }; @@ -13001,6 +13003,7 @@ isa = PBXGroup; children = ( 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */, + 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */, ); path = Middleware; sourceTree = ""; @@ -19231,6 +19234,7 @@ 8A5D1CBF2A30DC75005AD35C /* SyncNowSetting.swift in Sources */, 210E0EB8298D9D4500BB4F33 /* SearchEngineProvider.swift in Sources */, 21618A612A4201E500A5189E /* ThemeSettingsController.swift in Sources */, + 76206ED02F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift in Sources */, AB4FB4DB2C89FB10005EF0CC /* BlockedTrackersHeaderView.swift in Sources */, D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */, 8A15AB752D5FDB80008BB03C /* AutoplaySettingsViewController.swift in Sources */, diff --git a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift index d9cd09ff53929..4aa49369d638f 100644 --- a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift +++ b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift @@ -32,6 +32,7 @@ enum NimbusFeatureFlagID: String, CaseIterable { case homepageStoriesScrollDirection case homepageStoryCategories case needsReloadRefactor + case privateTabsLock case shouldUseBrandRefreshConfiguration case shouldUseJapanConfiguration case menuDefaultBrowserBanner @@ -105,6 +106,7 @@ enum NimbusFeatureFlagID: String, CaseIterable { .needsReloadRefactor, .noInternetConnectionErrorPage, .otherErrorPages, + .privateTabsLock, .recentSearches, .relayIntegration, .sentFromFirefox, @@ -160,6 +162,8 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar { return FlagKeys.SentFromFirefox case .startAtHome: return FlagKeys.StartAtHome + case .privateTabsLock: + fatalError("Please implement a key for this feature") // Cases where users do not have the option to manipulate a setting. Please add in alphabetical order. case .aiKillSwitch, .appearanceMenu, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 0db18a4b25889..0b410afa90305 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -8,7 +8,7 @@ import Shared import LocalAuthentication @MainActor -final class PrivateLockMiddleware { +final class PrivateLockMiddleware: FeatureFlaggable { private let prefs: Prefs @@ -159,8 +159,7 @@ final class PrivateLockMiddleware { } private func isPrivateLockFeatureEnabled() -> Bool { - let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false - return shouldLock + PrivateTabsLockFeatureGate(prefs: prefs).isEnabled } private func browserState(from appState: AppState, windowUUID: WindowUUID) -> BrowserViewControllerState? { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift new file mode 100644 index 0000000000000..a3acf41c52381 --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift @@ -0,0 +1,20 @@ +// 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 Foundation +import Shared + +final class PrivateTabsLockFeatureGate: FeatureFlaggable { + + private let prefs: Prefs + init(prefs: Prefs) { + self.prefs = prefs + } + + var isEnabled: Bool { + let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false + let featureEnabled = featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildOnly) + return shouldLock && featureEnabled + } +} diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index d7915d1414fdd..f09e386b5ef77 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -952,11 +952,11 @@ class BrowserViewController: UIViewController, private func showPrivacyOverlayIfNeeded(checkActualState: Bool = false) { if let state = browserViewControllerState { - let shouldLock = profile.prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false - if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && shouldLock { + let featureEnabled = PrivateTabsLockFeatureGate(prefs: profile.prefs).isEnabled + if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && featureEnabled { focusOnTabSegment(animated: false) if checkActualState { - sceneWillEnterForegroundNotification() + privateLockWillEnterForeground() } } } @@ -994,6 +994,10 @@ class BrowserViewController: UIViewController, } func sceneWillEnterForegroundNotification() { + privateLockWillEnterForeground() + } + + private func privateLockWillEnterForeground() { store.dispatch( PrivateLockAction( windowUUID: windowUUID, diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index 4c6550b9810a5..fad1f7cd45ea2 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -436,20 +436,22 @@ class AppSettingsTableViewController: SettingsTableViewController, } ) - privacySettings.append( - BoolSetting( - prefs: profile.prefs, - theme: themeManager.getCurrentTheme(for: windowUUID), - prefKey: PrefsKeys.Settings.lockPrivateTabs, - defaultValue: false, - titleText: "Lock Private Tabs", - statusText: "Use Biometrics or Passcode to see Private Tabs" - ) { _ in - let action = PrivateLockAction(windowUUID: self.windowUUID, - actionType: PrivateLockActionType.lockPrivateTabsSettingsDidChange) - store.dispatch(action) - } - ) + if featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildOnly) { + privacySettings.append( + BoolSetting( + prefs: profile.prefs, + theme: themeManager.getCurrentTheme(for: windowUUID), + prefKey: PrefsKeys.Settings.lockPrivateTabs, + defaultValue: false, + titleText: "Lock Private Tabs", + statusText: "Use Biometrics or Passcode to see Private Tabs" + ) { _ in + let action = PrivateLockAction(windowUUID: self.windowUUID, + actionType: PrivateLockActionType.lockPrivateTabsSettingsDidChange) + store.dispatch(action) + } + ) + } } privacySettings.append(ContentBlockerSetting(settings: self, settingsDelegate: parentCoordinator)) diff --git a/firefox-ios/Client/Frontend/Settings/Main/Debug/FeatureFlags/FeatureFlagsDebugViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/Debug/FeatureFlags/FeatureFlagsDebugViewController.swift index dbf9b8eddacc7..2fc42d1cabec1 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/Debug/FeatureFlags/FeatureFlagsDebugViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/Debug/FeatureFlags/FeatureFlagsDebugViewController.swift @@ -157,6 +157,13 @@ final class FeatureFlagsDebugViewController: SettingsTableViewController, Featur ) { [weak self] _ in self?.reloadView() }, + FeatureFlagsBoolSetting( + with: .privateTabsLock, + titleText: format(string: "Private Tabs Lock"), + statusText: format(string: "Toggle Private Tabs Lock") + ) { [weak self] _ in + self?.reloadView() + }, FeatureFlagsBoolSetting( with: .relayIntegration, titleText: format(string: "Relay Email Masks"), diff --git a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift index f3cef3d4b07bf..000158c6a23ae 100644 --- a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift +++ b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift @@ -76,6 +76,9 @@ final class NimbusFeatureFlagLayer: Sendable { case .needsReloadRefactor: return checkNeedsReloadRefactorFeature(from: nimbus) + case .privateTabsLock: + return checkPrivateTabsLockFeature(from: nimbus) + case .shouldUseBrandRefreshConfiguration: return checkShouldUseBrandRefreshConfigurationFeature(from: nimbus) @@ -559,4 +562,8 @@ final class NimbusFeatureFlagLayer: Sendable { private func checkAiKillSwitchFeature(from nimbus: FxNimbus) -> Bool { return nimbus.features.aiKillSwitchFeature.value().enabled } + + private func checkPrivateTabsLockFeature(from nimbus: FxNimbus) -> Bool { + return nimbus.features.privateTabsLock.value().enabled + } } diff --git a/firefox-ios/nimbus-features/privateTabsLockFeature.yaml b/firefox-ios/nimbus-features/privateTabsLockFeature.yaml new file mode 100644 index 0000000000000..155b90f55ae65 --- /dev/null +++ b/firefox-ios/nimbus-features/privateTabsLockFeature.yaml @@ -0,0 +1,18 @@ +# The configuration for the privateTabsLockFeature feature +features: + private-tabs-lock-feature: + description: > + Protect private tabs with authentication + variables: + enabled: + description: > + Whether or not this feature is enabled + type: Boolean + default: false + defaults: + - channel: beta + value: + enabled: false + - channel: developer + value: + enabled: true \ No newline at end of file diff --git a/firefox-ios/nimbus.fml.yaml b/firefox-ios/nimbus.fml.yaml index ceb4886c0fcb9..af13a6a4a5ce4 100644 --- a/firefox-ios/nimbus.fml.yaml +++ b/firefox-ios/nimbus.fml.yaml @@ -38,6 +38,7 @@ include: - nimbus-features/newAddressBarMenu.yaml - nimbus-features/newAppearanceMenu.yaml - nimbus-features/onboardingFrameworkFeature.yaml + - nimbus-features/privateTabsLockFeature.yaml - nimbus-features/recentSearchesFeature.yaml - nimbus-features/relayIntegrationFeature.yaml - nimbus-features/searchFeature.yaml From e4477434aa526c221580f28c0e92d0c7eaaa22bf Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 11 Mar 2026 16:47:50 +0200 Subject: [PATCH 15/27] fixed errors with feature name --- firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift | 2 +- firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift index 4aa49369d638f..682f3f49ea235 100644 --- a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift +++ b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift @@ -163,7 +163,7 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar { case .startAtHome: return FlagKeys.StartAtHome case .privateTabsLock: - fatalError("Please implement a key for this feature") + return PrefsKeys.Settings.lockPrivateTabs // Cases where users do not have the option to manipulate a setting. Please add in alphabetical order. case .aiKillSwitch, .appearanceMenu, diff --git a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift index 000158c6a23ae..ddfce0bc78717 100644 --- a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift +++ b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift @@ -564,6 +564,6 @@ final class NimbusFeatureFlagLayer: Sendable { } private func checkPrivateTabsLockFeature(from nimbus: FxNimbus) -> Bool { - return nimbus.features.privateTabsLock.value().enabled + return nimbus.features.privateTabsLockFeature.value().enabled } } From cd8cab7bfdbe56105f0dfde3c4d3294822f1de9a Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Wed, 11 Mar 2026 18:48:30 +0200 Subject: [PATCH 16/27] added auto biometry request prompt on view appearance --- .../Middleware/PrivateLockMiddleware.swift | 36 +++++++++--------- .../PrivateTabsLockFeatureGate.swift | 1 - .../State/BrowserViewControllerState.swift | 22 +++++------ .../Views/PrivateTabsLockOverlayView.swift | 34 +---------------- .../Views/TabDisplayPanelViewController.swift | 37 ++++++++++++++++--- 5 files changed, 60 insertions(+), 70 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 0b410afa90305..caf435a231fa2 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -9,13 +9,12 @@ import LocalAuthentication @MainActor final class PrivateLockMiddleware: FeatureFlaggable { - private let prefs: Prefs - + init(profile: Profile = AppContainer.shared.resolve()) { prefs = profile.prefs } - + lazy var lockProvider: Middleware = { state, action in if let action = action as? PrivateLockAction { self.resolveTabPrivateLockActions(action: action, state: state) @@ -23,17 +22,18 @@ final class PrivateLockMiddleware: FeatureFlaggable { self.resolveTabChange(action: action, state: state) } } - + private func resolveTabPrivateLockActions(action: PrivateLockAction, state: AppState) { guard isPrivateLockFeatureEnabled() else { Self.unlock(windowUUID: action.windowUUID) return } - + switch action.actionType { case PrivateLockActionType.requestAuth(let reason): let browserState = self.browserState(from: state, windowUUID: action.windowUUID) - guard browserState?.privateLockState.auth != .authenticating else { return } + let lockState = browserState?.privateLockState + guard lockState?.auth != .authenticating, lockState?.access == .locked else { return } auth(reason: reason, windowUUID: action.windowUUID) case PrivateLockActionType.setTrayDisplayContext: store.dispatch(PrivateLockMiddlewareAction( @@ -63,7 +63,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { break } } - + private func resolveTabChange(action: TabTrayAction, state: AppState) { switch action.actionType { case TabTrayActionType.changePanel: @@ -74,15 +74,14 @@ final class PrivateLockMiddleware: FeatureFlaggable { break } } - + private func resolveTabTrayPanelTypeChange(panel: TabTrayPanelType?, windowUUID: WindowUUID, state: AppState) { - guard let panelType = panel, let state = browserState(from: state, windowUUID: windowUUID) else { return } - + let privateLockEnabled = isPrivateLockFeatureEnabled() store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, @@ -90,21 +89,20 @@ final class PrivateLockMiddleware: FeatureFlaggable { trayPanelType: panel, privateLockEnabled: privateLockEnabled )) - + guard state.didBecomePrivateVisible(afterChangingPanelTo: panelType) else { return } guard privateLockEnabled else { return } guard state.privateLockState.auth != .authenticating else { return } guard state.privateLockState.shouldRelockByTime else { return } - + store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.setPrivateLockState, privatePanelLockState: state.privateLockState.locked() )) } - + private func auth(reason: String, windowUUID: WindowUUID) { - store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.setPrivateLockState, @@ -112,7 +110,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { auth: .authenticating, lastUnlockedAt: nil) )) - + let context = LAContext() context.localizedCancelTitle = "Cancel" @@ -136,7 +134,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { } } } - + private static func lock(triggeredByFailure: Bool, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, @@ -147,7 +145,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { lastUnlockedAt: nil) )) } - + private static func unlock(windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, @@ -157,11 +155,11 @@ final class PrivateLockMiddleware: FeatureFlaggable { lastUnlockedAt: Date()) )) } - + private func isPrivateLockFeatureEnabled() -> Bool { PrivateTabsLockFeatureGate(prefs: prefs).isEnabled } - + private func browserState(from appState: AppState, windowUUID: WindowUUID) -> BrowserViewControllerState? { appState.screenState( BrowserViewControllerState.self, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift index a3acf41c52381..0c40d29899b5c 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift @@ -6,7 +6,6 @@ import Foundation import Shared final class PrivateTabsLockFeatureGate: FeatureFlaggable { - private let prefs: Prefs init(prefs: Prefs) { self.prefs = prefs diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 0be14234a63c1..2ddd253145406 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -37,23 +37,23 @@ struct BrowserViewControllerState: ScreenState { // TODO: FXIOS-13118 Clean up and remove as we should have one navigation entry point case summarizer(config: SummarizerConfig?) } - + enum PrivateAccessState: Equatable { case locked case unlocked } - + enum PrivateAuthState: Equatable { case idle case authenticating case failed } - + enum TrayDisplayContext: Equatable { case page case tray } - + struct PrivateLockDomainState: Equatable { var access: PrivateAccessState = .locked var auth: PrivateAuthState = .idle @@ -64,7 +64,7 @@ struct BrowserViewControllerState: ScreenState { guard let lastUnlockedAt else { return true } return Date().timeIntervalSince(lastUnlockedAt) > relockInterval } - + func copy(access: PrivateAccessState? = nil, auth: PrivateAuthState? = nil, lastUnlockedAt: Date? = nil) -> PrivateLockDomainState { @@ -72,11 +72,11 @@ struct BrowserViewControllerState: ScreenState { auth: auth ?? self.auth, lastUnlockedAt: lastUnlockedAt ?? self.lastUnlockedAt) } - + func locked() -> PrivateLockDomainState { copy(access: .locked) } - + func withLastUnlocked(at: Date?) -> PrivateLockDomainState { PrivateLockDomainState(access: access, auth: auth, lastUnlockedAt: at) } @@ -97,7 +97,7 @@ struct BrowserViewControllerState: ScreenState { var navigationDestination: NavigationDestination? var privateLockState: PrivateLockDomainState var trayPanelType: TabTrayPanelType? - var trayDisplayContext: TrayDisplayContext = TrayDisplayContext.page + var trayDisplayContext = TrayDisplayContext.page init(appState: AppState, uuid: WindowUUID) { guard let bvcState = appState.screenState( @@ -287,7 +287,7 @@ struct BrowserViewControllerState: ScreenState { return passthroughState(from: state, action: action) } } - + @MainActor static func reduceStateForPrivateLockAction( action: PrivateLockMiddlewareAction, @@ -313,11 +313,11 @@ struct BrowserViewControllerState: ScreenState { return state } } - + func didBecomePrivateVisible(afterChangingPanelTo panelType: TabTrayPanelType) -> Bool { !isPrivateSurfaceVisible && panelType == .privateTabs } - + private var isPrivateSurfaceVisible: Bool { trayPanelType == .privateTabs } diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index 31b2f26f704d6..6403cd42026c8 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -5,14 +5,12 @@ import UIKit final class PrivateTabsLockOverlayView: UIView { - enum Mode: Equatable { case prompt case authenticating case failed } - var onUnlockTapped: (() -> Void)? var onRetryTapped: (() -> Void)? private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) @@ -29,7 +27,7 @@ final class PrivateTabsLockOverlayView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func render(access: BrowserViewControllerState.PrivateAccessState, auth: BrowserViewControllerState.PrivateAuthState) { switch access { case .unlocked: @@ -84,10 +82,6 @@ final class PrivateTabsLockOverlayView: UIView { spinner.hidesWhenStopped = true - unlockButton.setTitle("Unlock", for: .normal) - unlockButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) - unlockButton.addTarget(self, action: #selector(unlockTapped), for: .touchUpInside) - retryButton.setTitle("Try Again", for: .normal) retryButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside) @@ -101,7 +95,6 @@ final class PrivateTabsLockOverlayView: UIView { stack.addArrangedSubview(titleLabel) stack.addArrangedSubview(subtitleLabel) stack.addArrangedSubview(spinner) - stack.addArrangedSubview(unlockButton) stack.addArrangedSubview(retryButton) addSubview(stack) @@ -115,28 +108,7 @@ final class PrivateTabsLockOverlayView: UIView { apply(mode: .prompt) } - - private let unlockButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - - var config = UIButton.Configuration.filled() - config.title = "Unlock" - config.baseBackgroundColor = UIColor.systemBlue - config.baseForegroundColor = .white - config.cornerStyle = .capsule - config.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 28, bottom: 14, trailing: 28) - - button.configuration = config - button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) - button.layer.shadowColor = UIColor.black.cgColor - button.layer.shadowOpacity = 0.18 - button.layer.shadowRadius = 10 - button.layer.shadowOffset = CGSize(width: 0, height: 4) - return button - }() - private let retryButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false @@ -162,24 +134,20 @@ final class PrivateTabsLockOverlayView: UIView { switch mode { case .prompt: spinner.stopAnimating() - unlockButton.isHidden = false retryButton.isHidden = true subtitleLabel.text = "Unlock with Face ID to view private tabs" case .authenticating: spinner.startAnimating() - unlockButton.isHidden = true retryButton.isHidden = true subtitleLabel.text = "Authenticating…" case .failed: spinner.stopAnimating() - unlockButton.isHidden = true retryButton.isHidden = false subtitleLabel.text = "Face ID failed. Try again." } } - @objc private func unlockTapped() { onUnlockTapped?() } @objc private func retryTapped() { onRetryTapped?() } } diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index d73a5eb6154ce..e2d66417a3afe 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -29,6 +29,7 @@ final class TabDisplayPanelViewController: UIViewController, private let windowUUID: WindowUUID var currentWindowUUID: UUID? { windowUUID } private var viewHasAppeared = false + private var privateLockAuthTask: Task? private var isTabTrayUIExperimentsEnabled: Bool { return featureFlags.isFeatureEnabled(.tabTrayUIExperiments, checking: .buildOnly) @@ -131,6 +132,12 @@ final class TabDisplayPanelViewController: UIViewController, shouldShowFadeView() } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + privateLockAuthTask?.cancel() + privateLockAuthTask = nil + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() gradientLayer.frame = fadeView.bounds @@ -164,7 +171,7 @@ final class TabDisplayPanelViewController: UIViewController, backgroundPrivacyOverlay.isHidden = true setupEmptyView() setupFadeView() - + view.addSubview(privateLockOverlay) NSLayoutConstraint.activate([ privateLockOverlay.topAnchor.constraint(equalTo: view.topAnchor), @@ -202,15 +209,14 @@ final class TabDisplayPanelViewController: UIViewController, view.subviews.forEach { $0.removeFromSuperview() } view.removeFromSuperview() } - + private lazy var privateLockOverlay: PrivateTabsLockOverlayView = { let overlay = PrivateTabsLockOverlayView() overlay.isHidden = true - overlay.onUnlockTapped = { [weak self] in self?.startPrivateAuthFlow() } overlay.onRetryTapped = { [weak self] in self?.startPrivateAuthFlow() } return overlay }() - + private func startPrivateAuthFlow() { store.dispatch( PrivateLockAction( @@ -379,10 +385,12 @@ final class TabDisplayPanelViewController: UIViewController, applyTheme() } } - + private func applyPrivateLockUI(_ lock: BrowserViewControllerState.PrivateLockDomainState?) { - guard let lock else { + guard panelType == .privateTabs, let lock else { privateLockOverlay.renderHidden() + privateLockAuthTask?.cancel() + privateLockAuthTask = nil return } @@ -390,6 +398,23 @@ final class TabDisplayPanelViewController: UIViewController, access: lock.access, auth: lock.auth ) + + guard lock.access == .locked else { + privateLockAuthTask?.cancel() + privateLockAuthTask = nil + return + } + + guard lock.auth == .idle else { return } + + privateLockAuthTask?.cancel() + privateLockAuthTask = Task { [weak self] in + try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 sec delay for better ux + guard !Task.isCancelled else { return } + guard let self else { return } + guard self.isViewLoaded, self.view.window != nil else { return } + self.startPrivateAuthFlow() + } } // MARK: - EmptyPrivateTabsViewDelegate From 0b0e6f9ac44c34e2da775ce7df32cb7e52e95e33 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 11:34:59 +0200 Subject: [PATCH 17/27] renamed PrivateLockActionType.setTrayDisplayContextAndPanelType to PrivateLockActionType.didChangeTrayPresentation, suppressed lint error for tabManager(_ tabManager:..) delegate method and added comment to fix it later, made use of ensureMainThread instead of Tasl { @MainActor --- firefox-ios/Client.xcodeproj/project.pbxproj | 20 +++++++++------ .../Browser/BrowserCoordinator.swift | 2 +- .../Actions/PrivateLockAction.swift | 6 ++--- .../Middleware/PrivateLockMiddleware.swift | 4 +-- .../Views/BrowserViewController.swift | 25 +++++++++++-------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 1f7c341c3927c..0571029b47fe8 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -27782,6 +27782,8 @@ CODE_SIGN_ENTITLEMENTS = ""; COPY_PHASE_STRIP = NO; DEFINES_MODULE = YES; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; INFOPLIST_FILE = Sync/Info.plist; @@ -27815,6 +27817,8 @@ COPY_PHASE_STRIP = NO; DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 43AQ936H96; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; @@ -27926,7 +27930,7 @@ CODE_SIGN_ENTITLEMENTS = "$(inherit)Extensions/Entitlements/Fennec.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = Extensions/NotificationService/Info.plist; OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_developer -DMOZ_TARGET_NOTIFICATIONSERVICE"; @@ -28063,7 +28067,7 @@ CODE_SIGN_ENTITLEMENTS = Extensions/Entitlements/Fennec.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = WidgetKit/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -28148,6 +28152,8 @@ COPY_PHASE_STRIP = NO; DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 43AQ936H96; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; EAGER_LINKING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; @@ -29284,7 +29290,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -29856,7 +29862,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -31101,7 +31107,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -31402,7 +31408,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -31442,7 +31448,7 @@ CODE_SIGN_ENTITLEMENTS = "$(inherit)Extensions/Entitlements/Fennec.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 43AQ936H96; + DEVELOPMENT_TEAM = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = Extensions/ShareTo/Info.plist; OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_developer -DMOZ_TARGET_SHARETO"; diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index fc8bb4a20960d..f0668fa2805d1 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -1296,7 +1296,7 @@ final class BrowserCoordinator: BaseCoordinator, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.setTrayDisplayContextAndPanelType, + actionType: PrivateLockActionType.didChangeTrayPresentation, trayDisplayContext: .page, trayPanelType: panel ) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index 280f423ea16f7..b2a86a3da4d92 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -9,7 +9,7 @@ enum PrivateLockActionType: ActionType { case requestAuth(String) case setPrivateContext case setTrayDisplayContext - case setTrayDisplayContextAndPanelType + case didChangeTrayPresentation case didEnterBackground case willEnterForeground case lockPrivateTabsSettingsDidChange @@ -20,7 +20,7 @@ struct PrivateLockAction: Action { let actionType: ActionType let trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? let trayPanelType: TabTrayPanelType? - + init(windowUUID: WindowUUID, actionType: ActionType, trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? = nil, @@ -45,7 +45,7 @@ struct PrivateLockMiddlewareAction: Action { let trayPanelType: TabTrayPanelType? let trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? let privateLockEnabled: Bool? - + init(windowUUID: WindowUUID, actionType: ActionType, privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState? = nil, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index caf435a231fa2..be581857cf5dd 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -41,7 +41,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, trayDisplayContext: action.trayDisplayContext) ) - case PrivateLockActionType.setTrayDisplayContextAndPanelType: + case PrivateLockActionType.didChangeTrayPresentation: store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, @@ -125,7 +125,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { .deviceOwnerAuthentication, localizedReason: reason ) { success, authError in - Task { @MainActor in + ensureMainThread { if success { Self.unlock(windowUUID: windowUUID) } else { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index f09e386b5ef77..c8affbf4eb34a 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -946,10 +946,10 @@ class BrowserViewController: UIViewController, // individual TabManager instances for each BVC, so we perform these here instead. tabManager.preserveTabs() logTelemetryForAppDidEnterBackground() - + showPrivacyOverlayIfNeeded() } - + private func showPrivacyOverlayIfNeeded(checkActualState: Bool = false) { if let state = browserViewControllerState { let featureEnabled = PrivateTabsLockFeatureGate(prefs: profile.prefs).isEnabled @@ -980,7 +980,7 @@ class BrowserViewController: UIViewController, guard canShowPrivacyWindow else { return } privacyWindowHelper.showWindow(windowScene: currentWindowScene, withThemedColor: currentTheme().colors.layer3) - + store.dispatch( PrivateLockAction( windowUUID: windowUUID, @@ -992,11 +992,11 @@ class BrowserViewController: UIViewController, func sceneDidActivateNotification() { privacyWindowHelper.removeWindow() } - + func sceneWillEnterForegroundNotification() { privateLockWillEnterForeground() } - + private func privateLockWillEnterForeground() { store.dispatch( PrivateLockAction( @@ -1208,7 +1208,7 @@ class BrowserViewController: UIViewController, createMicrosurveyPrompt(with: state.microsurveyState) } } - + private func startPrivateAuthFlow() { store.dispatch( PrivateLockAction( @@ -2323,7 +2323,7 @@ class BrowserViewController: UIViewController, topTabsViewController?.refreshTabs() } setupMicrosurvey() - + store.dispatch( PrivateLockAction( windowUUID: windowUUID, @@ -3446,7 +3446,7 @@ class BrowserViewController: UIViewController, let isPrivateTab = tabManager.selectedTab?.isPrivate ?? false let selectedSegment: TabTrayPanelType = focusedSegment ?? (isPrivateTab ? .privateTabs : .tabs) navigationHandler?.showTabTray(selectedPanel: selectedSegment, animated: animated) - + store.dispatch( PrivateLockAction( windowUUID: windowUUID, @@ -4749,7 +4749,7 @@ extension BrowserViewController: SearchViewControllerDelegate { let navController = ModalSettingsNavigationController(rootViewController: searchSettingsTableViewController) self.present(navController, animated: true, completion: nil) } - + func settingsControllerDidHide() { showPrivacyOverlayIfNeeded(checkActualState: true) } @@ -4836,7 +4836,10 @@ extension BrowserViewController: SearchViewControllerDelegate { } extension BrowserViewController: TabManagerDelegate { + // FXIOS-15079 break this function down and remove swiftlint ignore function body length + // swiftlint:disable function_body_length func tabManager(_ tabManager: TabManager, didSelectedTabChange selectedTab: Tab, previousTab: Tab?, isRestoring: Bool) { + // swiftlint:enable function_body_length // Failing to have a non-nil webView by this point will cause the toolbar scrolling behaviour to regress, // back/forward buttons never to become enabled, etc. on tab restore after launch. [FXIOS-9785, FXIOS-9781] assert(selectedTab.webView != nil, "Setup will fail if the webView is not initialized for selectedTab") @@ -4984,12 +4987,12 @@ extension BrowserViewController: TabManagerDelegate { if needsReload { selectedTab.reloadPage() } - + let panel = TabTrayPanelType.convert(from: selectedTab) store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.setTrayDisplayContextAndPanelType, + actionType: PrivateLockActionType.didChangeTrayPresentation, trayPanelType: panel ) ) From c326d3abe8e4ff0a319e67ba7fcf211e5fd8549b Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:01:24 +0200 Subject: [PATCH 18/27] renamed cases for PrivateLockActionType enum to match firefox guidelines --- .../BrowserViewController/Actions/PrivateLockAction.swift | 7 +++---- .../Middleware/PrivateLockMiddleware.swift | 6 +++--- .../Views/BrowserViewController.swift | 6 +++--- .../Browser/Tabs/Views/TabDisplayPanelViewController.swift | 2 +- .../Settings/Main/AppSettingsTableViewController.swift | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index b2a86a3da4d92..da6f6c6c627ab 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -6,13 +6,12 @@ import Common import Redux enum PrivateLockActionType: ActionType { - case requestAuth(String) - case setPrivateContext - case setTrayDisplayContext + case privateAuthRequested(String) + case didChangeTrayDisplayContext case didChangeTrayPresentation case didEnterBackground case willEnterForeground - case lockPrivateTabsSettingsDidChange + case didChangePrivateTabsLockSetting } struct PrivateLockAction: Action { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index be581857cf5dd..79d4dba144893 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -30,12 +30,12 @@ final class PrivateLockMiddleware: FeatureFlaggable { } switch action.actionType { - case PrivateLockActionType.requestAuth(let reason): + case PrivateLockActionType.privateAuthRequested(let reason): let browserState = self.browserState(from: state, windowUUID: action.windowUUID) let lockState = browserState?.privateLockState guard lockState?.auth != .authenticating, lockState?.access == .locked else { return } auth(reason: reason, windowUUID: action.windowUUID) - case PrivateLockActionType.setTrayDisplayContext: + case PrivateLockActionType.didChangeTrayDisplayContext: store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, @@ -52,7 +52,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { state: state) case PrivateLockActionType.didEnterBackground, PrivateLockActionType.willEnterForeground: Self.lock(triggeredByFailure: false, windowUUID: action.windowUUID) - case PrivateLockActionType.lockPrivateTabsSettingsDidChange: + case PrivateLockActionType.didChangePrivateTabsLockSetting: let browserState = self.browserState(from: state, windowUUID: action.windowUUID) store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index c8affbf4eb34a..74dac3dcdb446 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -1213,7 +1213,7 @@ class BrowserViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.requestAuth("Unlock your private tabs") + actionType: PrivateLockActionType.privateAuthRequested("Unlock your private tabs") ) ) } @@ -2327,7 +2327,7 @@ class BrowserViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.setTrayDisplayContext, + actionType: PrivateLockActionType.didChangeTrayDisplayContext, trayDisplayContext: .page ) ) @@ -3450,7 +3450,7 @@ class BrowserViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.setTrayDisplayContext, + actionType: PrivateLockActionType.didChangeTrayDisplayContext, trayDisplayContext: .tray ) ) diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index e2d66417a3afe..f06b44790cc1b 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -221,7 +221,7 @@ final class TabDisplayPanelViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.requestAuth("Unlock your private tabs") + actionType: PrivateLockActionType.privateAuthRequested("Unlock your private tabs") ) ) } diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index fad1f7cd45ea2..8d5ea32c654af 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -447,7 +447,7 @@ class AppSettingsTableViewController: SettingsTableViewController, statusText: "Use Biometrics or Passcode to see Private Tabs" ) { _ in let action = PrivateLockAction(windowUUID: self.windowUUID, - actionType: PrivateLockActionType.lockPrivateTabsSettingsDidChange) + actionType: PrivateLockActionType.didChangePrivateTabsLockSetting) store.dispatch(action) } ) From 426916c6ee30fd3163c8b8787efb7d39feb2e843 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:10:03 +0200 Subject: [PATCH 19/27] renamed cases for PrivateLockMiddlewareActionType enum to match firefox guidelines --- .../Actions/PrivateLockAction.swift | 6 +++--- .../Middleware/PrivateLockMiddleware.swift | 16 ++++++++-------- .../State/BrowserViewControllerState.swift | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index da6f6c6c627ab..28cfe2261eae2 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -32,9 +32,9 @@ struct PrivateLockAction: Action { } enum PrivateLockMiddlewareActionType: ActionType { - case setPrivateLockState - case changedTabTrayPanelType - case setTrayDisplayContext + case didChangePrivateLockState + case didChangeTabTrayPanelType + case didChangeTrayDisplayContext } struct PrivateLockMiddlewareAction: Action { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 79d4dba144893..bdbb8c8e72228 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -38,13 +38,13 @@ final class PrivateLockMiddleware: FeatureFlaggable { case PrivateLockActionType.didChangeTrayDisplayContext: store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, + actionType: PrivateLockMiddlewareActionType.didChangeTrayDisplayContext, trayDisplayContext: action.trayDisplayContext) ) case PrivateLockActionType.didChangeTrayPresentation: store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.setTrayDisplayContext, + actionType: PrivateLockMiddlewareActionType.didChangeTrayDisplayContext, trayDisplayContext: action.trayDisplayContext) ) resolveTabTrayPanelTypeChange(panel: action.trayPanelType, @@ -56,7 +56,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { let browserState = self.browserState(from: state, windowUUID: action.windowUUID) store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: browserState?.privateLockState.withLastUnlocked(at: nil) )) default: @@ -85,7 +85,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { let privateLockEnabled = isPrivateLockFeatureEnabled() store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.changedTabTrayPanelType, + actionType: PrivateLockMiddlewareActionType.didChangeTabTrayPanelType, trayPanelType: panel, privateLockEnabled: privateLockEnabled )) @@ -97,7 +97,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: state.privateLockState.locked() )) } @@ -105,7 +105,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { private func auth(reason: String, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .locked, auth: .authenticating, lastUnlockedAt: nil) @@ -138,7 +138,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { private static func lock(triggeredByFailure: Bool, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .locked, auth: triggeredByFailure ? .failed : .idle, @@ -149,7 +149,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { private static func unlock(windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, - actionType: PrivateLockMiddlewareActionType.setPrivateLockState, + actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .unlocked, auth: .idle, lastUnlockedAt: Date()) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 2ddd253145406..82d9ad87735a3 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -294,17 +294,17 @@ struct BrowserViewControllerState: ScreenState { state: BrowserViewControllerState ) -> BrowserViewControllerState { switch action.actionType { - case PrivateLockMiddlewareActionType.setPrivateLockState: + case PrivateLockMiddlewareActionType.didChangePrivateLockState: guard let privateLockState = action.privateLockState else { return state } var newState = state newState.privateLockState = privateLockState return newState - case PrivateLockMiddlewareActionType.changedTabTrayPanelType: + case PrivateLockMiddlewareActionType.didChangeTabTrayPanelType: guard let panelType = action.trayPanelType else { return state } var newState = state newState.trayPanelType = panelType return newState - case PrivateLockMiddlewareActionType.setTrayDisplayContext: + case PrivateLockMiddlewareActionType.didChangeTrayDisplayContext: guard let trayDisplayContext = action.trayDisplayContext else { return state } var newState = state newState.trayDisplayContext = trayDisplayContext From 907be36a62888b8a03120e11153908710f57025f Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:20:34 +0200 Subject: [PATCH 20/27] combined separated guards into one, added explanation comment --- .../Middleware/PrivateLockMiddleware.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index bdbb8c8e72228..44fd9528ac91b 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -90,10 +90,14 @@ final class PrivateLockMiddleware: FeatureFlaggable { privateLockEnabled: privateLockEnabled )) - guard state.didBecomePrivateVisible(afterChangingPanelTo: panelType) else { return } - guard privateLockEnabled else { return } - guard state.privateLockState.auth != .authenticating else { return } - guard state.privateLockState.shouldRelockByTime else { return } + // Only trigger a relock when switching to the private panel, + // the feature is enabled, we are not already authenticating, + // and the relock timeout has elapsed + guard state.didBecomePrivateVisible(afterChangingPanelTo: panelType), + privateLockEnabled, + state.privateLockState.auth != .authenticating, + state.privateLockState.shouldRelockByTime + else { return } store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, From a5bdd56f6d8e65f8914e526af1dcfee503f4671b Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:26:50 +0200 Subject: [PATCH 21/27] renamed auth(reason:windowUUID:) to startPrivateTabsAuthFlow(reason:windowUUID:) --- .../Middleware/PrivateLockMiddleware.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 44fd9528ac91b..941dcc4a37b5d 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -34,7 +34,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { let browserState = self.browserState(from: state, windowUUID: action.windowUUID) let lockState = browserState?.privateLockState guard lockState?.auth != .authenticating, lockState?.access == .locked else { return } - auth(reason: reason, windowUUID: action.windowUUID) + startPrivateTabsAuthFlow(reason: reason, windowUUID: action.windowUUID) case PrivateLockActionType.didChangeTrayDisplayContext: store.dispatch(PrivateLockMiddlewareAction( windowUUID: action.windowUUID, @@ -106,7 +106,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { )) } - private func auth(reason: String, windowUUID: WindowUUID) { + private func startPrivateTabsAuthFlow(reason: String, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, From 925f80774fd341486e1202163c9713cf8927a5d6 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:38:13 +0200 Subject: [PATCH 22/27] moved PrivateLockDomainState and all related enums to separate file --- firefox-ios/Client.xcodeproj/project.pbxproj | 4 ++ .../Actions/PrivateLockAction.swift | 4 +- .../Middleware/PrivateLockMiddleware.swift | 18 ++++---- .../State/BrowserViewControllerState.swift | 39 ---------------- .../State/PrivateLockDomainState.swift | 44 +++++++++++++++++++ .../Browser/Tabs/State/TabsPanelState.swift | 4 +- .../Views/PrivateTabsLockOverlayView.swift | 2 +- .../Views/TabDisplayPanelViewController.swift | 2 +- 8 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/BrowserViewController/State/PrivateLockDomainState.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 0571029b47fe8..8eb2bae8197eb 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -782,6 +782,7 @@ 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivateLockAction.swift */; }; 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */; }; + 76DC672D2F641E6B00BE6F6A /* PrivateLockDomainState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76DC672C2F641E6B00BE6F6A /* PrivateLockDomainState.swift */; }; 76F1091F2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F1091E2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift */; }; 781C19CF2A780BEC0000DF46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 781C19CE2A780BEC0000DF46 /* Common */; }; 787EDD852943EE75002B93AE /* JumpBackInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787EDD832943EE75002B93AE /* JumpBackInTests.swift */; }; @@ -8776,6 +8777,7 @@ 7681461D8ADDD3E9662A2A53 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Shared.strings; sourceTree = ""; }; 7693448FA2E2865EA28005EE /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/ErrorPages.strings; sourceTree = ""; }; 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateLockMiddleware.swift; sourceTree = ""; }; + 76DC672C2F641E6B00BE6F6A /* PrivateLockDomainState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateLockDomainState.swift; sourceTree = ""; }; 76F1091E2F5C9F6600305C29 /* FirefoxGradientBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxGradientBackgroundView.swift; sourceTree = ""; }; 772744E9858179A908A070DB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ClearPrivateData.strings; sourceTree = ""; }; 77294827911067CD5994B5FD /* co */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = co; path = co.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; @@ -13109,6 +13111,7 @@ 81122E202B221AC0003DD9F8 /* SearchScreenState.swift */, 5A1947142B8FA9E0009C7A6C /* BrowserViewType.swift */, 8A8D277A2CBFFD710076AD3A /* BrowserNavigationType.swift */, + 76DC672C2F641E6B00BE6F6A /* PrivateLockDomainState.swift */, ); path = State; sourceTree = ""; @@ -18950,6 +18953,7 @@ CDB3BE8724746787009320EE /* FirefoxAccountSignInViewController.swift in Sources */, 8A471183287F6D9C00F5A6EA /* BookmarksPanelViewModel.swift in Sources */, C705FD582EF354D300AC5EE7 /* PrivacyNoticeUpdate.swift in Sources */, + 76DC672D2F641E6B00BE6F6A /* PrivateLockDomainState.swift in Sources */, 0B8A39EF2D3514C100853E47 /* EditBookmarkDiffableDataSource.swift in Sources */, 8A44F20E2B585E1F0016BC81 /* HomepageTelemetry.swift in Sources */, EDD2A7FA2CDBD1D100ED464C /* SearchEngineElement+initFromSearchEngine.swift in Sources */, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift index 28cfe2261eae2..6a44d94b25d0f 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Actions/PrivateLockAction.swift @@ -40,14 +40,14 @@ enum PrivateLockMiddlewareActionType: ActionType { struct PrivateLockMiddlewareAction: Action { let windowUUID: WindowUUID let actionType: ActionType - let privateLockState: BrowserViewControllerState.PrivateLockDomainState? + let privateLockState: PrivateLockDomainState? let trayPanelType: TabTrayPanelType? let trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? let privateLockEnabled: Bool? init(windowUUID: WindowUUID, actionType: ActionType, - privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState? = nil, + privatePanelLockState: PrivateLockDomainState? = nil, trayPanelType: TabTrayPanelType? = nil, trayDisplayContext: BrowserViewControllerState.TrayDisplayContext? = nil, privateLockEnabled: Bool? = nil) { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index 941dcc4a37b5d..a692490bff23b 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -110,9 +110,9 @@ final class PrivateLockMiddleware: FeatureFlaggable { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, - privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .locked, - auth: .authenticating, - lastUnlockedAt: nil) + privatePanelLockState: PrivateLockDomainState(access: .locked, + auth: .authenticating, + lastUnlockedAt: nil) )) let context = LAContext() @@ -144,9 +144,9 @@ final class PrivateLockMiddleware: FeatureFlaggable { windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, privatePanelLockState: - BrowserViewControllerState.PrivateLockDomainState(access: .locked, - auth: triggeredByFailure ? .failed : .idle, - lastUnlockedAt: nil) + PrivateLockDomainState(access: .locked, + auth: triggeredByFailure ? .failed : .idle, + lastUnlockedAt: nil) )) } @@ -154,9 +154,9 @@ final class PrivateLockMiddleware: FeatureFlaggable { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, - privatePanelLockState: BrowserViewControllerState.PrivateLockDomainState(access: .unlocked, - auth: .idle, - lastUnlockedAt: Date()) + privatePanelLockState: PrivateLockDomainState(access: .unlocked, + auth: .idle, + lastUnlockedAt: Date()) )) } diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift index 82d9ad87735a3..5e6833f0402b4 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/BrowserViewControllerState.swift @@ -38,50 +38,11 @@ struct BrowserViewControllerState: ScreenState { case summarizer(config: SummarizerConfig?) } - enum PrivateAccessState: Equatable { - case locked - case unlocked - } - - enum PrivateAuthState: Equatable { - case idle - case authenticating - case failed - } - enum TrayDisplayContext: Equatable { case page case tray } - struct PrivateLockDomainState: Equatable { - var access: PrivateAccessState = .locked - var auth: PrivateAuthState = .idle - var lastUnlockedAt: Date? - let relockInterval: TimeInterval = 120 - - var shouldRelockByTime: Bool { - guard let lastUnlockedAt else { return true } - return Date().timeIntervalSince(lastUnlockedAt) > relockInterval - } - - func copy(access: PrivateAccessState? = nil, - auth: PrivateAuthState? = nil, - lastUnlockedAt: Date? = nil) -> PrivateLockDomainState { - PrivateLockDomainState(access: access ?? self.access, - auth: auth ?? self.auth, - lastUnlockedAt: lastUnlockedAt ?? self.lastUnlockedAt) - } - - func locked() -> PrivateLockDomainState { - copy(access: .locked) - } - - func withLastUnlocked(at: Date?) -> PrivateLockDomainState { - PrivateLockDomainState(access: access, auth: auth, lastUnlockedAt: at) - } - } - let windowUUID: WindowUUID var searchScreenState: SearchScreenState var toast: ToastType? diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/PrivateLockDomainState.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/PrivateLockDomainState.swift new file mode 100644 index 0000000000000..761edcbeffa4c --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/State/PrivateLockDomainState.swift @@ -0,0 +1,44 @@ +// 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 Foundation + +struct PrivateLockDomainState: Equatable { + enum PrivateAccessState: Equatable { + case locked + case unlocked + } + + enum PrivateAuthState: Equatable { + case idle + case authenticating + case failed + } + + var access: PrivateAccessState = .locked + var auth: PrivateAuthState = .idle + var lastUnlockedAt: Date? + let relockInterval: TimeInterval = 120 + + var shouldRelockByTime: Bool { + guard let lastUnlockedAt else { return true } + return Date().timeIntervalSince(lastUnlockedAt) > relockInterval + } + + func copy(access: PrivateAccessState? = nil, + auth: PrivateAuthState? = nil, + lastUnlockedAt: Date? = nil) -> PrivateLockDomainState { + PrivateLockDomainState(access: access ?? self.access, + auth: auth ?? self.auth, + lastUnlockedAt: lastUnlockedAt ?? self.lastUnlockedAt) + } + + func locked() -> PrivateLockDomainState { + copy(access: .locked) + } + + func withLastUnlocked(at: Date?) -> PrivateLockDomainState { + PrivateLockDomainState(access: access, auth: auth, lastUnlockedAt: at) + } +} diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index 0030711228f73..19b42d845a7f9 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -19,7 +19,7 @@ struct TabsPanelState: ScreenState, Equatable { var scrollState: ScrollState? var didTapAddTab: Bool var urlRequest: URLRequest? - var privateLockState: BrowserViewControllerState.PrivateLockDomainState? + var privateLockState: PrivateLockDomainState? var isPrivateTabsEmpty: Bool { guard isPrivateMode else { return true } @@ -64,7 +64,7 @@ struct TabsPanelState: ScreenState, Equatable { scrollState: ScrollState? = nil, didTapAddTab: Bool = false, urlRequest: URLRequest? = nil, - privateLockState: BrowserViewControllerState.PrivateLockDomainState? = nil) { + privateLockState: PrivateLockDomainState? = nil) { self.isPrivateMode = isPrivateMode self.tabs = tabs self.windowUUID = windowUUID diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index 6403cd42026c8..e8b5c2030539d 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -28,7 +28,7 @@ final class PrivateTabsLockOverlayView: UIView { fatalError("init(coder:) has not been implemented") } - func render(access: BrowserViewControllerState.PrivateAccessState, auth: BrowserViewControllerState.PrivateAuthState) { + func render(access: PrivateLockDomainState.PrivateAccessState, auth: PrivateLockDomainState.PrivateAuthState) { switch access { case .unlocked: isHidden = true diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index f06b44790cc1b..a479870f672d2 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -386,7 +386,7 @@ final class TabDisplayPanelViewController: UIViewController, } } - private func applyPrivateLockUI(_ lock: BrowserViewControllerState.PrivateLockDomainState?) { + private func applyPrivateLockUI(_ lock: PrivateLockDomainState?) { guard panelType == .privateTabs, let lock else { privateLockOverlay.renderHidden() privateLockAuthTask?.cancel() From c2b128dad77e49fd85c5dccfec160ce452259b22 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Fri, 13 Mar 2026 12:45:58 +0200 Subject: [PATCH 23/27] removed PrivateTabsLockFeatureGate, changed feature flags check to .buildAndUser --- firefox-ios/Client.xcodeproj/project.pbxproj | 4 ---- .../Middleware/PrivateLockMiddleware.swift | 2 +- .../PrivateTabsLockFeatureGate.swift | 19 ------------------- .../Views/BrowserViewController.swift | 2 +- 4 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 8eb2bae8197eb..c56de6814907d 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -778,7 +778,6 @@ 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */; }; 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F80D332A0A52D700013C3D /* PrivacyPolicyViewController.swift */; }; - 76206ED02F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */; }; 767742A92F4F649500DEC06D /* PrivateLockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742A82F4F649500DEC06D /* PrivateLockAction.swift */; }; 767742AB2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767742AA2F4F6B4B00DEC06D /* PrivateTabsLockOverlayView.swift */; }; 76AFB6632F58B16C0064E6AC /* PrivateLockMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */; }; @@ -8767,7 +8766,6 @@ 75BA4E00A029A2AD26DF8960 /* anp */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = anp; path = anp.lproj/ClearPrivateData.strings; sourceTree = ""; }; 75BC41B2962DAC5B25A288B4 /* hsb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hsb; path = hsb.lproj/LoginManager.strings; sourceTree = ""; }; 75EF43EA84AEEBBE2C059310 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Menu.strings; sourceTree = ""; }; - 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateTabsLockFeatureGate.swift; sourceTree = ""; }; 764643DCB449658AAA8ED829 /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/InfoPlist.strings; sourceTree = ""; }; 765243D790B967A69BD5DCF7 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/ErrorPages.strings; sourceTree = ""; }; 766F49788D099E2201B74791 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Intro.strings; sourceTree = ""; }; @@ -13005,7 +13003,6 @@ isa = PBXGroup; children = ( 76AFB6622F58B16C0064E6AC /* PrivateLockMiddleware.swift */, - 76206ECF2F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift */, ); path = Middleware; sourceTree = ""; @@ -19238,7 +19235,6 @@ 8A5D1CBF2A30DC75005AD35C /* SyncNowSetting.swift in Sources */, 210E0EB8298D9D4500BB4F33 /* SearchEngineProvider.swift in Sources */, 21618A612A4201E500A5189E /* ThemeSettingsController.swift in Sources */, - 76206ED02F61ADA400A5AF66 /* PrivateTabsLockFeatureGate.swift in Sources */, AB4FB4DB2C89FB10005EF0CC /* BlockedTrackersHeaderView.swift in Sources */, D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */, 8A15AB752D5FDB80008BB03C /* AutoplaySettingsViewController.swift in Sources */, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index a692490bff23b..ca8cdfd4846a8 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -161,7 +161,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { } private func isPrivateLockFeatureEnabled() -> Bool { - PrivateTabsLockFeatureGate(prefs: prefs).isEnabled + featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildAndUser) } private func browserState(from appState: AppState, windowUUID: WindowUUID) -> BrowserViewControllerState? { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift deleted file mode 100644 index 0c40d29899b5c..0000000000000 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateTabsLockFeatureGate.swift +++ /dev/null @@ -1,19 +0,0 @@ -// 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 Foundation -import Shared - -final class PrivateTabsLockFeatureGate: FeatureFlaggable { - private let prefs: Prefs - init(prefs: Prefs) { - self.prefs = prefs - } - - var isEnabled: Bool { - let shouldLock = prefs.boolForKey(PrefsKeys.Settings.lockPrivateTabs) ?? false - let featureEnabled = featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildOnly) - return shouldLock && featureEnabled - } -} diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 74dac3dcdb446..d8dfcce18b37a 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -952,7 +952,7 @@ class BrowserViewController: UIViewController, private func showPrivacyOverlayIfNeeded(checkActualState: Bool = false) { if let state = browserViewControllerState { - let featureEnabled = PrivateTabsLockFeatureGate(prefs: profile.prefs).isEnabled + let featureEnabled = featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildAndUser) if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && featureEnabled { focusOnTabSegment(animated: false) if checkActualState { From 4e539abee99002bdb5a0dcdd93e03ebbb8de0d2a Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 14 Mar 2026 21:30:45 +0200 Subject: [PATCH 24/27] reverted proj file dev team changes --- firefox-ios/Client.xcodeproj/project.pbxproj | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index a66ffb9e785b0..081699a7f05e8 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -27814,8 +27814,6 @@ CODE_SIGN_ENTITLEMENTS = ""; COPY_PHASE_STRIP = NO; DEFINES_MODULE = YES; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; INFOPLIST_FILE = Sync/Info.plist; @@ -27849,8 +27847,6 @@ COPY_PHASE_STRIP = NO; DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 43AQ936H96; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; @@ -27962,7 +27958,7 @@ CODE_SIGN_ENTITLEMENTS = "$(inherit)Extensions/Entitlements/Fennec.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = Extensions/NotificationService/Info.plist; OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_developer -DMOZ_TARGET_NOTIFICATIONSERVICE"; @@ -28099,7 +28095,7 @@ CODE_SIGN_ENTITLEMENTS = Extensions/Entitlements/Fennec.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = WidgetKit/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -28184,8 +28180,6 @@ COPY_PHASE_STRIP = NO; DEFINES_MODULE = NO; DEVELOPMENT_TEAM = 43AQ936H96; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5R87WU4VHL; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 5R87WU4VHL; EAGER_LINKING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = NO; GCC_WARN_INHIBIT_ALL_WARNINGS = YES; @@ -29322,7 +29316,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -29894,7 +29888,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -31139,7 +31133,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -31440,7 +31434,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; GCC_TREAT_WARNINGS_AS_ERRORS = NO; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -31480,7 +31474,7 @@ CODE_SIGN_ENTITLEMENTS = "$(inherit)Extensions/Entitlements/Fennec.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5R87WU4VHL; + DEVELOPMENT_TEAM = 43AQ936H96; GCC_TREAT_WARNINGS_AS_ERRORS = NO; INFOPLIST_FILE = Extensions/ShareTo/Info.plist; OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_developer -DMOZ_TARGET_SHARETO"; From 8d972e9f22e7f593ba897bc429ff5dcdb19c869f Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 14 Mar 2026 21:54:55 +0200 Subject: [PATCH 25/27] replaced static funcs unlock, lock with instance funcs --- .../Middleware/PrivateLockMiddleware.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index ca8cdfd4846a8..e85bba8683593 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -25,7 +25,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { private func resolveTabPrivateLockActions(action: PrivateLockAction, state: AppState) { guard isPrivateLockFeatureEnabled() else { - Self.unlock(windowUUID: action.windowUUID) + unlock(windowUUID: action.windowUUID) return } @@ -51,7 +51,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { windowUUID: action.windowUUID, state: state) case PrivateLockActionType.didEnterBackground, PrivateLockActionType.willEnterForeground: - Self.lock(triggeredByFailure: false, windowUUID: action.windowUUID) + lock(triggeredByFailure: false, windowUUID: action.windowUUID) case PrivateLockActionType.didChangePrivateTabsLockSetting: let browserState = self.browserState(from: state, windowUUID: action.windowUUID) store.dispatch(PrivateLockMiddlewareAction( @@ -121,25 +121,27 @@ final class PrivateLockMiddleware: FeatureFlaggable { var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { - Self.lock(triggeredByFailure: true, windowUUID: windowUUID) + lock(triggeredByFailure: true, windowUUID: windowUUID) return } context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: reason - ) { success, authError in + ) { [weak self] success, _ in + guard let self else { return } + ensureMainThread { if success { - Self.unlock(windowUUID: windowUUID) + self.unlock(windowUUID: windowUUID) } else { - Self.lock(triggeredByFailure: true, windowUUID: windowUUID) + self.lock(triggeredByFailure: true, windowUUID: windowUUID) } } } } - private static func lock(triggeredByFailure: Bool, windowUUID: WindowUUID) { + private func lock(triggeredByFailure: Bool, windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, @@ -150,7 +152,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { )) } - private static func unlock(windowUUID: WindowUUID) { + private func unlock(windowUUID: WindowUUID) { store.dispatch(PrivateLockMiddlewareAction( windowUUID: windowUUID, actionType: PrivateLockMiddlewareActionType.didChangePrivateLockState, From 2607bbbf57bf199f23ff812b85f6437b18039a24 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 14 Mar 2026 22:34:21 +0200 Subject: [PATCH 26/27] pr review fixes, guards, strings, styling --- .../Middleware/PrivateLockMiddleware.swift | 2 +- .../Views/BrowserViewController.swift | 20 +++++++------- .../Browser/Tabs/State/TabsPanelState.swift | 4 +-- .../Views/PrivateTabsLockOverlayView.swift | 4 ++- .../Views/TabDisplayPanelViewController.swift | 4 +-- .../Main/AppSettingsTableViewController.swift | 6 ++--- firefox-ios/Shared/Strings.swift | 27 +++++++++++++++++++ 7 files changed, 47 insertions(+), 20 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift index e85bba8683593..40aba2a12c515 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Middleware/PrivateLockMiddleware.swift @@ -116,7 +116,7 @@ final class PrivateLockMiddleware: FeatureFlaggable { )) let context = LAContext() - context.localizedCancelTitle = "Cancel" + context.localizedCancelTitle = .PrivateLock.PrivateLockLAContextCancel var error: NSError? diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index d7a6de6e7db35..d6965cd3b0a7e 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -955,13 +955,14 @@ class BrowserViewController: UIViewController, } private func showPrivacyOverlayIfNeeded(checkActualState: Bool = false) { - if let state = browserViewControllerState { - let featureEnabled = featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildAndUser) - if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && featureEnabled { - focusOnTabSegment(animated: false) - if checkActualState { - privateLockWillEnterForeground() - } + guard let state = browserViewControllerState else { + return + } + let featureEnabled = featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildAndUser) + if state.trayDisplayContext == .page && state.trayPanelType == .privateTabs && featureEnabled { + focusOnTabSegment(animated: false) + if checkActualState { + privateLockWillEnterForeground() } } } @@ -1217,7 +1218,7 @@ class BrowserViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.privateAuthRequested("Unlock your private tabs") + actionType: PrivateLockActionType.privateAuthRequested(.PrivateLock.PrivateLockLAContextReason) ) ) } @@ -4889,9 +4890,8 @@ extension BrowserViewController: SearchViewControllerDelegate { extension BrowserViewController: TabManagerDelegate { // FXIOS-15079 break this function down and remove swiftlint ignore function body length - // swiftlint:disable function_body_length + // swiftlint:disable:next function_body_length func tabManager(_ tabManager: TabManager, didSelectedTabChange selectedTab: Tab, previousTab: Tab?, isRestoring: Bool) { - // swiftlint:enable function_body_length // Failing to have a non-nil webView by this point will cause the toolbar scrolling behaviour to regress, // back/forward buttons never to become enabled, etc. on tab restore after launch. [FXIOS-9785, FXIOS-9781] assert(selectedTab.webView != nil, "Setup will fail if the webView is not initialized for selectedTab") diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index 19b42d845a7f9..dc70c5779b8f3 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -7,7 +7,7 @@ import Redux import Common struct TabsPanelState: ScreenState, Equatable { - + struct ScrollState: Equatable { let toIndex: Int let withAnimation: Bool @@ -35,7 +35,7 @@ struct TabsPanelState: ScreenState, Equatable { self.init(windowUUID: uuid) return } - + let browserState = appState.screenState(BrowserViewControllerState.self, for: .browserViewController, window: uuid) self.init(windowUUID: panelState.windowUUID, isPrivateMode: panelState.isPrivateMode, diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift index e8b5c2030539d..354cccbb3988f 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/PrivateTabsLockOverlayView.swift @@ -149,5 +149,7 @@ final class PrivateTabsLockOverlayView: UIView { } } - @objc private func retryTapped() { onRetryTapped?() } + @objc private func retryTapped() { + onRetryTapped?() + } } diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift index a479870f672d2..b81de9baa92f1 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabDisplayPanelViewController.swift @@ -221,7 +221,7 @@ final class TabDisplayPanelViewController: UIViewController, store.dispatch( PrivateLockAction( windowUUID: windowUUID, - actionType: PrivateLockActionType.privateAuthRequested("Unlock your private tabs") + actionType: PrivateLockActionType.privateAuthRequested(.PrivateLock.PrivateLockLAContextReason) ) ) } @@ -375,8 +375,6 @@ final class TabDisplayPanelViewController: UIViewController, if panelType == .privateTabs, tabsState.isPrivateMode { // Only adjust the empty view if we are in private mode shouldShowEmptyView(tabsState.isPrivateTabsEmpty) - } - if panelType == .privateTabs { applyPrivateLockUI(tabsState.privateLockState) } shouldShowFadeView() diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index 8d5ea32c654af..6ed3b4cde0288 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -435,7 +435,7 @@ class AppSettingsTableViewController: SettingsTableViewController, store.dispatch(action) } ) - + if featureFlags.isFeatureEnabled(.privateTabsLock, checking: .buildOnly) { privacySettings.append( BoolSetting( @@ -443,8 +443,8 @@ class AppSettingsTableViewController: SettingsTableViewController, theme: themeManager.getCurrentTheme(for: windowUUID), prefKey: PrefsKeys.Settings.lockPrivateTabs, defaultValue: false, - titleText: "Lock Private Tabs", - statusText: "Use Biometrics or Passcode to see Private Tabs" + titleText: .AppSettingsLockPrivateTabs, + statusText: .AppSettingsLockPrivateTabsDescription ) { _ in let action = PrivateLockAction(windowUUID: self.windowUUID, actionType: PrivateLockActionType.didChangePrivateTabsLockSetting) diff --git a/firefox-ios/Shared/Strings.swift b/firefox-ios/Shared/Strings.swift index 901315c11977f..03f70712c6c3d 100644 --- a/firefox-ios/Shared/Strings.swift +++ b/firefox-ios/Shared/Strings.swift @@ -7881,6 +7881,16 @@ extension String { tableName: nil, value: nil, comment: "About settings section title") + public static let AppSettingsLockPrivateTabs = MZLocalizedString( + key: "Lock Private Tabs", + tableName: nil, + value: "Lock Private Tabs", + comment: "Lock Private Tabs title") + public static let AppSettingsLockPrivateTabsDescription = MZLocalizedString( + key: "Lock Private Tabs Section Description", + tableName: nil, + value: "Use Biometrics or Passcode to see Private Tabs", + comment: "Lock Private Tabs description") } // MARK: - Clearables @@ -8385,4 +8395,21 @@ extension String { } } +// MARK: - Private Lock Feature +extension String { + public struct PrivateLock { + public static let PrivateLockLAContextCancel = MZLocalizedString( + key: "Cancel", + tableName: nil, + value: nil, + comment: "Label for Cancel button") + + public static let PrivateLockLAContextReason = MZLocalizedString( + key: "PrivateLock.Reason", + tableName: nil, + value: "Unlock your private tabs", + comment: "Label for Biometry authorization request") + } +} + // swiftlint:enable line_length From 74ec4d9569563d735581fab3839ee57b7501e624 Mon Sep 17 00:00:00 2001 From: DarkSatyr Date: Sat, 14 Mar 2026 22:58:38 +0200 Subject: [PATCH 27/27] lint style fixes --- .../Client/Frontend/Browser/Tabs/State/TabsPanelState.swift | 1 - .../Browser/Tabs/Views/FirefoxGradientBackgroundView.swift | 2 +- .../Frontend/Browser/Tabs/Views/TabTrayViewController.swift | 2 +- firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift index dc70c5779b8f3..36791d4e05fcc 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/State/TabsPanelState.swift @@ -7,7 +7,6 @@ import Redux import Common struct TabsPanelState: ScreenState, Equatable { - struct ScrollState: Equatable { let toIndex: Int let withAnimation: Bool diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift index c23cbb9a4ed80..fe721aa5f7d06 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/FirefoxGradientBackgroundView.swift @@ -46,7 +46,7 @@ final class FirefoxGradientBackgroundView: UIView { } private func configureGlow(_ layerGlow: CALayer, color: UIColor) { - + layerGlow.backgroundColor = color.cgColor layerGlow.opacity = 0.8 diff --git a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift index bd9bc6200cb6f..a6ecfa5779915 100644 --- a/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/Tabs/Views/TabTrayViewController.swift @@ -420,7 +420,7 @@ final class TabTrayViewController: UIViewController, if !themeAnimator.isAnimating && swipeFromIndex == nil { applyTheme() } - + navigationController?.setToolbarHidden(state.hideToolbar ?? false, animated: false) } diff --git a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift index 10275234ab099..bda5ef5978fbc 100644 --- a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift +++ b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift @@ -78,7 +78,7 @@ final class NimbusFeatureFlagLayer: Sendable { case .privateTabsLock: return checkPrivateTabsLockFeature(from: nimbus) - + case .shouldUseBrandRefreshConfiguration: return checkShouldUseBrandRefreshConfigurationFeature(from: nimbus)