From b5cc1132390cdbfe21cd7bfb31a4069e61cf9dce Mon Sep 17 00:00:00 2001 From: Kyle Hickinson Date: Wed, 22 Nov 2023 10:14:06 -0500 Subject: [PATCH] Fix brave/brave-ios#8173: Update URL bar design & display origin-only URLs (brave/brave-ios#8417) --- .../Browser/BrowserViewController.swift | 150 +++++- .../BrowserViewController/BVC+Rewards.swift | 10 +- .../BrowserViewController+Onboarding.swift | 12 +- ...erViewController+ProductNotification.swift | 4 +- ...serViewController+TabManagerDelegate.swift | 1 + ...rowserViewController+ToolbarDelegate.swift | 109 ++-- ...rViewController+WKNavigationDelegate.swift | 6 + .../CertificateViewController.swift | 65 ++- Sources/Brave/Frontend/Browser/Tab.swift | 17 +- .../BottomToolbar/BottomToolbarView.swift | 13 +- .../Toolbars/HeaderContainerView.swift | 1 - .../Browser/Toolbars/PageSecurityView.swift | 104 ++++ .../Browser/Toolbars/ToolbarHelper.swift | 5 +- .../Browser/Toolbars/ToolbarProtocols.swift | 1 + .../Toolbars/UrlBar/CollapsedURLBarView.swift | 86 ++-- .../Toolbars/UrlBar/ReaderModeButton.swift | 7 + .../Toolbars/UrlBar/RewardsButton.swift | 2 +- .../Toolbars/UrlBar/TabLocationView.swift | 482 ++++++++++-------- .../Toolbars/UrlBar/TopToolbarView.swift | 172 ++++--- .../Brave/Frontend/Share/MenuActivity.swift | 52 ++ Sources/BraveStrings/BraveStrings.swift | 14 +- .../leo.lock.plain.symbolset/Contents.json | 11 + .../Contents.json | 11 + Tests/ClientTests/DisplayTextFieldTest.swift | 41 -- 24 files changed, 861 insertions(+), 515 deletions(-) create mode 100644 Sources/Brave/Frontend/Browser/Toolbars/PageSecurityView.swift create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.lock.plain.symbolset/Contents.json create mode 100644 Sources/DesignSystem/Icons/Symbols.xcassets/leo.verification.outline.symbolset/Contents.json delete mode 100644 Tests/ClientTests/DisplayTextFieldTest.swift diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController.swift index 53cc827e27f1..f0c17795305d 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController.swift @@ -656,7 +656,7 @@ public class BrowserViewController: UIViewController { updateTabsBarVisibility() } - private func updateToolbarSecureContentState(_ secureContentState: TabSecureContentState) { + func updateToolbarSecureContentState(_ secureContentState: TabSecureContentState) { topToolbar.secureContentState = secureContentState collapsedURLBarView.secureContentState = secureContentState } @@ -1789,8 +1789,12 @@ public class BrowserViewController: UIViewController { break } - if tab.secureContentState == .secure && !webView.hasOnlySecureContent { - tab.secureContentState = .insecure + if tab.secureContentState == .secure, !webView.hasOnlySecureContent, + tab.url?.origin == tab.webView?.url?.origin { + if let url = tab.webView?.url, url.isReaderModeURL { + break + } + tab.secureContentState = .mixedContent } if tabManager.selectedTab === tab { @@ -1809,9 +1813,9 @@ public class BrowserViewController: UIViewController { let internalUrl = InternalURL(url), (internalUrl.isAboutURL || internalUrl.isAboutHomeURL) { - tab.secureContentState = .localHost + tab.secureContentState = .localhost if tabManager.selectedTab === tab { - updateToolbarSecureContentState(.localHost) + updateToolbarSecureContentState(.localhost) } break } @@ -1821,27 +1825,29 @@ public class BrowserViewController: UIViewController { internalUrl.isErrorPage { if ErrorPageHelper.certificateError(for: url) != 0 { - tab.secureContentState = .insecure - if tabManager.selectedTab === tab { - updateToolbarSecureContentState(.insecure) - } - break + tab.secureContentState = .invalidCert + } else { + tab.secureContentState = .missingSSL + } + if tabManager.selectedTab === tab { + updateToolbarSecureContentState(tab.secureContentState) } + break } if url.isReaderModeURL || InternalURL.isValid(url: url) { - tab.secureContentState = .unknown + tab.secureContentState = .localhost if tabManager.selectedTab === tab { - updateToolbarSecureContentState(.unknown) + updateToolbarSecureContentState(.localhost) } break } // All our checks failed, we show the page as insecure - tab.secureContentState = .insecure + tab.secureContentState = .missingSSL } else { // When there is no URL, it's likely a new tab. - tab.secureContentState = .localHost + tab.secureContentState = .localhost } if tabManager.selectedTab === tab { @@ -1852,7 +1858,7 @@ public class BrowserViewController: UIViewController { guard let scheme = tab.webView?.url?.scheme, let host = tab.webView?.url?.host else { - tab.secureContentState = .insecure + tab.secureContentState = .unknown self.updateURLBar() return } @@ -1880,10 +1886,10 @@ public class BrowserViewController: UIViewController { try await BraveCertificateUtils.evaluateTrust(serverTrust, for: host) tab.secureContentState = .secure } else { - tab.secureContentState = .insecure + tab.secureContentState = .invalidCert } } catch { - tab.secureContentState = .insecure + tab.secureContentState = .invalidCert } Task { @MainActor in @@ -1938,7 +1944,7 @@ public class BrowserViewController: UIViewController { updateToolbarSecureContentState(tab.secureContentState) } - let isPage = tab.url?.displayURL?.isWebPage() ?? false + let isPage = tab.url?.isWebPage() ?? false navigationToolbar.updatePageStatus(isPage) updateWebViewPageZoom(tab: tab) } @@ -2119,6 +2125,22 @@ public class BrowserViewController: UIViewController { activities.append(sendTabToSelfActivity) } + if let tab = self.tabManager.selectedTab, tab.secureContentState.shouldDisplayWarning { + if tab.readerModeAvailableOrActive { + // If the reader mode button is occluded due to a secure content state warning add it as an activity + activities.append( + BasicMenuActivity( + title: Strings.toggleReaderMode, + braveSystemImage: "leo.product.speedreader", + callback: { [weak self] in + self?.toggleReaderMode() + } + ) + ) + } + // Any other buttons on the leading side of the location view should be added here as well + } + let findInPageActivity = FindInPageActivity() { [unowned self] in if #available(iOS 16.0, *), let findInteraction = self.tabManager.selectedTab?.webView?.findInteraction { findInteraction.searchText = "" @@ -2252,6 +2274,13 @@ public class BrowserViewController: UIViewController { activities.append(addSearchEngineActivity) } + if let secureState = tabManager.selectedTab?.secureContentState, secureState != .missingSSL && secureState != .unknown { + let displayCertificateActivity = BasicMenuActivity(title: Strings.displayCertificate, braveSystemImage: "leo.lock.plain") { [weak self] in + self?.displayPageCertificateInfo() + } + activities.append(displayCertificateActivity) + } + activities.append(ReportWebCompatibilityIssueActivity() { [weak self] in self?.showSubmitReportView(for: url) }) @@ -2463,6 +2492,20 @@ public class BrowserViewController: UIViewController { } } + func toggleReaderMode() { + guard let tab = tabManager.selectedTab else { return } + if let readerMode = tab.getContentScript(name: ReaderModeScriptHandler.scriptName) as? ReaderModeScriptHandler { + switch readerMode.state { + case .available: + enableReaderMode() + case .active: + disableReaderMode() + case .unavailable: + break + } + } + } + func handleToolbarVisibilityStateChange( _ state: ToolbarVisibilityViewModel.ToolbarState, progress: CGFloat? @@ -2774,7 +2817,7 @@ extension BrowserViewController: TabDelegate { contentController: vc, contentSizeBehavior: .preferredContentSize) popover.addsConvenientDismissalMargins = false - popover.present(from: topToolbar.locationView.rewardsButton, on: self) + popover.present(from: topToolbar.rewardsButton, on: self) popover.popoverDidDismiss = { _ in // This gets called if popover is dismissed by user gesture // This does not conflict with 'Enable Rewards' button. @@ -2791,7 +2834,7 @@ extension BrowserViewController: TabDelegate { let popover2 = PopoverController( contentController: vc2, contentSizeBehavior: .preferredContentSize) - popover2.present(from: self.topToolbar.locationView.rewardsButton, on: self) + popover2.present(from: self.topToolbar.rewardsButton, on: self) } vc.linkTapped = { [unowned self] request in @@ -3411,3 +3454,70 @@ extension BrowserViewController: IAPObserverDelegate { } } } + +// Certificate info +extension BrowserViewController { + + func displayPageCertificateInfo() { + guard let webView = tabManager.selectedTab?.webView else { + Logger.module.error("Invalid WebView") + return + } + + let getServerTrustForErrorPage = { () -> SecTrust? in + do { + if let url = webView.url { + return try ErrorPageHelper.serverTrust(from: url) + } + } catch { + Logger.module.error("\(error.localizedDescription)") + } + + return nil + } + + guard let trust = webView.serverTrust ?? getServerTrustForErrorPage() else { + return + } + + let host = webView.url?.host + + Task.detached { + let serverCertificates: [SecCertificate] = SecTrustCopyCertificateChain(trust) as? [SecCertificate] ?? [] + + // TODO: Instead of showing only the first cert in the chain, + // have a UI that allows users to select any certificate in the chain (similar to Desktop browsers) + if let serverCertificate = serverCertificates.first, + let certificate = BraveCertificateModel(certificate: serverCertificate) { + + var errorDescription: String? + + do { + try await BraveCertificateUtils.evaluateTrust(trust, for: host) + } catch { + Logger.module.error("\(error.localizedDescription)") + + // Remove the common-name from the first part of the error message + // This is because the certificate viewer already displays it. + // If it doesn't match, it won't be removed, so this is fine. + errorDescription = error.localizedDescription + if let range = errorDescription?.range(of: "“\(certificate.subjectName.commonName)” ") ?? + errorDescription?.range(of: "\"\(certificate.subjectName.commonName)\" ") { + errorDescription = errorDescription?.replacingCharacters(in: range, with: "").capitalizeFirstLetter + } + } + + await MainActor.run { [errorDescription] in + if #available(iOS 16.0, *) { + // System components sit on top so we want to dismiss it + webView.findInteraction?.dismissFindNavigator() + } + let certificateViewController = CertificateViewController(certificate: certificate, evaluationError: errorDescription) + certificateViewController.modalPresentationStyle = .pageSheet + certificateViewController.sheetPresentationController?.detents = [.medium(), .large()] + self.present(certificateViewController, animated: true) + } + } + } + } +} diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift index 30815525fb4e..adbfa912038a 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift @@ -22,11 +22,11 @@ extension BrowserViewController { func updateRewardsButtonState() { if !isViewLoaded { return } if !BraveRewards.isAvailable { - self.topToolbar.locationView.rewardsButton.isHidden = true + self.topToolbar.rewardsButton.isHidden = true return } - self.topToolbar.locationView.rewardsButton.isHidden = Preferences.Rewards.hideRewardsIcon.value || privateBrowsingManager.isPrivateBrowsing - self.topToolbar.locationView.rewardsButton.iconState = Preferences.Rewards.rewardsToggledOnce.value ? (rewards.isEnabled || rewards.isCreatingWallet ? .enabled : .disabled) : .initial + self.topToolbar.rewardsButton.isHidden = Preferences.Rewards.hideRewardsIcon.value || privateBrowsingManager.isPrivateBrowsing + self.topToolbar.rewardsButton.iconState = Preferences.Rewards.rewardsToggledOnce.value ? (rewards.isEnabled || rewards.isCreatingWallet ? .enabled : .disabled) : .initial } func showBraveRewardsPanel() { @@ -44,7 +44,7 @@ extension BrowserViewController { Preferences.FullScreenCallout.rewardsCalloutCompleted.value = true present(controller, animated: true) - topToolbar.locationView.rewardsButton.iconState = Preferences.Rewards.rewardsToggledOnce.value ? (rewards.isEnabled || rewards.isCreatingWallet ? .enabled : .disabled) : .initial + topToolbar.rewardsButton.iconState = Preferences.Rewards.rewardsToggledOnce.value ? (rewards.isEnabled || rewards.isCreatingWallet ? .enabled : .disabled) : .initial return } @@ -70,7 +70,7 @@ extension BrowserViewController { let popover = PopoverController(contentController: braveRewardsPanel) popover.addsConvenientDismissalMargins = false - popover.present(from: topToolbar.locationView.rewardsButton, on: self) + popover.present(from: topToolbar.rewardsButton, on: self) popover.popoverDidDismiss = { [weak self] _ in guard let self = self else { return } if let tabId = self.tabManager.selectedTab?.rewardsId, self.rewards.rewardsAPI?.selectedTabId == 0 { diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift index d286ea27dea2..ff8c9939ead1 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift @@ -82,10 +82,10 @@ extension BrowserViewController { guard presentedViewController == nil else { return } - + let frame = view.convert( - topToolbar.locationView.urlTextField.frame, - from: topToolbar.locationView).insetBy(dx: -7.0, dy: -1.0) + topToolbar.locationView.frame, + from: topToolbar.locationView).insetBy(dx: -1.0, dy: -1.0) // Present the popover let controller = WelcomeOmniBoxOnboardingController() @@ -95,7 +95,7 @@ extension BrowserViewController { presentPopoverContent( using: controller, - with: frame, cornerRadius: 6.0, + with: frame, cornerRadius: topToolbar.locationContainer.layer.cornerRadius, didDismiss: { [weak self] in guard let self = self else { return } @@ -275,12 +275,12 @@ extension BrowserViewController { } let popover = PopoverController(contentController: controller) - popover.previewForOrigin = .init(view: topToolbar.locationView.shieldsButton, action: { [weak self] popover in + popover.previewForOrigin = .init(view: topToolbar.shieldsButton, action: { [weak self] popover in popover.dismissPopover() { self?.presentBraveShieldsViewController() } }) - popover.present(from: topToolbar.locationView.shieldsButton, on: self) + popover.present(from: topToolbar.shieldsButton, on: self) popover.popoverDidDismiss = { [weak self] _ in DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift index 29c29cd21359..42d9a2b68b02 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift @@ -143,7 +143,7 @@ extension BrowserViewController { let popover = PopoverController(contentController: controller) popover.addsConvenientDismissalMargins = false - popover.previewForOrigin = .init(view: topToolbar.locationView.shieldsButton) - popover.present(from: topToolbar.locationView.shieldsButton, on: self) + popover.previewForOrigin = .init(view: topToolbar.shieldsButton) + popover.present(from: topToolbar.shieldsButton, on: self) } } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift index abde7acab7a8..fcf66a7f6c40 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift @@ -426,6 +426,7 @@ extension BrowserViewController: TabManagerDelegate { toolbar?.searchButton.menu = UIMenu(title: "", identifier: nil, children: addTabMenuActionList) // Update Actions for Add-Tab Button + topToolbar.addTabButton.menu = UIMenu(title: "", identifier: nil, children: addTabMenuActionList) toolbar?.addTabButton.menu = UIMenu(title: "", identifier: nil, children: addTabMenuActionList) } } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift index c34d1d1b6f31..981683d519e3 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift @@ -60,69 +60,6 @@ extension BrowserViewController: TopToolbarDelegate { present(container, animated: !isExternallyPresented) } - func topToolbarDidPressLockImageView(_ urlBar: TopToolbarView) { - guard let webView = tabManager.selectedTab?.webView else { - Logger.module.error("Invalid WebView") - return - } - - let getServerTrustForErrorPage = { () -> SecTrust? in - do { - if let url = webView.url { - return try ErrorPageHelper.serverTrust(from: url) - } - } catch { - Logger.module.error("\(error.localizedDescription)") - } - - return nil - } - - guard let trust = webView.serverTrust ?? getServerTrustForErrorPage() else { - return - } - - let host = webView.url?.host - - Task.detached { - let serverCertificates: [SecCertificate] = SecTrustCopyCertificateChain(trust) as? [SecCertificate] ?? [] - - // TODO: Instead of showing only the first cert in the chain, - // have a UI that allows users to select any certificate in the chain (similar to Desktop browsers) - if let serverCertificate = serverCertificates.first, - let certificate = BraveCertificateModel(certificate: serverCertificate) { - - var errorDescription: String? - - do { - try await BraveCertificateUtils.evaluateTrust(trust, for: host) - } catch { - Logger.module.error("\(error.localizedDescription)") - - // Remove the common-name from the first part of the error message - // This is because the certificate viewer already displays it. - // If it doesn't match, it won't be removed, so this is fine. - errorDescription = error.localizedDescription - if let range = errorDescription?.range(of: "“\(certificate.subjectName.commonName)” ") ?? - errorDescription?.range(of: "\"\(certificate.subjectName.commonName)\" ") { - errorDescription = errorDescription?.replacingCharacters(in: range, with: "").capitalizeFirstLetter - } - } - - await MainActor.run { [errorDescription] in - if #available(iOS 16.0, *) { - // System components sit on top so we want to dismiss it - webView.findInteraction?.dismissFindNavigator() - } - let certificateViewController = CertificateViewController(certificate: certificate, evaluationError: errorDescription) - let popover = PopoverController(contentController: certificateViewController, contentSizeBehavior: .autoLayout(.phoneBounds)) - popover.addsConvenientDismissalMargins = true - popover.present(from: self.topToolbar.locationView.lockImageView.imageView!, on: self) - } - } - } - } - func topToolbarDidPressReload(_ topToolbar: TopToolbarView) { if let url = topToolbar.currentURL { if url.isIPFSScheme { @@ -189,18 +126,7 @@ extension BrowserViewController: TopToolbarDelegate { } func topToolbarDidPressReaderMode(_ topToolbar: TopToolbarView) { - if let tab = tabManager.selectedTab { - if let readerMode = tab.getContentScript(name: ReaderModeScriptHandler.scriptName) as? ReaderModeScriptHandler { - switch readerMode.state { - case .available: - enableReaderMode() - case .active: - disableReaderMode() - case .unavailable: - break - } - } - } + toggleReaderMode() } func topToolbarDidPressPlaylistButton(_ urlBar: TopToolbarView) { @@ -518,7 +444,7 @@ extension BrowserViewController: TopToolbarDelegate { let container = PopoverNavigationController(rootViewController: shields) let popover = PopoverController(contentController: container, contentSizeBehavior: .preferredContentSize) - popover.present(from: topToolbar.locationView.shieldsButton, on: self) + popover.present(from: topToolbar.shieldsButton, on: self) } func showSubmitReportView(for url: URL) { @@ -535,8 +461,8 @@ extension BrowserViewController: TopToolbarDelegate { viewController.modalPresentationStyle = .popover if let popover = viewController.popoverPresentationController { - popover.sourceView = topToolbar.locationView.shieldsButton - popover.sourceRect = topToolbar.locationView.shieldsButton.bounds + popover.sourceView = topToolbar.shieldsButton + popover.sourceRect = topToolbar.shieldsButton.bounds let sheet = popover.adaptiveSheetPresentationController sheet.largestUndimmedDetentIdentifier = .medium @@ -886,8 +812,15 @@ extension BrowserViewController: ToolbarDelegate { let selectedTabURL: URL? = { guard let url = tabManager.selectedTab?.url else { return nil } - if (InternalURL.isValid(url: url) || url.isLocal) && !url.isReaderModeURL { return nil } - + if let internalURL = InternalURL(url) { + if internalURL.isErrorPage { + return internalURL.originalURLFromErrorPage + } + if internalURL.isReaderModePage { + return internalURL.extractedUrlParam + } + return nil + } return url }() @@ -936,6 +869,22 @@ extension BrowserViewController: ToolbarDelegate { func tabToolbarDidPressTabs(_ tabToolbar: ToolbarProtocol, button: UIButton) { showTabTray() } + + func topToolbarDidTapSecureContentState(_ urlBar: TopToolbarView) { + guard let tab = tabManager.selectedTab, let url = tab.url, let secureContentStateButton = urlBar.locationView.secureContentStateButton else { return } + let hasCertificate = (tab.webView?.serverTrust ?? (try? ErrorPageHelper.serverTrust(from: url))) != nil + let pageSecurityView = PageSecurityView( + displayURL: urlBar.locationView.urlDisplayLabel.text ?? url.absoluteDisplayString, + secureState: tab.secureContentState, + hasCertificate: hasCertificate, + presentCertificateViewer: { [weak self] in + self?.dismiss(animated: true) + self?.displayPageCertificateInfo() + } + ) + let popoverController = PopoverController(content: pageSecurityView) + popoverController.present(from: secureContentStateButton, on: self) + } func showBackForwardList() { if let backForwardList = tabManager.selectedTab?.webView?.backForwardList { diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift index fe0d147e9988..29b81f63fb62 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -63,8 +63,14 @@ extension BrowserViewController: WKNavigationDelegate { } toolbarVisibilityViewModel.toolbarState = .expanded + // check if web view is loading a different origin than the one currently loaded if let selectedTab = tabManager.selectedTab, selectedTab.url?.origin != webView.url?.origin { + // reset secure content state to unknown until page can be evaluated + if let url = webView.url, !InternalURL.isValid(url: url) { + selectedTab.secureContentState = .unknown + updateToolbarSecureContentState(.unknown) + } // new site has a different origin, hide wallet icon. tabManager.selectedTab?.isWalletIconVisible = false // new site, reset connected addresses diff --git a/Sources/Brave/Frontend/Browser/Certificate Viewer/CertificateViewController.swift b/Sources/Brave/Frontend/Browser/Certificate Viewer/CertificateViewController.swift index c1b43653bb3d..3ba9491aed48 100644 --- a/Sources/Brave/Frontend/Browser/Certificate Viewer/CertificateViewController.swift +++ b/Sources/Brave/Frontend/Browser/Certificate Viewer/CertificateViewController.swift @@ -10,43 +10,34 @@ import BraveUI import Shared import BraveShared import CertificateUtilities +import DesignSystem private struct CertificateTitleView: View { let isRootCertificate: Bool - let commonName: String let evaluationError: String? var body: some View { VStack { - Text(commonName) - .font(.callout.weight(.bold)) - .lineLimit(nil) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) HStack(alignment: .firstTextBaseline, spacing: 4.0) { if let evaluationError = evaluationError { - Image(systemName: "xmark.circle.fill") - .foregroundColor(Color(.braveErrorLabel)) - .font(.callout) + Image(braveSystemName: "leo.warning.triangle-filled") + .foregroundColor(Color(braveSystemName: .systemfeedbackErrorIcon)) Text(evaluationError) - .font(.callout) - .foregroundColor(Color(.braveErrorLabel)) + .foregroundColor(Color(braveSystemName: .systemfeedbackErrorText)) .lineLimit(nil) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) } else { - Image(systemName: "checkmark.seal") - .foregroundColor(Color(.braveSuccessLabel)) - .font(.callout) + Image(braveSystemName: "leo.verification.outline") + .foregroundColor(Color(braveSystemName: .systemfeedbackSuccessIcon)) Text(Strings.CertificateViewer.certificateIsValidTitle) - .font(.callout) - .foregroundColor(Color(.secondaryBraveLabel)) + .foregroundColor(Color(braveSystemName: .systemfeedbackSuccessText)) .lineLimit(nil) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) } } + .font(.callout) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } + .frame(maxWidth: .infinity) } } @@ -90,27 +81,33 @@ private struct CertificateView: View { let model: BraveCertificateModel let evaluationError: String? + @Environment(\.dismiss) private var dismiss + var body: some View { - VStack(spacing: 0.0) { - CertificateTitleView( - isRootCertificate: model.isRootCertificate, - commonName: model.subjectName.commonName, - evaluationError: evaluationError - ) - .padding() - .frame(maxWidth: .infinity, alignment: .center) - .background(Color(.secondaryBraveGroupedBackground)) - - Divider() - .shadow(color: Color.black.opacity(0.1), - radius: 5.0) - + NavigationView { List { + Section(header: Color.clear.frame(height: 0)) { + CertificateTitleView( + isRootCertificate: model.isRootCertificate, + evaluationError: evaluationError + ) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) + } content } .listStyle(InsetGroupedListStyle()) .listBackgroundColor(Color(UIColor.braveGroupedBackground)) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(Strings.done) { + dismiss() + } + } + } + .navigationTitle(model.subjectName.commonName) + .navigationBarTitleDisplayMode(.inline) } + .navigationViewStyle(.stack) } @ViewBuilder diff --git a/Sources/Brave/Frontend/Browser/Tab.swift b/Sources/Brave/Frontend/Browser/Tab.swift index 971f50ffea9a..330f5406fb62 100644 --- a/Sources/Brave/Frontend/Browser/Tab.swift +++ b/Sources/Brave/Frontend/Browser/Tab.swift @@ -58,10 +58,21 @@ protocol URLChangeDelegate { } enum TabSecureContentState { - case localHost - case secure - case insecure case unknown + case localhost + case secure + case invalidCert + case missingSSL + case mixedContent + + var shouldDisplayWarning: Bool { + switch self { + case .unknown, .invalidCert, .missingSSL, .mixedContent: + return true + case .localhost, .secure: + return false + } + } } class Tab: NSObject { diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift index 8d17d2547d8f..95a013bf9ce1 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift @@ -30,8 +30,7 @@ class BottomToolbarView: UIView, ToolbarProtocol { init(privateBrowsingManager: PrivateBrowsingManager) { self.privateBrowsingManager = privateBrowsingManager - let isBeta = AppConstants.buildChannel == .beta - actionButtons = [backButton, isBeta ? shareButton : forwardButton, addTabButton, searchButton, tabsButton, menuButton] + actionButtons = [backButton, forwardButton, addTabButton, searchButton, tabsButton, menuButton] super.init(frame: .zero) setupAccessibility() @@ -143,4 +142,14 @@ class BottomToolbarView: UIView, ToolbarProtocol { break } } + + func updateForwardStatus(_ canGoForward: Bool) { + if canGoForward, let shareIndex = contentView.arrangedSubviews.firstIndex(of: shareButton) { + shareButton.removeFromSuperview() + contentView.insertArrangedSubview(forwardButton, at: shareIndex) + } else if !canGoForward, let forwardIndex = contentView.arrangedSubviews.firstIndex(of: forwardButton) { + forwardButton.removeFromSuperview() + contentView.insertArrangedSubview(shareButton, at: forwardIndex) + } + } } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift b/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift index a03493ae81cb..34a02a4606e9 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift @@ -13,7 +13,6 @@ class HeaderContainerView: UIView { let expandedBarStackView = UIStackView().then { $0.axis = .vertical - $0.clipsToBounds = true } let collapsedBarContainerView = UIControl().then { $0.alpha = 0 diff --git a/Sources/Brave/Frontend/Browser/Toolbars/PageSecurityView.swift b/Sources/Brave/Frontend/Browser/Toolbars/PageSecurityView.swift new file mode 100644 index 000000000000..f292ad3a33e3 --- /dev/null +++ b/Sources/Brave/Frontend/Browser/Toolbars/PageSecurityView.swift @@ -0,0 +1,104 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import SwiftUI +import BraveCore +import BraveUI +import Shared + +/// Displays warnings about the pages security +/// +/// Currently this is only shown when the page security requires a visible warning on the URL bar +struct PageSecurityView: View { + var displayURL: String + var secureState: TabSecureContentState + var hasCertificate: Bool + var presentCertificateViewer: () -> Void + + @Environment(\.pixelLength) private var pixelLength + + private var warningTitle: String { + switch secureState { + case .secure, .localhost: + return "" + case .unknown, .invalidCert, .missingSSL: + return Strings.PageSecurityView.pageNotSecureTitle + case .mixedContent: + return Strings.PageSecurityView.pageNotFullySecureTitle + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { + Text(displayURL) + .font(.headline) + .foregroundStyle(Color(braveSystemName: .textPrimary)) + HStack(alignment: .firstTextBaseline) { + Image(braveSystemName: "leo.warning.triangle-filled") + .foregroundColor(Color(braveSystemName: .systemfeedbackErrorIcon)) + VStack(alignment: .leading, spacing: 4) { + Text(warningTitle) + .foregroundColor(Color(braveSystemName: .systemfeedbackErrorText)) + Text(Strings.PageSecurityView.pageNotSecureDetailedWarning) + .foregroundColor(Color(braveSystemName: .textTertiary)) + .font(.footnote) + } + } + } + .multilineTextAlignment(.leading) + .font(.subheadline) + .padding() + if hasCertificate { + Color(braveSystemName: .dividerSubtle) + .frame(height: pixelLength) + Button { + presentCertificateViewer() + } label: { + HStack(alignment: .firstTextBaseline) { + Label(Strings.PageSecurityView.viewCertificateButtonTitle, braveSystemImage: "leo.lock.plain") + Spacer() + Image(braveSystemName: "leo.carat.right") + .imageScale(.large) + } + .font(.subheadline) + .foregroundColor(Color(braveSystemName: .textInteractive)) + .padding() + } + } + } + .background(Color(.braveBackground)) + .frame(maxWidth: BraveUX.baseDimensionValue, alignment: .leading) +#if DEBUG + .onAppear { + assert(secureState.shouldDisplayWarning, + "Currently only supports displaying insecure warnings") + } +#endif + } +} + +extension PageSecurityView: PopoverContentComponent { + var popoverBackgroundColor: UIColor { + UIColor.braveBackground + } +} + +#if swift(>=5.9) +#if DEBUG +#Preview { + PageSecurityView( + displayURL: "http.badssl.com", + secureState: .missingSSL, + hasCertificate: false, + presentCertificateViewer: { } + ) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .shadow(radius: 10, x: 0, y: 1) + .padding() +} +#endif +#endif diff --git a/Sources/Brave/Frontend/Browser/Toolbars/ToolbarHelper.swift b/Sources/Brave/Frontend/Browser/Toolbars/ToolbarHelper.swift index 35e8845710c7..e9856a3041f6 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/ToolbarHelper.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/ToolbarHelper.swift @@ -20,7 +20,7 @@ class ToolbarHelper: NSObject { toolbar.backButton.addGestureRecognizer(longPressGestureBackButton) toolbar.backButton.addTarget(self, action: #selector(didClickBack), for: .touchUpInside) - toolbar.shareButton.setImage(UIImage(named: "nav-share", in: .module, compatibleWith: nil)!.template, for: .normal) + toolbar.shareButton.setImage(UIImage(braveSystemNamed: "leo.share.macos"), for: .normal) toolbar.shareButton.accessibilityLabel = Strings.tabToolbarShareButtonAccessibilityLabel toolbar.shareButton.addTarget(self, action: #selector(didClickShare), for: UIControl.Event.touchUpInside) @@ -93,7 +93,8 @@ class ToolbarHelper: NSObject { toolbar.forwardButton, toolbar.addTabButton, toolbar.menuButton, - toolbar.searchButton + toolbar.searchButton, + toolbar.shareButton ] + additionalButtons for button in buttons { button.setPreferredSymbolConfiguration(config, forImageIn: .normal) diff --git a/Sources/Brave/Frontend/Browser/Toolbars/ToolbarProtocols.swift b/Sources/Brave/Frontend/Browser/Toolbars/ToolbarProtocols.swift index 69c3f0d8321e..9e979428fd98 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/ToolbarProtocols.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/ToolbarProtocols.swift @@ -18,6 +18,7 @@ protocol ToolbarProtocol: AnyObject { var actionButtons: [UIButton] { get } func updateBackStatus(_ canGoBack: Bool) + func updateForwardStatus(_ canGoForward: Bool) func updatePageStatus(_ isWebPage: Bool) func updateTabCount(_ count: Int) } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/CollapsedURLBarView.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/CollapsedURLBarView.swift index 79df78221f94..da26110c5434 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/CollapsedURLBarView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/CollapsedURLBarView.swift @@ -14,17 +14,14 @@ class CollapsedURLBarView: UIView { private let stackView = UIStackView().then { $0.spacing = 4 $0.isUserInteractionEnabled = false + $0.alignment = .firstBaseline } - private let lockImageView = ToolbarButton().then { - $0.setImage(UIImage(braveSystemNamed: "brave.lock.alt", compatibleWith: nil), for: .normal) - $0.isHidden = true - $0.tintColor = .bravePrimary - $0.isAccessibilityElement = true - $0.imageView?.contentMode = .center - $0.contentHorizontalAlignment = .center - $0.accessibilityLabel = Strings.tabToolbarLockImageAccessibilityLabel - $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + private let secureContentStateView = UIButton() + private let separatorLine = UILabel().then { + $0.isUserInteractionEnabled = false + $0.isAccessibilityElement = false + $0.text = "–" // en dash } private let urlLabel = UILabel().then { @@ -49,20 +46,44 @@ class CollapsedURLBarView: UIView { } private func updateLockImageView() { - lockImageView.isHidden = false + secureContentStateView.isHidden = !secureContentState.shouldDisplayWarning + separatorLine.isHidden = secureContentStateView.isHidden + secureContentStateView.configuration = secureContentStateButtonConfiguration + } + + private var secureContentStateButtonConfiguration: UIButton.Configuration { + let clampedTraitCollection = traitCollection.clampingSizeCategory(maximum: .accessibilityLarge) + var configuration = UIButton.Configuration.plain() + configuration.preferredSymbolConfigurationForImage = .init(font: .preferredFont(forTextStyle: .caption1, compatibleWith: clampedTraitCollection), scale: .small) + configuration.buttonSize = .small + configuration.imagePadding = 4 + configuration.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) + + var title = AttributedString(Strings.tabToolbarNotSecureTitle) + title.font = .preferredFont(forTextStyle: .caption1, compatibleWith: clampedTraitCollection) + + let isTitleVisible = !traitCollection.preferredContentSizeCategory.isAccessibilityCategory switch secureContentState { - case .localHost: - lockImageView.isHidden = true - case .insecure: - lockImageView.setImage(UIImage(braveSystemNamed: "leo.info.filled")? - .withRenderingMode(.alwaysOriginal) - .withTintColor(.braveErrorLabel), for: .normal) - lockImageView.accessibilityLabel = Strings.tabToolbarWarningImageAccessibilityLabel - case .secure, .unknown: - lockImageView.setImage(UIImage(braveSystemNamed: "brave.lock.alt", compatibleWith: nil), for: .normal) - lockImageView.accessibilityLabel = Strings.tabToolbarLockImageAccessibilityLabel + case .localhost, .secure: + break + case .invalidCert: + configuration.baseForegroundColor = UIColor(braveSystemName: .systemfeedbackErrorIcon) + if isTitleVisible { + configuration.attributedTitle = title + } + configuration.image = UIImage(braveSystemNamed: "leo.warning.triangle-filled") + case .missingSSL, .mixedContent: + configuration.baseForegroundColor = UIColor(braveSystemName: .textTertiary) + if isTitleVisible { + configuration.attributedTitle = title + } + configuration.image = UIImage(braveSystemNamed: "leo.warning.triangle-filled") + case .unknown: + configuration.baseForegroundColor = UIColor(braveSystemName: .iconDefault) + configuration.image = UIImage(braveSystemNamed: "leo.warning.circle-filled") } + return configuration } var secureContentState: TabSecureContentState = .unknown { @@ -96,7 +117,8 @@ class CollapsedURLBarView: UIView { clipsToBounds = false addSubview(stackView) - stackView.addArrangedSubview(lockImageView) + stackView.addArrangedSubview(secureContentStateView) + stackView.addArrangedSubview(separatorLine) stackView.addArrangedSubview(urlLabel) stackView.snp.makeConstraints { @@ -107,6 +129,10 @@ class CollapsedURLBarView: UIView { $0.centerX.equalToSuperview() } + secureContentStateView.configurationUpdateHandler = { [unowned self] button in + button.configuration = secureContentStateButtonConfiguration + } + updateForTraitCollectionAndBrowserColors() } @@ -117,19 +143,15 @@ class CollapsedURLBarView: UIView { private func updateForTraitCollectionAndBrowserColors() { let clampedTraitCollection = traitCollection.clampingSizeCategory(maximum: .accessibilityLarge) - lockImageView.setPreferredSymbolConfiguration( - .init( - pointSize: UIFont.preferredFont( - forTextStyle: .footnote, - compatibleWith: clampedTraitCollection - ).pointSize, - weight: .semibold - ), - forImageIn: .normal - ) urlLabel.font = .preferredFont(forTextStyle: .caption1, compatibleWith: clampedTraitCollection) - lockImageView.tintColor = browserColors.iconDefault urlLabel.textColor = browserColors.textPrimary + separatorLine.font = urlLabel.font + separatorLine.textColor = browserColors.dividerSubtle + } + + override func didMoveToWindow() { + super.didMoveToWindow() + setNeedsUpdateConstraints() } override func updateConstraints() { diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/ReaderModeButton.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/ReaderModeButton.swift index 88e8ad0bbe09..13363d15772a 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/ReaderModeButton.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/ReaderModeButton.swift @@ -70,6 +70,13 @@ class ReaderModeButton: UIButton { ) } + override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + let toolbarTraitCollection = UITraitCollection(preferredContentSizeCategory: traitCollection.toolbarButtonContentSizeCategory) + size.width = UIFontMetrics(forTextStyle: .body).scaledValue(for: 44, compatibleWith: toolbarTraitCollection) + return size + } + var readerModeState: ReaderModeState { get { return _readerModeState diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/RewardsButton.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/RewardsButton.swift index 47a59840c37e..3190e57cf0d7 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/RewardsButton.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/RewardsButton.swift @@ -40,7 +40,7 @@ class RewardsButton: UIButton { updateView() lookAtMeBadge.snp.makeConstraints { - $0.top.equalToSuperview() + $0.centerY.equalTo(imageView!.snp.centerY).offset(-8) $0.leading.equalTo(imageView!.snp.centerX) $0.size.equalTo(16) } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift index 9e6675498edd..49a3869737c6 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift @@ -9,6 +9,9 @@ import Preferences import Combine import BraveCore import DesignSystem +import BraveStrings +import Strings +import NaturalLanguage protocol TabLocationViewDelegate { func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) @@ -16,13 +19,11 @@ protocol TabLocationViewDelegate { func tabLocationViewDidBeginDragInteraction(_ tabLocationView: TabLocationView) func tabLocationViewDidTapPlaylist(_ tabLocationView: TabLocationView) func tabLocationViewDidTapPlaylistMenuAction(_ tabLocationView: TabLocationView, action: PlaylistURLBarButton.MenuAction) - func tabLocationViewDidTapLockImageView(_ tabLocationView: TabLocationView) func tabLocationViewDidTapReload(_ tabLocationView: TabLocationView) func tabLocationViewDidTapStop(_ tabLocationView: TabLocationView) func tabLocationViewDidTapVoiceSearch(_ tabLocationView: TabLocationView) - func tabLocationViewDidTapShieldsButton(_ urlBar: TabLocationView) - func tabLocationViewDidTapRewardsButton(_ urlBar: TabLocationView) func tabLocationViewDidTapWalletButton(_ urlBar: TabLocationView) + func tabLocationViewDidTapSecureContentState(_ urlBar: TabLocationView) } private struct TabLocationViewUX { @@ -31,17 +32,18 @@ private struct TabLocationViewUX { static let TPIconSize: CGFloat = 24 static let buttonSize = CGSize(width: 44, height: 34.0) static let URLBarPadding = 4 + static let progressBarHeight: CGFloat = 3 } class TabLocationView: UIView { var delegate: TabLocationViewDelegate? - var contentView: UIStackView! + let contentView = UIView() private var tabObservers: TabObservers! private var privateModeCancellable: AnyCancellable? var url: URL? { didSet { - updateLockImageView() + updateLeadingItem() updateURLBarWithText() setNeedsUpdateConstraints() } @@ -49,7 +51,7 @@ class TabLocationView: UIView { var secureContentState: TabSecureContentState = .unknown { didSet { - updateLockImageView() + updateLeadingItem() } } @@ -64,22 +66,61 @@ class TabLocationView: UIView { } } } - - private func updateLockImageView() { - lockImageView.isHidden = false + + private var secureContentStateButtonConfiguration: UIButton.Configuration { + let clampedTraitCollection = traitCollection.clampingSizeCategory(maximum: .accessibilityLarge) + var configuration = UIButton.Configuration.plain() + configuration.preferredSymbolConfigurationForImage = .init(font: .preferredFont(forTextStyle: .subheadline, compatibleWith: clampedTraitCollection), scale: .small) + configuration.buttonSize = .small + configuration.imagePadding = 4 + // A bit extra on the leading edge for visual spacing + configuration.contentInsets = .init(top: 0, leading: 12, bottom: 0, trailing: 8) + + var title = AttributedString(Strings.tabToolbarNotSecureTitle) + title.font = .preferredFont(forTextStyle: .subheadline, compatibleWith: clampedTraitCollection) + + let isTitleVisible = !traitCollection.preferredContentSizeCategory.isAccessibilityCategory switch secureContentState { - case .localHost: - lockImageView.isHidden = true - case .insecure: - lockImageView.setImage(UIImage(braveSystemNamed: "leo.info.filled")? - .withRenderingMode(.alwaysOriginal) - .withTintColor(.braveErrorLabel), for: .normal) - lockImageView.accessibilityLabel = Strings.tabToolbarWarningImageAccessibilityLabel - case .secure, .unknown: - lockImageView.setImage(UIImage(braveSystemNamed: "brave.lock.alt", compatibleWith: nil), for: .normal) - lockImageView.accessibilityLabel = Strings.tabToolbarLockImageAccessibilityLabel + case .localhost, .secure: + break + case .invalidCert: + configuration.baseForegroundColor = UIColor(braveSystemName: .systemfeedbackErrorIcon) + if isTitleVisible { + configuration.attributedTitle = title + } + configuration.image = UIImage(braveSystemNamed: "leo.warning.triangle-filled") + case .missingSSL, .mixedContent: + configuration.baseForegroundColor = UIColor(braveSystemName: .textTertiary) + if isTitleVisible { + configuration.attributedTitle = title + } + configuration.image = UIImage(braveSystemNamed: "leo.warning.triangle-filled") + case .unknown: + configuration.baseForegroundColor = UIColor(braveSystemName: .iconDefault) + configuration.image = UIImage(braveSystemNamed: "leo.warning.circle-filled") } + return configuration + } + + private func updateLeadingItem() { + var leadingView: UIView? + defer { leadingItemView = leadingView } + if !secureContentState.shouldDisplayWarning { + // Consider reader mode + leadingView = readerModeState != .unavailable ? readerModeButton : nil + return + } + + let button = UIButton(configuration: secureContentStateButtonConfiguration, primaryAction: .init(handler: { [weak self] _ in + guard let self = self else { return } + self.delegate?.tabLocationViewDidTapSecureContentState(self) + })) + button.configurationUpdateHandler = { [unowned self] btn in + btn.configuration = secureContentStateButtonConfiguration + } + secureContentStateButton = button + leadingView = button } deinit { @@ -92,8 +133,9 @@ class TabLocationView: UIView { return readerModeButton.readerModeState } set(newReaderModeState) { + defer { updateLeadingItem() } if newReaderModeState != self.readerModeButton.readerModeState { - let wasHidden = readerModeButton.isHidden + let wasHidden = leadingItemView == nil self.readerModeButton.readerModeState = newReaderModeState if wasHidden != (newReaderModeState == ReaderModeState.unavailable) { UIAccessibility.post(notification: .layoutChanged, argument: nil) @@ -113,54 +155,25 @@ class TabLocationView: UIView { } } - func makePlaceholder(colors: some BrowserColors) -> NSAttributedString { - NSAttributedString(string: Strings.tabToolbarSearchAddressPlaceholderText, attributes: [NSAttributedString.Key.foregroundColor: colors.textSecondary]) - } - - lazy var urlTextField: UITextField = { - let urlTextField = DisplayTextField() + lazy var urlDisplayLabel: UILabel = { + let urlDisplayLabel = DisplayURLLabel() // Prevent the field from compressing the toolbar buttons on the 4S in landscape. - urlTextField.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 250), for: .horizontal) - urlTextField.setContentCompressionResistancePriority(.required, for: .vertical) - urlTextField.attributedPlaceholder = makePlaceholder(colors: .standard) - urlTextField.accessibilityIdentifier = "url" - urlTextField.font = .preferredFont(forTextStyle: .body) - urlTextField.backgroundColor = .clear - urlTextField.clipsToBounds = true - urlTextField.isEnabled = false - urlTextField.defaultTextAttributes = { - var attributes = urlTextField.defaultTextAttributes - let style = (attributes[.paragraphStyle, default: NSParagraphStyle.default] as! NSParagraphStyle).mutableCopy() as! NSMutableParagraphStyle // swiftlint:disable:this force_cast - style.lineBreakMode = .byClipping - attributes[.paragraphStyle] = style - return attributes - }() - // Remove the default drop interaction from the URL text field so that our - // custom drop interaction on the BVC can accept dropped URLs. - if let dropInteraction = urlTextField.textDropInteraction { - urlTextField.removeInteraction(dropInteraction) - } - - return urlTextField + urlDisplayLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 250), for: .horizontal) + urlDisplayLabel.setContentCompressionResistancePriority(.required, for: .vertical) + urlDisplayLabel.accessibilityIdentifier = "url" + urlDisplayLabel.font = .preferredFont(forTextStyle: .subheadline) + urlDisplayLabel.backgroundColor = .clear + urlDisplayLabel.clipsToBounds = true + urlDisplayLabel.lineBreakMode = .byClipping + urlDisplayLabel.numberOfLines = 1 + return urlDisplayLabel }() - private(set) lazy var lockImageView = ToolbarButton().then { - $0.setImage(UIImage(braveSystemNamed: "brave.lock.alt", compatibleWith: nil)?.withRenderingMode(.alwaysTemplate), for: .normal) - $0.isHidden = true - $0.isAccessibilityElement = true - $0.imageView?.contentMode = .center - $0.contentHorizontalAlignment = .center - $0.accessibilityLabel = Strings.tabToolbarLockImageAccessibilityLabel - $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) - $0.addTarget(self, action: #selector(didTapLockImageView), for: .touchUpInside) - } - private(set) lazy var readerModeButton: ReaderModeButton = { let readerModeButton = ReaderModeButton(frame: .zero) readerModeButton.addTarget(self, action: #selector(didTapReaderModeButton), for: .touchUpInside) readerModeButton.isAccessibilityElement = true - readerModeButton.isHidden = true readerModeButton.imageView?.contentMode = .scaleAspectFit readerModeButton.accessibilityLabel = Strings.tabToolbarReaderViewButtonAccessibilityLabel readerModeButton.accessibilityIdentifier = "TabLocationView.readerModeButton" @@ -201,38 +214,46 @@ class TabLocationView: UIView { $0.addTarget(self, action: #selector(didTapVoiceSearchButton), for: .touchUpInside) } - lazy var shieldsButton: ToolbarButton = { - let button = ToolbarButton() - button.setImage(UIImage(sharedNamed: "brave.logo"), for: .normal) - button.addTarget(self, action: #selector(didTapBraveShieldsButton), for: .touchUpInside) - button.imageView?.contentMode = .scaleAspectFit - button.accessibilityLabel = Strings.bravePanel - button.imageView?.adjustsImageSizeForAccessibilityContentSizeCategory = true - button.accessibilityIdentifier = "urlBar-shieldsButton" - return button - }() - - lazy var rewardsButton: RewardsButton = { - let button = RewardsButton() - button.addTarget(self, action: #selector(didTapBraveRewardsButton), for: .touchUpInside) - return button - }() - - lazy var separatorLine: UIView = CustomSeparatorView(lineSize: .init(width: 1, height: 26), cornerRadius: 2).then { - $0.isUserInteractionEnabled = false - $0.layoutMargins = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2) - } - - lazy var tabOptionsStackView = UIStackView().then { + lazy var trailingTabOptionsStackView = UIStackView().then { $0.alignment = .center - $0.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 3) - $0.isLayoutMarginsRelativeArrangement = true $0.insetsLayoutMarginsFromSafeArea = false } - + private var isVoiceSearchAvailable: Bool private let privateBrowsingManager: PrivateBrowsingManager + + private let placeholderLabel = UILabel().then { + $0.text = Strings.tabToolbarSearchAddressPlaceholderText + $0.isHidden = true + } + + // A layout guide defining the available space for the URL itself + private let urlLayoutGuide = UILayoutGuide().then { + $0.identifier = "url-layout-guide" + } + + private let leadingItemContainerView = UIView() + private var leadingItemView: UIView? { + willSet { + leadingItemView?.removeFromSuperview() + } + didSet { + if let leadingItemView { + leadingItemContainerView.addSubview(leadingItemView) + leadingItemView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + } + } + + private(set) var secureContentStateButton: UIButton? + private(set) lazy var progressBar = GradientProgressBar().then { + $0.clipsToBounds = false + $0.setGradientColors(startColor: .braveBlurpleTint, endColor: .braveBlurpleTint) + } + init(voiceSearchSupported: Bool, privateBrowsingManager: PrivateBrowsingManager) { self.privateBrowsingManager = privateBrowsingManager isVoiceSearchAvailable = voiceSearchSupported @@ -243,37 +264,73 @@ class TabLocationView: UIView { addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapLocationBar))) - var optionSubviews: [UIView] = [readerModeButton, walletButton, playlistButton] + readerModeButton.do { + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } + + var trailingOptionSubviews: [UIView] = [walletButton, playlistButton] if isVoiceSearchAvailable { - optionSubviews.append(voiceSearchButton) + trailingOptionSubviews.append(voiceSearchButton) } - optionSubviews.append(contentsOf: [reloadButton, separatorLine, shieldsButton, rewardsButton]) + trailingOptionSubviews.append(contentsOf: [reloadButton]) - optionSubviews.forEach { - ($0 as? UIButton)?.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + trailingOptionSubviews.forEach { $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) - tabOptionsStackView.addArrangedSubview($0) + trailingTabOptionsStackView.addArrangedSubview($0) } - - // Visual centering - rewardsButton.contentEdgeInsets = .init(top: 1, left: 5, bottom: 1, right: 5) - urlTextField.setContentHuggingPriority(.defaultLow, for: .horizontal) - - let subviews = [lockImageView, urlTextField, tabOptionsStackView] - contentView = UIStackView(arrangedSubviews: subviews) - contentView.layoutMargins = UIEdgeInsets(top: 2, left: TabLocationViewUX.spacing, bottom: 2, right: 0) - contentView.isLayoutMarginsRelativeArrangement = true - contentView.insetsLayoutMarginsFromSafeArea = false - contentView.spacing = 8 - contentView.setCustomSpacing(4, after: urlTextField) + urlDisplayLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + addLayoutGuide(urlLayoutGuide) + addSubview(contentView) - - contentView.snp.makeConstraints { make in - make.leading.trailing.top.bottom.equalTo(self) + contentView.addSubview(leadingItemContainerView) + contentView.addSubview(urlDisplayLabel) + contentView.addSubview(trailingTabOptionsStackView) + contentView.addSubview(placeholderLabel) + contentView.addSubview(progressBar) + + contentView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + urlDisplayLabel.snp.makeConstraints { + $0.center.equalToSuperview().priority(.low) + $0.leading.greaterThanOrEqualTo(urlLayoutGuide) + $0.trailing.lessThanOrEqualTo(urlLayoutGuide) + } + + leadingItemContainerView.snp.makeConstraints { + $0.leading.equalToSuperview() + $0.top.bottom.equalToSuperview() + } + + trailingTabOptionsStackView.snp.makeConstraints { + $0.trailing.equalToSuperview() + $0.top.bottom.equalToSuperview() } + urlLayoutGuide.snp.makeConstraints { + $0.leading.greaterThanOrEqualTo(TabLocationViewUX.spacing * 2) + $0.leading.equalTo(leadingItemContainerView.snp.trailing).priority(.medium) + $0.trailing.equalTo(trailingTabOptionsStackView.snp.leading) + $0.top.bottom.equalTo(self) + } + + placeholderLabel.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.leading.equalToSuperview().inset(TabLocationViewUX.spacing * 2) // Needs double spacing to line up + $0.trailing.lessThanOrEqualTo(trailingTabOptionsStackView.snp.leading) + } + + progressBar.snp.makeConstraints { + $0.bottom.equalToSuperview() + $0.height.equalTo(TabLocationViewUX.progressBarHeight) + $0.leading.trailing.equalToSuperview() + } + privateModeCancellable = privateBrowsingManager.$isPrivateBrowsing .removeDuplicates() .receive(on: RunLoop.main) @@ -286,7 +343,6 @@ class TabLocationView: UIView { } updateForTraitCollection() - updateColors() } @@ -296,14 +352,13 @@ class TabLocationView: UIView { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - updateForTraitCollection() - } + updateForTraitCollection() + updateColors() } override var accessibilityElements: [Any]? { get { - return [lockImageView, urlTextField, readerModeButton, playlistButton, reloadButton, shieldsButton].filter { !$0.isHidden } + return [urlDisplayLabel, placeholderLabel, readerModeButton, playlistButton, reloadButton].filter { !$0.isHidden } } set { super.accessibilityElements = newValue @@ -312,12 +367,9 @@ class TabLocationView: UIView { private func updateForTraitCollection() { let clampedTraitCollection = traitCollection.clampingSizeCategory(maximum: .accessibilityLarge) - lockImageView.setPreferredSymbolConfiguration( - .init(pointSize: UIFont.preferredFont(forTextStyle: .body, compatibleWith: clampedTraitCollection).pointSize, weight: .heavy, scale: .small), - forImageIn: .normal - ) let toolbarTraitCollection = UITraitCollection(preferredContentSizeCategory: traitCollection.toolbarButtonContentSizeCategory) - urlTextField.font = .preferredFont(forTextStyle: .body, compatibleWith: clampedTraitCollection) + urlDisplayLabel.font = .preferredFont(forTextStyle: .subheadline, compatibleWith: clampedTraitCollection) + placeholderLabel.font = urlDisplayLabel.font let pointSize = UIFont.preferredFont( forTextStyle: .footnote, compatibleWith: toolbarTraitCollection @@ -326,20 +378,34 @@ class TabLocationView: UIView { .init(pointSize: pointSize, weight: .regular, scale: .large), forImageIn: .normal ) + voiceSearchButton.setPreferredSymbolConfiguration( + .init(pointSize: pointSize, weight: .regular, scale: .large), + forImageIn: .normal + ) + let width = UIFontMetrics(forTextStyle: .body).scaledValue(for: 44, compatibleWith: toolbarTraitCollection) reloadButton.snp.remakeConstraints { - $0.width.equalTo(UIFontMetrics(forTextStyle: .body).scaledValue(for: 32, compatibleWith: toolbarTraitCollection)) + $0.width.equalTo(width) + } + voiceSearchButton.snp.remakeConstraints { + $0.width.equalTo(width) } } private func updateColors() { let browserColors = privateBrowsingManager.browserColors backgroundColor = browserColors.containerBackground - urlTextField.textColor = browserColors.textPrimary - urlTextField.attributedPlaceholder = makePlaceholder(colors: browserColors) - separatorLine.backgroundColor = browserColors.dividerSubtle + urlDisplayLabel.textColor = browserColors.textPrimary + placeholderLabel.textColor = browserColors.textTertiary readerModeButton.unselectedTintColor = browserColors.iconDefault readerModeButton.selectedTintColor = browserColors.iconActive - for button in [reloadButton, lockImageView, voiceSearchButton] { + // swiftlint:disable:next force_cast + (urlDisplayLabel as! DisplayURLLabel).clippingFade.gradientLayer.colors = [ + browserColors.containerBackground, + browserColors.containerBackground.withAlphaComponent(0.0) + ].map { + $0.resolvedColor(with: traitCollection).cgColor + } + for button in [reloadButton, voiceSearchButton] { button.primaryTintColor = browserColors.iconDefault button.disabledTintColor = browserColors.iconDisabled button.selectedTintColor = browserColors.iconActive @@ -347,29 +413,23 @@ class TabLocationView: UIView { } private func updateURLBarWithText() { - (urlTextField as? DisplayTextField)?.hostString = url?.withoutWWW.host ?? "" - - // Note: Only use `URLFormatter.formatURLOrigin(forSecurityDisplay: url?.withoutWWW.absoluteString ?? "", schemeDisplay: .omitHttpAndHttps)` - // If displaying the host ONLY! This follows Google Chrome and Safari. - // However, for Brave as no decision has been made on what shows YET, we will display the entire URL (truncated!) - // Therefore we only omit defaults (username & password, http [not https], and trailing slash) + omit "www". - // We must NOT un-escape the URL! - // -- - // The requirement to remove scheme comes from Desktop. Also we do not remove the path like in other browsers either. - // Therefore, we follow Brave Desktop instead of Chrome or Safari iOS - urlTextField.text = URLFormatter.formatURL(url?.withoutWWW.absoluteString ?? "", formatTypes: [.omitDefaults], unescapeOptions: []).removeSchemeFromURLString(url?.scheme) + // Matches LocationBarModelImpl::GetFormattedURL in Chromium (except for omitHTTP) + // components/omnibox/browser/location_bar_model_impl.cc + // TODO: Export omnibox related APIs and use directly + urlDisplayLabel.text = URLFormatter.formatURL( + url?.absoluteString ?? "", + formatTypes: [.trimAfterHost, .omitHTTP, .omitHTTPS, .omitTrivialSubdomains], + unescapeOptions: .normal + ) reloadButton.isHidden = url == nil voiceSearchButton.isHidden = (url != nil) || !isVoiceSearchAvailable + placeholderLabel.isHidden = url != nil + urlDisplayLabel.isHidden = url == nil + leadingItemContainerView.isHidden = url == nil } // MARK: Tap Actions - - @objc func didTapLockImageView() { - if !loading { - delegate?.tabLocationViewDidTapLockImageView(self) - } - } @objc func didTapReaderModeButton() { delegate?.tabLocationViewDidTapReaderMode(self) @@ -394,14 +454,6 @@ class TabLocationView: UIView { @objc func didTapLocationBar(_ recognizer: UITapGestureRecognizer) { delegate?.tabLocationViewDidTapLocation(self) } - - @objc func didTapBraveShieldsButton() { - delegate?.tabLocationViewDidTapShieldsButton(self) - } - - @objc func didTapBraveRewardsButton() { - delegate?.tabLocationViewDidTapRewardsButton(self) - } @objc func didTapWalletButton() { delegate?.tabLocationViewDidTapWalletButton(self) @@ -418,22 +470,10 @@ extension TabLocationView: TabEventHandler { } } -// MARK: - Hit Test -extension TabLocationView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if lockImageView.frame.insetBy(dx: -10, dy: -30).contains(point) { - return lockImageView - } - return super.hitTest(point, with: event) - } -} - -class DisplayTextField: UITextField { - weak var accessibilityActionsSource: AccessibilityActionsSource? - var hostString: String = "" +private class DisplayURLLabel: UILabel { let pathPadding: CGFloat = 5.0 - private let leadingClippingFade = GradientView( + let clippingFade = GradientView( colors: [.braveBackground, .braveBackground.withAlphaComponent(0.0)], positions: [0, 1], startPoint: .init(x: 0, y: 0.5), @@ -443,41 +483,51 @@ class DisplayTextField: UITextField { override init(frame: CGRect) { super.init(frame: frame) - addSubview(leadingClippingFade) - leadingClippingFade.snp.makeConstraints { - $0.leading.top.bottom.equalToSuperview() - $0.width.equalTo(20) + addSubview(clippingFade) + } + + private var textSize: CGSize = .zero + private var isRightToLeft: Bool = false + + override var font: UIFont! { + didSet { + updateTextSize(from: text) } } override var text: String? { didSet { - leadingClippingFade.isHidden = true + clippingFade.isHidden = true + if oldValue != text { + updateTextSize(from: text) + detectLanguageForNaturalDirectionClipping() + } + setNeedsDisplay() } } - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError() + private func updateTextSize(from value: String?) { + textSize = (value as? NSString)?.size(withAttributes: [.font: font!]) ?? .zero + setNeedsLayout() + setNeedsDisplay() } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - leadingClippingFade.gradientLayer.colors = [ - UIColor.braveBackground, - UIColor.braveBackground.withAlphaComponent(0.0) - ].map { - $0.resolvedColor(with: traitCollection).cgColor + private func detectLanguageForNaturalDirectionClipping() { + guard let text, let language = NLLanguageRecognizer.dominantLanguage(for: text) else { return } + switch language { + case .arabic, .hebrew, .persian, .urdu: + isRightToLeft = true + default: + isRightToLeft = false } + // Update clipping fade direction + clippingFade.gradientLayer.startPoint = .init(x: isRightToLeft ? 1 : 0, y: 0.5) + clippingFade.gradientLayer.endPoint = .init(x: isRightToLeft ? 0 : 1, y: 0.5) } - - override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { - get { - return accessibilityActionsSource?.accessibilityCustomActionsForView(self) - } - set { - super.accessibilityCustomActions = newValue - } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() } override var accessibilityTraits: UIAccessibilityTraits { @@ -488,52 +538,34 @@ class DisplayTextField: UITextField { override var canBecomeFirstResponder: Bool { return false } + + override func layoutSubviews() { + super.layoutSubviews() + + clippingFade.frame = .init( + x: isRightToLeft ? bounds.width - 20 : 0, + y: 0, + width: 20, + height: bounds.height + ) + } // This override is done in case the eTLD+1 string overflows the width of textField. // In that case the textRect is adjusted to show right aligned and truncate left. // Since this textField changes with WebView domain change, performance implications are low. - override func textRect(forBounds bounds: CGRect) -> CGRect { - var rect: CGRect = super.textRect(forBounds: bounds) - - if let size: CGSize = (self.hostString as NSString?)?.size(withAttributes: [.font: self.font!]) { - if size.width > self.bounds.width { - rect.origin.x = rect.origin.x - (size.width + pathPadding - self.bounds.width) - rect.size.width = size.width + pathPadding - bringSubviewToFront(leadingClippingFade) - leadingClippingFade.isHidden = false + override func drawText(in rect: CGRect) { + var rect = rect + if textSize.width > bounds.width { + let delta = (textSize.width - bounds.width) + if !isRightToLeft { + rect.origin.x -= delta + rect.size.width += delta } + bringSubviewToFront(clippingFade) + clippingFade.isHidden = false + } else { + clippingFade.isHidden = true } - return rect - } -} - -private class CustomSeparatorView: UIView { - - private let innerView: UIView - init(lineSize: CGSize, cornerRadius: CGFloat = 0) { - innerView = UIView(frame: .init(origin: .zero, size: lineSize)) - super.init(frame: .zero) - backgroundColor = .clear - innerView.layer.cornerRadius = cornerRadius - innerView.layer.cornerCurve = .continuous - addSubview(innerView) - innerView.snp.makeConstraints { - $0.width.height.equalTo(lineSize) - $0.centerY.equalTo(self) - $0.leading.trailing.equalTo(self.layoutMarginsGuide) - } - } - - override var backgroundColor: UIColor? { - get { - return innerView.backgroundColor - } - set { - innerView.backgroundColor = newValue - } - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.drawText(in: rect) } } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift index 08fb51bdfc40..a544de8f5ccf 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift @@ -33,8 +33,8 @@ protocol TopToolbarDelegate: AnyObject { func topToolbarDidPressStop(_ urlBar: TopToolbarView) func topToolbarDidPressReload(_ urlBar: TopToolbarView) func topToolbarDidPressQrCodeButton(_ urlBar: TopToolbarView) - func topToolbarDidPressLockImageView(_ urlBar: TopToolbarView) func topToolbarDidTapWalletButton(_ urlBar: TopToolbarView) + func topToolbarDidTapSecureContentState(_ urlBar: TopToolbarView) } class TopToolbarView: UIView, ToolbarProtocol { @@ -43,9 +43,9 @@ class TopToolbarView: UIView, ToolbarProtocol { struct UX { static let locationPadding: CGFloat = 8 - static let locationHeight: CGFloat = 34 - static let textFieldCornerRadius: CGFloat = 8 - static let progressBarHeight: CGFloat = 3 + static let locationHeight: CGFloat = 44 + static let textFieldCornerRadius: CGFloat = 10 + static let buttonWidth: CGFloat = 32 } // MARK: URLBarButton @@ -53,7 +53,6 @@ class TopToolbarView: UIView, ToolbarProtocol { enum URLBarButton { case wallet case playlist - case readerMode } // MARK: Internal @@ -75,8 +74,8 @@ class TopToolbarView: UIView, ToolbarProtocol { didSet { if isTransitioning { // Cancel any pending/in-progress animations related to the progress bar - progressBar.setProgress(1, animated: false) - progressBar.alpha = 0.0 + locationView.progressBar.setProgress(1, animated: false) + locationView.progressBar.alpha = 0.0 } } } @@ -119,15 +118,14 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.translatesAutoresizingMaskIntoConstraints = false $0.readerModeState = ReaderModeState.unavailable $0.delegate = self + $0.layer.cornerRadius = UX.textFieldCornerRadius + $0.layer.cornerCurve = .continuous + $0.clipsToBounds = true + $0.setContentCompressionResistancePriority(.required, for: .vertical) } let tabsButton = TabsButton() - private lazy var progressBar = GradientProgressBar().then { - $0.clipsToBounds = false - $0.setGradientColors(startColor: .braveBlurpleTint, endColor: .braveBlurpleTint) - } - private lazy var cancelButton = InsetButton().then { $0.setTitle(Strings.cancelButtonTitle, for: .normal) $0.setTitleColor(UIColor.secondaryBraveLabel, for: .normal) @@ -145,6 +143,9 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.setImage(UIImage(braveSystemNamed: "leo.product.bookmarks"), for: .normal) $0.accessibilityLabel = Strings.bookmarksMenuItem $0.addTarget(self, action: #selector(didClickBookmarkButton), for: .touchUpInside) + $0.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(UX.buttonWidth) + } } var forwardButton = ToolbarButton() @@ -164,12 +165,14 @@ class TopToolbarView: UIView, ToolbarProtocol { lazy var actionButtons: [UIButton] = [ shareButton, tabsButton, bookmarkButton, forwardButton, backButton, menuButton, + shieldsButton, rewardsButton ].compactMap { $0 } private let mainStackView = UIStackView().then { $0.spacing = 8 $0.isLayoutMarginsRelativeArrangement = true $0.insetsLayoutMarginsFromSafeArea = false + $0.alignment = .center } private let leadingItemsStackView = UIStackView().then { @@ -182,13 +185,18 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.distribution = .fillEqually $0.spacing = 8 } + + private let shieldsRewardsStack = UIStackView().then { + $0.distribution = .fillEqually + $0.spacing = 0 // buttons contain padding + $0.setContentHuggingPriority(.required, for: .horizontal) + } /// The currently visible URL bar button beside the refresh button. private(set) var currentURLBarButton: URLBarButton? { didSet { locationView.walletButton.isHidden = currentURLBarButton != .wallet locationView.playlistButton.isHidden = currentURLBarButton != .playlist - locationView.readerModeButton.isHidden = currentURLBarButton != .readerMode } } @@ -221,6 +229,25 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.setContentHuggingPriority(.defaultHigh, for: .vertical) } + private(set) lazy var shieldsButton: ToolbarButton = { + let button = ToolbarButton() + button.setImage(UIImage(sharedNamed: "brave.logo"), for: .normal) + button.addTarget(self, action: #selector(didTapBraveShieldsButton), for: .touchUpInside) + button.imageView?.contentMode = .scaleAspectFit + button.accessibilityLabel = Strings.bravePanel + button.imageView?.adjustsImageSizeForAccessibilityContentSizeCategory = true + button.accessibilityIdentifier = "urlBar-shieldsButton" + return button + }() + + private(set) lazy var rewardsButton: RewardsButton = { + let button = RewardsButton() + button.addTarget(self, action: #selector(didTapBraveRewardsButton), for: .touchUpInside) + // Visual centering + button.contentEdgeInsets = .init(top: 1, left: 0, bottom: 1, right: 0) + return button + }() + private lazy var locationBarOptionsStackView = UIStackView().then { $0.alignment = .center $0.isHidden = true @@ -234,7 +261,19 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.backgroundColor = .clear $0.layer.cornerRadius = UX.textFieldCornerRadius $0.layer.cornerCurve = .continuous - $0.layer.masksToBounds = true + $0.layer.shadowOffset = .init(width: 0, height: 1) + $0.layer.shadowRadius = 2 + $0.layer.shadowColor = UIColor.black.cgColor + $0.layer.shadowOpacity = 0.1 + } + + // The location container has a second shadow but we can't apply 2 shadows in UIKit, so adding a second view + private let secondLocationShadowView = UIView().then { + $0.backgroundColor = .clear + $0.layer.shadowOffset = .init(width: 0, height: 4) + $0.layer.shadowRadius = 16 + $0.layer.shadowOpacity = 0.08 + $0.layer.shadowColor = UIColor.black.cgColor } private var isVoiceSearchAvailable: Bool @@ -247,9 +286,10 @@ class TopToolbarView: UIView, ToolbarProtocol { super.init(frame: .zero) + addSubview(secondLocationShadowView) locationContainer.addSubview(locationView) - [scrollToTopButton, tabsButton, progressBar, cancelButton].forEach(addSubview(_:)) + [scrollToTopButton, tabsButton, cancelButton].forEach(addSubview(_:)) addSubview(mainStackView) helper = ToolbarHelper(toolbar: self) @@ -267,17 +307,23 @@ class TopToolbarView: UIView, ToolbarProtocol { leadingItemsStackView.addArrangedSubview(backButton) leadingItemsStackView.addArrangedSubview(forwardButton) leadingItemsStackView.addArrangedSubview(bookmarkButton) + leadingItemsStackView.addArrangedSubview(shareButton) [backButton, forwardButton].forEach { $0.contentEdgeInsets = UIEdgeInsets( top: 0, left: UX.locationPadding, bottom: 0, right: UX.locationPadding) } - bookmarkButton.contentEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 2) + if UIDevice.current.userInterfaceIdiom == .phone { + trailingItemsStackView.addArrangedSubview(addTabButton) + } trailingItemsStackView.addArrangedSubview(tabsButton) trailingItemsStackView.addArrangedSubview(menuButton) + + shieldsRewardsStack.addArrangedSubview(shieldsButton) + shieldsRewardsStack.addArrangedSubview(rewardsButton) - [leadingItemsStackView, locationContainer, trailingItemsStackView, cancelButton].forEach { + [leadingItemsStackView, locationContainer, shieldsRewardsStack, trailingItemsStackView, cancelButton].forEach { mainStackView.addArrangedSubview($0) } @@ -332,20 +378,23 @@ class TopToolbarView: UIView, ToolbarProtocol { private func updateForTraitCollection() { let toolbarSizeCategory = traitCollection.toolbarButtonContentSizeCategory - let pointSize = UIFont.preferredFont(forTextStyle: .body, compatibleWith: .init(preferredContentSizeCategory: toolbarSizeCategory)).lineHeight - locationView.shieldsButton.snp.remakeConstraints { + let pointSize = UIFont.preferredFont(forTextStyle: .headline, compatibleWith: .init(preferredContentSizeCategory: toolbarSizeCategory)).lineHeight + shieldsButton.snp.remakeConstraints { + $0.width.greaterThanOrEqualTo(UX.buttonWidth) $0.height.equalTo(pointSize) } - locationView.rewardsButton.snp.remakeConstraints { + rewardsButton.snp.remakeConstraints { + $0.width.greaterThanOrEqualTo(UX.buttonWidth) $0.height.equalTo(pointSize) } let clampedTraitCollection = traitCollection.clampingSizeCategory(maximum: .accessibilityLarge) - locationTextField?.font = .preferredFont(forTextStyle: .body, compatibleWith: clampedTraitCollection) + locationTextField?.font = .preferredFont(forTextStyle: .subheadline, compatibleWith: clampedTraitCollection) } private func setupConstraints() { locationContainer.snp.remakeConstraints { $0.top.bottom.equalToSuperview().inset(UX.locationPadding) + $0.height.greaterThanOrEqualTo(UX.locationHeight) } mainStackView.snp.remakeConstraints { make in @@ -358,12 +407,6 @@ class TopToolbarView: UIView, ToolbarProtocol { make.left.right.equalTo(self.locationContainer) } - progressBar.snp.makeConstraints { make in - make.top.equalTo(self.snp.bottom).inset(UX.progressBarHeight / 2) - make.height.equalTo(UX.progressBarHeight) - make.left.right.equalTo(self) - } - locationView.snp.makeConstraints { make in make.edges.equalTo(self.locationContainer) make.height.greaterThanOrEqualTo(UX.locationHeight) @@ -375,18 +418,34 @@ class TopToolbarView: UIView, ToolbarProtocol { // Increase the inset of the main stack view if there's no additional space from safe areas let horizontalInset: CGFloat = safeAreaInsets.left > 0 ? 0 : UX.locationPadding mainStackView.layoutMargins = .init(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset) + + locationContainer.layoutIfNeeded() + locationContainer.layer.shadowPath = UIBezierPath( + roundedRect: locationContainer.bounds.insetBy(dx: 1, dy: 1), // -1 spread in Figma + cornerRadius: locationContainer.layer.cornerRadius + ).cgPath + + secondLocationShadowView.frame = locationContainer.frame + secondLocationShadowView.layer.shadowPath = UIBezierPath( + roundedRect: secondLocationShadowView.bounds.insetBy(dx: 2, dy: 2), // -2 spread in Figma + cornerRadius: locationContainer.layer.cornerRadius + ).cgPath } override func becomeFirstResponder() -> Bool { return self.locationTextField?.becomeFirstResponder() ?? false } + private func makePlaceholder(colors: some BrowserColors) -> NSAttributedString { + NSAttributedString(string: Strings.tabToolbarSearchAddressPlaceholderText, attributes: [.foregroundColor: colors.textTertiary]) + } + private func updateColors() { let browserColors = privateBrowsingManager.browserColors backgroundColor = browserColors.chromeBackground locationTextField?.backgroundColor = browserColors.containerBackground locationTextField?.textColor = browserColors.textPrimary - locationTextField?.attributedPlaceholder = locationView.makePlaceholder(colors: browserColors) + locationTextField?.attributedPlaceholder = makePlaceholder(colors: browserColors) } /// Created whenever the location bar on top is selected @@ -413,7 +472,7 @@ class TopToolbarView: UIView, ToolbarProtocol { $0.font = .preferredFont(forTextStyle: .body) $0.accessibilityIdentifier = "address" $0.accessibilityLabel = Strings.URLBarViewLocationTextViewAccessibilityLabel - $0.attributedPlaceholder = self.locationView.makePlaceholder(colors: .standard) + $0.attributedPlaceholder = self.makePlaceholder(colors: .standard) $0.clearButtonMode = .whileEditing $0.rightViewMode = .never if let dropInteraction = $0.textDropInteraction { @@ -472,18 +531,18 @@ class TopToolbarView: UIView, ToolbarProtocol { } func currentProgress() -> Float { - return progressBar.progress + locationView.progressBar.progress } func updateProgressBar(_ progress: Float) { - progressBar.alpha = 1 - progressBar.isHidden = false - progressBar.setProgress(progress, animated: !isTransitioning) + locationView.progressBar.alpha = 1 + locationView.progressBar.isHidden = false + locationView.progressBar.setProgress(progress, animated: !isTransitioning) } func hideProgressBar() { - progressBar.isHidden = true - progressBar.setProgress(0, animated: false) + locationView.progressBar.isHidden = true + locationView.progressBar.setProgress(0, animated: false) } func updateReaderModeState(_ state: ReaderModeState) { @@ -507,8 +566,6 @@ class TopToolbarView: UIView, ToolbarProtocol { currentURLBarButton = .wallet } else if locationView.playlistButton.buttonState != .none { currentURLBarButton = .playlist - } else if locationView.readerModeState != .unavailable { - currentURLBarButton = .readerMode } else { currentURLBarButton = nil } @@ -595,11 +652,12 @@ class TopToolbarView: UIView, ToolbarProtocol { if cancelButton.isHidden == inOverlayMode { cancelButton.isHidden = !inOverlayMode } - progressBar.isHidden = inOverlayMode backButton.isHidden = !toolbarIsShowing || inOverlayMode forwardButton.isHidden = !toolbarIsShowing || inOverlayMode + shareButton.isHidden = !toolbarIsShowing || inOverlayMode trailingItemsStackView.isHidden = !toolbarIsShowing || inOverlayMode locationView.contentView.isHidden = inOverlayMode + shieldsRewardsStack.isHidden = inOverlayMode let showBookmarkPref = Preferences.General.showBookmarkToolbarShortcut.value bookmarkButton.isHidden = showBookmarkPref ? inOverlayMode : true @@ -614,15 +672,13 @@ class TopToolbarView: UIView, ToolbarProtocol { } if inOverlayMode { - [progressBar, leadingItemsStackView, bookmarkButton, trailingItemsStackView, locationView.contentView].forEach { + [leadingItemsStackView, bookmarkButton, shieldsRewardsStack, trailingItemsStackView, locationView.contentView].forEach { $0?.isHidden = true } cancelButton.isHidden = false } else { - UIView.animate(withDuration: 0.3) { - self.updateViewsForOverlayModeAndToolbarChanges() - } + updateViewsForOverlayModeAndToolbarChanges() } layoutIfNeeded() @@ -662,7 +718,7 @@ class TopToolbarView: UIView, ToolbarProtocol { shieldIcon = shieldsOffIcon } - locationView.shieldsButton.setImage(UIImage(sharedNamed: shieldIcon), for: .normal) + shieldsButton.setImage(UIImage(sharedNamed: shieldIcon), for: .normal) } // MARK: Actions @@ -683,10 +739,6 @@ class TopToolbarView: UIView, ToolbarProtocol { delegate?.topToolbarDidTapMenuButton(self) } - @objc func didClickBraveShieldsButton() { - delegate?.topToolbarDidTapBraveShieldsButton(self) - } - @objc func topToolbarDidPressQrCodeButton() { delegate?.topToolbarDidPressQrCodeButton(self) leaveOverlayMode(didCancel: true) @@ -700,6 +752,14 @@ class TopToolbarView: UIView, ToolbarProtocol { @objc private func swipedLocationView() { delegate?.topToolbarDidPressTabs(self) } + + @objc private func didTapBraveShieldsButton() { + delegate?.topToolbarDidTapBraveShieldsButton(self) + } + + @objc private func didTapBraveRewardsButton() { + delegate?.topToolbarDidTapBraveRewardsButton(self) + } } // MARK: PreferencesObserver @@ -713,14 +773,6 @@ extension TopToolbarView: PreferencesObserver { // MARK: TabLocationViewDelegate extension TopToolbarView: TabLocationViewDelegate { - func tabLocationViewDidTapShieldsButton(_ urlBar: TabLocationView) { - delegate?.topToolbarDidTapBraveShieldsButton(self) - } - - func tabLocationViewDidTapRewardsButton(_ urlBar: TabLocationView) { - delegate?.topToolbarDidTapBraveRewardsButton(self) - } - func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) { guard let (locationText, isSearchQuery) = delegate?.topToolbarDisplayTextForURL(locationView.url as URL?) else { return } @@ -733,10 +785,6 @@ extension TopToolbarView: TabLocationViewDelegate { enterOverlayMode(overlayText, pasted: false, search: isSearchQuery) } - func tabLocationViewDidTapLockImageView(_ tabLocationView: TabLocationView) { - delegate?.topToolbarDidPressLockImageView(self) - } - func tabLocationViewDidTapReload(_ tabLocationView: TabLocationView) { delegate?.topToolbarDidPressReload(self) } @@ -768,6 +816,10 @@ extension TopToolbarView: TabLocationViewDelegate { func tabLocationViewDidTapWalletButton(_ urlBar: TabLocationView) { delegate?.topToolbarDidTapWalletButton(self) } + + func tabLocationViewDidTapSecureContentState(_ urlBar: TabLocationView) { + delegate?.topToolbarDidTapSecureContentState(self) + } } // MARK: AutocompleteTextFieldDelegate @@ -794,7 +846,7 @@ extension TopToolbarView: AutocompleteTextFieldDelegate { func autocompleteTextFieldDidBeginEditing(_ autocompleteTextField: AutocompleteTextField) { autocompleteTextField.highlightAll() - updateLocationBarRightView(showToolbarActions: locationView.urlTextField.text?.isEmpty == true) + updateLocationBarRightView(showToolbarActions: locationView.urlDisplayLabel.text?.isEmpty == true) } func autocompleteTextFieldShouldClear(_ autocompleteTextField: AutocompleteTextField) -> Bool { diff --git a/Sources/Brave/Frontend/Share/MenuActivity.swift b/Sources/Brave/Frontend/Share/MenuActivity.swift index 6697c2ec764c..1462e0700c5d 100644 --- a/Sources/Brave/Frontend/Share/MenuActivity.swift +++ b/Sources/Brave/Frontend/Share/MenuActivity.swift @@ -11,3 +11,55 @@ protocol MenuActivity: UIActivity { /// The image to use when shown on the menu. var menuImage: Image { get } } + +/// A standard activity that will appear in the apps menu and executes a callback when the user selects it +class BasicMenuActivity: UIActivity, MenuActivity { + private let title: String + private let braveSystemImage: String + private let callback: () -> Bool + + init( + title: String, + braveSystemImage: String, + callback: @escaping () -> Bool + ) { + self.title = title + self.braveSystemImage = braveSystemImage + self.callback = callback + } + + convenience init( + title: String, + braveSystemImage: String, + callback: @escaping () -> Void + ) { + self.init(title: title, braveSystemImage: braveSystemImage, callback: { + callback() + return true + }) + } + + // MARK: - UIActivity + + override var activityTitle: String? { + return title + } + + override var activityImage: UIImage? { + return UIImage(braveSystemNamed: braveSystemImage)?.applyingSymbolConfiguration(.init(scale: .large)) + } + + override func perform() { + activityDidFinish(callback()) + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + // MARK: - MenuActivity + + var menuImage: Image { + Image(braveSystemName: braveSystemImage) + } +} diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 51ce7bbe6d6c..e76c7b152faa 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -444,7 +444,17 @@ extension Strings { public static let findOnPageSectionHeader = NSLocalizedString("FindOnPageSectionHeader", tableName: "BraveShared", bundle: .module, value: "On This Page", comment: "Section header for find in page option") public static let searchHistorySectionHeader = NSLocalizedString("SearchHistorySectionHeader", tableName: "BraveShared", bundle: .module, value: "Open Tabs & Bookmarks & History", comment: "Section header for history and bookmarks and open tabs option") public static let searchSuggestionOpenTabActionTitle = NSLocalizedString("searchSuggestionOpenTabActionTitle", tableName: "BraveShared", bundle: .module, value: "Switch to this tab", comment: "Action title for Switching to an existing tab for the suggestion item shown on the table list") - + public static let tabToolbarNotSecureTitle = NSLocalizedString("tabToolbarNotSecureTitle", tableName: "BraveShared", bundle: .module, value: "Not Secure", comment: "A label shown next to a URL that loaded in some insecure way") +} + +// MARK: - PageSecurityView.swift +extension Strings { + public enum PageSecurityView { + public static let pageNotSecureTitle = NSLocalizedString("pageSecurityView.pageNotSecureTitle", tableName: "BraveShared", bundle: .module, value: "Your connection to this site is not secure.", comment: "") + public static let pageNotFullySecureTitle = NSLocalizedString("pageSecurityView.pageNotFullySecureTitle", tableName: "BraveShared", bundle: .module, value: "Your connection to this site is not fully secure.", comment: "") + public static let pageNotSecureDetailedWarning = NSLocalizedString("pageSecurityView.pageNotSecureDetailedWarning", tableName: "BraveShared", bundle: .module, value: "You should not enter any sensitive information on this site (for example, passwords or credit cards), because it could be stolen by attackers.", comment: "") + public static let viewCertificateButtonTitle = NSLocalizedString("pageSecurityView.viewCertificateButtonTitle", tableName: "BraveShared", bundle: .module, value: "View Certificate", comment: "") + } } // MARK:- TabToolbar.swift @@ -1081,6 +1091,8 @@ extension Strings { public static let forcePaste = NSLocalizedString("ForcePaste", tableName: "BraveShared", bundle: .module, value: "Force Paste", comment: "A label which when tapped pastes from the users clipboard forcefully (so as to ignore any paste restrictions placed by the website)") public static let addToFavorites = NSLocalizedString("AddToFavorites", tableName: "BraveShared", bundle: .module, value: "Add to Favorites", comment: "Add to favorites share action.") public static let createPDF = NSLocalizedString("CreatePDF", tableName: "BraveShared", bundle: .module, value: "Create PDF", comment: "Create PDF share action.") + public static let displayCertificate = NSLocalizedString("DisplayCertificate", tableName: "BraveShared", bundle: .module, value: "Security Certificate", comment: "Button title that when tapped displays a websites HTTPS security certificate information") + public static let toggleReaderMode = NSLocalizedString("ToggleReaderMode", tableName: "BraveShared", bundle: .module, value: "Toggle Reader Mode", comment: "Button title that when tapped toggles the web page in our out of reader mode") public static let showBookmarks = NSLocalizedString("ShowBookmarks", tableName: "BraveShared", bundle: .module, value: "Show Bookmarks", comment: "Button to show the bookmarks list") public static let showHistory = NSLocalizedString("ShowHistory", tableName: "BraveShared", bundle: .module, value: "Show History", comment: "Button to show the history list") diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.lock.plain.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.lock.plain.symbolset/Contents.json new file mode 100644 index 000000000000..2f415ce6e4c0 --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.lock.plain.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.verification.outline.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.verification.outline.symbolset/Contents.json new file mode 100644 index 000000000000..2f415ce6e4c0 --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.verification.outline.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Tests/ClientTests/DisplayTextFieldTest.swift b/Tests/ClientTests/DisplayTextFieldTest.swift deleted file mode 100644 index 4fb8ac336c3e..000000000000 --- a/Tests/ClientTests/DisplayTextFieldTest.swift +++ /dev/null @@ -1,41 +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/. - -@testable import Brave -import XCTest - -class DisplayTextFieldTest: XCTestCase { - - func testRightAlignedTLD() { - let textField = DisplayTextField(frame: CGRect(width: 250, height: 44)) - - let urlCases: [URL] = [ - URL(string: "https://www.google.com")!, - URL(string: "http://myaccountsecure.testmycase.co.uk/asd?a=1")!, - URL(string: "http://testmycase.co.uk/something/path/asd?a=1")!, - URL(string: "http://myaccountsecure.secured.testmycase.co.uk/asd?a=1")!, - URL(string: "http://myaccountsecure.testmycase.co.uk")!, - ] - urlCases.forEach({ textField.assertWidth(text: $0.schemelessAbsoluteString, hostString: $0.host ?? "") }) - - } - -} - -extension DisplayTextField { - func assertWidth(text: String, hostString: String) { - self.hostString = hostString - self.text = text - let rect = self.textRect(forBounds: self.bounds) - if rect.width > self.bounds.width, let upperBound = text.range(of: hostString)?.upperBound { - let pathString = text[upperBound...] - let widthPath = (pathString as NSString).size(withAttributes: [.font: self.font!]).width - let widthText = (text as NSString).size(withAttributes: [.font: self.font!]).width - - let test1 = rect.width + widthPath - pathPadding == widthText - let test2 = rect.origin.x == self.bounds.width - widthText + widthPath - pathPadding - XCTAssert(test1 && test2) - } - } -}