diff --git a/.github/workflows/firefox-ios-autofill-playwrite-tests.yml b/.github/workflows/firefox-ios-autofill-playwrite-tests.yml new file mode 100644 index 000000000000..3b59d856e0d2 --- /dev/null +++ b/.github/workflows/firefox-ios-autofill-playwrite-tests.yml @@ -0,0 +1,41 @@ +name: Build and Run Autofill Automation +permissions: read-all +on: + pull_request: + paths: + - 'firefox-ios/Client/Assets/CC_Script/**' + push: + branches: ['main'] + paths: + - 'firefox-ios/Client/Assets/CC_Script/**' +jobs: + build: + runs-on: macos-13 + timeout-minutes: 40 + strategy: + matrix: + python-version: [3.9] + xcode: ["15.2"] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Clone repository + run: | + git clone https://github.com/issammani/test-playwright.git + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: 18 + - run: | + echo "Install dependencies and run tests" + npm i + npm install --save concurrently + npm i -D @playwright/test + + cd test-playwright + echo "Install playwright" + npx playwright install + + echo "Run tests" + npm test diff --git a/BrowserKit/Sources/Common/Constants/StandardImageIndentifiers.swift b/BrowserKit/Sources/Common/Constants/StandardImageIndentifiers.swift index 7f4d8ac25a5d..02bef5d33ce4 100644 --- a/BrowserKit/Sources/Common/Constants/StandardImageIndentifiers.swift +++ b/BrowserKit/Sources/Common/Constants/StandardImageIndentifiers.swift @@ -44,6 +44,7 @@ public struct StandardImageIdentifiers { public static let creditCard = "creditCardLarge" public static let criticalFill = "criticalFillLarge" public static let cross = "crossLarge" + public static let crossCircleFill = "crossCircleFillLarge" public static let dataClearance = "dataClearanceLarge" public static let delete = "deleteLarge" public static let deviceDesktop = "deviceDesktopLarge" diff --git a/BrowserKit/Sources/Common/Extensions/URLExtension.swift b/BrowserKit/Sources/Common/Extensions/URLExtension.swift index 44ca0e439771..0cbf64b04f0c 100644 --- a/BrowserKit/Sources/Common/Extensions/URLExtension.swift +++ b/BrowserKit/Sources/Common/Extensions/URLExtension.swift @@ -71,7 +71,9 @@ extension URL { if let range = host.range(of: "^(www|mobile|m)\\.", options: .regularExpression) { host.replaceSubrange(range, with: "") } - guard host != publicSuffix else { return nil } + // If the host equals the public suffix, it means that the host is already normalized. + // Therefore, we return the original host without any modifications. + guard host != publicSuffix else { return components.host } return host } @@ -80,6 +82,45 @@ extension URL { return normalizedHost.flatMap { $0 + self.path } } + /// Extracts the subdomain and host from a given URL string and appends a dot to the subdomain. + /// + /// This function takes a URL string as input and returns a tuple containing the subdomain and the normalized host. + /// If the URL string does not contain a subdomain, the function returns `nil` for the subdomain. + /// If a subdomain is present, it is returned with a trailing dot. + /// + /// - Parameter urlString: The URL string to extract the subdomain and host from. + /// + /// - Returns: A tuple containing the subdomain (with a trailing dot) and the normalized host. + /// The subdomain is optional and may be `nil`. + /// + /// # Example + /// ``` + /// let (subdomain, host) = getSubdomainAndHost(from: "https://docs.github.com") + /// print(subdomain) // Prints "docs." + /// print(host) // Prints "docs.github.com" + /// ``` + public static func getSubdomainAndHost(from urlString: String) -> (subdomain: String?, normalizedHost: String) { + guard let url = URL(string: urlString) else { return (nil, urlString) } + let normalizedHost = url.normalizedHost ?? urlString + + guard let publicSuffix = url.publicSuffix else { return (nil, normalizedHost) } + + let publicSuffixComponents = publicSuffix.split(separator: ".") + + let normalizedHostWithoutSuffix = normalizedHost + .split(separator: ".") + .dropLast(publicSuffixComponents.count) + .joined(separator: ".") + + let components = normalizedHostWithoutSuffix.split(separator: ".") + + guard components.count >= 2 else { return (nil, normalizedHost) } + let subdomain = components.dropLast() + .joined(separator: ".") + .appending(".") + return (subdomain, normalizedHost) + } + /// Returns the public portion of the host name determined by the public suffix list found here: https://publicsuffix.org/list/. /// For example for the url www.bbc.co.uk, based on the entries in the TLD list, the public suffix would return co.uk. /// :returns: The public suffix for within the given hostname. diff --git a/BrowserKit/Sources/TabDataStore/WindowData.swift b/BrowserKit/Sources/TabDataStore/WindowData.swift index 50e67b584740..4891389b2780 100644 --- a/BrowserKit/Sources/TabDataStore/WindowData.swift +++ b/BrowserKit/Sources/TabDataStore/WindowData.swift @@ -6,7 +6,6 @@ import Foundation public struct WindowData: Codable { public let id: UUID - public let isPrimary: Bool public let activeTabId: UUID public let tabData: [TabData] @@ -18,11 +17,9 @@ public struct WindowData: Codable { /// - activeTabId: the ID of the currently selected tab /// - tabData: a list of all tabs associated with the window public init(id: UUID, - isPrimary: Bool = true, activeTabId: UUID, tabData: [TabData]) { self.id = id - self.isPrimary = isPrimary self.activeTabId = activeTabId self.tabData = tabData } diff --git a/BrowserKit/Sources/ToolbarKit/AddressToolbarState.swift b/BrowserKit/Sources/ToolbarKit/AddressToolbarState.swift index 1303306d24ea..d3586bb68022 100644 --- a/BrowserKit/Sources/ToolbarKit/AddressToolbarState.swift +++ b/BrowserKit/Sources/ToolbarKit/AddressToolbarState.swift @@ -6,8 +6,8 @@ import Foundation /// Defines the state for the address toolbar. public struct AddressToolbarState { - /// URL displayed in the address toolbar - let url: String? + /// View state for the `Location View` in the address toolbar + let locationViewState: LocationViewState /// Navigation actions of the address toolbar let navigationActions: [ToolbarElement] @@ -26,13 +26,13 @@ public struct AddressToolbarState { // We need this init as by default the init generated by the compiler for the struct will be internal and // can therefor not be used outside of the ToolbarKit - public init(url: String?, + public init(locationViewState: LocationViewState, navigationActions: [ToolbarElement], pageActions: [ToolbarElement], browserActions: [ToolbarElement], shouldDisplayTopBorder: Bool, shouldDisplayBottomBorder: Bool) { - self.url = url + self.locationViewState = locationViewState self.navigationActions = navigationActions self.pageActions = pageActions self.browserActions = browserActions diff --git a/BrowserKit/Sources/ToolbarKit/BrowserAddressToolbar.swift b/BrowserKit/Sources/ToolbarKit/BrowserAddressToolbar.swift index 51ae9d077e5b..6477cf07ff1c 100644 --- a/BrowserKit/Sources/ToolbarKit/BrowserAddressToolbar.swift +++ b/BrowserKit/Sources/ToolbarKit/BrowserAddressToolbar.swift @@ -64,7 +64,7 @@ public class BrowserAddressToolbar: UIView, AddressToolbar, ThemeApplicable, Loc shouldDisplayBottomBorder: state.shouldDisplayBottomBorder) self.toolbarDelegate = toolbarDelegate - locationView.configure(state.url, delegate: self) + locationView.configure(state.locationViewState, delegate: self) setNeedsLayout() layoutIfNeeded() @@ -105,17 +105,7 @@ public class BrowserAddressToolbar: UIView, AddressToolbar, ThemeApplicable, Loc dividerWidthConstraint = locationDividerView.widthAnchor.constraint(equalToConstant: UX.dividerWidth) dividerWidthConstraint?.isActive = true - let navigationActionWidthAnchor = navigationActionStack.widthAnchor.constraint(equalToConstant: 0) - navigationActionWidthAnchor.isActive = true - navigationActionWidthAnchor.priority = .defaultLow - - let pageActionWidthAnchor = pageActionStack.widthAnchor.constraint(equalToConstant: 0) - pageActionWidthAnchor.isActive = true - pageActionWidthAnchor.priority = .defaultLow - - let browserActionWidthAnchor = browserActionStack.widthAnchor.constraint(equalToConstant: 0) - browserActionWidthAnchor.isActive = true - browserActionWidthAnchor.priority = .defaultLow + [navigationActionStack, pageActionStack, browserActionStack].forEach(setZeroWidthConstraint) toolbarTopBorderHeightConstraint = toolbarTopBorderView.heightAnchor.constraint(equalToConstant: 0) toolbarBottomBorderHeightConstraint = toolbarBottomBorderView.heightAnchor.constraint(equalToConstant: 0) @@ -181,6 +171,12 @@ public class BrowserAddressToolbar: UIView, AddressToolbar, ThemeApplicable, Loc updateActionSpacing() } + private func setZeroWidthConstraint(_ stackView: UIStackView) { + let widthAnchor = stackView.widthAnchor.constraint(equalToConstant: 0) + widthAnchor.isActive = true + widthAnchor.priority = .defaultHigh + } + private func updateActionStack(stackView: UIStackView, toolbarElements: [ToolbarElement]) { stackView.removeAllArrangedViews() toolbarElements.forEach { toolbarElement in diff --git a/BrowserKit/Sources/ToolbarKit/BrowserNavigationToolbar.swift b/BrowserKit/Sources/ToolbarKit/BrowserNavigationToolbar.swift new file mode 100644 index 000000000000..e94501aadd56 --- /dev/null +++ b/BrowserKit/Sources/ToolbarKit/BrowserNavigationToolbar.swift @@ -0,0 +1,84 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import UIKit +import Common + +/// Navigation toolbar implementation. +public class BrowserNavigationToolbar: UIView, NavigationToolbar, ThemeApplicable { + private enum UX { + static let horizontalEdgeSpace: CGFloat = 16 + static let buttonSize = CGSize(width: 48, height: 48) + static let borderHeight: CGFloat = 1 + } + + private lazy var actionStack: UIStackView = .build { view in + view.distribution = .equalSpacing + } + private lazy var toolbarBorderView: UIView = .build() + private var toolbarBorderHeightConstraint: NSLayoutConstraint? + private var theme: Theme? + + override init(frame: CGRect) { + super.init(frame: .zero) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func configure(state: NavigationToolbarState) { + updateActionStack(toolbarElements: state.actions) + + // Update border + toolbarBorderHeightConstraint?.constant = state.shouldDisplayBorder ? UX.borderHeight : 0 + } + + // MARK: - Private + private func setupLayout() { + addSubview(toolbarBorderView) + addSubview(actionStack) + + toolbarBorderHeightConstraint = toolbarBorderView.heightAnchor.constraint(equalToConstant: 0) + toolbarBorderHeightConstraint?.isActive = true + + NSLayoutConstraint.activate([ + toolbarBorderView.leadingAnchor.constraint(equalTo: leadingAnchor), + toolbarBorderView.topAnchor.constraint(equalTo: topAnchor), + toolbarBorderView.trailingAnchor.constraint(equalTo: trailingAnchor), + + actionStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: UX.horizontalEdgeSpace), + actionStack.topAnchor.constraint(equalTo: toolbarBorderView.bottomAnchor), + actionStack.bottomAnchor.constraint(equalTo: bottomAnchor), + actionStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -UX.horizontalEdgeSpace), + ]) + } + + private func updateActionStack(toolbarElements: [ToolbarElement]) { + actionStack.removeAllArrangedViews() + toolbarElements.forEach { toolbarElement in + let button = ToolbarButton() + button.configure(element: toolbarElement) + actionStack.addArrangedSubview(button) + + NSLayoutConstraint.activate([ + button.widthAnchor.constraint(equalToConstant: UX.buttonSize.width), + button.heightAnchor.constraint(equalToConstant: UX.buttonSize.height), + ]) + + if let theme { + // As we recreate the buttons we need to apply the theme for them to be displayed correctly + button.applyTheme(theme: theme) + } + } + } + + // MARK: - ThemeApplicable + public func applyTheme(theme: Theme) { + backgroundColor = theme.colors.layer1 + toolbarBorderView.backgroundColor = theme.colors.borderPrimary + self.theme = theme + } +} diff --git a/BrowserKit/Sources/ToolbarKit/CompactBrowserAddressToolbar.swift b/BrowserKit/Sources/ToolbarKit/CompactBrowserAddressToolbar.swift index 028a296bb9a7..48726d564026 100644 --- a/BrowserKit/Sources/ToolbarKit/CompactBrowserAddressToolbar.swift +++ b/BrowserKit/Sources/ToolbarKit/CompactBrowserAddressToolbar.swift @@ -7,7 +7,7 @@ import UIKit public class CompactBrowserAddressToolbar: BrowserAddressToolbar { override internal func updateActions(state: AddressToolbarState) { // In compact mode no browser actions will be displayed - let compactState = AddressToolbarState(url: state.url, + let compactState = AddressToolbarState(locationViewState: state.locationViewState, navigationActions: state.navigationActions, pageActions: state.pageActions, browserActions: [], diff --git a/BrowserKit/Sources/ToolbarKit/LocationView.swift b/BrowserKit/Sources/ToolbarKit/LocationView.swift index fead984a4945..095df500c95f 100644 --- a/BrowserKit/Sources/ToolbarKit/LocationView.swift +++ b/BrowserKit/Sources/ToolbarKit/LocationView.swift @@ -22,25 +22,64 @@ protocol LocationViewDelegate: AnyObject { func locationViewShouldSearchFor(_ text: String) } +public struct LocationViewState { + public let accessibilityIdentifier: String + public let accessibilityHint: String + public let accessibilityLabel: String + public let url: String? + + public init( + accessibilityIdentifier: String, + accessibilityHint: String, + accessibilityLabel: String, + url: String? + ) { + self.accessibilityIdentifier = accessibilityIdentifier + self.accessibilityHint = accessibilityHint + self.accessibilityLabel = accessibilityLabel + self.url = url + } +} + class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { // MARK: - Properties private enum UX { - static let horizontalSpace: CGFloat = 16 + static let horizontalSpace: CGFloat = 8 static let gradientViewVerticalPadding: CGFloat = 8 static let gradientViewWidth: CGFloat = 40 + static let clearButtonSize: CGFloat = 40 + static let transitionDuration: TimeInterval = 0.3 } + private var urlAbsolutePath: String? private var notifyTextChanged: (() -> Void)? private var locationViewDelegate: LocationViewDelegate? + private lazy var urlTextFieldSubdomainColor: UIColor = .clear private lazy var gradientLayer = CAGradientLayer() private lazy var gradientView: UIView = .build() + private var clearButtonWidthConstraint: NSLayoutConstraint? + private var gradientViewWidthConstraint: NSLayoutConstraint? + + private lazy var clearButton: UIButton = .build { button in + button.setImage( + UIImage(named: StandardImageIdentifiers.Large.crossCircleFill)?.withRenderingMode(.alwaysTemplate), + for: .normal + ) + button.addTarget(self, action: #selector(self.clearURLText), for: .touchUpInside) + } + private lazy var urlTextField: UITextField = .build { urlTextField in urlTextField.accessibilityIdentifier = "url" urlTextField.backgroundColor = .clear urlTextField.font = UIFont.preferredFont(forTextStyle: .body) urlTextField.adjustsFontForContentSizeCategory = true + let isRightToLeft = Locale.characterDirection(forLanguage: Locale.preferredLanguages.first ?? "") == .rightToLeft + urlTextField.leftView = isRightToLeft ? self.clearButton : nil + urlTextField.rightView = isRightToLeft ? nil : self.clearButton + urlTextField.leftViewMode = isRightToLeft ? .whileEditing : .never + urlTextField.rightViewMode = isRightToLeft ? .never : .whileEditing urlTextField.delegate = self } @@ -54,8 +93,15 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { notifyTextChanged = { [self] in guard urlTextField.isEditing else { return } + if urlTextField.text?.isEmpty == true { + hideClearButton() + } else { + showClearButton() + } + urlTextField.text = urlTextField.text?.lowercased() - locationViewDelegate?.locationViewDidEnterText(urlTextField.text?.lowercased() ?? "") + urlAbsolutePath = urlTextField.text + locationViewDelegate?.locationViewDidEnterText(urlTextField.text ?? "") } } @@ -73,19 +119,31 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { return urlTextField.resignFirstResponder() } - func configure(_ text: String?, delegate: LocationViewDelegate) { - urlTextField.text = getHost(from: text) + func configure(_ state: LocationViewState, delegate: LocationViewDelegate) { + urlTextField.text = state.url + configureA11yForClearButton(state) + urlAbsolutePath = urlTextField.text + formatAndTruncateURLTextField() locationViewDelegate = delegate } // MARK: - Layout + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + DispatchQueue.main.async { [self] in + formatAndTruncateURLTextField() + performURLTextFieldAnimationIfPossible() + } + } + override func layoutSubviews() { super.layoutSubviews() updateGradientLayerFrame() + performURLTextFieldAnimationIfPossible() } private func setupLayout() { - addSubviews(urlTextField, gradientView) + addSubviews(urlTextField, gradientView, clearButton) NSLayoutConstraint.activate( [ @@ -98,13 +156,14 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { constant: -UX.gradientViewVerticalPadding ), gradientView.leadingAnchor.constraint(equalTo: urlTextField.leadingAnchor), - gradientView.widthAnchor.constraint(equalToConstant: UX.gradientViewWidth), gradientView.centerYAnchor.constraint(equalTo: urlTextField.centerYAnchor), urlTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: UX.horizontalSpace), urlTextField.topAnchor.constraint(equalTo: topAnchor), - urlTextField.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -UX.horizontalSpace), + urlTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -UX.horizontalSpace), urlTextField.bottomAnchor.constraint(equalTo: bottomAnchor), + + clearButton.heightAnchor.constraint(equalToConstant: UX.clearButtonSize) ] ) } @@ -116,19 +175,59 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { gradientView.layer.addSublayer(gradientLayer) } - private func updateGradientLayerFrame() { + private var doesURLTextFieldExceedViewWidth: Bool { + guard let text = urlTextField.text, let font = urlTextField.font else { + return false + } let locationViewWidth = frame.width - (UX.horizontalSpace * 2) - let urlTextFieldWidth = urlTextField.frame.width - let showGradientForLongURL = urlTextFieldWidth >= locationViewWidth && !urlTextField.isFirstResponder + let fontAttributes = [NSAttributedString.Key.font: font] + let urlTextFieldWidth = text.size(withAttributes: fontAttributes).width + return urlTextFieldWidth >= locationViewWidth + } + + private func updateGradientLayerFrame() { + let showGradientForLongURL = doesURLTextFieldExceedViewWidth && !urlTextField.isFirstResponder gradientLayer.frame = if showGradientForLongURL { gradientView.bounds } else { CGRect() } } + private func updateClearButtonWidthConstraint(to widthConstant: CGFloat) { + clearButtonWidthConstraint?.isActive = false + clearButtonWidthConstraint = clearButton.widthAnchor.constraint(equalToConstant: widthConstant) + clearButtonWidthConstraint?.isActive = true + } + + private func updateGradientViewWidthConstraint(to widthConstant: CGFloat) { + gradientViewWidthConstraint?.isActive = false + gradientViewWidthConstraint = gradientView.widthAnchor.constraint(equalToConstant: widthConstant) + gradientViewWidthConstraint?.isActive = true + } + + private func showClearButton() { + clearButton.isHidden = false + updateClearButtonWidthConstraint(to: UX.clearButtonSize) + updateGradientViewWidthConstraint(to: 0) + } + + private func hideClearButton() { + clearButton.isHidden = true + updateClearButtonWidthConstraint(to: 0) + updateGradientViewWidthConstraint(to: UX.gradientViewWidth) + } + // MARK: - `urlTextField` Configuration - private func applyTruncationStyleToURLTextField() { + private func formatAndTruncateURLTextField() { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byTruncatingHead - let attributedString = NSMutableAttributedString(string: urlTextField.text ?? "") + let urlString = urlAbsolutePath ?? "" + let (subdomain, normalizedHost) = URL.getSubdomainAndHost(from: urlString) + + let attributedString = NSMutableAttributedString(string: normalizedHost) + + if let subdomain { + let range = NSRange(location: 0, length: subdomain.count) + attributedString.addAttribute(.foregroundColor, value: urlTextFieldSubdomainColor, range: range) + } attributedString.addAttribute( .paragraphStyle, value: paragraphStyle, @@ -140,12 +239,35 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { urlTextField.attributedText = attributedString } - private func getHost(from stringURL: String?) -> String { - guard let stringURL, let url = URL(string: stringURL) else { return "" } - return url.host ?? "" + private func animateURLText( + _ textField: UITextField, + options: UIView.AnimationOptions, + textAlignment: NSTextAlignment, + completion: (() -> Void)? = nil + ) { + UIView.transition( + with: textField, + duration: UX.transitionDuration, + options: options) { + textField.textAlignment = textAlignment + } completion: { _ in + completion?() + } + } + + private func performURLTextFieldAnimationIfPossible() { + if !doesURLTextFieldExceedViewWidth, !urlTextField.isFirstResponder { + animateURLText(urlTextField, options: .transitionFlipFromLeft, textAlignment: .center) + } } // MARK: - Selectors + @objc + private func clearURLText() { + urlTextField.text = "" + notifyTextChanged?() + } + @objc func textDidChange(_ textField: UITextField) { notifyTextChanged?() @@ -153,13 +275,24 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { // MARK: - UITextFieldDelegate public func textFieldDidBeginEditing(_ textField: UITextField) { - gradientLayer.frame = CGRect() + if textField.text?.isEmpty == false { showClearButton() } else { hideClearButton() } + + updateGradientLayerFrame() + DispatchQueue.main.async { + // `attributedText` property is set to nil to remove all formatting and truncation set before. + textField.attributedText = nil + textField.text = self.urlAbsolutePath + textField.selectAll(nil) + } + + animateURLText(textField, options: .transitionFlipFromRight, textAlignment: .natural) { + textField.textAlignment = .natural + } locationViewDelegate?.locationViewDidBeginEditing(textField.text?.lowercased() ?? "") } public func textFieldDidEndEditing(_ textField: UITextField) { - updateGradientLayerFrame() - applyTruncationStyleToURLTextField() + hideClearButton() } public func textFieldShouldReturn(_ textField: UITextField) -> Bool { @@ -170,9 +303,19 @@ class LocationView: UIView, UITextFieldDelegate, ThemeApplicable { return true } + // MARK: - Accessibility + private func configureA11yForClearButton(_ model: LocationViewState) { + clearButton.accessibilityIdentifier = model.accessibilityIdentifier + clearButton.accessibilityHint = model.accessibilityHint + clearButton.accessibilityLabel = model.accessibilityLabel + } + // MARK: - ThemeApplicable func applyTheme(theme: any Common.Theme) { let colors = theme.colors + urlTextField.textColor = colors.textPrimary + urlTextFieldSubdomainColor = colors.textSecondary gradientLayer.colors = colors.layerGradientURL.cgColors.reversed() + clearButton.tintColor = colors.iconPrimary } } diff --git a/BrowserKit/Sources/ToolbarKit/NavigationToolbar.swift b/BrowserKit/Sources/ToolbarKit/NavigationToolbar.swift new file mode 100644 index 000000000000..b99d6a9f2a21 --- /dev/null +++ b/BrowserKit/Sources/ToolbarKit/NavigationToolbar.swift @@ -0,0 +1,10 @@ +// 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 + +/// Protocol representing an navigation toolbar. +public protocol NavigationToolbar { + func configure(state: NavigationToolbarState) +} diff --git a/BrowserKit/Sources/ToolbarKit/NavigationToolbarState.swift b/BrowserKit/Sources/ToolbarKit/NavigationToolbarState.swift new file mode 100644 index 000000000000..dd3e622cac44 --- /dev/null +++ b/BrowserKit/Sources/ToolbarKit/NavigationToolbarState.swift @@ -0,0 +1,22 @@ +// 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 + +/// Defines the state for the navigation toolbar. +public struct NavigationToolbarState { + /// Actions of the navigation toolbar + let actions: [ToolbarElement] + + /// Whether the toolbar border at the top should be displayed + let shouldDisplayBorder: Bool + + // We need this init as by default the init generated by the compiler for the struct will be internal and + // can therefor not be used outside of the ToolbarKit + public init(actions: [ToolbarElement], + shouldDisplayBorder: Bool) { + self.actions = actions + self.shouldDisplayBorder = shouldDisplayBorder + } +} diff --git a/BrowserKit/Sources/ToolbarKit/ToolbarButton.swift b/BrowserKit/Sources/ToolbarKit/ToolbarButton.swift index 0091b6f4b4a5..38cac967157b 100644 --- a/BrowserKit/Sources/ToolbarKit/ToolbarButton.swift +++ b/BrowserKit/Sources/ToolbarKit/ToolbarButton.swift @@ -39,6 +39,7 @@ class ToolbarButton: UIButton, ThemeApplicable { }) config.image = image + isEnabled = element.isEnabled accessibilityIdentifier = element.a11yId accessibilityLabel = element.a11yLabel addAction(action, for: .touchUpInside) diff --git a/BrowserKit/Sources/ToolbarKit/ToolbarManager.swift b/BrowserKit/Sources/ToolbarKit/ToolbarManager.swift index 5e28887a5b9b..fc1b052ab6ed 100644 --- a/BrowserKit/Sources/ToolbarKit/ToolbarManager.swift +++ b/BrowserKit/Sources/ToolbarKit/ToolbarManager.swift @@ -11,19 +11,22 @@ public enum AddressToolbarBorderPosition { public protocol ToolbarManager { /// Determines whether a border on top/bottom of the address toolbar should be displayed - func shouldDisplayBorder(borderPosition: AddressToolbarBorderPosition, - toolbarPosition: AddressToolbarPosition, - isPrivate: Bool, - scrollY: Int) -> Bool + func shouldDisplayAddressBorder(borderPosition: AddressToolbarBorderPosition, + toolbarPosition: AddressToolbarPosition, + isPrivate: Bool, + scrollY: Int) -> Bool + + /// Determines whether a border on top of the navigation toolbar should be displayed + func shouldDisplayNavigationBorder(toolbarPosition: AddressToolbarPosition) -> Bool } public class DefaultToolbarManager: ToolbarManager { public init() {} - public func shouldDisplayBorder(borderPosition: AddressToolbarBorderPosition, - toolbarPosition: AddressToolbarPosition, - isPrivate: Bool, - scrollY: Int) -> Bool { + public func shouldDisplayAddressBorder(borderPosition: AddressToolbarBorderPosition, + toolbarPosition: AddressToolbarPosition, + isPrivate: Bool, + scrollY: Int) -> Bool { // display the top border if // - the toolbar is displayed at the bottom // display the bottom border if @@ -35,4 +38,8 @@ public class DefaultToolbarManager: ToolbarManager { return (toolbarPosition == .top && scrollY > 0) || isPrivate } } + + public func shouldDisplayNavigationBorder(toolbarPosition: AddressToolbarPosition) -> Bool { + return toolbarPosition == .top + } } diff --git a/BrowserKit/Tests/CommonTests/Extensions/URLExtensionTests.swift b/BrowserKit/Tests/CommonTests/Extensions/URLExtensionTests.swift index fe3108dede78..c8d1f74cefc0 100644 --- a/BrowserKit/Tests/CommonTests/Extensions/URLExtensionTests.swift +++ b/BrowserKit/Tests/CommonTests/Extensions/URLExtensionTests.swift @@ -107,6 +107,12 @@ final class URLExtensionTests: XCTestCase { XCTAssertEqual(url!.fragment!, "h=dupes%7CData%20%26%20BI%20Services%20Team%7C") } + func testNormalizedHostReturnsOriginalHost() { + let url = URL(string: "https://mobile.co.uk")! + let host = url.normalizedHost + XCTAssertEqual(host, "mobile.co.uk") + } + func testIPv6Domain() { let url = URL(string: "http://[::1]/foo/bar")! XCTAssertTrue(url.isIPv6) @@ -139,6 +145,31 @@ final class URLExtensionTests: XCTestCase { badurls.forEach { XCTAssertNil(URL(string: $0)!.normalizedHostAndPath) } } + func testGetSubdomainAndHost() { + let testCases = [ + ("https://www.google.com", (nil, "google.com")), + ("https://blog.engineering.company.com", ("blog.engineering.", "blog.engineering.company.com")), + ("https://long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com", ("long-extended-subdomain-name-containing-many-letters-and-dashes.", "long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com")), + ("http://com:org@m.canadacomputers.co.uk", (nil, "canadacomputers.co.uk")), + ("https://www.wix.com/blog/what-is-a-subdomain", (nil, "wix.com")), + ("nothing", (nil, "nothing")), + ("https://super-long-url-with-dashes-and-things.badssl.com/xyz-something", ("super-long-url-with-dashes-and-things.", "super-long-url-with-dashes-and-things.badssl.com")), + ("https://accounts.firefox.com", ("accounts.", "accounts.firefox.com")), + ("http://username:password@subdomain.example.com:8080", ("subdomain.", "subdomain.example.com")), + ("https://example.com:8080#fragment", (nil, "example.com")), + ("http://username:password@subdomain.example.com:8080#fragment", ("subdomain.", "subdomain.example.com")), + ("https://www.amazon.co.uk", (nil, "amazon.co.uk")), + ("https://mobile.co.uk", (nil, "mobile.co.uk")) + ] + + for testCase in testCases { + let (urlString, expected) = testCase + let result = URL.getSubdomainAndHost(from: urlString) + XCTAssertEqual(result.subdomain, expected.0, "Unexpected subdomain for URL: \(urlString)") + XCTAssertEqual(result.normalizedHost, expected.1, "Unexpected normalized host for URL: \(urlString)") + } + } + func testShortDisplayString() { let urls = [ ("https://www.example.com/index.html", "example"), diff --git a/BrowserKit/Tests/TabDataStoreTests/TabDataStoreTests.swift b/BrowserKit/Tests/TabDataStoreTests/TabDataStoreTests.swift index 3d8a9e9bc323..b122ca950883 100644 --- a/BrowserKit/Tests/TabDataStoreTests/TabDataStoreTests.swift +++ b/BrowserKit/Tests/TabDataStoreTests/TabDataStoreTests.swift @@ -239,7 +239,6 @@ final class TabDataStoreTests: XCTestCase { func createMockWindow(uuid: UUID) -> WindowData { let tabs = createMockTabs() return WindowData(id: uuid, - isPrimary: true, activeTabId: tabs[0].id, tabData: tabs) } diff --git a/BrowserKit/Tests/TabDataStoreTests/TabFileManagerTests.swift b/BrowserKit/Tests/TabDataStoreTests/TabFileManagerTests.swift index 96582ecf41dc..c0884a24877b 100644 --- a/BrowserKit/Tests/TabDataStoreTests/TabFileManagerTests.swift +++ b/BrowserKit/Tests/TabDataStoreTests/TabFileManagerTests.swift @@ -64,7 +64,6 @@ final class TabFileManagerTests: XCTestCase { func createMockWindow() -> WindowData { let tabs = createMockTabs() return WindowData(id: defaultTestTabWindowUUID, - isPrimary: true, activeTabId: tabs[0].id, tabData: tabs) } diff --git a/BrowserKit/Tests/ToolbarKitTests/ToolbarManagerTests.swift b/BrowserKit/Tests/ToolbarKitTests/ToolbarManagerTests.swift index b7123e2a0ee7..9d97a20b7e71 100644 --- a/BrowserKit/Tests/ToolbarKitTests/ToolbarManagerTests.swift +++ b/BrowserKit/Tests/ToolbarKitTests/ToolbarManagerTests.swift @@ -7,60 +7,71 @@ import XCTest import Common final class ToolbarManagerTests: XCTestCase { - func testDisplayToolbarTopBorderWhenScrolledThenShouldNotDisplay() { + // Address toolbar border + func testDisplayAddressToolbarTopBorderWhenScrolledThenShouldNotDisplay() { let subject = createSubject() - XCTAssertFalse(subject.shouldDisplayBorder(borderPosition: .top, - toolbarPosition: .top, - isPrivate: false, - scrollY: 10)) + XCTAssertFalse(subject.shouldDisplayAddressBorder(borderPosition: .top, + toolbarPosition: .top, + isPrivate: false, + scrollY: 10)) } - func testDisplayToolbarTopBorderWhenBottomPlacementThenShouldDisplay() { + func testDisplayAddressToolbarTopBorderWhenBottomPlacementThenShouldDisplay() { let subject = createSubject() - XCTAssertTrue(subject.shouldDisplayBorder(borderPosition: .top, - toolbarPosition: .bottom, - isPrivate: false, - scrollY: 0)) + XCTAssertTrue(subject.shouldDisplayAddressBorder(borderPosition: .top, + toolbarPosition: .bottom, + isPrivate: false, + scrollY: 0)) } - func testDisplayToolbarTopBorderWhenPrivateModeThenShouldNotDisplay() { + func testDisplayAddressToolbarTopBorderWhenPrivateModeThenShouldNotDisplay() { let subject = createSubject() - XCTAssertFalse(subject.shouldDisplayBorder(borderPosition: .top, - toolbarPosition: .top, - isPrivate: true, - scrollY: 0)) + XCTAssertFalse(subject.shouldDisplayAddressBorder(borderPosition: .top, + toolbarPosition: .top, + isPrivate: true, + scrollY: 0)) } - func testDisplayToolbarTopBorderWhenNotScrolledNonPrivateModeWithTopPlacementThenShouldNotDisplay() { + func testDisplayAddressToolbarTopBorderWhenNotScrolledNonPrivateModeWithTopPlacementThenShouldNotDisplay() { let subject = createSubject() - XCTAssertFalse(subject.shouldDisplayBorder(borderPosition: .top, - toolbarPosition: .top, - isPrivate: false, - scrollY: 0)) + XCTAssertFalse(subject.shouldDisplayAddressBorder(borderPosition: .top, + toolbarPosition: .top, + isPrivate: false, + scrollY: 0)) } - func testDisplayToolbarBottomBorderWhenBottomPlacementThenShouldNotDisplay() { + func testDisplayAddressToolbarBottomBorderWhenBottomPlacementThenShouldNotDisplay() { let subject = createSubject() - XCTAssertFalse(subject.shouldDisplayBorder(borderPosition: .bottom, - toolbarPosition: .bottom, - isPrivate: false, - scrollY: 0)) + XCTAssertFalse(subject.shouldDisplayAddressBorder(borderPosition: .bottom, + toolbarPosition: .bottom, + isPrivate: false, + scrollY: 0)) } - func testDisplayToolbarBottomBorderWhenPrivateModeThenShouldDisplay() { + func testDisplayAddressToolbarBottomBorderWhenPrivateModeThenShouldDisplay() { let subject = createSubject() - XCTAssertTrue(subject.shouldDisplayBorder(borderPosition: .bottom, - toolbarPosition: .bottom, - isPrivate: true, - scrollY: 0)) + XCTAssertTrue(subject.shouldDisplayAddressBorder(borderPosition: .bottom, + toolbarPosition: .bottom, + isPrivate: true, + scrollY: 0)) } - func testDisplayToolbarBottomBorderWhenNotScrolledNonPrivateModeWithTopPlacementThenShouldNotDisplay() { + func testDisplayAddressToolbarBottomBorderWhenNotScrolledNonPrivateModeWithTopPlacementThenShouldNotDisplay() { let subject = createSubject() - XCTAssertFalse(subject.shouldDisplayBorder(borderPosition: .bottom, - toolbarPosition: .top, - isPrivate: false, - scrollY: 0)) + XCTAssertFalse(subject.shouldDisplayAddressBorder(borderPosition: .bottom, + toolbarPosition: .top, + isPrivate: false, + scrollY: 0)) + } + + // Navigation toolbar border + func testDisplayNavigationToolbarBorderWhenTopPlacementThenShouldDisplay() { + let subject = createSubject() + XCTAssertTrue(subject.shouldDisplayNavigationBorder(toolbarPosition: .top)) + } + func testDisplayNavigationToolbarBorderWhenBottomPlacementThenShouldNotDisplay() { + let subject = createSubject() + XCTAssertFalse(subject.shouldDisplayNavigationBorder(toolbarPosition: .bottom)) } // MARK: - Helpers diff --git a/SampleBrowser/SampleBrowser.xcodeproj/project.pbxproj b/SampleBrowser/SampleBrowser.xcodeproj/project.pbxproj index 9dd6f4664c7a..c58acc6680d5 100644 --- a/SampleBrowser/SampleBrowser.xcodeproj/project.pbxproj +++ b/SampleBrowser/SampleBrowser.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 8A4601EE2B0FE20700FFD17F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8A4601ED2B0FE20700FFD17F /* Assets.xcassets */; }; 8A4601F12B0FE20700FFD17F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8A4601EF2B0FE20700FFD17F /* LaunchScreen.storyboard */; }; 8A4602172B0FE43A00FFD17F /* BrowserSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4602162B0FE43A00FFD17F /* BrowserSearchBar.swift */; }; - 8A4602192B0FE45200FFD17F /* BrowserToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4602182B0FE45200FFD17F /* BrowserToolbar.swift */; }; 8A46021B2B0FE47C00FFD17F /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A46021A2B0FE47C00FFD17F /* BrowserViewController.swift */; }; 8A46021E2B0FE50D00FFD17F /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A46021D2B0FE50D00FFD17F /* UIView+Extension.swift */; }; 8A4602202B0FE52F00FFD17F /* UISearchbar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A46021F2B0FE52F00FFD17F /* UISearchbar+Extension.swift */; }; @@ -49,7 +48,10 @@ E132973C2BA9E71B0095FF61 /* ToolbarKit in Frameworks */ = {isa = PBXBuildFile; productRef = E132973B2BA9E71B0095FF61 /* ToolbarKit */; }; E189C24B2BB2D1140065CEF2 /* DependencyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E189C2492BB2D1140065CEF2 /* DependencyHelper.swift */; }; E1A58CC52BC3FF8F004009F1 /* AddressToolbarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A58CC42BC3FF8F004009F1 /* AddressToolbarContainer.swift */; }; + E1C525C62BCFF77900073A6D /* RootViewControllerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C525C52BCFF77900073A6D /* RootViewControllerModel.swift */; }; E1CF5D1C2BC82F9E00F2287F /* AddressToolbarContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CF5D1B2BC82F9E00F2287F /* AddressToolbarContainerModel.swift */; }; + E1F816B12BCEBB940043E2E0 /* NavigationToolbarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F816B02BCEBB940043E2E0 /* NavigationToolbarContainer.swift */; }; + E1F816B32BCEC4A80043E2E0 /* NavigationToolbarContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F816B22BCEC4A80043E2E0 /* NavigationToolbarContainerModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -82,7 +84,6 @@ 8A4601F02B0FE20700FFD17F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 8A4601F22B0FE20700FFD17F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8A4602162B0FE43A00FFD17F /* BrowserSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserSearchBar.swift; sourceTree = ""; }; - 8A4602182B0FE45200FFD17F /* BrowserToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserToolbar.swift; sourceTree = ""; }; 8A46021A2B0FE47C00FFD17F /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; 8A46021D2B0FE50D00FFD17F /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; 8A46021F2B0FE52F00FFD17F /* UISearchbar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchbar+Extension.swift"; sourceTree = ""; }; @@ -96,7 +97,10 @@ 8AEBDD702B69A8C300FE9192 /* AppLaunchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLaunchUtil.swift; sourceTree = ""; }; E189C2492BB2D1140065CEF2 /* DependencyHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DependencyHelper.swift; sourceTree = ""; }; E1A58CC42BC3FF8F004009F1 /* AddressToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressToolbarContainer.swift; sourceTree = ""; }; + E1C525C52BCFF77900073A6D /* RootViewControllerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewControllerModel.swift; sourceTree = ""; }; E1CF5D1B2BC82F9E00F2287F /* AddressToolbarContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressToolbarContainerModel.swift; sourceTree = ""; }; + E1F816B02BCEBB940043E2E0 /* NavigationToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationToolbarContainer.swift; sourceTree = ""; }; + E1F816B22BCEC4A80043E2E0 /* NavigationToolbarContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationToolbarContainerModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -144,6 +148,7 @@ 8A0F447C2B56FD9600438589 /* SearchModel.swift */, 8A0F44742B56FD5300438589 /* SearchViewModel.swift */, 1D96A2D62BA14D9500EF7CC7 /* DefaultAdsTrackerDefinitions.swift */, + E1C525C52BCFF77900073A6D /* RootViewControllerModel.swift */, ); path = Model; sourceTree = ""; @@ -236,9 +241,10 @@ children = ( 8ADF72CC2B73DE6A00530E7A /* FindInPage.swift */, 8A4602162B0FE43A00FFD17F /* BrowserSearchBar.swift */, - 8A4602182B0FE45200FFD17F /* BrowserToolbar.swift */, E1A58CC42BC3FF8F004009F1 /* AddressToolbarContainer.swift */, E1CF5D1B2BC82F9E00F2287F /* AddressToolbarContainerModel.swift */, + E1F816B02BCEBB940043E2E0 /* NavigationToolbarContainer.swift */, + E1F816B22BCEC4A80043E2E0 /* NavigationToolbarContainerModel.swift */, ); path = Components; sourceTree = ""; @@ -368,6 +374,7 @@ 8A3DB3352B5AD47500F89705 /* SettingsType.swift in Sources */, 8A0F44832B56FE1300438589 /* UIAlertController+Error.swift in Sources */, 8A4602172B0FE43A00FFD17F /* BrowserSearchBar.swift in Sources */, + E1C525C62BCFF77900073A6D /* RootViewControllerModel.swift in Sources */, 8A46021E2B0FE50D00FFD17F /* UIView+Extension.swift in Sources */, 8A3DB3332B5AD3EC00F89705 /* SettingsCellViewModel.swift in Sources */, 8A46021B2B0FE47C00FFD17F /* BrowserViewController.swift in Sources */, @@ -383,8 +390,8 @@ 8A0F44792B56FD6E00438589 /* SuggestionCellViewModel.swift in Sources */, 8ADF72CD2B73DE6A00530E7A /* FindInPage.swift in Sources */, 8A3DB32D2B5AD3C700F89705 /* SettingsViewController.swift in Sources */, - 8A4602192B0FE45200FFD17F /* BrowserToolbar.swift in Sources */, 8AEBDD712B69A8C300FE9192 /* AppLaunchUtil.swift in Sources */, + E1F816B32BCEC4A80043E2E0 /* NavigationToolbarContainerModel.swift in Sources */, 8A0F44732B56FD4500438589 /* SuggestionViewController.swift in Sources */, E1A58CC52BC3FF8F004009F1 /* AddressToolbarContainer.swift in Sources */, 8A0F446B2B56EDC900438589 /* EngineProvider.swift in Sources */, @@ -394,6 +401,7 @@ 8A0687612B6D9F990031427A /* SettingsDelegate.swift in Sources */, E1CF5D1C2BC82F9E00F2287F /* AddressToolbarContainerModel.swift in Sources */, 8A0F44812B56FDEA00438589 /* SearchDataProvider.swift in Sources */, + E1F816B12BCEBB940043E2E0 /* NavigationToolbarContainer.swift in Sources */, 8A4602202B0FE52F00FFD17F /* UISearchbar+Extension.swift in Sources */, 8A3DB3312B5AD3E000F89705 /* SettingsDataSource.swift in Sources */, ); diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/updating.png b/SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/updating.png deleted file mode 100644 index dd560e3ef45f..000000000000 Binary files a/SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/updating.png and /dev/null differ diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/close.png b/SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/close.png deleted file mode 100644 index 41ca1ba5c56c..000000000000 Binary files a/SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/close.png and /dev/null differ diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/Contents.json b/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/Contents.json new file mode 100644 index 000000000000..b4d17a41a835 --- /dev/null +++ b/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "crossCircleFillLarge.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf b/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf new file mode 100644 index 000000000000..bc272de27e63 Binary files /dev/null and b/SampleBrowser/SampleBrowser/Assets.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf differ diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/Contents.json b/SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/Contents.json similarity index 53% rename from SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/Contents.json rename to SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/Contents.json index e1a42f4e88d7..9da3ace162e8 100644 --- a/SampleBrowser/SampleBrowser/Assets.xcassets/Stop.imageset/Contents.json +++ b/SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "close.png", + "filename" : "crossLarge.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/crossLarge.pdf b/SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/crossLarge.pdf new file mode 100644 index 000000000000..22af6fadaecd Binary files /dev/null and b/SampleBrowser/SampleBrowser/Assets.xcassets/crossLarge.imageset/crossLarge.pdf differ diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/Contents.json b/SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/Contents.json similarity index 53% rename from SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/Contents.json rename to SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/Contents.json index 2a8a741755de..e46920ee78cc 100644 --- a/SampleBrowser/SampleBrowser/Assets.xcassets/Reload.imageset/Contents.json +++ b/SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "updating.png", + "filename" : "syncLarge.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/syncLarge.pdf b/SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/syncLarge.pdf new file mode 100644 index 000000000000..49ce0b017a27 Binary files /dev/null and b/SampleBrowser/SampleBrowser/Assets.xcassets/syncLarge.imageset/syncLarge.pdf differ diff --git a/SampleBrowser/SampleBrowser/Model/RootViewControllerModel.swift b/SampleBrowser/SampleBrowser/Model/RootViewControllerModel.swift new file mode 100644 index 000000000000..e78fcf1af1f7 --- /dev/null +++ b/SampleBrowser/SampleBrowser/Model/RootViewControllerModel.swift @@ -0,0 +1,107 @@ +// 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 Common +import ToolbarKit + +class RootViewControllerModel { + // By default the state is set to reload. We save the state to avoid setting the toolbar + // button multiple times when a page load is in progress + private var isReloading = false + private var canGoBack = false + private var canGoForward = false + + var navigationToolbarDelegate: NavigationToolbarDelegate? + var addressToolbarDelegate: AddressToolbarContainerDelegate? + + // MARK: - Navigation toolbar + var navigationToolbarContainerModel: NavigationToolbarContainerModel { + let backButton = ToolbarElement( + iconName: "Back", + isEnabled: canGoBack, + a11yLabel: "Navigate Back", + a11yId: "backButton", + onSelected: { + self.navigationToolbarDelegate?.backButtonTapped() + }) + let forwardButton = ToolbarElement( + iconName: "Forward", + isEnabled: canGoForward, + a11yLabel: "Navigate Forward", + a11yId: "forwardButton", + onSelected: { + self.navigationToolbarDelegate?.forwardButtonTapped() + }) + let reloadButton = ToolbarElement( + iconName: isReloading ? StandardImageIdentifiers.Large.cross : StandardImageIdentifiers.Large.sync, + isEnabled: isReloading, + a11yLabel: isReloading ? "Stop loading website" : "Reload website", + a11yId: isReloading ? "stopButton" : "reloadButton", + onSelected: { + if self.isReloading { + self.navigationToolbarDelegate?.stopButtonTapped() + } else { + self.navigationToolbarDelegate?.reloadButtonTapped() + } + }) + let menuButton = ToolbarElement( + iconName: StandardImageIdentifiers.Large.appMenu, + isEnabled: true, + a11yLabel: "Open Menu", + a11yId: "appMenuButton", + onSelected: { + self.navigationToolbarDelegate?.menuButtonTapped() + }) + let actions = [backButton, forwardButton, reloadButton, menuButton] + + return NavigationToolbarContainerModel(toolbarPosition: .top, actions: actions) + } + + func updateReloadStopButton(loading: Bool) { + guard loading != isReloading else { return } + self.isReloading = loading + } + + func updateBackForwardButtons(canGoBack: Bool, canGoForward: Bool) { + self.canGoBack = canGoBack + self.canGoForward = canGoForward + } + + // MARK: - Address toolbar + func addressToolbarContainerModel(url: String?) -> AddressToolbarContainerModel { + let pageActions = [ToolbarElement( + iconName: StandardImageIdentifiers.Large.qrCode, + isEnabled: true, + a11yLabel: "Read QR Code", + a11yId: "qrCodeButton", + onSelected: nil)] + + let browserActions = [ToolbarElement( + iconName: StandardImageIdentifiers.Large.appMenu, + isEnabled: true, + a11yLabel: "Open Menu", + a11yId: "appMenuButton", + onSelected: { + self.addressToolbarDelegate?.didTapMenu() + })] + + let locationViewState = LocationViewState( + accessibilityIdentifier: "clearButton", + accessibilityHint: "Double tap to clear text", + accessibilityLabel: "Clean", + url: url + ) + + // FXIOS-8947: Use scroll position + return AddressToolbarContainerModel( + toolbarPosition: .top, + scrollY: 0, + isPrivate: false, + locationViewState: locationViewState, + navigationActions: [], + pageActions: pageActions, + browserActions: browserActions) + } +} diff --git a/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainer.swift b/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainer.swift index d97960c0718a..1f530e262824 100644 --- a/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainer.swift +++ b/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainer.swift @@ -7,13 +7,12 @@ import ToolbarKit import UIKit protocol AddressToolbarContainerDelegate: AnyObject { - func didClickMenu() + func didTapMenu() } class AddressToolbarContainer: UIView, ThemeApplicable { private lazy var compactToolbar: CompactBrowserAddressToolbar = .build { _ in } private lazy var regularToolbar: RegularBrowserAddressToolbar = .build() - private weak var delegate: AddressToolbarContainerDelegate? override init(frame: CGRect) { super.init(frame: .zero) @@ -25,11 +24,9 @@ class AddressToolbarContainer: UIView, ThemeApplicable { } func configure(_ model: AddressToolbarContainerModel, - toolbarDelegate: AddressToolbarDelegate, - toolbarContainerDelegate: AddressToolbarContainerDelegate) { + toolbarDelegate: AddressToolbarDelegate) { compactToolbar.configure(state: model.state, toolbarDelegate: toolbarDelegate) regularToolbar.configure(state: model.state, toolbarDelegate: toolbarDelegate) - delegate = toolbarContainerDelegate } override func becomeFirstResponder() -> Bool { diff --git a/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainerModel.swift b/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainerModel.swift index ec575ea2aa29..7fff3dcc695e 100644 --- a/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainerModel.swift +++ b/SampleBrowser/SampleBrowser/UI/Components/AddressToolbarContainerModel.swift @@ -9,7 +9,7 @@ struct AddressToolbarContainerModel { let toolbarPosition: AddressToolbarPosition let scrollY: Int let isPrivate: Bool - let url: String? + let locationViewState: LocationViewState let navigationActions: [ToolbarElement] let pageActions: [ToolbarElement] let browserActions: [ToolbarElement] @@ -17,7 +17,7 @@ struct AddressToolbarContainerModel { var state: AddressToolbarState { return AddressToolbarState( - url: url, + locationViewState: locationViewState, navigationActions: navigationActions, pageActions: pageActions, browserActions: browserActions, @@ -26,7 +26,7 @@ struct AddressToolbarContainerModel { } private var shouldDisplayTopBorder: Bool { - manager.shouldDisplayBorder( + manager.shouldDisplayAddressBorder( borderPosition: .top, toolbarPosition: toolbarPosition, isPrivate: false, @@ -34,7 +34,7 @@ struct AddressToolbarContainerModel { } private var shouldDisplayBottomBorder: Bool { - manager.shouldDisplayBorder( + manager.shouldDisplayAddressBorder( borderPosition: .bottom, toolbarPosition: toolbarPosition, isPrivate: false, diff --git a/SampleBrowser/SampleBrowser/UI/Components/BrowserSearchBar.swift b/SampleBrowser/SampleBrowser/UI/Components/BrowserSearchBar.swift index 34a106bc51ec..179cee924b09 100644 --- a/SampleBrowser/SampleBrowser/UI/Components/BrowserSearchBar.swift +++ b/SampleBrowser/SampleBrowser/UI/Components/BrowserSearchBar.swift @@ -15,7 +15,7 @@ protocol SearchBarDelegate: AnyObject { } protocol MenuDelegate: AnyObject { - func didClickMenu() + func didTapMenu() } class BrowserSearchBar: UIView, UISearchBarDelegate { @@ -30,7 +30,7 @@ class BrowserSearchBar: UIView, UISearchBarDelegate { private lazy var menuButton: UIButton = .build { [self] button in let image = UIImage(named: "Menu")?.withTintColor(UIColor(named: "AccentColor")!) button.setImage(image, for: .normal) - button.addTarget(self, action: #selector(didClickMenu), for: .touchUpInside) + button.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside) button.backgroundColor = .systemBackground } @@ -75,8 +75,8 @@ class BrowserSearchBar: UIView, UISearchBarDelegate { // MARK: - Private @objc - private func didClickMenu() { - menuDelegate?.didClickMenu() + private func didTapMenu() { + menuDelegate?.didTapMenu() } private func setupSearchBar() { diff --git a/SampleBrowser/SampleBrowser/UI/Components/BrowserToolbar.swift b/SampleBrowser/SampleBrowser/UI/Components/BrowserToolbar.swift deleted file mode 100644 index d09737d1a53a..000000000000 --- a/SampleBrowser/SampleBrowser/UI/Components/BrowserToolbar.swift +++ /dev/null @@ -1,97 +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 UIKit - -protocol ToolbarDelegate: AnyObject { - func backButtonClicked() - func forwardButtonClicked() - func reloadButtonClicked() - func stopButtonClicked() -} - -class BrowserToolbar: UIToolbar { - weak var toolbarDelegate: ToolbarDelegate? - private var reloadStopButton: UIBarButtonItem! - private var backButton: UIBarButtonItem! - private var forwardButton: UIBarButtonItem! - - // By default the state is set to reload. We save the state to avoid setting the toolbar - // button multiple times when a page load is in progress - private var isReloading = true - - // MARK: - Init - - override init(frame: CGRect) { - super.init(frame: frame) - - backButton = UIBarButtonItem(image: UIImage(named: "Back"), - style: .plain, - target: self, - action: #selector(backButtonClicked)) - forwardButton = UIBarButtonItem(image: UIImage(named: "Forward"), - style: .plain, - target: self, - action: #selector(forwardButtonClicked)) - reloadStopButton = UIBarButtonItem(image: UIImage(named: "Reload"), - style: .plain, - target: self, - action: #selector(reloadButtonClicked)) - - var items = [UIBarButtonItem]() - items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)) - items.append(backButton) - items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)) - items.append(forwardButton) - items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)) - items.append(reloadStopButton) - items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)) - setItems(items, animated: false) - - barTintColor = .white - - // initial state for buttons - updateBackForwardButtons(canGoBack: false, canGoForward: false) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Button states - - func updateReloadStopButton(loading: Bool) { - guard loading != isReloading else { return } - reloadStopButton.image = loading ? UIImage(named: "Stop") : UIImage(named: "Reload") - reloadStopButton.action = loading ? #selector(stopButtonClicked) : #selector(reloadButtonClicked) - self.isReloading = loading - } - - func updateBackForwardButtons(canGoBack: Bool, canGoForward: Bool) { - backButton.isEnabled = canGoBack - forwardButton.isEnabled = canGoForward - } - - // MARK: - Actions - - @objc - func backButtonClicked() { - toolbarDelegate?.backButtonClicked() - } - - @objc - func forwardButtonClicked() { - toolbarDelegate?.forwardButtonClicked() - } - - @objc - func reloadButtonClicked() { - toolbarDelegate?.reloadButtonClicked() - } - - @objc - func stopButtonClicked() { - toolbarDelegate?.stopButtonClicked() - } -} diff --git a/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainer.swift b/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainer.swift new file mode 100644 index 000000000000..5c874592d959 --- /dev/null +++ b/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainer.swift @@ -0,0 +1,84 @@ +// 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 ToolbarKit +import UIKit + +protocol NavigationToolbarDelegate: AnyObject { + func backButtonTapped() + func forwardButtonTapped() + func reloadButtonTapped() + func stopButtonTapped() + func menuButtonTapped() +} + +class NavigationToolbarContainer: UIView, ThemeApplicable { + private enum UX { + static let toolbarHeight: CGFloat = 48 + } + + private lazy var toolbar: BrowserNavigationToolbar = .build { _ in } + private var toolbarHeightConstraint: NSLayoutConstraint? + + private var bottomToolbarHeight: CGFloat { return UX.toolbarHeight + bottomInset } + + private var bottomInset: CGFloat { + var bottomInset: CGFloat = 0.0 + if let window = attachedKeyWindow { + bottomInset = window.safeAreaInsets.bottom + } + return bottomInset + } + + private var attachedKeyWindow: UIWindow? { + // swiftlint:disable first_where + return UIApplication.shared.connectedScenes + .filter { $0.activationState != .unattached } + .first(where: { $0 is UIWindowScene }) + .flatMap({ $0 as? UIWindowScene })?.windows + .first(where: \.isKeyWindow) + // swiftlint:enable first_where + } + + override init(frame: CGRect) { + super.init(frame: .zero) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + // when the layout is setup the window scene is not attached yet so we need to update the constant later + toolbarHeightConstraint?.constant = bottomToolbarHeight + } + + func configure(_ model: NavigationToolbarContainerModel) { + toolbar.configure(state: model.state) + } + + private func setupLayout() { + addSubview(toolbar) + + toolbarHeightConstraint = heightAnchor.constraint(equalToConstant: bottomToolbarHeight) + toolbarHeightConstraint?.isActive = true + + NSLayoutConstraint.activate([ + toolbar.topAnchor.constraint(equalTo: topAnchor), + toolbar.leadingAnchor.constraint(equalTo: leadingAnchor), + toolbar.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), + toolbar.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + } + + // MARK: - ThemeApplicable + func applyTheme(theme: Theme) { + toolbar.applyTheme(theme: theme) + backgroundColor = theme.colors.layer1 + } +} diff --git a/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainerModel.swift b/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainerModel.swift new file mode 100644 index 000000000000..90885770919f --- /dev/null +++ b/SampleBrowser/SampleBrowser/UI/Components/NavigationToolbarContainerModel.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 Common +import ToolbarKit + +struct NavigationToolbarContainerModel { + let toolbarPosition: AddressToolbarPosition + let actions: [ToolbarElement] + var manager: ToolbarManager = DefaultToolbarManager() + + var state: NavigationToolbarState { + return NavigationToolbarState(actions: actions, shouldDisplayBorder: shouldDisplayBorder) + } + + private var shouldDisplayBorder: Bool { + manager.shouldDisplayNavigationBorder(toolbarPosition: toolbarPosition) + } +} diff --git a/SampleBrowser/SampleBrowser/UI/RootViewController.swift b/SampleBrowser/SampleBrowser/UI/RootViewController.swift index b2cc1af71503..69e85c47e384 100644 --- a/SampleBrowser/SampleBrowser/UI/RootViewController.swift +++ b/SampleBrowser/SampleBrowser/UI/RootViewController.swift @@ -8,7 +8,7 @@ import UIKit // Holds toolbar, search bar, search and browser VCs class RootViewController: UIViewController, - ToolbarDelegate, + NavigationToolbarDelegate, NavigationDelegate, AddressToolbarDelegate, AddressToolbarContainerDelegate, @@ -21,7 +21,7 @@ class RootViewController: UIViewController, var themeObserver: NSObjectProtocol? var notificationCenter: NotificationProtocol = NotificationCenter.default - private lazy var toolbar: BrowserToolbar = .build { _ in } + private lazy var navigationToolbar: NavigationToolbarContainer = .build { _ in } private lazy var addressToolbarContainer: AddressToolbarContainer = .build { _ in } private lazy var statusBarFiller: UIView = .build { view in view.backgroundColor = .white @@ -31,8 +31,12 @@ class RootViewController: UIViewController, private var searchVC: SearchViewController private var findInPageBar: FindInPageBar? + private var model = RootViewControllerModel() + // MARK: - Init - init(engineProvider: EngineProvider, windowUUID: UUID?, themeManager: ThemeManager = AppContainer.shared.resolve()) { + init(engineProvider: EngineProvider, + windowUUID: UUID?, + themeManager: ThemeManager = AppContainer.shared.resolve()) { self.browserVC = BrowserViewController(engineProvider: engineProvider) self.searchVC = SearchViewController() self.themeManager = themeManager @@ -53,7 +57,7 @@ class RootViewController: UIViewController, configureBrowserView() configureAddressToolbar() configureSearchView() - configureToolbar() + configureNavigationToolbar() listenForThemeChange(view) applyTheme() @@ -103,37 +107,14 @@ class RootViewController: UIViewController, addressToolbarContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) + model.addressToolbarDelegate = self updateAddressToolbar(url: nil) _ = addressToolbarContainer.becomeFirstResponder() } private func updateAddressToolbar(url: String?) { - let pageActions = [ToolbarElement( - iconName: StandardImageIdentifiers.Large.qrCode, - isEnabled: true, - a11yLabel: "Read QR Code", - a11yId: "qrCodeButton", - onSelected: nil)] - - let browserActions = [ToolbarElement( - iconName: StandardImageIdentifiers.Large.appMenu, - isEnabled: true, - a11yLabel: "Open Menu", - a11yId: "appMenuButton", - onSelected: { - self.didClickMenu() - })] - - // FXIOS-8947: Use scroll position - let model = AddressToolbarContainerModel( - toolbarPosition: .top, - scrollY: 0, - isPrivate: false, - url: url, - navigationActions: [], - pageActions: pageActions, - browserActions: browserActions) - addressToolbarContainer.configure(model, toolbarDelegate: self, toolbarContainerDelegate: self) + let model = model.addressToolbarContainerModel(url: url) + addressToolbarContainer.configure(model, toolbarDelegate: self) } private func configureSearchView() { @@ -149,21 +130,26 @@ class RootViewController: UIViewController, searchVC.view.topAnchor.constraint(equalTo: addressToolbarContainer.bottomAnchor), searchVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), searchVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - searchVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor) + searchVC.view.bottomAnchor.constraint(equalTo: navigationToolbar.topAnchor) ]) } - private func configureToolbar() { - view.addSubview(toolbar) + private func configureNavigationToolbar() { + view.addSubview(navigationToolbar) NSLayoutConstraint.activate([ - toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - toolbar.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), - toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - toolbar.topAnchor.constraint(equalTo: browserVC.view.bottomAnchor) + navigationToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationToolbar.bottomAnchor.constraint(equalTo: view.bottomAnchor), + navigationToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + navigationToolbar.topAnchor.constraint(equalTo: browserVC.view.bottomAnchor) ]) - toolbar.toolbarDelegate = self + model.navigationToolbarDelegate = self + updateNavigationToolbar() + } + + private func updateNavigationToolbar() { + navigationToolbar.configure(model.navigationToolbarContainerModel) } // MARK: - Private @@ -176,30 +162,36 @@ class RootViewController: UIViewController, // MARK: - BrowserToolbarDelegate - func backButtonClicked() { + func backButtonTapped() { browserVC.goBack() } - func forwardButtonClicked() { + func forwardButtonTapped() { browserVC.goForward() } - func reloadButtonClicked() { + func reloadButtonTapped() { browserVC.reload() } - func stopButtonClicked() { + func stopButtonTapped() { browserVC.stop() } + func menuButtonTapped() { + didTapMenu() + } + // MARK: - NavigationDelegate func onLoadingStateChange(loading: Bool) { - toolbar.updateReloadStopButton(loading: loading) + model.updateReloadStopButton(loading: loading) + updateNavigationToolbar() } func onNavigationStateChange(canGoBack: Bool, canGoForward: Bool) { - toolbar.updateBackForwardButtons(canGoBack: canGoBack, canGoForward: canGoForward) + model.updateBackForwardButtons(canGoBack: canGoBack, canGoForward: canGoForward) + updateNavigationToolbar() } func onURLChange(url: String) { @@ -295,14 +287,14 @@ class RootViewController: UIViewController, NSLayoutConstraint.activate([ findInPageBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), - findInPageBar.bottomAnchor.constraint(equalTo: toolbar.topAnchor), + findInPageBar.bottomAnchor.constraint(equalTo: navigationToolbar.topAnchor), findInPageBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), findInPageBar.heightAnchor.constraint(equalToConstant: 46) ]) } // MARK: - AddressToolbarContainerDelegate - func didClickMenu() { + func didTapMenu() { let settingsVC = SettingsViewController() settingsVC.delegate = self present(settingsVC, animated: true) diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index c5f56473e626..340bdd7f0cdb 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 1DDE3DB52AC360EC0039363B /* TabCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDE3DB42AC360EC0039363B /* TabCellTests.swift */; }; 1DDE3DB72AC3820A0039363B /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDE3DB62AC3820A0039363B /* TabModel.swift */; }; 1DEBC55E2AC4ED70006E4801 /* RemoteTabsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEBC55D2AC4ED70006E4801 /* RemoteTabsPanel.swift */; }; + 1DF2BDC32BD1BCF300E53C57 /* WindowManager+DebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BDC22BD1BCF300E53C57 /* WindowManager+DebugUtilities.swift */; }; 1DF426CF251BDF6A0086386A /* photon-colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C49854D206173C800893DAE /* photon-colors.swift */; }; 1DFE57FB27B2CB870025DE58 /* HighlightItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE57FA27B2CB870025DE58 /* HighlightItem.swift */; }; 1DFE57FD27BADD7D0025DE58 /* HomepageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE57FC27BADD7C0025DE58 /* HomepageViewModel.swift */; }; @@ -288,7 +289,7 @@ 39EF434E260A73950011E22E /* Experiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EF434D260A73950011E22E /* Experiments.swift */; }; 39F4C0FA2045D87400746155 /* FocusHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 39F4C0F92045D87400746155 /* FocusHelper.js */; }; 39F4C10A2045DB2E00746155 /* FocusHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F4C1092045DB2E00746155 /* FocusHelper.swift */; }; - 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */; }; + 39F819C61FD70F5D009E31E4 /* GlobalTabEventHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F819C51FD70F5D009E31E4 /* GlobalTabEventHandlers.swift */; }; 3B39EDBA1E16E18900EF029F /* CustomSearchEnginesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B39EDB91E16E18900EF029F /* CustomSearchEnginesTest.swift */; }; 3B39EDCB1E16E1AA00EF029F /* CustomSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */; }; 3B43E3D31D95C48D00BBA9DB /* StoragePerfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B43E3D21D95C48D00BBA9DB /* StoragePerfTests.swift */; }; @@ -627,6 +628,7 @@ 8A2825352760399B00395E66 /* KeyboardPressesHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2825342760399B00395E66 /* KeyboardPressesHandlerTests.swift */; }; 8A285B08294A5D4C00149B0F /* HomepageHeroImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A285B07294A5D4C00149B0F /* HomepageHeroImageViewModel.swift */; }; 8A28C628291028870078A81A /* CanRemoveQuickActionBookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A28C627291028870078A81A /* CanRemoveQuickActionBookmarkTests.swift */; }; + 8A28F3FD2BD6B7A400A93410 /* MicroSurveyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A28F3FC2BD6B7A400A93410 /* MicroSurveyViewModel.swift */; }; 8A2B1A5D28216C4D0061216B /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 8A2B1A5A28216C4C0061216B /* Debug.xcconfig */; }; 8A2B1A5E28216C4D0061216B /* Common.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 8A2B1A5B28216C4C0061216B /* Common.xcconfig */; }; 8A2B1A5F28216C4D0061216B /* Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 8A2B1A5C28216C4D0061216B /* Release.xcconfig */; }; @@ -702,6 +704,7 @@ 8A5D1CC12A30DCA4005AD35C /* SettingDisclosureUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A5D1CC02A30DCA4005AD35C /* SettingDisclosureUtility.swift */; }; 8A635ECD289437A8006378BA /* SyncedTabCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A635ECC289437A8006378BA /* SyncedTabCellTests.swift */; }; 8A6904802B97BBAE00E30047 /* SplashScreenAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A69047F2B97BBAE00E30047 /* SplashScreenAnimation.swift */; }; + 8A6A3D472BD0390100BFDB64 /* MicroSurveyPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A6A3D462BD0390100BFDB64 /* MicroSurveyPrompt.swift */; }; 8A6A796D27F773550022D6C6 /* HomepageContextMenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A6A796C27F773550022D6C6 /* HomepageContextMenuHelper.swift */; }; 8A6B77CC2811C468001110D2 /* URLProtocolStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A6B77CB2811C468001110D2 /* URLProtocolStub.swift */; }; 8A6E13982A71BA4E00A88FA8 /* TabWebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A6E13972A71BA4E00A88FA8 /* TabWebViewTests.swift */; }; @@ -951,6 +954,8 @@ AB03032B2AB47AF300DCD8EF /* FakespotOptInCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0303292AB47AF300DCD8EF /* FakespotOptInCardView.swift */; }; AB03032C2AB47AF300DCD8EF /* FakespotOptInCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB03032A2AB47AF300DCD8EF /* FakespotOptInCardViewModel.swift */; }; AB03032F2AB8561700DCD8EF /* FakespotOptInViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB03032D2AB484B700DCD8EF /* FakespotOptInViewModelTests.swift */; }; + AB2AC6632BCFD0A200022AAB /* X509 in Frameworks */ = {isa = PBXBuildFile; productRef = AB2AC6622BCFD0A200022AAB /* X509 */; }; + AB2AC6662BD15E6300022AAB /* CertificatesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB2AC6652BD15E6300022AAB /* CertificatesHandler.swift */; }; AB3DB0C92B596739001D32CB /* AppStartupTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3DB0C82B596739001D32CB /* AppStartupTelemetry.swift */; }; AB42CC742A1F5240003C9594 /* CreditCardBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB42CC722A1F523F003C9594 /* CreditCardBottomSheetViewController.swift */; }; AB42CC752A1F5240003C9594 /* CreditCardBottomSheetHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB42CC732A1F5240003C9594 /* CreditCardBottomSheetHeaderView.swift */; }; @@ -2250,6 +2255,7 @@ 1DDE3DB62AC3820A0039363B /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; 1DE6449A95FE587845F9A459 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/ErrorPages.strings; sourceTree = ""; }; 1DEBC55D2AC4ED70006E4801 /* RemoteTabsPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTabsPanel.swift; sourceTree = ""; }; + 1DF2BDC22BD1BCF300E53C57 /* WindowManager+DebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowManager+DebugUtilities.swift"; sourceTree = ""; }; 1DFE57FA27B2CB870025DE58 /* HighlightItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightItem.swift; sourceTree = ""; }; 1DFE57FC27BADD7C0025DE58 /* HomepageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageViewModel.swift; sourceTree = ""; }; 1DFE57FE27BAE3150025DE58 /* HomepageSectionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageSectionType.swift; sourceTree = ""; }; @@ -2637,7 +2643,7 @@ 39EF434D260A73950011E22E /* Experiments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Experiments.swift; sourceTree = ""; }; 39F4C0F92045D87400746155 /* FocusHelper.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = FocusHelper.js; sourceTree = ""; }; 39F4C1092045DB2E00746155 /* FocusHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusHelper.swift; sourceTree = ""; }; - 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabEventHandlers.swift; sourceTree = ""; }; + 39F819C51FD70F5D009E31E4 /* GlobalTabEventHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalTabEventHandlers.swift; sourceTree = ""; }; 3A274ACCA78FA70F683301A1 /* sat-Olck */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sat-Olck"; path = "sat-Olck.lproj/3DTouchActions.strings"; sourceTree = ""; }; 3A434F2D835AA5B03CD68347 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Intro.strings; sourceTree = ""; }; 3A704356AA5D6BCCF526AAB4 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/AuthenticationManager.strings; sourceTree = ""; }; @@ -5795,6 +5801,7 @@ 8A2825342760399B00395E66 /* KeyboardPressesHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPressesHandlerTests.swift; sourceTree = ""; }; 8A285B07294A5D4C00149B0F /* HomepageHeroImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageHeroImageViewModel.swift; sourceTree = ""; }; 8A28C627291028870078A81A /* CanRemoveQuickActionBookmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanRemoveQuickActionBookmarkTests.swift; sourceTree = ""; }; + 8A28F3FC2BD6B7A400A93410 /* MicroSurveyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicroSurveyViewModel.swift; sourceTree = ""; }; 8A2B1A5A28216C4C0061216B /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configuration/Debug.xcconfig; sourceTree = ""; }; 8A2B1A5B28216C4C0061216B /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Common.xcconfig; path = Configuration/Common.xcconfig; sourceTree = ""; }; 8A2B1A5C28216C4D0061216B /* Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configuration/Release.xcconfig; sourceTree = ""; }; @@ -5871,6 +5878,7 @@ 8A5D1CC02A30DCA4005AD35C /* SettingDisclosureUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingDisclosureUtility.swift; sourceTree = ""; }; 8A635ECC289437A8006378BA /* SyncedTabCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncedTabCellTests.swift; sourceTree = ""; }; 8A69047F2B97BBAE00E30047 /* SplashScreenAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenAnimation.swift; sourceTree = ""; }; + 8A6A3D462BD0390100BFDB64 /* MicroSurveyPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicroSurveyPrompt.swift; sourceTree = ""; }; 8A6A796C27F773550022D6C6 /* HomepageContextMenuHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageContextMenuHelper.swift; sourceTree = ""; }; 8A6B77CB2811C468001110D2 /* URLProtocolStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLProtocolStub.swift; sourceTree = ""; }; 8A6E13972A71BA4E00A88FA8 /* TabWebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabWebViewTests.swift; sourceTree = ""; }; @@ -6386,6 +6394,7 @@ AB0303292AB47AF300DCD8EF /* FakespotOptInCardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakespotOptInCardView.swift; sourceTree = ""; }; AB03032A2AB47AF300DCD8EF /* FakespotOptInCardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakespotOptInCardViewModel.swift; sourceTree = ""; }; AB03032D2AB484B700DCD8EF /* FakespotOptInViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakespotOptInViewModelTests.swift; sourceTree = ""; }; + AB2AC6652BD15E6300022AAB /* CertificatesHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatesHandler.swift; sourceTree = ""; }; AB2B45078A7F1E09F65ACEC5 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/Shared.strings; sourceTree = ""; }; AB3DB0C82B596739001D32CB /* AppStartupTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartupTelemetry.swift; sourceTree = ""; }; AB42CC722A1F523F003C9594 /* CreditCardBottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreditCardBottomSheetViewController.swift; sourceTree = ""; }; @@ -7994,6 +8003,7 @@ 432BD0242790EBD000A0F3C3 /* Adjust in Frameworks */, 0B8E0FF41A932BD500161DC3 /* ImageIO.framework in Frameworks */, 435C85F02788F4D00072B526 /* Glean in Frameworks */, + AB2AC6632BCFD0A200022AAB /* X509 in Frameworks */, 433F87CE2788EAB600693368 /* GCDWebServers in Frameworks */, 5A06135A29D6052E008F3D38 /* TabDataStore in Frameworks */, 216A0D762A40E7AB008077BA /* Redux in Frameworks */, @@ -9517,7 +9527,7 @@ D3A994961A3686BD008AD1AC /* Tab.swift */, C82CDD45233E8996002E2743 /* Tab+ChangeUserAgent.swift */, 39455F761FC83F430088A22C /* TabEventHandler.swift */, - 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */, + 39F819C51FD70F5D009E31E4 /* GlobalTabEventHandlers.swift */, D3968F241A38FE8500CEFD3B /* TabManager.swift */, 5A8017DF29CE15D90047120D /* TabManagerImplementation.swift */, 6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */, @@ -9577,6 +9587,15 @@ path = SplashScreenAnimation; sourceTree = ""; }; + 8A6A3D452BD038EF00BFDB64 /* MicroSurvey */ = { + isa = PBXGroup; + children = ( + 8A6A3D462BD0390100BFDB64 /* MicroSurveyPrompt.swift */, + 8A28F3FC2BD6B7A400A93410 /* MicroSurveyViewModel.swift */, + ); + path = MicroSurvey; + sourceTree = ""; + }; 8A7653C028A2E54800924ABF /* Pocket */ = { isa = PBXGroup; children = ( @@ -10116,6 +10135,14 @@ path = SearchSettings; sourceTree = ""; }; + AB2AC6642BD15E2C00022AAB /* TrackingProtection */ = { + isa = PBXGroup; + children = ( + AB2AC6652BD15E6300022AAB /* CertificatesHandler.swift */, + ); + path = TrackingProtection; + sourceTree = ""; + }; ABEF80CD2A24BEF1003F52C4 /* CreditCardBottomSheet */ = { isa = PBXGroup; children = ( @@ -11882,6 +11909,7 @@ 5A271ABB2860B0BD00471CE4 /* WebServer */, 1DA6F6502B48B42900BB5AD6 /* WindowEventCoordinator.swift */, 1DC372012B23C80F000F96C8 /* WindowManager.swift */, + 1DF2BDC22BD1BCF300E53C57 /* WindowManager+DebugUtilities.swift */, ); path = Application; sourceTree = ""; @@ -11931,6 +11959,7 @@ EBC486972195F46A00CDA48D /* InternalSchemeHandler */, D05434F3225FDA3400FDE4EF /* Library */, 7BC7B4571C903A6A0046E9D2 /* Menu */, + 8A6A3D452BD038EF00BFDB64 /* MicroSurvey */, E1FE132D29C0B334002A65FF /* NotificationSurface */, C8BD875D2A0C23F500CD803A /* Onboarding */, E63ED8DF1BFD254E0097D08E /* PasswordManagement */, @@ -11940,6 +11969,7 @@ C855728029AE7EF900AF32B0 /* SurveySurface */, 8AD40FB827BAD1DD00672675 /* TabContentsScripts */, EB9A179720E69A7E00B12184 /* Theme */, + AB2AC6642BD15E2C00022AAB /* TrackingProtection */, 8AD40FB727BAD1D600672675 /* Toolbar+URLBar */, 2816EFFF1B33E05400522243 /* UIConstants.swift */, C87DF9DA267247190097E707 /* UIConstants+BottomInset.swift */, @@ -12445,6 +12475,7 @@ 216A0D752A40E7AB008077BA /* Redux */, 8AF2D0FB2A5F272A00C7DD69 /* ComponentLibrary */, 8AB30EC72B6C038600BD9A9B /* Lottie */, + AB2AC6622BCFD0A200022AAB /* X509 */, ); productName = Client; productReference = F84B21BE1A090F8100AAB793 /* Client.app */; @@ -12764,6 +12795,7 @@ 43C6A47D27A0679300C79856 /* XCRemoteSwiftPackageReference "MappaMundi" */, 5A37861729A2C337006B3A34 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, 8AB30EC62B6C038600BD9A9B /* XCRemoteSwiftPackageReference "lottie-ios" */, + AB2AC6612BCFD0A200022AAB /* XCRemoteSwiftPackageReference "swift-certificates" */, ); productRefGroup = F84B21BF1A090F8100AAB793 /* Products */; projectDirPath = ""; @@ -13740,6 +13772,7 @@ C8680C5728BFDF7F00BC902A /* WallpaperThumbnailUtility.swift in Sources */, C23889DF2A4EFCE500429673 /* ShareExtensionCoordinator.swift in Sources */, 8AE80BBE2891C21A00BC12EA /* JumpBackInSyncedTab.swift in Sources */, + 8A28F3FD2BD6B7A400A93410 /* MicroSurveyViewModel.swift in Sources */, 8C6F94652A972EB300415FF6 /* FakespotAdjustRatingView.swift in Sources */, 8A3EF7FD2A2FCFAC00796E3A /* AppReviewPromptSetting.swift in Sources */, D3B6923F1B9F9A58004B87A4 /* FindInPageHelper.swift in Sources */, @@ -13754,6 +13787,7 @@ DA52E1DA25F5961F0092204C /* LegacyTabTrayViewController.swift in Sources */, 8AD40FD327BB068F00672675 /* MainMenuActionHelper.swift in Sources */, EB1C84BF212EFFBF001489DF /* BrowserViewController+ReaderMode.swift in Sources */, + AB2AC6662BD15E6300022AAB /* CertificatesHandler.swift in Sources */, 81020C922BB5AFA2007B8481 /* OnboardingMultipleChoiceButtonView.swift in Sources */, 96D95016270238500079D39D /* Throttler.swift in Sources */, 8A93080B27C01AD60052167D /* SingleActionViewModel.swift in Sources */, @@ -13791,7 +13825,7 @@ DFACBF7F277B5F7B003D5F41 /* WallpaperBackgroundView.swift in Sources */, D01017F5219CB6BD009CBB5A /* DownloadContentScript.swift in Sources */, 8A093D832A4B68940099ABA5 /* PrivacySettingsDelegate.swift in Sources */, - 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */, + 39F819C61FD70F5D009E31E4 /* GlobalTabEventHandlers.swift in Sources */, C8DC90C32A066B4A0008832B /* MarkupToken.swift in Sources */, FA6B2AC21D41F02D00429414 /* String+Punycode.swift in Sources */, E174963C2992B6A60096900A /* HostingTableViewSectionHeader.swift in Sources */, @@ -13926,6 +13960,7 @@ CA90753824929B22005B794D /* NoLoginsView.swift in Sources */, E4B423BE1AB9FE6A007E66C8 /* ReaderModeCache.swift in Sources */, 396CDB55203C5B870034A3A3 /* TabTrayController+KeyCommands.swift in Sources */, + 1DF2BDC32BD1BCF300E53C57 /* WindowManager+DebugUtilities.swift in Sources */, C855728629AEA3FB00AF32B0 /* SurveySurfaceViewController.swift in Sources */, 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */, EBB8950C21939E4100EB91A0 /* FirefoxTabContentBlocker.swift in Sources */, @@ -14356,6 +14391,7 @@ E633E2DA1C21EAF8001FFF6C /* PasswordDetailViewController.swift in Sources */, 8A9AC46B276D11280047F5B0 /* PocketViewModel.swift in Sources */, 8A7653BF28A2C92600924ABF /* PocketStandardCellViewModel.swift in Sources */, + 8A6A3D472BD0390100BFDB64 /* MicroSurveyPrompt.swift in Sources */, 8A5D1CAE2A30D71A005AD35C /* ThemeSetting.swift in Sources */, C82F4C2B29AE2DF1005BD116 /* NotificationsSettingsViewController.swift in Sources */, 59A68B280D62462B85CF57A4 /* HistoryPanel.swift in Sources */, @@ -21540,7 +21576,7 @@ repositoryURL = "https://github.com/mozilla/rust-components-swift.git"; requirement = { kind = exactVersion; - version = 126.0.20240410050314; + version = 127.0.20240417050328; }; }; 435C85EE2788F4D00072B526 /* XCRemoteSwiftPackageReference "glean-swift" */ = { @@ -21591,6 +21627,14 @@ version = 4.4.0; }; }; + AB2AC6612BCFD0A200022AAB /* XCRemoteSwiftPackageReference "swift-certificates" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-certificates.git"; + requirement = { + kind = exactVersion; + version = 1.2.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -21776,6 +21820,11 @@ isa = XCSwiftPackageProductDependency; productName = ComponentLibrary; }; + AB2AC6622BCFD0A200022AAB /* X509 */ = { + isa = XCSwiftPackageProductDependency; + package = AB2AC6612BCFD0A200022AAB /* XCRemoteSwiftPackageReference "swift-certificates" */; + productName = X509; + }; D433852B27ABC8150069DD33 /* MappaMundi */ = { isa = XCSwiftPackageProductDependency; package = 43C6A47D27A0679300C79856 /* XCRemoteSwiftPackageReference "MappaMundi" */; diff --git a/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5eca130af8c3..cfd6eed8c61a 100644 --- a/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/firefox-ios/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mozilla/rust-components-swift.git", "state" : { - "revision" : "89bb410fd78fabb688cd6378c0841c343e3ab9e9", - "version" : "126.0.20240410050314" + "revision" : "6fc91097ce72934b00fe1fa22de2f0020bfa896d", + "version" : "127.0.20240417050328" } }, { @@ -117,6 +117,33 @@ "version" : "5.7.0" } }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "c7e239b5c1492ffc3ebd7fbcc7a92548ce4e78f0", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "bc566f88842b3b8001717326d935c2d113af5741", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "f0525da24dc3c6cbb2b6b338b65042bc91cbc4bb", + "version" : "3.3.0" + } + }, { "identity" : "swiftybeaver", "kind" : "remoteSourceControl", diff --git a/firefox-ios/Client/AccountSyncHandler.swift b/firefox-ios/Client/AccountSyncHandler.swift index 7e6de689e253..7d814620e9af 100644 --- a/firefox-ios/Client/AccountSyncHandler.swift +++ b/firefox-ios/Client/AccountSyncHandler.swift @@ -11,6 +11,7 @@ import Common class AccountSyncHandler: TabEventHandler { private let throttler: Throttler private let profile: Profile + let tabEventWindowResponseType: TabEventHandlerWindowResponseType = .allWindows init(with profile: Profile, throttleTime: Double = 5.0, diff --git a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift index c4b541ecf27f..b768f8959c78 100644 --- a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift +++ b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift @@ -43,6 +43,7 @@ public struct AccessibilityIdentifiers { static let scanQRCodeButton = "urlBar-scanQRCode" static let cancelButton = "urlBar-cancel" static let searchTextField = "address" + static let url = "url" } struct KeyboardAccessory { @@ -130,6 +131,13 @@ public struct AccessibilityIdentifiers { public static let back = "Back" } + struct MicroSurvey { + struct Prompt { + static let closeButton = "MicroSurvey.Prompt.CloseButton" + static let takeSurveyButton = "MicroSurvey.Prompt.TakeSurveyButton" + } + } + struct PrivateMode { static let dimmingView = "PrivateMode.DimmingView" struct Homepage { @@ -440,6 +448,7 @@ public struct AccessibilityIdentifiers { static let deleteMozillaEngine = "Remove Mozilla Engine" static let deleteButton = "Delete" static let disableSearchSuggestsInPrivateMode = "PrivateMode.DisableSearchSuggests" + static let showSearchSuggestions = "FirefoxSuggestShowSearchSuggestions" } struct AdvancedAccountSettings { diff --git a/firefox-ios/Client/Application/AppDelegate.swift b/firefox-ios/Client/Application/AppDelegate.swift index 39fe5d42abc1..125e188340e0 100644 --- a/firefox-ios/Client/Application/AppDelegate.swift +++ b/firefox-ios/Client/Application/AppDelegate.swift @@ -33,6 +33,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { lazy var notificationSurfaceManager = NotificationSurfaceManager() lazy var tabDataStore = DefaultTabDataStore() lazy var windowManager = WindowManagerImplementation() + lazy var backgroundTabLoader: BackgroundTabLoader = { + return DefaultBackgroundTabLoader(tabQueue: (AppContainer.shared.resolve() as Profile).queue) + }() + private var isLoadingBackgroundTabs = false private var shutdownWebServer: DispatchSourceTimer? private var webServerUtil: WebServerUtil? @@ -41,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var widgetManager: TopSitesWidgetManager? private var menuBuilderHelper: MenuBuilderHelper? private var metricKitWrapper = MetricKitWrapper() + private let wallpaperMetadataQueue = DispatchQueue(label: "com.moz.wallpaperVerification.queue") func application( _ application: UIApplication, @@ -164,6 +169,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self?.profile.pollCommands(forcePoll: false) } + updateWallpaperMetadata() + loadBackgroundTabs() + logger.log("applicationDidBecomeActive end", level: .info, category: .lifecycle) @@ -211,6 +219,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Since we only need the topSites data in the archiver, let's write it widgetManager?.writeWidgetKitTopSites() } + + private func loadBackgroundTabs() { + guard !isLoadingBackgroundTabs else { return } + + // We want to ensure that both the startup flow as well as all window tab restorations + // are completed before we attempt to load a background tab. Reminder: we currently do + // not know which window will actually open the tab, that is determined by iOS because + // the tab is opened via `applicationHelper.open()`. + var requiredEvents: [AppEvent] = [.startupFlowComplete] + requiredEvents += windowManager.allWindowUUIDs(includingReserved: true).map { .tabRestoration($0) } + isLoadingBackgroundTabs = true + AppEventQueue.wait(for: requiredEvents) { [weak self] in + self?.isLoadingBackgroundTabs = false + self?.backgroundTabLoader.loadBackgroundTabs() + } + } + + private func updateWallpaperMetadata() { + wallpaperMetadataQueue.async { + let wallpaperManager = WallpaperManager() + wallpaperManager.checkForUpdates() + } + } } extension AppDelegate: Notifiable { diff --git a/firefox-ios/Client/Application/WindowManager+DebugUtilities.swift b/firefox-ios/Client/Application/WindowManager+DebugUtilities.swift new file mode 100644 index 000000000000..93b7bbba6dfc --- /dev/null +++ b/firefox-ios/Client/Application/WindowManager+DebugUtilities.swift @@ -0,0 +1,53 @@ +// 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 Common +import Shared +import TabDataStore + +extension WindowManagerImplementation { + /// For developer and internal debugging of Multi-window on iPad + func _debugDiagnostics() -> String { + func short(_ uuid: UUID) -> String { return String(uuid.uuidString.prefix(4)) } + guard let del = (UIApplication.shared.delegate as? AppDelegate) else { return ""} + var result = "----------- Window Debug Info ------------\n" + result.append("Open windows (\(windows.count)) & normal tabs (via TabManager):\n") + for (idx, (uuid, _)) in windows.enumerated() { + result.append(" \(idx + 1): \(short(uuid))\n") + let tabMgr = tabManager(for: uuid) + for (tabIdx, tab) in tabMgr.normalTabs.enumerated() { + result.append(" \(tabIdx): \(tab.url?.absoluteString ?? "")\n") + } + } + result.append("\n") + result.append("Ordering prefs:\n") + for (idx, pref) in windowOrderingPriority.enumerated() { + result.append(" \(idx + 1): \(short(pref))\n") + } + + let tabDataStore = del.tabDataStore + result.append("\n") + result.append("Persisted tabs:\n") + let fileManager: TabFileManager = DefaultTabFileManager() + + // Note: this is provided as a convenience for internal debugging. See `DefaultTabDataStore.swift`. + for (idx, uuid) in tabDataStore.fetchWindowDataUUIDs().enumerated() { + result.append(" \(idx + 1): Window \(short(uuid))\n") + let baseURL = fileManager.windowDataDirectory(isBackup: false)! + let dataURL = baseURL.appendingPathComponent("window-" + uuid.uuidString) + guard let data = try? fileManager.getWindowDataFromPath(path: dataURL) else { continue } + for (tabIdx, tabData) in data.tabData.enumerated() { + result.append(" \(tabIdx + 1): \(tabData.siteUrl)\n") + } + } + return result + } +} + +/// Convenience. For developer and internal debugging +func _wndMgrDebug() -> String { + let windowMgr: WindowManager = AppContainer.shared.resolve() + return (windowMgr as? WindowManagerImplementation)?._debugDiagnostics() ?? "" +} diff --git a/firefox-ios/Client/Application/WindowManager.swift b/firefox-ios/Client/Application/WindowManager.swift index 0d72b743a6e4..5697e2185632 100644 --- a/firefox-ios/Client/Application/WindowManager.swift +++ b/firefox-ios/Client/Application/WindowManager.swift @@ -28,6 +28,13 @@ protocol WindowManager { /// Convenience. Returns all TabManagers for all open windows. func allWindowTabManagers() -> [TabManager] + /// Returns the UUIDs for all open windows, optionally also including windows that + /// are still in the process of being configured but have not yet completed. + /// Note: the order of the UUIDs is undefined. + /// - Parameter includingReserved: whether to include windows that are still launching. + /// - Returns: a list of UUIDs. Order is undefined. + func allWindowUUIDs(includingReserved: Bool) -> [WindowUUID] + /// Signals the WindowManager that a window was closed. /// - Parameter uuid: the ID of the window. func windowWillClose(uuid: WindowUUID) @@ -56,6 +63,10 @@ struct AppWindowInfo { } final class WindowManagerImplementation: WindowManager { + enum WindowPrefKeys { + static let windowOrdering = "windowOrdering" + } + private(set) var windows: [WindowUUID: AppWindowInfo] = [:] private var reservedUUIDs: [WindowUUID] = [] var activeWindow: WindowUUID { @@ -64,14 +75,31 @@ final class WindowManagerImplementation: WindowManager { } private let logger: Logger private let tabDataStore: TabDataStore + private let defaults: UserDefaultsInterface private var _activeWindowUUID: WindowUUID? + // Ordered set of UUIDs which determines the order that windows are re-opened on iPad + // UUIDs at the beginning of the list are prioritized over UUIDs at the end + private(set) var windowOrderingPriority: [WindowUUID] { + get { + let stored = defaults.object(forKey: WindowPrefKeys.windowOrdering) + guard let prefs: [String] = stored as? [String] else { return [] } + return prefs.compactMap({ UUID(uuidString: $0) }) + } + set { + let mapped: [String] = newValue.compactMap({ $0.uuidString }) + defaults.set(mapped, forKey: WindowPrefKeys.windowOrdering) + } + } + // MARK: - Initializer init(logger: Logger = DefaultLogger.shared, - tabDataStore: TabDataStore = AppContainer.shared.resolve()) { + tabDataStore: TabDataStore = AppContainer.shared.resolve(), + userDefaults: UserDefaultsInterface = UserDefaults.standard) { self.logger = logger self.tabDataStore = tabDataStore + self.defaults = userDefaults } // MARK: - Public API @@ -97,9 +125,21 @@ final class WindowManagerImplementation: WindowManager { return windows.compactMap { uuid, window in window.tabManager } } + func allWindowUUIDs(includingReserved: Bool) -> [WindowUUID] { + return Array(windows.keys) + (includingReserved ? reservedUUIDs : []) + } + func windowWillClose(uuid: WindowUUID) { postWindowEvent(event: .windowWillClose, windowUUID: uuid) updateWindow(nil, for: uuid) + + // Closed windows are popped off and moved behind any already-open windows in the list + var prefs = windowOrderingPriority + prefs.removeAll(where: { $0 == uuid }) + let openWindows = Array(windows.keys) + let idx = prefs.firstIndex(where: { !openWindows.contains($0) }) + prefs.insert(uuid, at: idx ?? prefs.count) + windowOrderingPriority = prefs } func reserveNextAvailableWindowUUID() -> WindowUUID { @@ -110,15 +150,28 @@ final class WindowManagerImplementation: WindowManager { // • If user has saved windows (tab data), we return the first available UUID // not already associated with an open window. // • If multiple window UUIDs are available, we currently return the first one - // after sorting based on the uuid value. - // TODO: [FXIOS-7929] This ^ is temporary, part of ongoing multi-window work, eventually - // we'll be updating this (to use `isPrimary` on WindowData etc). Forthcoming. + // after sorting based on the order they were last closed (which we track in + // client user defaults). + // • If for some reason the user defaults are unavailable we sort open the + // windows by order of their UUID value. + + // Fetch available window data on disk, and remove any already-opened windows + // or UUIDs that are already reserved and in the process of opening. let openWindowUUIDs = windows.keys - let uuids = tabDataStore.fetchWindowDataUUIDs().filter { + let filteredUUIDs = tabDataStore.fetchWindowDataUUIDs().filter { !openWindowUUIDs.contains($0) && !reservedUUIDs.contains($0) } - let sortedUUIDs = uuids.sorted(by: { return $0.uuidString > $1.uuidString }) - let resultUUID = sortedUUIDs.first ?? WindowUUID() + + let result = nextWindowUUIDToOpen(filteredUUIDs) + let resultUUID = result.uuid + if result.isNew { + // Be sure to add any brand-new windows to our ordering preferences + var prefs = windowOrderingPriority + prefs.insert(resultUUID, at: 0) + windowOrderingPriority = prefs + } + + // Reserve the UUID until the Client finishes the window configuration process reservedUUIDs.append(resultUUID) return resultUUID } @@ -137,6 +190,43 @@ final class WindowManagerImplementation: WindowManager { // MARK: - Internal Utilities + /// When provided a list of UUIDs of available window data files on disk, + /// this function determines which of them should be the next to be + /// opened. This allows multiple windows to be restored in a sensible way. + /// - Parameter onDiskUUIDs: on-disk UUIDs representing windows that are not + /// already open or reserved (this is important - these UUIDs should be pre- + /// filtered). + /// - Returns: the UUID for the next window that will be opened on iPad. + private func nextWindowUUIDToOpen(_ onDiskUUIDs: [WindowUUID]) -> (uuid: WindowUUID, isNew: Bool) { + func nextUUIDUsingFallbackSorting() -> (uuid: WindowUUID, isNew: Bool) { + let sortedUUIDs = onDiskUUIDs.sorted(by: { return $0.uuidString > $1.uuidString }) + if let resultUUID = sortedUUIDs.first { + return (uuid: resultUUID, isNew: false) + } + return (uuid: WindowUUID(), isNew: true) + } + + guard !onDiskUUIDs.isEmpty else { + return (uuid: WindowUUID(), isNew: true) + } + + // Get the ordering preference + let priorityPreference = windowOrderingPriority + guard !priorityPreference.isEmpty else { + // Preferences are empty. Could be initial launch after multi-window release + // or preferences have been cleared. Fallback to default sort. + return nextUUIDUsingFallbackSorting() + } + + // Iterate and return the first UUID that is available within our on-disk UUIDs + // (which excludes windows already open or reserved). + for uuid in priorityPreference where onDiskUUIDs.contains(uuid) { + return (uuid: uuid, isNew: false) + } + + return nextUUIDUsingFallbackSorting() + } + private func updateWindow(_ info: AppWindowInfo?, for uuid: WindowUUID) { windows[uuid] = info didUpdateWindow(uuid) diff --git a/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/Contents.json b/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/Contents.json new file mode 100644 index 000000000000..b4d17a41a835 --- /dev/null +++ b/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "crossCircleFillLarge.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf b/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf new file mode 100644 index 000000000000..bc272de27e63 Binary files /dev/null and b/firefox-ios/Client/Assets/Images.xcassets/crossCircleFillLarge.imageset/crossCircleFillLarge.pdf differ diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index 0b66700cdfd9..0d06420c5242 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -326,7 +326,7 @@ class BrowserCoordinator: BaseCoordinator, return true } - private func handleSettings(with section: Route.SettingsSection) { + private func handleSettings(with section: Route.SettingsSection, onDismiss: (() -> Void)? = nil) { guard !childCoordinators.contains(where: { $0 is SettingsCoordinator }) else { return // route is handled with existing child coordinator } @@ -344,6 +344,7 @@ class BrowserCoordinator: BaseCoordinator, navigationController.onViewDismissed = { [weak self] in self?.didFinishSettings(from: settingsCoordinator) + onDismiss?() } present(navigationController) } @@ -435,9 +436,9 @@ class BrowserCoordinator: BaseCoordinator, // MARK: - BrowserNavigationHandler - func show(settings: Route.SettingsSection) { + func show(settings: Route.SettingsSection, onDismiss: (() -> Void)? = nil) { presentWithModalDismissIfNeeded { - self.handleSettings(with: settings) + self.handleSettings(with: settings, onDismiss: onDismiss) } } diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift index 0866d8f9fe4e..d1fba622cb18 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserNavigationHandler.swift @@ -9,7 +9,9 @@ import WebKit protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler { /// Asks to show a settings page, can be a general settings page or a child page /// - Parameter settings: The settings route we're trying to get to - func show(settings: Route.SettingsSection) + /// - Parameter onDismiss: An optional closure that is executed when the settings page is dismissed. + /// This closure takes no parameters and returns no value. + func show(settings: Route.SettingsSection, onDismiss: (() -> Void)?) /// Asks to show a enhancedTrackingProtection page, can be a general /// enhancedTrackingProtection page or a child page @@ -103,4 +105,8 @@ extension BrowserNavigationHandler { popoverArrowDirection: popoverArrowDirection ) } + + func show(settings: Route.SettingsSection) { + show(settings: settings, onDismiss: nil) + } } diff --git a/firefox-ios/Client/Coordinators/CredentialAutofillCoordinator.swift b/firefox-ios/Client/Coordinators/CredentialAutofillCoordinator.swift index 3f39dab4c451..00ef041d9205 100644 --- a/firefox-ios/Client/Coordinators/CredentialAutofillCoordinator.swift +++ b/firefox-ios/Client/Coordinators/CredentialAutofillCoordinator.swift @@ -148,12 +148,16 @@ class CredentialAutofillCoordinator: BaseCoordinator { ) ) + LoginsHelper.yieldFocusBackToField(with: currentTab) router.dismiss(animated: true) parentCoordinator?.didFinish(from: self) }, manageLoginInfoAction: { [weak self] in guard let self else { return } - parentCoordinator?.show(settings: .password) + parentCoordinator?.show(settings: .password, onDismiss: { + guard let currentTab = self.tabManager.selectedTab else { return } + LoginsHelper.yieldFocusBackToField(with: currentTab) + }) parentCoordinator?.didFinish(from: self) } ) @@ -164,6 +168,11 @@ class CredentialAutofillCoordinator: BaseCoordinator { viewController.controllerWillDismiss = { [weak self] in guard let currentTab = self?.tabManager.selectedTab else { return } LoginsHelper.yieldFocusBackToField(with: currentTab) + TelemetryWrapper.recordEvent( + category: .action, + method: .close, + object: .loginsAutofillPromptDismissed + ) } var bottomSheetViewModel = BottomSheetViewModel(closeButtonA11yLabel: .CloseButtonTitle) @@ -174,6 +183,11 @@ class CredentialAutofillCoordinator: BaseCoordinator { childViewController: viewController ) router.present(bottomSheetVC) + TelemetryWrapper.recordEvent( + category: .action, + method: .tap, + object: .loginsAutofillPromptExpanded + ) } func showPassCodeController() { diff --git a/firefox-ios/Client/Experiments/initial_experiments.json b/firefox-ios/Client/Experiments/initial_experiments.json index 0bda5a80e80a..6615c1a3db96 100644 --- a/firefox-ios/Client/Experiments/initial_experiments.json +++ b/firefox-ios/Client/Experiments/initial_experiments.json @@ -1,5 +1,364 @@ { "data": [ + { + "schemaVersion": "1.12.0", + "slug": "app-customization-in-onboarding-ios", + "id": "app-customization-in-onboarding-ios", + "arguments": {}, + "application": "org.mozilla.ios.Firefox", + "appName": "firefox_ios", + "appId": "org.mozilla.ios.Firefox", + "channel": "release", + "userFacingName": "App Customization in Onboarding (iOS)", + "userFacingDescription": "Include App Customization options in IOS Onboarding", + "isEnrollmentPaused": false, + "isRollout": false, + "bucketConfig": { + "randomizationUnit": "nimbus_id", + "namespace": "ios-onboarding-framework-feature-release-12", + "start": 0, + "count": 10000, + "total": 10000 + }, + "featureIds": [ + "onboarding-framework-feature" + ], + "probeSets": [], + "outcomes": [ + { + "slug": "onboarding", + "priority": "primary" + }, + { + "slug": "default_browser", + "priority": "secondary" + } + ], + "branches": [ + { + "slug": "control", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "onboarding-framework-feature", + "enabled": true, + "value": {} + } + ] + }, + { + "slug": "treatment-a", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "onboarding-framework-feature", + "enabled": true, + "value": { + "cards": { + "customization-theme": { + "card-type": "multiple-choice", + "order": 40, + "title": "Onboarding/Onboarding.Customization.Theme.Title.v123", + "body": "Onboarding/Onboarding.Customization.Theme.Description.v123", + "image": "themeing", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Theme.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Theme.System.Action.v123", + "image": "theme-system", + "action": "theme-system-default" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Light.Action.v123", + "image": "theme-light", + "action": "theme-light" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Dark.Action.v123", + "image": "theme-dark", + "action": "theme-dark" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + }, + "customization-toolbar": { + "card-type": "multiple-choice", + "order": 41, + "title": "Onboarding/Onboarding.Customization.Toolbar.Title.v123", + "body": "Onboarding/Onboarding.Customization.Toolbar.Description.v123", + "image": "toolbar", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Toolbar.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Top.Action.v123", + "image": "toolbar-top", + "action": "toolbar-top" + }, + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Bottom.Action.v123", + "image": "toolbar-bottom", + "action": "toolbar-bottom" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + } + } + } + } + ] + }, + { + "slug": "treatment-b", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "onboarding-framework-feature", + "enabled": true, + "value": { + "cards": { + "customization-intro": { + "card-type": "basic", + "order": 41, + "title": "Onboarding/Onboarding.Customization.Intro.Title.v123", + "body": "Onboarding/Onboarding.Customization.Intro.Description.v123", + "image": "themeing", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Intro.Continue.Action.v123", + "action": "forward-one-card" + }, + "secondary": { + "title": "Onboarding/Onboarding.Customization.Intro.Skip.Action.v123", + "action": "end-onboarding" + } + }, + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + }, + "customization-theme": { + "card-type": "multiple-choice", + "order": 42, + "title": "Onboarding/Onboarding.Customization.Theme.Title.v123", + "body": "Onboarding/Onboarding.Customization.Theme.Description.v123", + "image": "themeing", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Theme.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Theme.System.Action.v123", + "image": "theme-system", + "action": "theme-system-default" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Light.Action.v123", + "image": "theme-light", + "action": "theme-light" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Dark.Action.v123", + "image": "theme-dark", + "action": "theme-dark" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + }, + "customization-toolbar": { + "card-type": "multiple-choice", + "order": 43, + "title": "Onboarding/Onboarding.Customization.Toolbar.Title.v123", + "body": "Onboarding/Onboarding.Customization.Toolbar.Description.v123", + "image": "toolbar", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Toolbar.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Top.Action.v123", + "image": "toolbar-top", + "action": "toolbar-top" + }, + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Bottom.Action.v123", + "image": "toolbar-bottom", + "action": "toolbar-bottom" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + } + } + } + } + ] + }, + { + "slug": "treatment-c", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "onboarding-framework-feature", + "enabled": true, + "value": { + "cards": { + "welcome": { + "link": null + }, + "customization-intro": { + "card-type": "basic", + "order": 1, + "title": "Onboarding/Onboarding.Customization.Intro.Title.v123", + "body": "Onboarding/Onboarding.Customization.Intro.Description.v123", + "image": "toolbar", + "link": { + "title": "Onboarding/Onboarding.Welcome.Link.Action.v114", + "url": "https://www.mozilla.org/privacy/firefox/" + }, + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Intro.Continue.Action.v123", + "action": "forward-one-card" + }, + "secondary": { + "title": "Onboarding/Onboarding.Customization.Theme.Skip.Action.v123", + "action": "forward-three-card" + } + }, + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + }, + "customization-theme": { + "card-type": "multiple-choice", + "order": 2, + "title": "Onboarding/Onboarding.Customization.Theme.Title.v123", + "body": "Onboarding/Onboarding.Customization.Theme.Description.v123", + "image": "themeing", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Theme.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Theme.System.Action.v123", + "image": "theme-system", + "action": "theme-system-default" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Light.Action.v123", + "image": "theme-light", + "action": "theme-light" + }, + { + "title": "Onboarding/Onboarding.Customization.Theme.Dark.Action.v123", + "image": "theme-dark", + "action": "theme-dark" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + }, + "customization-toolbar": { + "card-type": "multiple-choice", + "order": 3, + "title": "Onboarding/Onboarding.Customization.Toolbar.Title.v123", + "body": "Onboarding/Onboarding.Customization.Toolbar.Description.v123", + "image": "toolbar", + "buttons": { + "primary": { + "title": "Onboarding/Onboarding.Customization.Theme.Continue.Action.v123", + "action": "forward-one-card" + } + }, + "multiple-choice-buttons": [ + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Top.Action.v123", + "image": "toolbar-top", + "action": "toolbar-top" + }, + { + "title": "Onboarding/Onboarding.Customization.Toolbar.Bottom.Action.v123", + "image": "toolbar-bottom", + "action": "toolbar-bottom" + } + ], + "onboarding-type": "fresh-install", + "prerequisites": [ + "ALWAYS" + ] + } + } + } + } + ] + } + ], + "targeting": "((is_already_enrolled) || ((isFirstRun == 'true' && is_phone) && (app_version|versionCompare('125.2.0') >= 0)))", + "startDate": "2024-04-19", + "enrollmentEndDate": null, + "endDate": null, + "proposedDuration": 28, + "proposedEnrollment": 7, + "referenceBranch": "control", + "featureValidationOptOut": false, + "localizations": null, + "locales": null, + "publishedDate": "2024-04-19T19:58:53.497219Z" + }, { "schemaVersion": "1.12.0", "slug": "ios-dma-onboarding", @@ -139,6 +498,111 @@ "localizations": null, "locales": null, "publishedDate": null + }, + { + "schemaVersion": "1.12.0", + "slug": "ios-splash-screen-timing-test-release", + "id": "ios-splash-screen-timing-test-release", + "arguments": {}, + "application": "org.mozilla.ios.Firefox", + "appName": "firefox_ios", + "appId": "org.mozilla.ios.Firefox", + "channel": "release", + "userFacingName": "iOS Splash Screen Timing Test Release", + "userFacingDescription": "Adds a splash screen prior to first-run onboarding and gauges the impact to retention.", + "isEnrollmentPaused": false, + "isRollout": false, + "bucketConfig": { + "randomizationUnit": "nimbus_id", + "namespace": "ios-splash-screen-release-2", + "start": 0, + "count": 10000, + "total": 10000 + }, + "featureIds": [ + "splash-screen" + ], + "probeSets": [], + "outcomes": [ + { + "slug": "onboarding", + "priority": "primary" + }, + { + "slug": "default_browser", + "priority": "secondary" + } + ], + "branches": [ + { + "slug": "control", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "splash-screen", + "enabled": true, + "value": { + "enabled": true, + "maximum_duration_ms": 0 + } + } + ] + }, + { + "slug": "treatment-a", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "splash-screen", + "enabled": true, + "value": { + "enabled": true, + "maximum_duration_ms": 1500 + } + } + ] + }, + { + "slug": "treatment-b", + "ratio": 1, + "feature": { + "featureId": "this-is-included-for-mobile-pre-96-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "splash-screen", + "enabled": true, + "value": { + "enabled": true, + "maximum_duration_ms": 2000 + } + } + ] + } + ], + "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('125.2.0') >= 0)))", + "startDate": "2024-04-19", + "enrollmentEndDate": null, + "endDate": null, + "proposedDuration": 28, + "proposedEnrollment": 14, + "referenceBranch": "control", + "featureValidationOptOut": false, + "localizations": null, + "locales": null, + "publishedDate": "2024-04-19T18:09:09.240697Z" } ] } diff --git a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift index 01b3d65d8294..0479e38c364c 100644 --- a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift +++ b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift @@ -26,6 +26,7 @@ enum NimbusFeatureFlagID: String, CaseIterable { case isToolbarCFREnabled case jumpBackIn case loginAutofill + case microSurvey case nightMode case preferSwitchToOpenTabOverDuplicate case reduxSearchSettings @@ -74,6 +75,7 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar { .fakespotProductAds, .isToolbarCFREnabled, .loginAutofill, + .microSurvey, .nightMode, .preferSwitchToOpenTabOverDuplicate, .reduxSearchSettings, diff --git a/firefox-ios/Client/Frontend/Browser/Authenticator.swift b/firefox-ios/Client/Frontend/Browser/Authenticator.swift index e0d7ef1990c9..67e9ef106a3e 100644 --- a/firefox-ios/Client/Frontend/Browser/Authenticator.swift +++ b/firefox-ios/Client/Frontend/Browser/Authenticator.swift @@ -13,11 +13,13 @@ class Authenticator { static func handleAuthRequest( _ viewController: UIViewController, challenge: URLAuthenticationChallenge, - loginsHelper: LoginsHelper? - ) -> Deferred> { + loginsHelper: LoginsHelper?, + completionHandler: @escaping ((Result) -> Void) + ) { // If there have already been too many login attempts, we'll just fail. if challenge.previousFailureCount >= Authenticator.MaxAuthenticationAttempts { - return deferMaybe(LoginRecordError(description: "Too many attempts to open site")) + completionHandler(.failure(LoginRecordError(description: "Too many attempts to open site"))) + return } var credential = challenge.proposedCredential @@ -26,7 +28,10 @@ class Authenticator { if let proposed = credential { if !(proposed.user?.isEmpty ?? true) { if challenge.previousFailureCount == 0 { - return deferMaybe(LoginEntry(credentials: proposed, protectionSpace: challenge.protectionSpace)) + completionHandler(.success( + LoginEntry(credentials: proposed, protectionSpace: challenge.protectionSpace) + )) + return } } else { credential = nil @@ -35,96 +40,121 @@ class Authenticator { // If we have some credentials, we'll show a prompt with them. if let credential = credential { - return promptForUsernamePassword( + promptForUsernamePassword( viewController, credentials: credential, protectionSpace: challenge.protectionSpace, - loginsHelper: loginsHelper + loginsHelper: loginsHelper, + completionHandler: completionHandler ) + return } // Otherwise, try to look them up and show the prompt. if let loginsHelper = loginsHelper { - return findMatchingCredentialsForChallenge( + findMatchingCredentialsForChallenge( challenge, fromLoginsProvider: loginsHelper.logins - ).bindQueue(.main) { result in - guard let credentials = result.successValue else { - sendLoginsAutofillFailedTelemetry() - return deferMaybe( - result.failureValue ?? LoginRecordError(description: "Unknown error when finding credentials") + ) { credentials in + DispatchQueue.main.async { + guard let credentials = credentials else { + sendLoginsAutofillFailedTelemetry() + completionHandler(.failure( + LoginRecordError(description: "Unknown error when finding credentials") + )) + return + } + self.promptForUsernamePassword( + viewController, + credentials: credentials, + protectionSpace: challenge.protectionSpace, + loginsHelper: loginsHelper, + completionHandler: completionHandler ) } - return self.promptForUsernamePassword( - viewController, - credentials: credentials, - protectionSpace: challenge.protectionSpace, - loginsHelper: loginsHelper - ) } + return } // No credentials, so show an empty prompt. - return self.promptForUsernamePassword( + self.promptForUsernamePassword( viewController, credentials: nil, protectionSpace: challenge.protectionSpace, - loginsHelper: nil + loginsHelper: nil, + completionHandler: completionHandler ) } static func findMatchingCredentialsForChallenge( _ challenge: URLAuthenticationChallenge, fromLoginsProvider loginsProvider: RustLogins, - logger: Logger = DefaultLogger.shared - ) -> Deferred> { - return loginsProvider.getLoginsForProtectionSpace(challenge.protectionSpace) >>== { cursor in - guard cursor.count >= 1 else { return deferMaybe(nil) } - - let logins = cursor.compactMap { - // HTTP Auth must have nil formSubmitUrl and a non-nil httpRealm. - return $0?.formSubmitUrl == nil && $0?.httpRealm != nil ? $0 : nil - } - var credentials: URLCredential? - - // It is possible that we might have duplicate entries since we match against host and scheme://host. - // This is a side effect of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103. - if logins.count > 1 { - credentials = (logins.first(where: { login in - (login.protectionSpace.`protocol` == challenge.protectionSpace.`protocol`) - && !login.hasMalformedHostname - }))?.credentials - - let malformedGUIDs: [GUID] = logins.compactMap { login in - if login.hasMalformedHostname { - return login.id + logger: Logger = DefaultLogger.shared, + completionHandler: @escaping (URLCredential?) -> Void + ) { + loginsProvider.getLoginsFor(protectionSpace: challenge.protectionSpace, withUsername: nil) { result in + switch result { + case .success(let logins): + guard logins.count >= 1 else { + completionHandler(nil) + return + } + + let logins = logins.compactMap { + // HTTP Auth must have nil formSubmitUrl and a non-nil httpRealm. + return $0.formSubmitUrl == nil && $0.httpRealm != nil ? $0 : nil + } + var credentials: URLCredential? + + // It is possible that we might have duplicate entries since we match against host and scheme://host. + // This is a side effect of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103. + if logins.count > 1 { + credentials = (logins.first(where: { login in + (login.protectionSpace.`protocol` == challenge.protectionSpace.`protocol`) + && !login.hasMalformedHostname + }))?.credentials + + let malformedGUIDs: [GUID] = logins.compactMap { login in + if login.hasMalformedHostname { + return login.id + } + return nil } - return nil + loginsProvider.deleteLogins(ids: malformedGUIDs) { _ in } } - loginsProvider.deleteLogins(ids: malformedGUIDs).upon { _ in } - } - // Found a single entry but the schemes don't match. This is a result of a schemeless entry that we - // saved in a previous iteration of the app so we need to migrate it. We only care about the - // the username/password so we can rewrite the scheme to be correct. - else if logins.count == 1 && logins[0].protectionSpace.`protocol` != challenge.protectionSpace.`protocol` { - let login = logins[0] - credentials = login.credentials - let new = LoginEntry(credentials: login.credentials, protectionSpace: challenge.protectionSpace) - return loginsProvider.updateLogin(id: login.id, login: new) - >>> { deferMaybe(credentials) } - } + // Found a single entry but the schemes don't match. This is a result of a schemeless entry that we + // saved in a previous iteration of the app so we need to migrate it. We only care about the + // the username/password so we can rewrite the scheme to be correct. + else if logins.count == 1 && logins[0].protectionSpace.`protocol` != challenge.protectionSpace.`protocol` { + let login = logins[0] + credentials = login.credentials + let new = LoginEntry(credentials: login.credentials, protectionSpace: challenge.protectionSpace) + loginsProvider.updateLogin(id: login.id, login: new) { result in + switch result { + case .success: + completionHandler(credentials) + case .failure: + completionHandler(nil) + } + } + return + } - // Found a single entry that matches the scheme and host - good to go. - else if logins.count == 1 { - credentials = logins[0].credentials - } else { - logger.log("No logins found for Authenticator", - level: .info, - category: .webview) - } + // Found a single entry that matches the scheme and host - good to go. + else if logins.count == 1 { + credentials = logins[0].credentials + } else { + logger.log("No logins found for Authenticator", + level: .info, + category: .webview) + } + + completionHandler(credentials) - return deferMaybe(credentials) + case .failure: + completionHandler(nil) + } } } @@ -133,14 +163,15 @@ class Authenticator { credentials: URLCredential?, protectionSpace: URLProtectionSpace, loginsHelper: LoginsHelper?, - logger: Logger = DefaultLogger.shared - ) -> Deferred> { + logger: Logger = DefaultLogger.shared, + completionHandler: @escaping ((Result) -> Void) + ) { if protectionSpace.host.isEmpty { logger.log("Unable to show a password prompt without a hostname", level: .warning, category: .sync) - return deferMaybe(LoginRecordError(description: "Unable to show a password prompt without a hostname")) + completionHandler(.failure(LoginRecordError(description: "Unable to show a password prompt without a hostname"))) + return } - let deferred = Deferred>() let alert: AlertController let title: String = .AuthenticatorPromptTitle if !(protectionSpace.realm?.isEmpty ?? true) { @@ -165,7 +196,7 @@ class Authenticator { guard let user = alert.textFields?[0].text, let pass = alert.textFields?[1].text else { - deferred.fill(Maybe(failure: LoginRecordError(description: "Username and Password required"))) + completionHandler(.failure(LoginRecordError(description: "Username and Password required"))) return } @@ -174,14 +205,14 @@ class Authenticator { protectionSpace: protectionSpace ) self.sendLoginsAutofilledTelemetry() - deferred.fill(Maybe(success: login)) + completionHandler(.success(login)) loginsHelper?.setCredentials(login) } alert.addAction(action, accessibilityIdentifier: "authenticationAlert.loginRequired") // Add a cancel button. let cancel = UIAlertAction(title: .AuthenticatorCancel, style: .cancel) { (action) -> Void in - deferred.fill(Maybe(failure: LoginRecordError(description: "Save password cancelled"))) + completionHandler(.failure(LoginRecordError(description: "Save password cancelled"))) } alert.addAction(cancel, accessibilityIdentifier: "authenticationAlert.cancel") @@ -199,7 +230,6 @@ class Authenticator { } viewController.present(alert, animated: true) { () -> Void in } - return deferred } // MARK: Telemetry diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift index 3fda7411a2e6..9c24d5712419 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift @@ -862,11 +862,14 @@ extension BrowserViewController: WKNavigationDelegate { self, challenge: challenge, loginsHelper: loginsHelper - ).uponQueue(.main) { res in - if let credentials = res.successValue { - completionHandler(.useCredential, credentials.credentials) - } else { - completionHandler(.rejectProtectionSpace, nil) + ) { res in + DispatchQueue.main.async { + switch res { + case .success(let credentials): + completionHandler(.useCredential, credentials.credentials) + case .failure: + completionHandler(.rejectProtectionSpace, nil) + } } } } @@ -908,6 +911,8 @@ extension BrowserViewController: WKNavigationDelegate { tab.contentBlocker?.notifyContentBlockingChanged() self.scrollController.resetZoomState() + scrollController.shouldScrollToTop = true + if tabManager.selectedTab === tab { updateUIForReaderHomeStateForTab(tab, focusUrlBar: true) updateFakespot(tab: tab, isReload: true) @@ -1043,19 +1048,25 @@ private extension BrowserViewController { func handleServerTrust(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { - // If this is a certificate challenge, see if the certificate has previously been - // accepted by the user. - let origin = "\(challenge.protectionSpace.host):\(challenge.protectionSpace.port)" + DispatchQueue.global(qos: .userInitiated).async { + // If this is a certificate challenge, see if the certificate has previously been + // accepted by the user. + let origin = "\(challenge.protectionSpace.host):\(challenge.protectionSpace.port)" + + guard let trust = challenge.protectionSpace.serverTrust, + let cert = SecTrustCopyCertificateChain(trust) as? [SecCertificate], + self.profile.certStore.containsCertificate(cert[0], forOrigin: origin) + else { + DispatchQueue.main.async { + completionHandler(.performDefaultHandling, nil) + } + return + } - guard let trust = challenge.protectionSpace.serverTrust, - let cert = SecTrustCopyCertificateChain(trust) as? [SecCertificate], - profile.certStore.containsCertificate(cert[0], forOrigin: origin) - else { - completionHandler(.performDefaultHandling, nil) - return + DispatchQueue.main.async { + completionHandler(.useCredential, URLCredential(trust: trust)) + } } - - completionHandler(.useCredential, URLCredential(trust: trust)) } func updateObservationReferral(metadataManager: LegacyTabMetadataManager, url: String?, isPrivate: Bool) { diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 08cfadb953cb..ec4ccba0c3f2 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -73,6 +73,7 @@ class BrowserViewController: UIViewController, var searchLoader: SearchLoader? var findInPageBar: FindInPageBar? var zoomPageBar: ZoomPageBar? + var microSurvey: MicroSurveyPromptView? lazy var mailtoLinkHandler = MailtoLinkHandler() var urlFromAnotherApp: UrlToOpenModel? var isCrashAlertShowing = false @@ -83,7 +84,6 @@ class BrowserViewController: UIViewController, var toolbarContextHintVC: ContextualHintViewController var dataClearanceContextHintVC: ContextualHintViewController let shoppingContextHintVC: ContextualHintViewController - private var backgroundTabLoader: DefaultBackgroundTabLoader var windowUUID: WindowUUID { return tabManager.windowUUID } var currentWindowUUID: UUID? { return windowUUID } private var observedWebViews = WeakList() @@ -241,7 +241,6 @@ class BrowserViewController: UIViewController, ) self.dataClearanceContextHintVC = ContextualHintViewController(with: dataClearanceViewProvider, windowUUID: windowUUID) - self.backgroundTabLoader = DefaultBackgroundTabLoader(tabQueue: profile.queue) super.init(nibName: nil, bundle: nil) didInit() } @@ -331,6 +330,7 @@ class BrowserViewController: UIViewController, updateHeaderConstraints() toolbar.setNeedsDisplay() urlBar.updateConstraints() + updateMicroSurveyConstraints() } func shouldShowToolbarForTraitCollection(_ previousTraitCollection: UITraitCollection) -> Bool { @@ -495,14 +495,7 @@ class BrowserViewController: UIViewController, urlBar.locationView.tabDidChangeContentBlocking(tab) } - updateWallpaperMetadata() dismissModalsIfStartAtHome() - - // When, for example, you "Load in Background" via the share sheet, the tab is added to `Profile`'s `TabQueue`. - // Make sure that our startup flow is completed and other tabs have been restored before we load. - AppEventQueue.wait(for: [.startupFlowComplete, .tabRestoration(tabManager.windowUUID)]) { [weak self] in - self?.backgroundTabLoader.loadBackgroundTabs() - } } private func nightModeUpdates() { @@ -783,6 +776,8 @@ class BrowserViewController: UIViewController, browserDelegate?.browserHasLoaded() AppEventQueue.signal(event: .browserIsReady) + + setupMicroSurvey() } private func prepareURLOnboardingContextualHint() { @@ -1062,14 +1057,6 @@ class BrowserViewController: UIViewController, } } - private func updateWallpaperMetadata() { - let metadataQueue = DispatchQueue(label: "com.moz.wallpaperVerification.queue") - metadataQueue.async { - let wallpaperManager = WallpaperManager() - wallpaperManager.checkForUpdates() - } - } - func resetBrowserChrome() { // animate and reset transform for tab chrome urlBar.updateAlphaForSubviews(1) @@ -1147,6 +1134,62 @@ class BrowserViewController: UIViewController, browserDelegate?.show(webView: webView) } + // MARK: - Micro Survey + private func setupMicroSurvey() { + guard featureFlags.isFeatureEnabled(.microSurvey, checking: .buildOnly) else { return } + + // TODO: FXIOS-8990: Create Micro Survey Surface Manager to handle showing survey prompt + if microSurvey != nil { + removeMicroSurveyPrompt() + } + + createMicroSurveyPrompt() + } + + private func updateMicroSurveyConstraints() { + guard let microSurvey else { return } + + microSurvey.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(microSurvey) + + if urlBar.isBottomSearchBar { + overKeyboardContainer.addArrangedViewToTop(microSurvey, animated: false, completion: { + self.view.layoutIfNeeded() + }) + } else { + bottomContainer.addArrangedViewToTop(microSurvey, animated: false, completion: { + self.view.layoutIfNeeded() + }) + } + + microSurvey.applyTheme(theme: themeManager.currentTheme(for: windowUUID)) + + updateViewConstraints() + } + + private func createMicroSurveyPrompt() { + let viewModel = MicroSurveyViewModel(openAction: { + // TODO: FXIOS-8895: Create Micro Survey Modal View + }) { + // TODO: FXIOS-8898: Setup Redux to handle open and dismissing modal + } + + self.microSurvey = MicroSurveyPromptView(viewModel: viewModel) + + updateMicroSurveyConstraints() + } + + private func removeMicroSurveyPrompt() { + guard let microSurvey else { return } + if urlBar.isBottomSearchBar { + overKeyboardContainer.removeArrangedView(microSurvey) + } else { + bottomContainer.removeArrangedView(microSurvey) + } + + self.microSurvey = nil + updateViewConstraints() + } // MARK: - Update content func updateContentInHomePanel(_ browserViewType: BrowserViewType) { @@ -2311,6 +2354,11 @@ extension BrowserViewController: LegacyTabDelegate { if !loginsForCurrentTab.isEmpty { tab?.webView?.accessoryView.reloadViewFor(.login) tab?.webView?.reloadInputViews() + TelemetryWrapper.recordEvent( + category: .action, + method: .view, + object: .loginsAutofillPromptShown + ) } tab?.webView?.accessoryView.savedLoginsClosure = { Task { @MainActor [weak self] in diff --git a/firefox-ios/Client/Frontend/Browser/ButtonToast.swift b/firefox-ios/Client/Frontend/Browser/ButtonToast.swift index 3b479d2f8975..e3600aa9fa0d 100644 --- a/firefox-ios/Client/Frontend/Browser/ButtonToast.swift +++ b/firefox-ios/Client/Frontend/Browser/ButtonToast.swift @@ -21,8 +21,6 @@ class ButtonToast: Toast { static let buttonPadding: CGFloat = 10 static let buttonBorderRadius: CGFloat = 5 static let buttonBorderWidth: CGFloat = 1 - static let titleFontSize: CGFloat = 15 - static let descriptionFontSize: CGFloat = 13 static let widthOffset: CGFloat = 20 } @@ -41,22 +39,19 @@ class ButtonToast: Toast { } private var titleLabel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .body, - size: UX.titleFontSize) + label.font = FXFontStyles.Bold.subheadline.scaledFont() label.numberOfLines = 0 } private var descriptionLabel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .body, - size: UX.descriptionFontSize) + label.font = FXFontStyles.Bold.footnote.scaledFont() label.numberOfLines = 0 } private var roundedButton: UIButton = .build { button in button.layer.cornerRadius = UX.buttonBorderRadius button.layer.borderWidth = UX.buttonBorderWidth - button.titleLabel?.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .body, - size: Toast.UX.fontSize) + button.titleLabel?.font = FXFontStyles.Regular.subheadline.scaledFont() button.titleLabel?.numberOfLines = 1 button.titleLabel?.lineBreakMode = .byClipping button.titleLabel?.adjustsFontSizeToFitWidth = true diff --git a/firefox-ios/Client/Frontend/Browser/DownloadToast.swift b/firefox-ios/Client/Frontend/Browser/DownloadToast.swift index 00d3b3d39352..0e841d5ec494 100644 --- a/firefox-ios/Client/Frontend/Browser/DownloadToast.swift +++ b/firefox-ios/Client/Frontend/Browser/DownloadToast.swift @@ -31,14 +31,12 @@ class DownloadToast: Toast { } private var titleLabel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .body, - size: ButtonToast.UX.titleFontSize) + label.font = FXFontStyles.Bold.subheadline.scaledFont() label.numberOfLines = 0 } private var descriptionLabel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .body, - size: ButtonToast.UX.descriptionFontSize) + label.font = FXFontStyles.Bold.footnote.scaledFont() label.numberOfLines = 0 } diff --git a/firefox-ios/Client/Frontend/Browser/FindInPageBar.swift b/firefox-ios/Client/Frontend/Browser/FindInPageBar.swift index 491ff716c27b..c8467e820d94 100644 --- a/firefox-ios/Client/Frontend/Browser/FindInPageBar.swift +++ b/firefox-ios/Client/Frontend/Browser/FindInPageBar.swift @@ -14,10 +14,6 @@ protocol FindInPageBarDelegate: AnyObject { } class FindInPageBar: UIView, ThemeApplicable { - private struct UX { - static let fontSize: CGFloat = 16 - } - private static let savedTextKey = "findInPageSavedTextKey" weak var delegate: FindInPageBarDelegate? @@ -26,7 +22,7 @@ class FindInPageBar: UIView, ThemeApplicable { private lazy var searchText: UITextField = .build { textField in textField.addTarget(self, action: #selector(self.didTextChange), for: .editingChanged) - textField.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .callout, size: UX.fontSize) + textField.font = FXFontStyles.Regular.callout.scaledFont() textField.setContentHuggingPriority(.defaultLow, for: .horizontal) textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) textField.adjustsFontForContentSizeCategory = true @@ -41,7 +37,7 @@ class FindInPageBar: UIView, ThemeApplicable { } private lazy var matchCountView: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .callout, size: UX.fontSize) + label.font = FXFontStyles.Regular.callout.scaledFont() label.isHidden = true label.accessibilityIdentifier = "FindInPage.matchCount" label.setContentHuggingPriority(.defaultHigh, for: .horizontal) diff --git a/firefox-ios/Client/Frontend/Browser/MetadataParserHelper.swift b/firefox-ios/Client/Frontend/Browser/MetadataParserHelper.swift index 0675e993d348..348dc41b65c0 100644 --- a/firefox-ios/Client/Frontend/Browser/MetadataParserHelper.swift +++ b/firefox-ios/Client/Frontend/Browser/MetadataParserHelper.swift @@ -8,6 +8,8 @@ import Storage import WebKit class MetadataParserHelper: TabEventHandler { + let tabEventWindowResponseType: TabEventHandlerWindowResponseType = .allWindows + init() { register(self, forTabEvents: .didChangeURL) } diff --git a/firefox-ios/Client/Frontend/Browser/SimpleToast.swift b/firefox-ios/Client/Frontend/Browser/SimpleToast.swift index 9da36cd520a0..0aa6ad29b1e7 100644 --- a/firefox-ios/Client/Frontend/Browser/SimpleToast.swift +++ b/firefox-ios/Client/Frontend/Browser/SimpleToast.swift @@ -8,8 +8,7 @@ import Shared struct SimpleToast: ThemeApplicable { private let toastLabel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .body, - size: Toast.UX.fontSize) + label.font = FXFontStyles.Bold.subheadline.scaledFont() label.numberOfLines = 0 label.textAlignment = .center } diff --git a/firefox-ios/Client/Frontend/Browser/TabDisplayManager.swift b/firefox-ios/Client/Frontend/Browser/TabDisplayManager.swift index a75cc3617e1b..68070e687fb5 100644 --- a/firefox-ios/Client/Frontend/Browser/TabDisplayManager.swift +++ b/firefox-ios/Client/Frontend/Browser/TabDisplayManager.swift @@ -71,6 +71,7 @@ class LegacyTabDisplayManager: NSObject, FeatureFlaggable { var operations = [(TabAnimationType, (() -> Void))]() var refreshStoreOperation: (() -> Void)? var tabDisplayType: TabDisplayType = .TabGrid + var windowUUID: WindowUUID { return tabManager.windowUUID } private let tabManager: TabManager private let collectionView: UICollectionView @@ -859,6 +860,8 @@ extension LegacyTabDisplayManager: UICollectionViewDropDelegate { } extension LegacyTabDisplayManager: TabEventHandler { + var tabEventWindowResponseType: TabEventHandlerWindowResponseType { return .singleWindow(windowUUID) } + func tabDidSetScreenshot(_ tab: Tab, hasHomeScreenshot: Bool) { guard let indexPath = getIndexPath(tab: tab) else { return } refreshCell(atIndexPath: indexPath) diff --git a/firefox-ios/Client/Frontend/Browser/TabScrollController.swift b/firefox-ios/Client/Frontend/Browser/TabScrollController.swift index d3c2763fbdbd..3af03b63a2cc 100644 --- a/firefox-ios/Client/Frontend/Browser/TabScrollController.swift +++ b/firefox-ios/Client/Frontend/Browser/TabScrollController.swift @@ -60,6 +60,12 @@ class TabScrollingController: NSObject, FeatureFlaggable, SearchBarLocationProvi return isBottomSearchBar ? bottomShowing : headerTopOffset == 0 } + private var shouldSetInitialScrollToTop: Bool { + return tab?.mimeType == MIMEType.PDF && shouldScrollToTop + } + + var shouldScrollToTop = false + private var isZoomedOut = false private var lastZoomedScale: CGFloat = 0 private var isUserZoom = false @@ -432,6 +438,13 @@ private extension TabScrollingController { } return 1 - abs(headerTopOffset / topScrollHeight) } + + private func setOffset(y: CGFloat, for scrollView: UIScrollView) { + scrollView.contentOffset = CGPoint( + x: contentOffsetBeforeAnimation.x, + y: y + ) + } } extension TabScrollingController: UIGestureRecognizerDelegate { @@ -445,6 +458,8 @@ extension TabScrollingController: UIScrollViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !tabIsLoading(), !isBouncingAtBottom(), isAbleToScroll else { return } + shouldScrollToTop = false + if decelerate || (toolbarState == .animating && !decelerate) { if scrollDirection == .up { showToolbars(animated: true) @@ -457,12 +472,14 @@ extension TabScrollingController: UIScrollViewDelegate { // checking if an abrupt scroll event was triggered and adjusting the offset to the one // before the WKWebView's contentOffset is reset as a result of the contentView's frame becoming smaller func scrollViewDidScroll(_ scrollView: UIScrollView) { + // for PDFs, we should set the initial offset to 0 (ZERO) + if shouldSetInitialScrollToTop { + setOffset(y: 0, for: scrollView) + } + guard isAnimatingToolbar else { return } if contentOffsetBeforeAnimation.y - scrollView.contentOffset.y > UX.abruptScrollEventOffset { - scrollView.contentOffset = CGPoint( - x: contentOffsetBeforeAnimation.x, - y: contentOffsetBeforeAnimation.y + self.topScrollHeight - ) + setOffset(y: contentOffsetBeforeAnimation.y + self.topScrollHeight, for: scrollView) contentOffsetBeforeAnimation.y = 0 } } diff --git a/firefox-ios/Client/Frontend/Browser/Toast.swift b/firefox-ios/Client/Frontend/Browser/Toast.swift index 32fa8db21d91..91fddff8c42c 100644 --- a/firefox-ios/Client/Frontend/Browser/Toast.swift +++ b/firefox-ios/Client/Frontend/Browser/Toast.swift @@ -14,7 +14,6 @@ class Toast: UIView, ThemeApplicable { static let toastDelayBefore = DispatchTimeInterval.milliseconds(0) // 0 seconds static let toastPrivateModeDelayBefore = DispatchTimeInterval.milliseconds(750) static let toastAnimationDuration = 0.5 - static let fontSize: CGFloat = 15 } var animationConstraint: NSLayoutConstraint? diff --git a/firefox-ios/Client/Frontend/Browser/ZoomPageBar.swift b/firefox-ios/Client/Frontend/Browser/ZoomPageBar.swift index dd045be0f1fa..d6908aba5d8f 100644 --- a/firefox-ios/Client/Frontend/Browser/ZoomPageBar.swift +++ b/firefox-ios/Client/Frontend/Browser/ZoomPageBar.swift @@ -13,7 +13,7 @@ protocol ZoomPageBarDelegate: AnyObject { func didChangeZoomLevel() } -class ZoomPageBar: UIView, ThemeApplicable, AlphaDimmable { +final class ZoomPageBar: UIView, ThemeApplicable, AlphaDimmable { // MARK: - Constants private struct UX { @@ -31,7 +31,6 @@ class ZoomPageBar: UIView, ThemeApplicable, AlphaDimmable { static let stepperShadowOffset = CGSize(width: 0, height: 4) static let separatorWidth: CGFloat = 1 static let separatorHeightMultiplier = 0.74 - static let fontSize: CGFloat = 16 static let lowerZoomLimit: CGFloat = 0.5 static let upperZoomLimit: CGFloat = 2.0 } @@ -74,9 +73,7 @@ class ZoomPageBar: UIView, ThemeApplicable, AlphaDimmable { } private lazy var zoomLevel: UILabel = .build { label in - label.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .callout, - size: UX.fontSize, - weight: .semibold) + label.font = FXFontStyles.Regular.body.scaledFont() label.accessibilityIdentifier = AccessibilityIdentifiers.ZoomPageBar.zoomPageZoomLevelLabel label.isUserInteractionEnabled = true label.adjustsFontForContentSizeCategory = true diff --git a/firefox-ios/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift b/firefox-ios/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift index 638cbd76e9c3..861172867a90 100644 --- a/firefox-ios/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift +++ b/firefox-ios/Client/Frontend/Fakespot/FakespotOptInCardViewModel.swift @@ -46,7 +46,7 @@ struct FakespotOptInCardViewModel { let secondaryButtonA11yId: String = AccessibilityIdentifiers.Shopping.OptInCard.secondaryButton // MARK: Button Actions - var dismissViewController: ((TelemetryWrapper.EventExtraKey.Shopping?) -> Void)? + var dismissViewController: ((Bool, TelemetryWrapper.EventExtraKey.Shopping?) -> Void)? var onOptIn: (() -> Void)? // MARK: Links @@ -71,7 +71,7 @@ struct FakespotOptInCardViewModel { object: .shoppingLearnMoreButton) guard let fakespotLearnMoreLink else { return } tabManager.addTabsForURLs([fakespotLearnMoreLink], zombie: false, shouldSelectTab: true) - dismissViewController?(.interactionWithALink) + dismissViewController?(false, .interactionWithALink) } func onTapTermsOfUse() { @@ -80,7 +80,7 @@ struct FakespotOptInCardViewModel { object: .shoppingTermsOfUseButton) guard let fakespotTermsOfUseLink else { return } tabManager.addTabsForURLs([fakespotTermsOfUseLink], zombie: false, shouldSelectTab: true) - dismissViewController?(.interactionWithALink) + dismissViewController?(false, .interactionWithALink) } func onTapPrivacyPolicy() { @@ -89,7 +89,7 @@ struct FakespotOptInCardViewModel { object: .shoppingPrivacyPolicyButton) guard let fakespotPrivacyPolicyLink else { return } tabManager.addTabsForURLs([fakespotPrivacyPolicyLink], zombie: false, shouldSelectTab: true) - dismissViewController?(.interactionWithALink) + dismissViewController?(false, .interactionWithALink) } func onTapMainButton() { @@ -105,7 +105,7 @@ struct FakespotOptInCardViewModel { TelemetryWrapper.recordEvent(category: .action, method: .tap, object: .shoppingNotNowButton) - dismissViewController?(nil) + dismissViewController?(true, nil) } var orderWebsites: [String] { diff --git a/firefox-ios/Client/Frontend/Fakespot/FakespotViewController.swift b/firefox-ios/Client/Frontend/Fakespot/FakespotViewController.swift index e7e69309f0ad..061316afa59e 100644 --- a/firefox-ios/Client/Frontend/Fakespot/FakespotViewController.swift +++ b/firefox-ios/Client/Frontend/Fakespot/FakespotViewController.swift @@ -415,9 +415,13 @@ class FakespotViewController: UIViewController, return view case .onboarding: let view: FakespotOptInCardView = .build() - viewModel.optInCardViewModel.dismissViewController = { [weak self] action in - store.dispatch(FakespotAction.setAppearanceTo(BoolValueContext(boolValue: false, windowUUID: windowUUID))) - + viewModel.optInCardViewModel.dismissViewController = { [weak self] dismissPermanently, action in + if dismissPermanently { + self?.triggerDismiss() + } else { + store.dispatch(FakespotAction.setAppearanceTo(BoolValueContext(boolValue: false, + windowUUID: windowUUID))) + } guard let self = self, let action else { return } viewModel.recordDismissTelemetry(by: action) } @@ -468,11 +472,14 @@ class FakespotViewController: UIViewController, case .settingsCard: let view: FakespotSettingsCardView = .build() viewModel.settingsCardViewModel.expandState = fakespotState.isSettingsExpanded ? .expanded : .collapsed - viewModel.settingsCardViewModel.dismissViewController = { [weak self] action in + viewModel.settingsCardViewModel.dismissViewController = { [weak self] dismissPermanently, action in guard let self = self, let action else { return } - - store.dispatch(FakespotAction.setAppearanceTo(BoolValueContext(boolValue: false, windowUUID: windowUUID))) - store.dispatch(FakespotAction.surfaceDisplayedEventSend(windowUUID.context)) + if dismissPermanently { + self.triggerDismiss() + } else { + store.dispatch(FakespotAction.setAppearanceTo(BoolValueContext(boolValue: false, + windowUUID: windowUUID))) + } viewModel.recordDismissTelemetry(by: action) } viewModel.settingsCardViewModel.toggleAdsEnabled = { [weak self] in diff --git a/firefox-ios/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift b/firefox-ios/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift index dee065621248..0289b5f0fef0 100644 --- a/firefox-ios/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift +++ b/firefox-ios/Client/Frontend/Fakespot/Views/FakespotSettingsCardView.swift @@ -26,7 +26,7 @@ class FakespotSettingsCardViewModel { let footerA11yTitleIdentifier: String = a11yIds.footerTitle let footerA11yActionIdentifier: String = a11yIds.footerAction let footerActionUrl = FakespotUtils.fakespotUrl - var dismissViewController: ((TelemetryWrapper.EventExtraKey.Shopping?) -> Void)? + var dismissViewController: ((Bool, TelemetryWrapper.EventExtraKey.Shopping?) -> Void)? var toggleAdsEnabled: (() -> Void)? var onExpandStateChanged: ((CollapsibleCardView.ExpandButtonState) -> Void)? var expandState: CollapsibleCardView.ExpandButtonState = .collapsed @@ -74,7 +74,7 @@ class FakespotSettingsCardViewModel { guard let footerActionUrl else { return } TelemetryWrapper.recordEvent(category: .action, method: .tap, object: .shoppingPoweredByFakespotLabel) tabManager.addTabsForURLs([footerActionUrl], zombie: false, shouldSelectTab: true) - dismissViewController?(.interactionWithALink) + dismissViewController?(false, .interactionWithALink) } } @@ -231,7 +231,7 @@ final class FakespotSettingsCardView: UIView, ThemeApplicable { // Send settings telemetry for Fakespot FakespotUtils().addSettingTelemetry() - viewModel?.dismissViewController?(.optingOutOfTheFeature) + viewModel?.dismissViewController?(true, .optingOutOfTheFeature) } // MARK: - Accessibility diff --git a/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift b/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift index 1a9e5d319619..c40c16e72a08 100644 --- a/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift +++ b/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift @@ -190,6 +190,8 @@ extension TopSitesViewModel: HomepageViewModelProtocol, FeatureFlaggable { device: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom) { let interface = TopSitesUIInterface(trait: traitCollection, availableWidth: size.width) + numberOfRows = topSitesDataAdaptor.numberOfRows + unfilteredTopSites = topSitesDataAdaptor.getTopSitesData() let sectionDimension = dimensionManager.getSectionDimension(for: topSites, numberOfRows: numberOfRows, interface: interface) diff --git a/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyPrompt.swift b/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyPrompt.swift new file mode 100644 index 000000000000..902493aaaa3e --- /dev/null +++ b/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyPrompt.swift @@ -0,0 +1,127 @@ +// 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 Foundation +import ComponentLibrary + +/* + |----------------| + | [Logo] Title X | + |----------------| + | [Button] | + |----------------| + */ + +class MicroSurveyPromptView: UIView, ThemeApplicable { + private var viewModel: MicroSurveyViewModel + struct UX { + static let headerStackSpacing: CGFloat = 8 + static let stackSpacing: CGFloat = 17 + static let closeButtonSize = CGSize(width: 30, height: 30) + static let logoSize = CGSize(width: 24, height: 24) + static let padding = NSDirectionalEdgeInsets( + top: 14, + leading: 16, + bottom: -12, + trailing: -16 + ) + } + + private lazy var logoImage: UIImageView = .build { imageView in + imageView.image = UIImage(imageLiteralResourceName: ImageIdentifiers.homeHeaderLogoBall) + imageView.contentMode = .scaleAspectFit + } + + private var titleLabel: UILabel = .build { label in + label.adjustsFontForContentSizeCategory = true + label.font = FXFontStyles.Regular.body.scaledFont() + label.numberOfLines = 0 + } + + private lazy var closeButton: UIButton = .build { button in + // TODO: FXIOS-8987 - Add accessibility labels + button.accessibilityIdentifier = AccessibilityIdentifiers.MicroSurvey.Prompt.closeButton + button.setImage(UIImage(named: StandardImageIdentifiers.ExtraLarge.crossCircleFill), for: .normal) + button.addTarget(self, action: #selector(self.closeMicroSurvey), for: .touchUpInside) + } + + private lazy var headerView: UIStackView = .build { stack in + stack.distribution = .fillProportionally + stack.axis = .horizontal + stack.alignment = .top + stack.spacing = UX.headerStackSpacing + } + + private lazy var surveyButton: SecondaryRoundedButton = .build { button in + button.addTarget(self, action: #selector(self.openMicroSurvey), for: .touchUpInside) + } + + private lazy var toastView: UIStackView = .build { stack in + stack.spacing = UX.stackSpacing + stack.distribution = .fillProportionally + stack.axis = .vertical + } + + @objc + func closeMicroSurvey() { + viewModel.closeAction() + } + + @objc + func openMicroSurvey() { + viewModel.openAction() + } + + init(viewModel: MicroSurveyViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupView() + configure() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + headerView.addArrangedSubview(logoImage) + headerView.addArrangedSubview(titleLabel) + headerView.addArrangedSubview(closeButton) + + toastView.addArrangedSubview(headerView) + toastView.addArrangedSubview(surveyButton) + + addSubview(toastView) + + NSLayoutConstraint.activate([ + toastView.topAnchor.constraint(equalTo: topAnchor, constant: UX.padding.top), + toastView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: UX.padding.leading), + toastView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: UX.padding.trailing), + toastView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: UX.padding.bottom), + + logoImage.widthAnchor.constraint(equalToConstant: UX.logoSize.width), + logoImage.heightAnchor.constraint(equalToConstant: UX.logoSize.height), + + closeButton.widthAnchor.constraint(equalToConstant: UX.closeButtonSize.width), + closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonSize.height), + ]) + } + + private func configure() { + titleLabel.text = viewModel.title + let roundedButtonViewModel = SecondaryRoundedButtonViewModel( + title: viewModel.buttonText, + a11yIdentifier: AccessibilityIdentifiers.MicroSurvey.Prompt.takeSurveyButton + ) + surveyButton.configure(viewModel: roundedButtonViewModel) + } + + func applyTheme(theme: Theme) { + backgroundColor = theme.colors.layer1 + titleLabel.textColor = theme.colors.textPrimary + closeButton.tintColor = theme.colors.textSecondary + surveyButton.applyTheme(theme: theme) + } +} diff --git a/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyViewModel.swift b/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyViewModel.swift new file mode 100644 index 000000000000..d50b87623e90 --- /dev/null +++ b/firefox-ios/Client/Frontend/MicroSurvey/MicroSurveyViewModel.swift @@ -0,0 +1,14 @@ +// 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 MicroSurveyViewModel { + // TODO: FXIOS-8987 - Add Strings + FXIOS-8990 - Mobile Messaging Structure + // Title + button text comes from mobile messaging; button text will also be hard coded in Strings file when defined + var title: String = "" + var buttonText: String = "" + var openAction: () -> Void + var closeAction: () -> Void +} diff --git a/firefox-ios/Client/Frontend/PasswordManagement/PasswordManagerViewModel.swift b/firefox-ios/Client/Frontend/PasswordManagement/PasswordManagerViewModel.swift index 87472070f5db..588cbcbf7e64 100644 --- a/firefox-ios/Client/Frontend/PasswordManagement/PasswordManagerViewModel.swift +++ b/firefox-ios/Client/Frontend/PasswordManagement/PasswordManagerViewModel.swift @@ -74,21 +74,17 @@ final class PasswordManagerViewModel { /// Searches SQLite database for logins that match query. /// Wraps the SQLiteLogins method to allow us to cancel it from our end. - func queryLogins(_ query: String, completion: @escaping (([LoginRecord]) -> Void)) { - profile.logins.searchLoginsWithQuery(query).upon { result in + func queryLogins(_ query: String, completion: @escaping ([EncryptedLogin]) -> Void) { + profile.logins.searchLoginsWithQuery(query) { result in ensureMainThread { - // Check any failure, Ex. database is closed - guard result.failureValue == nil else { + switch result { + case .success(let logins): + completion(logins) + case .failure: self.delegate?.loginSectionsDidUpdate() completion([]) return } - // Make sure logins exist - guard let logins = result.successValue else { - completion([]) - return - } - completion(logins.asArray()) } } } @@ -156,12 +152,15 @@ final class PasswordManagerViewModel { } public func save(loginRecord: LoginEntry, completion: @escaping ((String?) -> Void)) { - profile.logins.addLogin(login: loginRecord).upon { result in - if result.isSuccess { + profile.logins.addLogin(login: loginRecord, completionHandler: { result in + switch result { + case .success(let encryptedLogin): self.sendLoginsSavedTelemetry() + completion(encryptedLogin?.id) + case .failure(let error): + completion(error as? String) } - completion(result.successValue) - } + }) } func setBreachIndexPath(indexPath: IndexPath) { diff --git a/firefox-ios/Client/Frontend/Settings/HomepageSettings/TopSitesSettings/TopSitesRowCountSettingsController.swift b/firefox-ios/Client/Frontend/Settings/HomepageSettings/TopSitesSettings/TopSitesRowCountSettingsController.swift index 3eb353248afc..e8dcf5497316 100644 --- a/firefox-ios/Client/Frontend/Settings/HomepageSettings/TopSitesSettings/TopSitesRowCountSettingsController.swift +++ b/firefox-ios/Client/Frontend/Settings/HomepageSettings/TopSitesSettings/TopSitesRowCountSettingsController.swift @@ -33,6 +33,7 @@ class TopSitesRowCountSettingsController: SettingsTableViewController { self.numberOfRows = num self.prefs.setInt(Int32(num), forKey: PrefsKeys.NumberOfTopSiteRows) self.tableView.reloadData() + NotificationCenter.default.post(name: .HomePanelPrefsChanged, object: nil) }) } diff --git a/firefox-ios/Client/Frontend/Settings/Main/Account/AccountStatusSetting.swift b/firefox-ios/Client/Frontend/Settings/Main/Account/AccountStatusSetting.swift index e3cd69a5dac3..d8393c3b92fa 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/Account/AccountStatusSetting.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/Account/AccountStatusSetting.swift @@ -48,11 +48,7 @@ class AccountStatusSetting: WithAccountSetting { return NSAttributedString( string: string, attributes: [ - NSAttributedString.Key.font: DefaultDynamicFontHelper.preferredFont( - withTextStyle: .body, - size: 17, - weight: .semibold - ), + NSAttributedString.Key.font: FXFontStyles.Bold.body.scaledFont(), NSAttributedString.Key.foregroundColor: theme.colors.textPrimary] ) } diff --git a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift index d45d08a24ed3..96788fa50a74 100644 --- a/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift +++ b/firefox-ios/Client/Frontend/Settings/Main/AppSettingsTableViewController.swift @@ -283,15 +283,6 @@ class AppSettingsTableViewController: SettingsTableViewController, privacySettings.append(ClearPrivateDataSetting(settings: self, settingsDelegate: parentCoordinator)) - privacySettings += [ - BoolSetting(prefs: profile.prefs, - theme: themeManager.currentTheme(for: windowUUID), - prefKey: "settings.closePrivateTabs", - defaultValue: false, - titleText: .AppSettingsClosePrivateTabsTitle, - statusText: .AppSettingsClosePrivateTabsDescription) - ] - privacySettings.append(ContentBlockerSetting(settings: self, settingsDelegate: parentCoordinator)) privacySettings.append(NotificationsSetting(theme: themeManager.currentTheme(for: windowUUID), diff --git a/firefox-ios/Client/Frontend/Strings.swift b/firefox-ios/Client/Frontend/Strings.swift index c1a9c9bedfff..4e8cae6edb4a 100644 --- a/firefox-ios/Client/Frontend/Strings.swift +++ b/firefox-ios/Client/Frontend/Strings.swift @@ -4146,9 +4146,9 @@ extension String { value: "The *adjusted rating* is based only on reviews we believe to be reliable.", comment: "Adjusted rating label from How we determine review quality card displayed in the shopping review quality bottom sheet. The *text inside asterisks* denotes part of the string to bold, please leave the text inside the '*' so that it is bolded correctly.") public static let ReviewQualityCardHighlightsLabel = MZLocalizedString( - key: "Shopping.ReviewQualityCard.Highlights.Label.v120", + key: "Shopping.ReviewQualityCard.Highlights.Label.v126", tableName: "Shopping", - value: "*Highlights* are from %1@ reviews within the last 80 days that we believe to be reliable.", + value: "*Highlights* are from %@ reviews within the last 80 days that we believe to be reliable.", comment: "Highlights label from How we determine review quality card displayed in the shopping review quality bottom sheet. The parameter substitutes the partner website the user is coming from. The *text inside asterisks* denotes part of the string to bold, please leave the text inside the '*' so that it is bolded correctly.") public static let ReviewQualityCardLearnMoreButtonTitle = MZLocalizedString( key: "Shopping.ReviewQualityCard.LearnMoreButton.Title.v120", @@ -5905,6 +5905,11 @@ extension String { tableName: "Settings", value: "Autofill Addresses", comment: "Label used as an item in Settings screen. When touched, it will take user to address autofill settings page to that will allow user to add or modify saved addresses to allow for autofill in a webpage.") + public static let ReviewQualityCardHighlightsLabel = MZLocalizedString( + key: "Shopping.ReviewQualityCard.Highlights.Label.v120", + tableName: "Shopping", + value: "*Highlights* are from %1@ reviews within the last 80 days that we believe to be reliable.", + comment: "Highlights label from How we determine review quality card displayed in the shopping review quality bottom sheet. The parameter substitutes the partner website the user is coming from. The *text inside asterisks* denotes part of the string to bold, please leave the text inside the '*' so that it is bolded correctly.") } } } diff --git a/firefox-ios/Client/Frontend/TabContentsScripts/LoginsHelper.swift b/firefox-ios/Client/Frontend/TabContentsScripts/LoginsHelper.swift index c01e522ee2b9..eafa06bc8f09 100644 --- a/firefox-ios/Client/Frontend/TabContentsScripts/LoginsHelper.swift +++ b/firefox-ios/Client/Frontend/TabContentsScripts/LoginsHelper.swift @@ -207,25 +207,27 @@ class LoginsHelper: TabContentScript { return } - profile.logins.getLoginsForProtectionSpace( - login.protectionSpace, + profile.logins.getLoginsFor( + protectionSpace: login.protectionSpace, withUsername: login.username - ).uponQueue(.main) { res in - if let data = res.successValue { - for saved in data { - if let saved = saved { + ) { res in + DispatchQueue.main.async { + switch res { + case .success(let successValue): + for saved in successValue { if saved.decryptedPassword == login.password { - _ = self.profile.logins.use(login: saved) + self.profile.logins.use(login: saved, completionHandler: { _ in }) return } self.promptUpdateFromLogin(login: saved, toLogin: login) return } + case .failure: + break } + self.promptSave(login) } - - self.promptSave(login) } } @@ -264,7 +266,7 @@ class LoginsHelper: TabContentScript { self.tab?.removeSnackbar(bar) self.snackBar = nil self.sendLoginsSavedTelemetry() - _ = self.profile.logins.addLogin(login: login) + self.profile.logins.addLogin(login: login, completionHandler: { _ in }) } applyTheme(for: dontSave, save) @@ -305,7 +307,7 @@ class LoginsHelper: TabContentScript { self.tab?.removeSnackbar(bar) self.snackBar = nil self.sendLoginsModifiedTelemetry() - _ = self.profile.logins.updateLogin(id: old.id, login: new) + self.profile.logins.updateLogin(id: old.id, login: new, completionHandler: { _ in }) } applyTheme(for: dontSave, update) diff --git a/firefox-ios/Client/Frontend/TabContentsScripts/NightModeHelper.swift b/firefox-ios/Client/Frontend/TabContentsScripts/NightModeHelper.swift index 78b5c9339098..7005ec484a3d 100644 --- a/firefox-ios/Client/Frontend/TabContentsScripts/NightModeHelper.swift +++ b/firefox-ios/Client/Frontend/TabContentsScripts/NightModeHelper.swift @@ -25,7 +25,9 @@ class NightModeHelper: TabContentScript, FeatureFlaggable { _ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage ) { - // Do nothing. + guard let webView = message.frameInfo.webView else { return } + let jsCallback = "window.__firefox__.NightMode.setEnabled(\(NightModeHelper.isActivated()))" + webView.evaluateJavascriptInDefaultContentWorld(jsCallback) } static func toggle( diff --git a/firefox-ios/Client/Frontend/Toolbar+URLBar/TabLocationView.swift b/firefox-ios/Client/Frontend/Toolbar+URLBar/TabLocationView.swift index 8bff5736c5fd..bb918f9fb4b0 100644 --- a/firefox-ios/Client/Frontend/Toolbar+URLBar/TabLocationView.swift +++ b/firefox-ios/Client/Frontend/Toolbar+URLBar/TabLocationView.swift @@ -46,7 +46,7 @@ class TabLocationView: UIView, FeatureFlaggable { var notificationCenter: NotificationProtocol = NotificationCenter.default private var themeManager: ThemeManager = AppContainer.shared.resolve() - private let windowUUID: WindowUUID + let windowUUID: WindowUUID /// Tracking protection button, gets updated from tabDidChangeContentBlocking var blockerStatus: BlockerStatus = .noBlockedURLs { @@ -564,6 +564,8 @@ extension TabLocationView: ThemeApplicable { } extension TabLocationView: TabEventHandler { + var tabEventWindowResponseType: TabEventHandlerWindowResponseType { return .singleWindow(windowUUID) } + func tabDidChangeContentBlocking(_ tab: Tab) { guard let blocker = tab.contentBlocker else { return } diff --git a/firefox-ios/Client/Frontend/TrackingProtection/CertificatesHandler.swift b/firefox-ios/Client/Frontend/TrackingProtection/CertificatesHandler.swift new file mode 100644 index 000000000000..ab7fa050e61f --- /dev/null +++ b/firefox-ios/Client/Frontend/TrackingProtection/CertificatesHandler.swift @@ -0,0 +1,36 @@ +// 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 Security +import CryptoKit +import X509 +import SwiftASN1 + +class CertificatesHandler { + private let serverTrust: SecTrust + var certificates = [Certificate]() + + /// Initializes a new `CertificatesHandler` with the given server trust. + /// - Parameters: + /// - serverTrust: The server trust containing the certificate chain. + init(serverTrust: SecTrust) { + self.serverTrust = serverTrust + } + + /// Extracts and handles the certificate chain. + func handleCertificates() { + guard let certificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] else { + return + } + for (_, certificate) in certificateChain.enumerated() { + let certificateData = SecCertificateCopyData(certificate) as Data + do { + let certificate = try Certificate(derEncoded: Array(certificateData)) + certificates.append(certificate) + } catch { + } + } + } +} diff --git a/firefox-ios/Client/Frontend/UserContent/UserScripts/MainFrame/AtDocumentStart/NightModeHelper.js b/firefox-ios/Client/Frontend/UserContent/UserScripts/MainFrame/AtDocumentStart/NightModeHelper.js index 7047003c0d30..57f90a0596a8 100644 --- a/firefox-ios/Client/Frontend/UserContent/UserScripts/MainFrame/AtDocumentStart/NightModeHelper.js +++ b/firefox-ios/Client/Frontend/UserContent/UserScripts/MainFrame/AtDocumentStart/NightModeHelper.js @@ -129,6 +129,6 @@ Object.defineProperty(window.__firefox__.NightMode, "setEnabled", { } }); -window.addEventListener("DOMContentLoaded", function() { - window.__firefox__.NightMode.setEnabled(window.__firefox__.NightMode.enabled); +window.addEventListener("pageshow", () => { + webkit.messageHandlers.NightMode.postMessage({ state: "ready" }); }); diff --git a/firefox-ios/Client/Helpers/UserActivityHandler.swift b/firefox-ios/Client/Helpers/UserActivityHandler.swift index 3e9cdc0c413b..d47a1ed36d6c 100644 --- a/firefox-ios/Client/Helpers/UserActivityHandler.swift +++ b/firefox-ios/Client/Helpers/UserActivityHandler.swift @@ -55,6 +55,8 @@ class UserActivityHandler { } extension UserActivityHandler: TabEventHandler { + var tabEventWindowResponseType: TabEventHandlerWindowResponseType { return .allWindows } + func tabDidGainFocus(_ tab: Tab) { tab.userActivity?.becomeCurrent() } diff --git a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift index e9a3a94c3cfd..6ab2903b7e6f 100644 --- a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift +++ b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift @@ -53,6 +53,9 @@ final class NimbusFeatureFlagLayer { case .loginAutofill: return checkNimbusForLoginAutofill(for: featureID, from: nimbus) + case .microSurvey: + return checkMicroSurveyFeature(from: nimbus) + case .nightMode: return checkNightModeFeature(from: nimbus) @@ -259,6 +262,12 @@ final class NimbusFeatureFlagLayer { return config.enabled } + private func checkMicroSurveyFeature(from nimbus: FxNimbus) -> Bool { + let config = nimbus.features.microSurveyFeature.value() + + return config.enabled + } + private func checkNightModeFeature(from nimbus: FxNimbus) -> Bool { let config = nimbus.features.nightModeFeature.value() diff --git a/firefox-ios/Client/TabManagement/TabEventHandlers.swift b/firefox-ios/Client/TabManagement/GlobalTabEventHandlers.swift similarity index 53% rename from firefox-ios/Client/TabManagement/TabEventHandlers.swift rename to firefox-ios/Client/TabManagement/GlobalTabEventHandlers.swift index c9e52d7aa89e..75526532ed38 100644 --- a/firefox-ios/Client/TabManagement/TabEventHandlers.swift +++ b/firefox-ios/Client/TabManagement/GlobalTabEventHandlers.swift @@ -5,18 +5,21 @@ import Foundation import Shared -class TabEventHandlers { - /// Create handlers that observe specified tab events. +class GlobalTabEventHandlers { + private static var globalHandlers: [TabEventHandler] = [] + + /// Creates and configures the client's global TabEvent handlers. These handlers are created + /// singularly for the entire app and respond to tab events across all windows. If the handlers + /// have already been created this function does nothing. /// /// For anything that needs to react to tab events notifications (see `TabEventLabel`), the /// pattern is to implement a handler and specify which events to observe. - static func create(with profile: Profile) -> [TabEventHandler] { - let handlers: [TabEventHandler] = [ + static func configure(with profile: Profile) { + guard globalHandlers.isEmpty else { return } + globalHandlers = [ UserActivityHandler(), MetadataParserHelper(), AccountSyncHandler(with: profile) ] - - return handlers } } diff --git a/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift b/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift index 8b82c773c003..a3e02f099860 100644 --- a/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift +++ b/firefox-ios/Client/TabManagement/Legacy/LegacyTabManager.swift @@ -65,9 +65,9 @@ struct BackupCloseTab { // TabManager must extend NSObjectProtocol in order to implement WKNavigationDelegate class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler { // MARK: - Variables - private let tabEventHandlers: [TabEventHandler] let profile: Profile let windowUUID: WindowUUID + var tabEventWindowResponseType: TabEventHandlerWindowResponseType { return .singleWindow(windowUUID) } var isRestoringTabs = false var tabRestoreHasFinished = false var tabs = [Tab]() @@ -163,11 +163,11 @@ class LegacyTabManager: NSObject, FeatureFlaggable, TabManager, TabEventHandler self.windowUUID = uuid self.profile = profile self.navDelegate = TabManagerNavDelegate() - self.tabEventHandlers = TabEventHandlers.create(with: profile) self.logger = logger super.init() + GlobalTabEventHandlers.configure(with: profile) register(self, forTabEvents: .didSetScreenshot) addNavigationDelegate(self) diff --git a/firefox-ios/Client/TabManagement/TabEventHandler.swift b/firefox-ios/Client/TabManagement/TabEventHandler.swift index f34e8d21e9f6..29ea08504080 100644 --- a/firefox-ios/Client/TabManagement/TabEventHandler.swift +++ b/firefox-ios/Client/TabManagement/TabEventHandler.swift @@ -42,7 +42,32 @@ import Storage /// 4. a TabEvent, with whatever parameters are needed. /// i) a case to map the event to the event label (var label) /// ii) a case to map the event to the event handler (func handle:with:) +/// +/// ========= TabEvents & Multi-Window ========= +/// +/// Some event handlers are meant to operate in a global manner, responding to tab events +/// across all windows (see: GlobalTabEventHandlers.swift). Other handlers may only care +/// about tab events specific to their window. You may control how tab events are delivered +/// to your handler by the response type setting for `tabEventWindowResponseType`. + +enum TabEventHandlerWindowResponseType { + /// The tab event handler will receive tab events for all windows on iPad. + case allWindows + /// The tab event handler will receive tab events only for a specific window on iPad. + case singleWindow(WindowUUID) + + func shouldSendHandlerEvent(for tabWindowUUID: WindowUUID) -> Bool { + switch self { + case .allWindows: + return true + case .singleWindow(let targetUUID): + return targetUUID == tabWindowUUID + } + } +} + protocol TabEventHandler: AnyObject { + var tabEventWindowResponseType: TabEventHandlerWindowResponseType { get } func tab(_ tab: Tab, didChangeURL url: URL) func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) func tab(_ tab: Tab, didLoadReadability page: ReadabilityResult) @@ -169,14 +194,14 @@ extension TabEventHandler { let wrapper = ObserverWrapper() wrapper.observers = events.map { [weak self] eventType in center.addObserver(forName: eventType.name, object: nil, queue: .main) { notification in + guard let self else { return } guard let tab = notification.object as? Tab, - let event = notification.userInfo?["payload"] as? TabEvent else { - return + let event = notification.userInfo?["payload"] as? TabEvent, + self.tabEventWindowResponseType.shouldSendHandlerEvent(for: tab.windowUUID) else { + return } - if let me = self { - event.handle(tab, with: me) - } + event.handle(tab, with: self) } } diff --git a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift index ce12fa6068df..39cd924e7c1e 100644 --- a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift +++ b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift @@ -245,7 +245,7 @@ class TabManagerImplementation: LegacyTabManager, Notifiable { } private func generateTabDataForSaving() -> [TabData] { - let tabData = tabs.map { tab in + let tabData = normalTabs.map { tab in let oldTabGroupData = tab.metadataManager?.tabGroupData let state = TabGroupTimerState(rawValue: oldTabGroupData?.tabHistoryCurrentState ?? "") let groupData = TabGroupData(searchTerm: oldTabGroupData?.tabAssociatedSearchTerm, @@ -287,6 +287,7 @@ class TabManagerImplementation: LegacyTabManager, Notifiable { private func saveCurrentTabSessionData() { guard let selectedTab = self.selectedTab, + !selectedTab.isPrivate, let tabSession = selectedTab.webView?.interactionState as? Data, let tabID = UUID(uuidString: selectedTab.tabUUID) else { return } diff --git a/firefox-ios/Client/TabManagement/TabMigrationUtility.swift b/firefox-ios/Client/TabManagement/TabMigrationUtility.swift index bafd63801b3f..f7d8a66af1ad 100644 --- a/firefox-ios/Client/TabManagement/TabMigrationUtility.swift +++ b/firefox-ios/Client/TabManagement/TabMigrationUtility.swift @@ -100,7 +100,6 @@ class DefaultTabMigrationUtility: TabMigrationUtility { } let windowData = WindowData(id: windowUUID, - isPrimary: true, activeTabId: selectTabUUID ?? UUID(), tabData: tabsToMigrate) diff --git a/firefox-ios/Client/Telemetry/TelemetryWrapper.swift b/firefox-ios/Client/Telemetry/TelemetryWrapper.swift index 398bf1844ce2..2b6d0235b6c4 100644 --- a/firefox-ios/Client/Telemetry/TelemetryWrapper.swift +++ b/firefox-ios/Client/Telemetry/TelemetryWrapper.swift @@ -388,6 +388,9 @@ extension TelemetryWrapper { case settingsMenuShowTour = "show-tour" case settingsMenuPasswords = "passwords" // MARK: Logins and Passwords + case loginsAutofillPromptDismissed = "logins-autofill-prompt-dismissed" + case loginsAutofillPromptExpanded = "logins-autofill-prompt-expanded" + case loginsAutofillPromptShown = "logins-autofill-prompt-shown" case loginsAutofilled = "logins-autofilled" case loginsAutofillFailed = "logins-autofill-failed" case loginsManagementAddTapped = "logins-management-add-tapped" @@ -1058,6 +1061,12 @@ extension TelemetryWrapper { GleanMetrics.SettingsMenu.showTourPressed.record() // MARK: Logins and Passwords + case(.action, .view, .loginsAutofillPromptShown, _, _): + GleanMetrics.Logins.autofillPromptShown.record() + case(.action, .tap, .loginsAutofillPromptExpanded, _, _): + GleanMetrics.Logins.autofillPromptExpanded.record() + case(.action, .close, .loginsAutofillPromptDismissed, _, _): + GleanMetrics.Logins.autofillPromptDismissed.record() case(.action, .tap, .loginsAutofilled, _, _): GleanMetrics.Logins.autofilled.record() case(.action, .tap, .loginsAutofillFailed, _, _): diff --git a/firefox-ios/Client/metrics.yaml b/firefox-ios/Client/metrics.yaml index 6c84741b3c11..9810b4f1a777 100755 --- a/firefox-ios/Client/metrics.yaml +++ b/firefox-ios/Client/metrics.yaml @@ -2905,6 +2905,43 @@ logins: notification_emails: - fx-ios-data-stewards@mozilla.com expires: "2024-06-01" + autofill_prompt_shown: + type: event + description: | + Password autofill prompt was shown. + bugs: + - https://github.com/mozilla-mobile/firefox-ios/issues/19722 + data_reviews: + - https://github.com/mozilla-mobile/firefox-ios/pull/19791 + data_sensitivity: + - interaction + notification_emails: + - fx-ios-data-stewards@mozilla.com + expires: "2025-01-01" + autofill_prompt_expanded: + type: event + description: | + Password autofill prompt was expanded. + bugs: + - https://github.com/mozilla-mobile/firefox-ios/issues/19722 + data_reviews: + - https://github.com/mozilla-mobile/firefox-ios/pull/19791 + notification_emails: + - fx-ios-data-stewards@mozilla.com + expires: "2025-01-01" + autofill_prompt_dismissed: + type: event + description: | + Password autofill prompt was dismissed. + bugs: + - https://github.com/mozilla-mobile/firefox-ios/issues/19722 + data_reviews: + - https://github.com/mozilla-mobile/firefox-ios/pull/19791 + data_sensitivity: + - interaction + notification_emails: + - fx-ios-data-stewards@mozilla.com + expires: "2025-01-01" # QR Code metrics qr_code: scanned: diff --git a/firefox-ios/Extensions/NotificationService/NotificationService.swift b/firefox-ios/Extensions/NotificationService/NotificationService.swift index b2cdbc51d80e..416f7b5eabfb 100644 --- a/firefox-ios/Extensions/NotificationService/NotificationService.swift +++ b/firefox-ios/Extensions/NotificationService/NotificationService.swift @@ -113,8 +113,6 @@ class NotificationService: UNNotificationServiceExtension { class SyncDataDisplay { var contentHandler: (UNNotificationContent) -> Void var notificationContent: UNMutableNotificationContent - - var tabQueue: TabQueue? var messageDelivered = false init(content: UNMutableNotificationContent, diff --git a/firefox-ios/RustFxA/FirefoxAccountSignInViewController.swift b/firefox-ios/RustFxA/FirefoxAccountSignInViewController.swift index da65d0ae7111..0ca90326e1a1 100644 --- a/firefox-ios/RustFxA/FirefoxAccountSignInViewController.swift +++ b/firefox-ios/RustFxA/FirefoxAccountSignInViewController.swift @@ -24,9 +24,6 @@ class FirefoxAccountSignInViewController: UIViewController, Themeable { static let buttonCornerRadius: CGFloat = 8 static let buttonVerticalInset: CGFloat = 12 static let buttonHorizontalInset: CGFloat = 16 - static let buttonFontSize: CGFloat = 16 - static let signInLabelFontSize: CGFloat = 20 - static let descriptionFontSize: CGFloat = 17 } // MARK: - Properties @@ -66,8 +63,7 @@ class FirefoxAccountSignInViewController: UIViewController, Themeable { label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping label.text = .FxASignin_Subtitle - label.font = DefaultDynamicFontHelper.preferredBoldFont(withTextStyle: .headline, - size: UX.signInLabelFontSize) + label.font = FXFontStyles.Bold.headline.scaledFont() label.adjustsFontForContentSizeCategory = true } @@ -80,8 +76,7 @@ class FirefoxAccountSignInViewController: UIViewController, Themeable { label.textAlignment = .center label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping - label.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .headline, - size: UX.signInLabelFontSize) + label.font = FXFontStyles.Regular.headline.scaledFont() label.adjustsFontForContentSizeCategory = true let placeholder = "firefox.com/pair" @@ -89,8 +84,8 @@ class FirefoxAccountSignInViewController: UIViewController, Themeable { manager.getPairingAuthorityURL { result in guard let url = try? result.get(), let host = url.host else { return } - let font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .headline, - size: UX.signInLabelFontSize) + let font = FXFontStyles.Regular.headline.scaledFont() + let shortUrl = host + url.path // "firefox.com" + "/pair" let msg: String = .FxASignin_QRInstructions.replaceFirstOccurrence(of: placeholder, with: shortUrl) label.attributedText = msg.attributedText(boldString: shortUrl, font: font) diff --git a/firefox-ios/Storage/Rust/RustLogins.swift b/firefox-ios/Storage/Rust/RustLogins.swift index c3f4469fc882..585a569496e4 100644 --- a/firefox-ios/Storage/Rust/RustLogins.swift +++ b/firefox-ios/Storage/Rust/RustLogins.swift @@ -508,7 +508,7 @@ protocol LoginsProtocol { func listLogins(completionHandler: @escaping (Result<[EncryptedLogin], Error>) -> Void) func updateLogin(id: String, login: LoginEntry, completionHandler: @escaping (Result) -> Void) func use(login: EncryptedLogin, completionHandler: @escaping (Result) -> Void) - func searchLoginsWithQuery(_ query: String?, completionHandler: @escaping (Result) -> Void) + func searchLoginsWithQuery(_ query: String?, completionHandler: @escaping (Result<[EncryptedLogin], Error>) -> Void) func deleteLogins(ids: [String], completionHandler: @escaping ([Result]) -> Void) func deleteLogin(id: String, completionHandler: @escaping (Result) -> Void) } @@ -688,7 +688,7 @@ public class RustLogins: LoginsProtocol { public func searchLoginsWithQuery( _ query: String?, - completionHandler: @escaping (Result) -> Void) { + completionHandler: @escaping (Result<[EncryptedLogin], Error>) -> Void) { let rustKeys = RustLoginEncryptionKeys() listLogins { result in switch result { @@ -696,7 +696,7 @@ public class RustLogins: LoginsProtocol { let records = logins guard let query = query?.lowercased(), !query.isEmpty else { - completionHandler(.success(records.first)) + completionHandler(.success(records)) return } @@ -704,11 +704,9 @@ public class RustLogins: LoginsProtocol { let username = rustKeys.decryptSecureFields(login: $0)?.secFields.username ?? "" return $0.fields.origin.lowercased().contains(query) || username.lowercased().contains(query) } - if let firstFilteredRecord = filteredRecords.first { - completionHandler(.success(firstFilteredRecord)) - } else { - completionHandler(.success(nil)) - } + + completionHandler(.success(filteredRecords)) + case .failure(let error): completionHandler(.failure(error)) } @@ -824,7 +822,7 @@ public class RustLogins: LoginsProtocol { return deferred } - func addLogin(login: LoginEntry, completionHandler: @escaping (Result) -> Void) { + public func addLogin(login: LoginEntry, completionHandler: @escaping (Result) -> Void) { queue.async { guard self.isOpen else { let error = LoginsStoreError.UnexpectedLoginsApiError(reason: "Database is closed") 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 f5488e06e9b5..deb28951cafc 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 @@ -27,7 +27,7 @@ class MockBrowserCoordinator: BrowserNavigationHandler, ParentCoordinatorDelegat var dismissFakespotSidebarCalled = 0 var updateFakespotSidebarCalled = 0 - func show(settings: Route.SettingsSection) { + func show(settings: Client.Route.SettingsSection, onDismiss: (() -> Void)?) { showSettingsCalled += 1 } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabManagement/Legacy/TabEventHandlerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabManagement/Legacy/TabEventHandlerTests.swift index a02afc1ed5b8..735fd620286d 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabManagement/Legacy/TabEventHandlerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabManagement/Legacy/TabEventHandlerTests.swift @@ -82,6 +82,8 @@ class DummyHandler: TabEventHandler { // of individual tab state. var isFocused: Bool? + let tabEventWindowResponseType: TabEventHandlerWindowResponseType = .singleWindow(.XCTestDefaultUUID) + init() { register(self, forTabEvents: .didGainFocus, .didLoseFocus) } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/WindowManagerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/WindowManagerTests.swift index 85acf04691a3..f80966428d2d 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/WindowManagerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/WindowManagerTests.swift @@ -208,6 +208,47 @@ class WindowManagerTests: XCTestCase { XCTAssertNotEqual(requestedUUID2, savedUUID) } + func testClosingTwoWindowsInDifferentOrdersResultsInSensibleExpectedOrderWhenOpening() { + let subject = createSubject() + let tabDataStore: TabDataStore = AppContainer.shared.resolve() + let mockTabDataStore = tabDataStore as! MockTabDataStore + mockTabDataStore.resetMockTabWindowUUIDs() + + let uuid1 = UUID() + mockTabDataStore.injectMockTabWindowUUID(uuid1) + let uuid2 = UUID() + mockTabDataStore.injectMockTabWindowUUID(uuid2) + + // Open a window using UUID 1 + subject.newBrowserWindowConfigured(AppWindowInfo(), uuid: uuid1) + // Open a window using UUID 2 + subject.newBrowserWindowConfigured(AppWindowInfo(), uuid: uuid2) + + // Close window 2, then window 1 + subject.windowWillClose(uuid: uuid2) + subject.windowWillClose(uuid: uuid1) + + // Now attempt to re-open two windows in order. We expect window + // 1 to open, then window 2 + let result1 = subject.reserveNextAvailableWindowUUID() + let result2 = subject.reserveNextAvailableWindowUUID() + XCTAssertEqual(result1, uuid1) + XCTAssertEqual(result2, uuid2) + + // Now re-open both windows in order... + subject.newBrowserWindowConfigured(AppWindowInfo(), uuid: uuid1) + subject.newBrowserWindowConfigured(AppWindowInfo(), uuid: uuid2) + // ...but close them in the opposite order as before (close window 1, then 2) + subject.windowWillClose(uuid: uuid1) + subject.windowWillClose(uuid: uuid2) + + // Check that the next time we open the windows the order is now reversed + let result2_1 = subject.reserveNextAvailableWindowUUID() + let result2_2 = subject.reserveNextAvailableWindowUUID() + XCTAssertEqual(result2_1, uuid2) + XCTAssertEqual(result2_2, uuid1) + } + // MARK: - Test Subject private func createSubject() -> WindowManager { diff --git a/firefox-ios/firefox-ios-tests/Tests/FullFunctionalTestPlan.xctestplan b/firefox-ios/firefox-ios-tests/Tests/FullFunctionalTestPlan.xctestplan index 3e945477ae4e..ea3ffdcb18ce 100644 --- a/firefox-ios/firefox-ios-tests/Tests/FullFunctionalTestPlan.xctestplan +++ b/firefox-ios/firefox-ios-tests/Tests/FullFunctionalTestPlan.xctestplan @@ -64,11 +64,8 @@ "DragAndDropTestIpad\/testTryDragAndDropHistoryToURLBar()", "DragAndDropTests\/testRearrangeTabsTabTray()", "ExperimentIntegrationTests", - "FakespotTests\/testAcceptTheRejectedOptInNotification()", "FakespotTests\/testPriceTagIconAvailableOnlyOnDetailPage()", "FakespotTests\/testPriceTagNotDisplayedInPrivateMode()", - "FakespotTests\/testReviewQualityCheckBottomSheetUI()", - "FakespotTests\/testSettingsSectionUI()", "FindInPageTests\/testFindFromMenu()", "HistoryTests\/testClearPrivateData()", "HistoryTests\/testClearPrivateDataButtonDisabled()", diff --git a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nBaseSnapshotTests.swift b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nBaseSnapshotTests.swift index 16ed1875d888..f9960ed1889e 100644 --- a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nBaseSnapshotTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nBaseSnapshotTests.swift @@ -65,6 +65,10 @@ class L10nBaseSnapshotTests: XCTestCase { } } + func waitForTabsButton() { + mozWaitForElementToExist(app.buttons[AccessibilityIdentifiers.Toolbar.tabsButton], timeout: TIMEOUT) + } + private func waitFor( _ element: XCUIElement, with predicateString: String, @@ -119,6 +123,6 @@ class L10nBaseSnapshotTests: XCTestCase { func waitUntilPageLoad() { let app = XCUIApplication() let progressIndicator = app.progressIndicators.element(boundBy: 0) - mozWaitForElementToNotExist(progressIndicator, timeout: 20.0) + mozWaitForElementToNotExist(progressIndicator, timeout: 30.0) } } diff --git a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite1SnapshotTests.swift b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite1SnapshotTests.swift index 1f33a2af2229..e02c2eb538d5 100644 --- a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite1SnapshotTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite1SnapshotTests.swift @@ -113,6 +113,7 @@ class L10nSuite1SnapshotTests: L10nBaseSnapshotTests { @MainActor func testTopSitesMenu() { sleep(3) + waitForTabsButton() // mozWaitForElementToExist(app.buttons[AccessibilityIdentifiers.Toolbar.settingsMenuButton], timeout: 15) navigator.nowAt(NewTabScreen) app.collectionViews.cells[AccessibilityIdentifiers.FirefoxHomepage.TopSites.itemCell].firstMatch.swipeUp() diff --git a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite2SnapshotTests.swift b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite2SnapshotTests.swift index 9b7cf20897c2..ab1ad204af59 100644 --- a/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite2SnapshotTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/L10nSnapshotTests/L10nSuite2SnapshotTests.swift @@ -167,18 +167,21 @@ class L10nSuite2SnapshotTests: L10nBaseSnapshotTests { @MainActor func testFakespotAvailable() throws { - navigator.openURL("https://www.amazon.com") + navigator.openURL("https://www.walmart.com") waitUntilPageLoad() // Search for and open a shoe listing let website = app.webViews["contentView"].firstMatch - mozWaitForElementToExist(website.textFields.firstMatch) - website.textFields.firstMatch.tap() - website.textFields.firstMatch.typeText("Shoe") - mozWaitForElementToExist(website.otherElements.buttons.firstMatch) + mozWaitForElementToExist(website.searchFields["Search"]) + website.searchFields["Search"].tap() + website.searchFields["Search"].typeText("end table") + mozWaitForElementToExist(website.otherElements.buttons.firstMatch, timeout: 30) website.otherElements.buttons.element(boundBy: 1).tap() waitUntilPageLoad() - website.images.firstMatch.tap() + app.swipeUp() + mozWaitForElementToExist(website.staticTexts["Sponsored"].firstMatch) + mozWaitForElementToExist(website.staticTexts["Options"].firstMatch) + website.staticTexts["Options"].firstMatch.tap() // Tap the shopping cart icon waitUntilPageLoad() diff --git a/firefox-ios/firefox-ios-tests/Tests/Smoketest1.xctestplan b/firefox-ios/firefox-ios-tests/Tests/Smoketest1.xctestplan index c6a3927a1d0b..c5c19154268d 100644 --- a/firefox-ios/firefox-ios-tests/Tests/Smoketest1.xctestplan +++ b/firefox-ios/firefox-ios-tests/Tests/Smoketest1.xctestplan @@ -47,6 +47,7 @@ "BookmarksTests\/testAddBookmark()", "BookmarksTests\/testAddNewFolder()", "BookmarksTests\/testAddNewMarker()", + "BookmarksTests\/testBookmark()", "BookmarksTests\/testBookmarkingUI()", "BookmarksTests\/testDeleteBookmarkContextMenu()", "BookmarksTests\/testDeleteBookmarkSwiping()", @@ -55,6 +56,7 @@ "BookmarksTests\/testRecentlyBookmarked()", "BrowsingPDFTests", "ClipBoardTests\/testClipboard()", + "ClipBoardTests\/testCopyLink()", "CreditCardsTests\/testAccessingTheCreditCardsSection()", "CreditCardsTests\/testCreditCardsAutofill()", "CreditCardsTests\/testDeleteButtonFromEditCard()", diff --git a/firefox-ios/firefox-ios-tests/Tests/Smoketest2.xctestplan b/firefox-ios/firefox-ios-tests/Tests/Smoketest2.xctestplan index 6761c97881d4..2cd52c7c9d82 100644 --- a/firefox-ios/firefox-ios-tests/Tests/Smoketest2.xctestplan +++ b/firefox-ios/firefox-ios-tests/Tests/Smoketest2.xctestplan @@ -96,6 +96,7 @@ "SearchTests\/testSearchEngine()", "SearchTests\/testSearchIconOnAboutHome()", "SearchTests\/testSearchStartAfterTypingTwoWords()", + "SearchTests\/testSearchSuggestions()", "SearchTests\/testSearchWithFirefoxOption()", "SettingsTests\/testCopiedLinks()", "SettingsTests\/testHelpOpensSUMOInTab()", diff --git a/firefox-ios/firefox-ios-tests/Tests/Smoketest3.xctestplan b/firefox-ios/firefox-ios-tests/Tests/Smoketest3.xctestplan index 3981319a7da7..6bc8fb648d96 100644 --- a/firefox-ios/firefox-ios-tests/Tests/Smoketest3.xctestplan +++ b/firefox-ios/firefox-ios-tests/Tests/Smoketest3.xctestplan @@ -42,6 +42,7 @@ "DragAndDropTests\/testRearrangeTabsTabTrayLandscape()", "EngagementNotificationTests", "ExperimentIntegrationTests", + "FakespotTests\/testAcceptTheRejectedOptInNotification()", "FakespotTests\/testFakespotAvailable()", "FakespotTests\/testLearnMoreAboutFakespotHyperlink()", "FakespotTests\/testLearnMoreLink()", @@ -51,6 +52,7 @@ "FakespotTests\/testPriceTagNotDisplayedInPrivateMode()", "FakespotTests\/testPriceTagNotDisplayedOnSitesNotIntegratedFakespot()", "FakespotTests\/testPrivacyPolicyLink()", + "FakespotTests\/testReviewQualityCheckBottomSheetUI()", "FakespotTests\/testSettingsSectionUI()", "FakespotTests\/testTermsOfUseLink()", "FakespotTests\/testTurnOffAndOnTheReviewQualityCheck()", diff --git a/firefox-ios/firefox-ios-tests/Tests/Smoketest4.xctestplan b/firefox-ios/firefox-ios-tests/Tests/Smoketest4.xctestplan index 0e04a98ec676..325e82f9a0e7 100644 --- a/firefox-ios/firefox-ios-tests/Tests/Smoketest4.xctestplan +++ b/firefox-ios/firefox-ios-tests/Tests/Smoketest4.xctestplan @@ -43,6 +43,7 @@ "FakespotTests\/testPriceTagNotDisplayedOnSitesNotIntegratedFakespot()", "FakespotTests\/testPrivacyPolicyLink()", "FakespotTests\/testReviewQualityCheckBottomSheetUI()", + "FakespotTests\/testSettingsSectionUI()", "FakespotTests\/testTermsOfUseLink()", "FakespotTests\/testTurnOffAndOnTheReviewQualityCheck()", "FindInPageTests", @@ -105,6 +106,7 @@ "PhotonActionSheetTests", "PocketTest", "PocketTests", + "PrivateBrowsingTest\/testAllPrivateTabsRestore()", "PrivateBrowsingTest\/testClearIndexedDB()", "PrivateBrowsingTest\/testClosePrivateTabsOptionClosesPrivateTabs()", "PrivateBrowsingTest\/testPrivateBrowserPanelView()", @@ -121,6 +123,7 @@ "SearchTests\/testDoNotShowSuggestionsWhenEnteringURL()", "SearchTests\/testPromptPresence()", "SearchTests\/testSearchIconOnAboutHome()", + "SearchTests\/testSearchSuggestions()", "SearchTests\/testSearchWithFirefoxOption()", "SettingsTests", "SiteLoadTest", diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift index 47f59581ce33..4dc5e98316c7 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ActivityStreamTest.swift @@ -253,7 +253,6 @@ class ActivityStreamTest: BaseTestCase { app.collectionViews.cells.element(boundBy: 3).press(forDuration: 1) // Verify options given let ContextMenuTable = app.tables["Context Menu"] - print(app.debugDescription) mozWaitForElementToExist(ContextMenuTable) mozWaitForElementToExist(ContextMenuTable.cells.otherElements["pinLarge"]) mozWaitForElementToExist(ContextMenuTable.cells.otherElements["plusLarge"]) diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/BookmarksTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/BookmarksTests.swift index ac6ce7651809..bb7b5595f513 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/BookmarksTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/BookmarksTests.swift @@ -377,6 +377,16 @@ class BookmarksTests: BaseTestCase { mozWaitForElementToExist(ContextMenuTable.cells.otherElements[StandardImageIdentifiers.Large.shareApple]) } + // https://testrail.stage.mozaws.net/index.php?/cases/view/2307054 + func testBookmark() { + navigator.openURL(url_3) + waitForTabsButton() + bookmark() + mozWaitForElementToExist(app.staticTexts["Bookmark Added"]) + unbookmark() + mozWaitForElementToExist(app.staticTexts["Bookmark Removed"]) + } + private func bookmarkPageAndTapEdit() { bookmark() mozWaitForElementToExist(app.buttons["Edit"]) diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/BrowsingPDFTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/BrowsingPDFTests.swift index 205e6d8ccd72..91dc95e85310 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/BrowsingPDFTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/BrowsingPDFTests.swift @@ -6,10 +6,10 @@ import XCTest import Common let PDF_website = [ - "url": "www.orimi.com/pdf-test.pdf", - "pdfValue": "www.orimi.com/pdf", + "url": "https://storage.googleapis.com/mobile_test_assets/public/pdf-test.pdf", + "pdfValue": "storage.googleapis.com/mobile_test_assets/public/pdf-test.pdf", "urlValue": "yukon.ca/en/educat", - "bookmarkLabel": "https://www.orimi.com/pdf-test.pdf", + "bookmarkLabel": "https://storage.googleapis.com/mobile_test_assets/public/pdf-test.pdf", "longUrlValue": "http://www.education.gov.yk.ca/" ] class BrowsingPDFTests: BaseTestCase { @@ -26,10 +26,8 @@ class BrowsingPDFTests: BaseTestCase { } // https://testrail.stage.mozaws.net/index.php?/cases/view/2307117 - // Disabled due to link not loading - func testOpenLinkFromPDF() throws { - throw XCTSkip("Link inside pfd is not loading anymore") - /* + // Smoketest + func testOpenLinkFromPDF() { navigator.openURL(PDF_website["url"]!) waitUntilPageLoad() @@ -42,7 +40,6 @@ class BrowsingPDFTests: BaseTestCase { // Go back to pdf view app.buttons[AccessibilityIdentifiers.Toolbar.backButton].tap() mozWaitForValueContains(app.textFields["url"], value: PDF_website["pdfValue"]!) - */ } // https://testrail.stage.mozaws.net/index.php?/cases/view/2307118 @@ -78,6 +75,7 @@ class BrowsingPDFTests: BaseTestCase { } // https://testrail.stage.mozaws.net/index.php?/cases/view/2307120 + // Smoketest func testPinPDFtoTopSites() { navigator.openURL(PDF_website["url"]!) waitUntilPageLoad() @@ -110,6 +108,7 @@ class BrowsingPDFTests: BaseTestCase { } // https://testrail.stage.mozaws.net/index.php?/cases/view/2307121 + // Smoketest func testBookmarkPDF() { navigator.openURL(PDF_website["url"]!) waitUntilPageLoad() diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ClipBoardTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ClipBoardTests.swift index 4ef9a3bc77dd..a6e3fa2ffba7 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/ClipBoardTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/ClipBoardTests.swift @@ -67,6 +67,26 @@ class ClipBoardTests: BaseTestCase { mozWaitForValueContains(app.textFields["address"], value: "www.example.com") } + // https://testrail.stage.mozaws.net/index.php?/cases/view/2307051 + func testCopyLink() { + // Tap on "Copy Link + navigator.openURL(url_3) + waitForTabsButton() + navigator.performAction(Action.CopyAddressPAM) + // The Link is copied to clipboard + mozWaitForElementToExist(app.staticTexts["URL Copied To Clipboard"]) + // Open a new tab. Long tap on the URL and tap "Paste & Go" + navigator.performAction(Action.OpenNewTabFromTabTray) + let urlBar = app.textFields[AccessibilityIdentifiers.Browser.UrlBar.url] + mozWaitForElementToExist(urlBar) + urlBar.press(forDuration: 1.5) + app.otherElements[AccessibilityIdentifiers.Photon.pasteAndGoAction].tap() + // The URL is pasted and the page is correctly loaded + mozWaitForElementToExist(urlBar) + waitForValueContains(urlBar, value: "test-example.html") + mozWaitForElementToExist(app.staticTexts["Example Domain"]) + } + // https://testrail.stage.mozaws.net/index.php?/cases/view/2325691 // Smoketest func testClipboardPasteAndGo() { diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/CreditCardsTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/CreditCardsTests.swift index 3a8ac99cffc9..205f5e63ec45 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/CreditCardsTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/CreditCardsTests.swift @@ -382,7 +382,12 @@ class CreditCardsTests: BaseTestCase { let cvc = app.webViews["contentView"].webViews.textFields["CVC"] let zip = app.webViews["contentView"].webViews.textFields["ZIP"] mozWaitForElementToExist(email) - email.tap() + email.tapOnApp() + var nrOfRetries = 3 + while email.value(forKey: "hasKeyboardFocus") as? Bool == false && nrOfRetries > 0 { + email.tapOnApp() + nrOfRetries -= 1 + } email.typeText("foo@mozilla.org") mozWaitForElementToExist(cardNumber) cardNumber.tapOnApp() @@ -395,17 +400,23 @@ class CreditCardsTests: BaseTestCase { expiration.typeText(expirationDate) cvc.tapOnApp() cvc.typeText("123") - mozWaitForElementToExist(name) - name.tapOnApp() - app.swipeUp() zip.tapOnApp() + while zip.value(forKey: "hasKeyboardFocus") as? Bool == false && nrOfRetries > 0 { + // Series of swipes are required to reach the bottom part of the webview + app.webViews["Web content"].swipeDown() + dismissSavedCardsPrompt() + app.webViews["Web content"].swipeUp() + app.webViews["Web content"].swipeUp() + zip.tapOnApp() + nrOfRetries -= 1 + } zip.typeText("12345") name.tapOnApp() name.typeText(nameOnCard) } private func dismissSavedCardsPrompt() { - if app.staticTexts["TEST CARDS"].exists { + if app.staticTexts["Tap"].isVisible() { app.staticTexts["TEST CARDS"].tap() } } diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/NavigationTest.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/NavigationTest.swift index cb0eb6f4c73e..977e3aee41bc 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/NavigationTest.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/NavigationTest.swift @@ -436,10 +436,10 @@ class NavigationTest: BaseTestCase { app.buttons["Open in New Tab"].tap() // A new tab loading the article page should open navigator.goto(TabTray) + mozWaitForElementToExist(app.otherElements["Tabs Tray"].cells.staticTexts["Example Domain"]) let numTabs = app.otherElements["Tabs Tray"].cells.count XCTAssertEqual(numTabs, 2, "Total number of opened tabs should be 2") XCTAssertTrue(app.otherElements["Tabs Tray"].cells.elementContainingText("Example Domain.").exists) - XCTAssertTrue(app.otherElements["Tabs Tray"].cells.staticTexts["Example Domains"].exists) } // https://testrail.stage.mozaws.net/index.php?/cases/view/2441773 diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/PrivateBrowsingTest.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/PrivateBrowsingTest.swift index 5272469c7eb5..2eb5ee307fa7 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/PrivateBrowsingTest.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/PrivateBrowsingTest.swift @@ -230,6 +230,45 @@ class PrivateBrowsingTest: BaseTestCase { XCTAssertTrue(app.buttons["Copy Link"].exists, "The option is not shown") XCTAssertTrue(app.buttons["Download Link"].exists, "The option is not shown") } + + // https://testrail.stage.mozaws.net/index.php?/cases/view/2497357 + func testAllPrivateTabsRestore() { + // Several tabs opened in private tabs tray. Tap on the trashcan + navigator.nowAt(NewTabScreen) + for _ in 1...4 { + navigator.createNewTab(isPrivate: true) + if app.keyboards.element.isVisible() && !iPad() { + mozWaitForElementToExist(app.buttons["urlBar-cancel"], timeout: TIMEOUT) + navigator.performAction(Action.CloseURLBarOpen) + } + } + navigator.goto(TabTray) + var numTab = app.otherElements["Tabs Tray"].cells.count + XCTAssertEqual(4, numTab, "The number of counted tabs is not equal to \(String(describing: numTab))") + app.buttons[AccessibilityIdentifiers.TabTray.closeAllTabsButton].tap() + + // Validate Close All Tabs and Cancel options + mozWaitForElementToExist(app.buttons[AccessibilityIdentifiers.TabTray.deleteCloseAllButton]) + if !iPad() { + mozWaitForElementToExist(app.buttons[AccessibilityIdentifiers.TabTray.deleteCancelButton]) + } + + // Tap on "Close All Tabs" + app.buttons[AccessibilityIdentifiers.TabTray.deleteCloseAllButton].tap() + // The private tabs are closed + numTab = app.otherElements["Tabs Tray"].cells.count + XCTAssertEqual(0, numTab, "The number of counted tabs is not equal to \(String(describing: numTab))") + mozWaitForElementToExist(app.staticTexts["Private Browsing"]) + + // "Undo" toast message is displayed. Tap on "Undo" button + mozWaitForElementToExist(app.buttons["Undo"]) + app.buttons["Undo"].tap() + + // All the private tabs are restored + navigator.goto(TabTray) + numTab = app.otherElements["Tabs Tray"].cells.count + XCTAssertEqual(4, numTab, "The number of counted tabs is not equal to \(String(describing: numTab))") + } } fileprivate extension BaseTestCase { diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/SearchTest.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/SearchTest.swift index 24bc4535d097..401bc1cf9d85 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/SearchTest.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/SearchTest.swift @@ -351,6 +351,82 @@ class SearchTests: BaseTestCase { } } + // https://testrail.stage.mozaws.net/index.php?/cases/view/2306942 + func testSearchSuggestions() { + // Tap on URL Bar and type "g" + navigator.nowAt(NewTabScreen) + typeTextAndValidateSearchSuggestions(text: "g", totalSuggestedSearches: 4, isSwitchOn: true) + + // Tap on the "Append Arrow button" + app.tables.buttons["appendUpLarge"].firstMatch.tap() + + // The search suggestion fills the URL bar but does not conduct the search + let urlBarAddress = app.textFields[AccessibilityIdentifiers.Browser.UrlBar.searchTextField] + waitForValueContains(urlBarAddress, value: "google") + XCTAssertEqual(app.tables.cells.count, 4, "There should be 4 search suggestions") + + // Delete the text and type "g" + mozWaitForElementToExist(app.buttons["Clear text"]) + app.buttons["Clear text"].tap() + typeTextAndValidateSearchSuggestions(text: "g", totalSuggestedSearches: 4, isSwitchOn: true) + + // Tap on the text letter "g" + app.tables.cells.firstMatch.tap() + waitUntilPageLoad() + + // The search is conducted through the default search engine + let urlBar = app.textFields[AccessibilityIdentifiers.Browser.UrlBar.url] + waitForValueContains(urlBar, value: "www.google.com/search?q=") + + // Disable "Show search suggestions" from Settings and type text in a new tab + createNewTabAfterModifyingSearchSuggestions(turnOnSwitch: false) + + // No search suggestions are displayed + // Firefox suggest adds 2 more cells + typeTextAndValidateSearchSuggestions(text: "g", totalSuggestedSearches: 2, isSwitchOn: false) + + // Enable "Show search suggestions" from Settings and type text in a new tab + app.tables.cells.firstMatch.tap() + waitUntilPageLoad() + createNewTabAfterModifyingSearchSuggestions(turnOnSwitch: true) + + // Search suggestions are displayed + // Firefox suggest adds 2 more cells + typeTextAndValidateSearchSuggestions(text: "g", totalSuggestedSearches: 6, isSwitchOn: true) + } + + private func turnOnOffSearchSuggestions(turnOnSwitch: Bool) { + let showSearchSuggestions = app.switches[AccessibilityIdentifiers.Settings.Search.showSearchSuggestions] + mozWaitForElementToExist(showSearchSuggestions) + let switchValue = showSearchSuggestions.value + if switchValue as? String == "0", true && turnOnSwitch == true { + showSearchSuggestions.tap() + } else if switchValue as? String == "1", true && turnOnSwitch == false { + showSearchSuggestions.tap() + } + } + + private func createNewTabAfterModifyingSearchSuggestions(turnOnSwitch: Bool) { + navigator.goto(SearchSettings) + turnOnOffSearchSuggestions(turnOnSwitch: turnOnSwitch) + navigator.goto(NewTabScreen) + navigator.createNewTab() + navigator.nowAt(NewTabScreen) + } + + private func typeTextAndValidateSearchSuggestions(text: String, totalSuggestedSearches: Int, isSwitchOn: Bool) { + typeOnSearchBar(text: text) + // Search suggestions are shown + if isSwitchOn { + mozWaitForElementToExist(app.staticTexts["Google Search"]) + mozWaitForElementToExist(app.tables.cells.staticTexts["g"]) + } else { + mozWaitForElementToNotExist(app.staticTexts["Google Search"]) + mozWaitForElementToNotExist(app.tables.cells.staticTexts["g"]) + } + XCTAssertEqual(app.tables.cells.count, totalSuggestedSearches) + } + private func validateUrlHasFocusAndKeyboardIsDisplayed() { let addressBar = app.textFields["address"] XCTAssertTrue(addressBar.value(forKey: "hasKeyboardFocus") as? Bool ?? false) diff --git a/firefox-ios/firefox-ios-tests/Tests/XCUITests/SettingsTests.swift b/firefox-ios/firefox-ios-tests/Tests/XCUITests/SettingsTests.swift index 02dccc214e33..08cae6fbdea4 100644 --- a/firefox-ios/firefox-ios-tests/Tests/XCUITests/SettingsTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/XCUITests/SettingsTests.swift @@ -147,7 +147,6 @@ class SettingsTests: BaseTestCase { table.cells[settingsQuery.NoImageMode.title], app.switches[settingsQuery.OfferToOpen.title], table.cells[settingsQuery.Logins.title], app.switches[settingsQuery.ShowLink.title], table.cells[settingsQuery.CreditCards.title], table.cells[settingsQuery.Address.title], - table.cells[settingsQuery.ClearData.title], app.switches[settingsQuery.ClosePrivateTabs.title], table.cells[settingsQuery.ContentBlocker.title], table.cells[settingsQuery.Notifications.title], table.cells[settingsQuery.ShowIntroduction.title], table.cells[settingsQuery.SendAnonymousUsageData.title], table.cells[settingsQuery.StudiesToggle.title], table.cells[settingsQuery.Version.title], diff --git a/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml b/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml index 6526aba6d93e..b57ade9b44db 100644 --- a/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml +++ b/firefox-ios/nimbus-features/messaging/messaging-firefox-ios.fml.yaml @@ -63,6 +63,9 @@ import: DEFAULT: priority: 50 max-display-count: 5 + MICRO_SURVEY: + priority: 50 + max-display-count: 5 NOTIFICATION: priority: 50 max-display-count: 1 diff --git a/firefox-ios/nimbus-features/microSurveyFeature.yaml b/firefox-ios/nimbus-features/microSurveyFeature.yaml new file mode 100644 index 000000000000..d58df5c8a640 --- /dev/null +++ b/firefox-ios/nimbus-features/microSurveyFeature.yaml @@ -0,0 +1,19 @@ +# The configuration for the microSurveyFeature feature +features: + micro-survey-feature: + description: > + A feature that shows the microsurvey for users to interact with and submit responses. + variables: + enabled: + description: > + If true, the feature is active. + type: Boolean + default: false + + defaults: + - channel: beta + value: + enabled: false + - channel: developer + value: + enabled: false diff --git a/firefox-ios/nimbus.fml.yaml b/firefox-ios/nimbus.fml.yaml index 794085ba7d27..ce9d758564bc 100644 --- a/firefox-ios/nimbus.fml.yaml +++ b/firefox-ios/nimbus.fml.yaml @@ -23,6 +23,7 @@ include: - nimbus-features/generalFeatures.yaml - nimbus-features/homescreenFeature.yaml - nimbus-features/messagingFeature.yaml + - nimbus-features/microSurveyFeature.yaml - nimbus-features/nightModeFeature.yaml - nimbus-features/onboardingFrameworkFeature.yaml - nimbus-features/reduxSearchSettingsFeature.yaml diff --git a/focus-ios/.swiftlint.yml b/focus-ios/.swiftlint.yml index d70c6a46668a..51f593a8d387 100644 --- a/focus-ios/.swiftlint.yml +++ b/focus-ios/.swiftlint.yml @@ -7,16 +7,16 @@ only_rules: # Only enforce these rules, ignore all others # - colon # - comma # - comment_spacing - # - compiler_protocol_init - # - computed_accessors_order + - compiler_protocol_init + - computed_accessors_order # - control_statement # - duplicate_conditions # - dynamic_inline - # - empty_enum_arguments - # - empty_parameters + - empty_enum_arguments + - empty_parameters # - empty_parentheses_with_trailing_closure # - for_where - # - force_try + - force_try - implicit_getter - inclusive_language - invalid_swiftlint_command @@ -66,18 +66,18 @@ only_rules: # Only enforce these rules, ignore all others # - contains_over_range_nil_comparison # - empty_collection_literal # - empty_count - # - empty_string + - empty_string # - empty_xctest_method - # - explicit_init + - explicit_init # - first_where # - discouraged_assert - # - duplicate_imports - # - duplicate_enum_cases + - duplicate_imports + - duplicate_enum_cases # - last_where # - modifier_order # - multiline_arguments - # - opening_brace - # - overridden_super_call + - opening_brace + - overridden_super_call - vertical_parameter_alignment_on_call - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces @@ -120,6 +120,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - firefox-ios/Sync/Generated/Metrics.swift - firefox-ios/Storage/Generated/Metrics.swift - firefox-ios/ThirdParty + - focus-ios-tests/tools/Localizations - test-fixtures/tmp - firefox-ios/firefox-ios-tests/Tests/UITests/ - l10n-screenshots-dd/ diff --git a/focus-ios/Blockzilla.xcodeproj/project.pbxproj b/focus-ios/Blockzilla.xcodeproj/project.pbxproj index c652debc4eb2..36ad39a0fa99 100644 --- a/focus-ios/Blockzilla.xcodeproj/project.pbxproj +++ b/focus-ios/Blockzilla.xcodeproj/project.pbxproj @@ -7185,7 +7185,7 @@ repositoryURL = "https://github.com/mozilla/rust-components-swift"; requirement = { kind = exactVersion; - version = 126.0.20240410050314; + version = 127.0.20240417050328; }; }; 8A0E7F2C2BA0F0E0006BC6B6 /* XCRemoteSwiftPackageReference "Fuzi" */ = { diff --git a/focus-ios/Blockzilla.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/focus-ios/Blockzilla.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 51cf82665680..ef151688ba7b 100644 --- a/focus-ios/Blockzilla.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/focus-ios/Blockzilla.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/mozilla/rust-components-swift", "state": { "branch": null, - "revision": "89bb410fd78fabb688cd6378c0841c343e3ab9e9", - "version": "126.0.20240410050314" + "revision": "6fc91097ce72934b00fe1fa22de2f0020bfa896d", + "version": "127.0.20240417050328" } }, { diff --git a/focus-ios/Blockzilla/Extensions/StringExtensions.swift b/focus-ios/Blockzilla/Extensions/StringExtensions.swift index fddacbaf557d..6805b15ca3c2 100644 --- a/focus-ios/Blockzilla/Extensions/StringExtensions.swift +++ b/focus-ios/Blockzilla/Extensions/StringExtensions.swift @@ -23,12 +23,17 @@ fileprivate extension String { extension String { var isUrl: Bool { - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - guard let match = detector.firstMatch(in: self, range: NSRange(location: 0, length: self.count)), match.range.length == self.count else { + do { + let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + guard let match = detector.firstMatch(in: self, range: NSRange(location: 0, length: self.count)), match.range.length == self.count else { + return false + } + + return true + } catch { + assertionFailure("Couldn't find data detector for type link") return false } - - return true } fileprivate func toValue(_ index: Int) -> Character { diff --git a/focus-ios/Blockzilla/Extensions/URLExtensions.swift b/focus-ios/Blockzilla/Extensions/URLExtensions.swift index 50aaa0e9aaf9..51b0b6ba8519 100644 --- a/focus-ios/Blockzilla/Extensions/URLExtensions.swift +++ b/focus-ios/Blockzilla/Extensions/URLExtensions.swift @@ -32,7 +32,7 @@ private func loadEntriesFromDisk() -> TLDEntryMap? { } let lines = data.components(separatedBy: "\n") - let trimmedLines = lines.filter { !$0.hasPrefix("//") && $0 != "\n" && $0 != "" } + let trimmedLines = lines.filter { !$0.hasPrefix("//") && $0 != "\n" && !$0.isEmpty } var entries = TLDEntryMap() for line in trimmedLines { @@ -253,7 +253,7 @@ extension URL { public var normalizedHost: String? { // Use components.host instead of self.host since the former correctly preserves // brackets for IPv6 hosts, whereas the latter strips them. - guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var host = components.host, host != "" else { + guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var host = components.host, !host.isEmpty else { return nil } @@ -431,7 +431,7 @@ private extension URL { .backwards, // Search from the end. .anchored] // Stick to the end. let suffixlessHost = host.replacingOccurrences(of: suffix, with: "", options: literalFromEnd, range: nil) - let suffixlessTokens = suffixlessHost.components(separatedBy: ".").filter { $0 != "" } + let suffixlessTokens = suffixlessHost.components(separatedBy: ".").filter { !$0.isEmpty } let maxAdditionalCount = max(0, suffixlessTokens.count - additionalPartCount) let additionalParts = suffixlessTokens[maxAdditionalCount.. Bool var getCustomDomainSetting: () -> AutoCompleteSuggestions var setCustomDomainSetting: ([String]) -> Void @@ -41,6 +41,14 @@ public class CustomCompletionSource: CustomAutocompleteSource { self.setCustomDomainSetting = setCustomDomainSetting } + private func getRegex() -> NSRegularExpression { + do { + return try NSRegularExpression(pattern: "^(\\s+)?(?:https?:\\/\\/)?(?:www\\.)?", options: [.caseInsensitive]) + } catch { + fatalError("Invalid regex pattern") + } + } + public var enabled: Bool { return enableCustomDomainAutocomplete() } public func getSuggestions() -> AutoCompleteSuggestions { @@ -107,7 +115,11 @@ class TopDomainsCompletionSource: AutocompleteSource { private lazy var topDomains: [String] = { let filePath = Bundle.main.path(forResource: "topdomains", ofType: "txt") - return try! String(contentsOfFile: filePath!).components(separatedBy: "\n") + do { + return try String(contentsOfFile: filePath!).components(separatedBy: "\n") + } catch { + fatalError("Invalid content in \(filePath!)") + } }() func getSuggestions() -> AutoCompleteSuggestions { diff --git a/focus-ios/Blockzilla/Menu/Old/PhotonActionSheet.swift b/focus-ios/Blockzilla/Menu/Old/PhotonActionSheet.swift index d4cae713b3fc..67c3a9199218 100644 --- a/focus-ios/Blockzilla/Menu/Old/PhotonActionSheet.swift +++ b/focus-ios/Blockzilla/Menu/Old/PhotonActionSheet.swift @@ -308,6 +308,7 @@ private class PhotonActionSheetTitleHeaderView: UITableViewHeaderFooterView { } override func prepareForReuse() { + super.prepareForReuse() self.titleLabel.text = nil } } diff --git a/focus-ios/Blockzilla/Menu/Old/PhotonActionSheetCell.swift b/focus-ios/Blockzilla/Menu/Old/PhotonActionSheetCell.swift index 33ec26a3adea..29f2f3c7c16b 100644 --- a/focus-ios/Blockzilla/Menu/Old/PhotonActionSheetCell.swift +++ b/focus-ios/Blockzilla/Menu/Old/PhotonActionSheetCell.swift @@ -67,6 +67,7 @@ class PhotonActionSheetCell: UITableViewCell { }() override func prepareForReuse() { + super.prepareForReuse() self.statusIcon.image = nil disclosureIndicator.removeFromSuperview() disclosureLabel.removeFromSuperview() diff --git a/focus-ios/Blockzilla/Modules/WebView/LegacyWebViewController.swift b/focus-ios/Blockzilla/Modules/WebView/LegacyWebViewController.swift index c42dc2f81367..150e1d7f5888 100644 --- a/focus-ios/Blockzilla/Modules/WebView/LegacyWebViewController.swift +++ b/focus-ios/Blockzilla/Modules/WebView/LegacyWebViewController.swift @@ -224,9 +224,13 @@ class LegacyWebViewController: UIViewController, LegacyWebController { } private func addScript(forResource resource: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly mainFrameOnly: Bool) { - let source = try! String(contentsOf: Bundle.main.url(forResource: resource, withExtension: "js")!) - let script = WKUserScript(source: source, injectionTime: injectionTime, forMainFrameOnly: mainFrameOnly) - browserView.configuration.userContentController.addUserScript(script) + do { + let source = try String(contentsOf: Bundle.main.url(forResource: resource, withExtension: "js")!) + let script = WKUserScript(source: source, injectionTime: injectionTime, forMainFrameOnly: mainFrameOnly) + browserView.configuration.userContentController.addUserScript(script) + } catch { + fatalError("Invalid data in file \(resource)") + } } private func setupTrackingProtectionScripts() { @@ -337,6 +341,7 @@ class LegacyWebViewController: UIViewController, LegacyWebController { } override func viewDidLoad() { + super.viewDidLoad() self.browserView.addObserver(self, forKeyPath: "URL", options: .new, context: nil) searchInContentTelemetry = SearchInContentTelemetry() } diff --git a/focus-ios/Blockzilla/Pro Tips/TipManager.swift b/focus-ios/Blockzilla/Pro Tips/TipManager.swift index 680776bd5289..fd70f5d6d4f5 100644 --- a/focus-ios/Blockzilla/Pro Tips/TipManager.swift +++ b/focus-ios/Blockzilla/Pro Tips/TipManager.swift @@ -156,10 +156,11 @@ class TipManager { /// Return a string representing the trackers tip. It will include the current number of trackers blocked, formatted as a decimal. func shareTrackersDescription() -> String { - let numberOfTrackersBlocked = NSNumber(integerLiteral: UserDefaults.standard.integer(forKey: BrowserViewController.userDefaultsTrackersBlockedKey)) + let numberOfTrackersBlocked = UserDefaults.standard.integer(forKey: BrowserViewController.userDefaultsTrackersBlockedKey) let formatter = NumberFormatter() formatter.numberStyle = .decimal - return String(format: .shareTrackersTipDescription, formatter.string(from: numberOfTrackersBlocked) ?? "0") + let formattedNumber = formatter.string(from: NSNumber(value: numberOfTrackersBlocked)) ?? "0" + return String(format: .shareTrackersTipDescription, formattedNumber) } private var shareTrackersTip: Tip { diff --git a/focus-ios/Blockzilla/Settings/Controller/AboutViewController.swift b/focus-ios/Blockzilla/Settings/Controller/AboutViewController.swift index d87fc6d5f05b..0929bbf788de 100644 --- a/focus-ios/Blockzilla/Settings/Controller/AboutViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/AboutViewController.swift @@ -86,6 +86,7 @@ class AboutViewController: UIViewController, UITableViewDataSource, UITableViewD private let headerView = AboutHeaderView() override func viewDidLoad() { + super.viewDidLoad() headerView.delegate = self navigationController?.navigationBar.tintColor = .accent diff --git a/focus-ios/Blockzilla/Settings/Controller/AddCustomDomainViewController.swift b/focus-ios/Blockzilla/Settings/Controller/AddCustomDomainViewController.swift index 5355a1dad794..b35f68f0abd8 100644 --- a/focus-ios/Blockzilla/Settings/Controller/AddCustomDomainViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/AddCustomDomainViewController.swift @@ -59,6 +59,7 @@ class AddCustomDomainViewController: UIViewController, UITextFieldDelegate { } override func viewDidLoad() { + super.viewDidLoad() title = UIConstants.strings.autocompleteAddCustomUrl navigationController?.navigationBar.tintColor = .accent self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: UIConstants.strings.cancel, style: .plain, target: self, action: #selector(AddCustomDomainViewController.cancelTapped)) diff --git a/focus-ios/Blockzilla/Settings/Controller/AddSearchEngineViewController.swift b/focus-ios/Blockzilla/Settings/Controller/AddSearchEngineViewController.swift index 73152e4462c9..efa3a0fd22bb 100644 --- a/focus-ios/Blockzilla/Settings/Controller/AddSearchEngineViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/AddSearchEngineViewController.swift @@ -135,6 +135,7 @@ class AddSearchEngineViewController: UIViewController, UITextViewDelegate { } override func viewDidLoad() { + super.viewDidLoad() KeyboardHelper.defaultHelper.addDelegate(delegate: self) title = UIConstants.strings.AddSearchEngineTitle diff --git a/focus-ios/Blockzilla/Settings/Controller/AutocompleteCustomUrlViewController.swift b/focus-ios/Blockzilla/Settings/Controller/AutocompleteCustomUrlViewController.swift index 7651bd266392..333745f05443 100644 --- a/focus-ios/Blockzilla/Settings/Controller/AutocompleteCustomUrlViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/AutocompleteCustomUrlViewController.swift @@ -56,6 +56,7 @@ class AutocompleteCustomUrlViewController: UIViewController { } override func viewDidLoad() { + super.viewDidLoad() title = UIConstants.strings.autocompleteManageSitesLabel tableView.dataSource = self diff --git a/focus-ios/Blockzilla/Settings/Controller/AutocompleteSettingViewController.swift b/focus-ios/Blockzilla/Settings/Controller/AutocompleteSettingViewController.swift index 7be6ad1fe912..94626cfd13e8 100644 --- a/focus-ios/Blockzilla/Settings/Controller/AutocompleteSettingViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/AutocompleteSettingViewController.swift @@ -29,6 +29,7 @@ class AutocompleteSettingViewController: UIViewController, UITableViewDelegate, } override func viewDidLoad() { + super.viewDidLoad() title = UIConstants.strings.settingsAutocompleteSection navigationController?.navigationBar.tintColor = .accent diff --git a/focus-ios/Blockzilla/Settings/Controller/SafariInstructionsViewController.swift b/focus-ios/Blockzilla/Settings/Controller/SafariInstructionsViewController.swift index 32e89bc8faa7..b157c22aa9c7 100644 --- a/focus-ios/Blockzilla/Settings/Controller/SafariInstructionsViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/SafariInstructionsViewController.swift @@ -14,6 +14,7 @@ class SafariInstructionsViewController: UIViewController { }() override func viewDidLoad() { + super.viewDidLoad() view.backgroundColor = .systemBackground navigationController?.navigationBar.tintColor = .accent diff --git a/focus-ios/Blockzilla/Settings/Controller/SettingsViewController.swift b/focus-ios/Blockzilla/Settings/Controller/SettingsViewController.swift index ac8e22adc2c1..7a9876d4d129 100644 --- a/focus-ios/Blockzilla/Settings/Controller/SettingsViewController.swift +++ b/focus-ios/Blockzilla/Settings/Controller/SettingsViewController.swift @@ -161,6 +161,7 @@ class SettingsViewController: UIViewController, UITableViewDataSource, UITableVi } override func viewDidLoad() { + super.viewDidLoad() title = UIConstants.strings.settingsTitle let navigationBar = navigationController!.navigationBar diff --git a/focus-ios/Blockzilla/Siri/SiriFavoriteViewController.swift b/focus-ios/Blockzilla/Siri/SiriFavoriteViewController.swift index 9b718199a778..e6be8ce3985b 100644 --- a/focus-ios/Blockzilla/Siri/SiriFavoriteViewController.swift +++ b/focus-ios/Blockzilla/Siri/SiriFavoriteViewController.swift @@ -105,6 +105,7 @@ class SiriFavoriteViewController: UIViewController { } override func viewDidLoad() { + super.viewDidLoad() self.edgesForExtendedLayout = [] setUpInputUI() setUpEditUI() @@ -205,25 +206,29 @@ class SiriFavoriteViewController: UIViewController { Toast(text: UIConstants.strings.autocompleteAddCustomUrlError).show() return false } - let regex = try! NSRegularExpression(pattern: "^(\\s+)?(?:https?:\\/\\/)?(?:www\\.)?", options: [.caseInsensitive]) - var sanitizedDomain = regex.stringByReplacingMatches(in: domain, options: [], range: NSRange(location: 0, length: domain.count), withTemplate: "") - - guard !sanitizedDomain.isEmpty, sanitizedDomain.contains(".") else { - Toast(text: UIConstants.strings.autocompleteAddCustomUrlError).show() - return false - } - if sanitizedDomain.suffix(1) == "/" { - sanitizedDomain = String(sanitizedDomain.dropLast()) - } - if !sanitizedDomain.hasPrefix("http://") && !sanitizedDomain.hasPrefix("https://") { - sanitizedDomain = String(format: "https://%@", sanitizedDomain) - } - guard let url = URL(string: sanitizedDomain, invalidCharacters: false) else { - Toast(text: UIConstants.strings.autocompleteAddCustomUrlError).show() - return false + do { + let regex = try NSRegularExpression(pattern: "^(\\s+)?(?:https?:\\/\\/)?(?:www\\.)?", options: [.caseInsensitive]) + var sanitizedDomain = regex.stringByReplacingMatches(in: domain, options: [], range: NSRange(location: 0, length: domain.count), withTemplate: "") + + guard !sanitizedDomain.isEmpty, sanitizedDomain.contains(".") else { + Toast(text: UIConstants.strings.autocompleteAddCustomUrlError).show() + return false + } + if sanitizedDomain.suffix(1) == "/" { + sanitizedDomain = String(sanitizedDomain.dropLast()) + } + if !sanitizedDomain.hasPrefix("http://") && !sanitizedDomain.hasPrefix("https://") { + sanitizedDomain = String(format: "https://%@", sanitizedDomain) + } + guard let url = URL(string: sanitizedDomain, invalidCharacters: false) else { + Toast(text: UIConstants.strings.autocompleteAddCustomUrlError).show() + return false + } + UserDefaults.standard.set(url.absoluteString, forKey: "favoriteUrl") + TipManager.siriFavoriteTip = false + } catch { + fatalError("Invalid regular expression") } - UserDefaults.standard.set(url.absoluteString, forKey: "favoriteUrl") - TipManager.siriFavoriteTip = false return true } } diff --git a/focus-ios/Blockzilla/Theme/ThemeViewController.swift b/focus-ios/Blockzilla/Theme/ThemeViewController.swift index af372b241bc6..a12a27df5cca 100644 --- a/focus-ios/Blockzilla/Theme/ThemeViewController.swift +++ b/focus-ios/Blockzilla/Theme/ThemeViewController.swift @@ -64,6 +64,7 @@ class ThemeViewController: UIViewController { } override func viewDidLoad() { + super.viewDidLoad() title = UIConstants.strings.theme navigationController?.navigationBar.tintColor = .accent diff --git a/focus-ios/Blockzilla/Tracking Protection/TrackingProtectionViewController.swift b/focus-ios/Blockzilla/Tracking Protection/TrackingProtectionViewController.swift index 4f9fadc68584..69284b37eedd 100644 --- a/focus-ios/Blockzilla/Tracking Protection/TrackingProtectionViewController.swift +++ b/focus-ios/Blockzilla/Tracking Protection/TrackingProtectionViewController.swift @@ -357,7 +357,7 @@ class TrackingProtectionViewController: UIViewController { let urlToDocumentsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium - if let installDate = (try! FileManager.default.attributesOfItem(atPath: urlToDocumentsFolder.path)[FileAttributeKey.creationDate]) as? Date { + if let installDate = (try? FileManager.default.attributesOfItem(atPath: urlToDocumentsFolder.path)[FileAttributeKey.creationDate]) as? Date { let stringDate = dateFormatter.string(from: installDate) return stringDate } @@ -365,10 +365,10 @@ class TrackingProtectionViewController: UIViewController { } private func getNumberOfTrackersBlocked() -> String { - let numberOfTrackersBlocked = NSNumber(integerLiteral: UserDefaults.standard.integer(forKey: BrowserViewController.userDefaultsTrackersBlockedKey)) + let numberOfTrackersBlocked = UserDefaults.standard.integer(forKey: BrowserViewController.userDefaultsTrackersBlockedKey) let formatter = NumberFormatter() formatter.numberStyle = .decimal - return formatter.string(from: numberOfTrackersBlocked) ?? "0" + return formatter.string(from: NSNumber(value: numberOfTrackersBlocked)) ?? "0" } private func toggleProtection(isOn: Bool) { diff --git a/focus-ios/Blockzilla/UIComponents/ErrorPage/ErrorPage.swift b/focus-ios/Blockzilla/UIComponents/ErrorPage/ErrorPage.swift index e6144cb7bf10..5f5880bd166c 100644 --- a/focus-ios/Blockzilla/UIComponents/ErrorPage/ErrorPage.swift +++ b/focus-ios/Blockzilla/UIComponents/ErrorPage/ErrorPage.swift @@ -13,10 +13,13 @@ class ErrorPage { var data: Data { let file = Bundle.main.path(forResource: "errorPage", ofType: "html")! - - let page = try! String(contentsOfFile: file) - .replacingOccurrences(of: "%messageLong%", with: error.localizedDescription) - .replacingOccurrences(of: "%button%", with: UIConstants.strings.errorTryAgain) - return page.data(using: .utf8)! + do { + let page = try String(contentsOfFile: file) + .replacingOccurrences(of: "%messageLong%", with: error.localizedDescription) + .replacingOccurrences(of: "%button%", with: UIConstants.strings.errorTryAgain) + return page.data(using: .utf8)! + } catch { + fatalError("Invalid content in \(file)") + } } } diff --git a/focus-ios/ContentBlocker/ActionRequestHandler.swift b/focus-ios/ContentBlocker/ActionRequestHandler.swift index 9d5efd67a0a3..1696b7d9ba95 100644 --- a/focus-ios/ContentBlocker/ActionRequestHandler.swift +++ b/focus-ios/ContentBlocker/ActionRequestHandler.swift @@ -17,17 +17,25 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { } } - let mergedListJSON = try! JSONSerialization.data(withJSONObject: mergedList, options: JSONSerialization.WritingOptions(rawValue: 0)) - let attachment = NSItemProvider(item: mergedListJSON as NSSecureCoding?, typeIdentifier: kUTTypeJSON as String) - let item = NSExtensionItem() - item.attachments = [attachment] - context.completeRequest(returningItems: [item], completionHandler: nil) + do { + let mergedListJSON = try JSONSerialization.data(withJSONObject: mergedList, options: JSONSerialization.WritingOptions(rawValue: 0)) + let attachment = NSItemProvider(item: mergedListJSON as NSSecureCoding?, typeIdentifier: kUTTypeJSON as String) + let item = NSExtensionItem() + item.attachments = [attachment] + context.completeRequest(returningItems: [item], completionHandler: nil) + } catch { + fatalError("Invalid json list \(mergedList)") + } } /// Gets the dictionary form of the tracking list with the specified file name. private func itemsFromFile(_ name: String) -> [NSDictionary] { let url = Bundle.main.url(forResource: name, withExtension: "json") - let data = try! Data(contentsOf: url!) - return try! JSONSerialization.jsonObject(with: data, options: []) as! [NSDictionary] + do { + let data = try Data(contentsOf: url!) + return try JSONSerialization.jsonObject(with: data, options: []) as! [NSDictionary] + } catch { + fatalError("Invalid data at \(url!)") + } } } diff --git a/taskcluster/kinds/bitrise-performance/kind.yml b/taskcluster/kinds/bitrise-performance/kind.yml index f57011bfb3e4..a8e4eefcde3b 100644 --- a/taskcluster/kinds/bitrise-performance/kind.yml +++ b/taskcluster/kinds/bitrise-performance/kind.yml @@ -15,5 +15,6 @@ tasks: run-on-tasks-for: [] worker-type: bitrise bitrise: + artifact_prefix: public workflows: - Bitrise_Performance_Test diff --git a/taskcluster/kinds/firebase-performance/kind.yml b/taskcluster/kinds/firebase-performance/kind.yml index f180f11a3382..abca0ca6f3b7 100644 --- a/taskcluster/kinds/firebase-performance/kind.yml +++ b/taskcluster/kinds/firebase-performance/kind.yml @@ -15,5 +15,6 @@ tasks: run-on-tasks-for: [] worker-type: bitrise bitrise: + artifact_prefix: public workflows: - Firebase_Performance_Test