diff --git a/BrowserKit/Sources/ComponentLibrary/Buttons/PrimaryRoundedButton.swift b/BrowserKit/Sources/ComponentLibrary/Buttons/PrimaryRoundedButton.swift index e154055be464..5ff4f0253719 100644 --- a/BrowserKit/Sources/ComponentLibrary/Buttons/PrimaryRoundedButton.swift +++ b/BrowserKit/Sources/ComponentLibrary/Buttons/PrimaryRoundedButton.swift @@ -30,6 +30,9 @@ public class PrimaryRoundedButton: ResizableButton, ThemeApplicable { configuration = UIButton.Configuration.filled() titleLabel?.adjustsFontForContentSizeCategory = true + + // Fix for https://openradar.appspot.com/FB12472792 + titleLabel?.textAlignment = .center } required init?(coder aDecoder: NSCoder) { diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index d4d2e5ab7764..4545f5eff67c 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 047F9B3224E1FE1F00CD7DF7 /* WidgetKitExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 047F9B2724E1FE1C00CD7DF7 /* WidgetKitExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 047F9B3E24E1FF4000CD7DF7 /* SearchQuickLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F9B3A24E1FF4000CD7DF7 /* SearchQuickLinks.swift */; }; 047F9B4224E1FF4000CD7DF7 /* ImageButtonWithLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F9B3C24E1FF4000CD7DF7 /* ImageButtonWithLabel.swift */; }; + 0A3F57F92CEDD4CA0051B001 /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3F57F82CEDD4CA0051B001 /* TermsOfServiceViewController.swift */; }; 0A49784A2C53E63200B1E82A /* TrackingProtectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4978492C53E63200B1E82A /* TrackingProtectionViewController.swift */; }; 0A686B3C2CDB70DC0090E146 /* MainMenuTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A686B3B2CDB70DC0090E146 /* MainMenuTelemetryTests.swift */; }; 0A6875152C91886A00606F53 /* CertificatesHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6875142C91886A00606F53 /* CertificatesHeaderView.swift */; }; @@ -2286,6 +2287,7 @@ 09D94B5E8CF4C06397168F78 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = "my.lproj/Default Browser.strings"; sourceTree = ""; }; 09F14D989587092C60A0078F /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Shared.strings; sourceTree = ""; }; 0A3A4A9EB784F7903FB13B7A /* ms */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ms; path = ms.lproj/LoginManager.strings; sourceTree = ""; }; + 0A3F57F82CEDD4CA0051B001 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = ""; }; 0A4978492C53E63200B1E82A /* TrackingProtectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingProtectionViewController.swift; sourceTree = ""; }; 0A574D09BF8D9E37D6C9C654 /* bn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bn; path = bn.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; 0A5B4CE9B0996AE804491134 /* an */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = an; path = an.lproj/Shared.strings; sourceTree = ""; }; @@ -10965,6 +10967,7 @@ 74F80D332A0A52D700013C3D /* PrivacyPolicyViewController.swift */, 742BD99D2A13AC9000BA6B15 /* OnboardingInstructionPopupViewController.swift */, 81020C912BB5AFA2007B8481 /* OnboardingMultipleChoiceButtonView.swift */, + 0A3F57F82CEDD4CA0051B001 /* TermsOfServiceViewController.swift */, ); path = Views; sourceTree = ""; @@ -16426,6 +16429,7 @@ E18CE8DA2BDA3F6B00EE2BCD /* NavigationToolbarContainer.swift in Sources */, C8E2E80E23D20FD2005AACE6 /* FxAWebViewController.swift in Sources */, E14F7DF2288F3F9F00E3722C /* ThemedTableSectionHeaderFooterView.swift in Sources */, + 0A3F57F92CEDD4CA0051B001 /* TermsOfServiceViewController.swift in Sources */, 8A3EF7F22A2FCF4000796E3A /* DeleteExportedDataSetting.swift in Sources */, 8ADC2A142A33762900543DAA /* ReferringPage.swift in Sources */, 0E6C1E212C909AD7001A43BB /* PasswordGeneratorAction.swift in Sources */, diff --git a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift index 5f7585f680bf..0b457674ea3e 100644 --- a/firefox-ios/Client/Application/AccessibilityIdentifiers.swift +++ b/firefox-ios/Client/Application/AccessibilityIdentifiers.swift @@ -512,6 +512,15 @@ public struct AccessibilityIdentifiers { } } + struct TermsOfService { + static let logo = "TermsOfService.Logo" + static let title = "TermsOfService.Title" + static let termsOfServiceAgreement = "TermsOfService.TermsOfServiceAgreement" + static let privacyNoticeAgreement = "TermsOfService.PrivacyNoticeAgreement" + static let manageDataCollectionAgreement = "TermsOfService.ManageDataCollectionAgreement" + static let agreeAndContinueButton = "TermsOfService.AgreeAndContinueButton" + } + struct Upgrade { static let backgroundImage = "Upgrade.BackgroundImage" static let upgrade = "upgrade." diff --git a/firefox-ios/Client/Frontend/Onboarding/Views/TermsOfServiceViewController.swift b/firefox-ios/Client/Frontend/Onboarding/Views/TermsOfServiceViewController.swift new file mode 100644 index 000000000000..4ec84b9a28a0 --- /dev/null +++ b/firefox-ios/Client/Frontend/Onboarding/Views/TermsOfServiceViewController.swift @@ -0,0 +1,191 @@ +// 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 Shared +import UIKit +import ComponentLibrary + +class TermsOfServiceViewController: UIViewController, + Themeable { + struct UX { + static let horizontalMargin: CGFloat = 24 + static let logoIconSize: CGFloat = 160 + static let margin: CGFloat = 20 + static let agreementContentSpacing: CGFloat = 15 + static let distanceBetweenViews = 2 * margin + } + + // MARK: - Properties + var windowUUID: WindowUUID + var themeManager: ThemeManager + var themeObserver: (any NSObjectProtocol)? + var currentWindowUUID: UUID? { windowUUID } + var notificationCenter: NotificationProtocol + + // MARK: - UI elements + private lazy var contentScrollView: UIScrollView = .build() + + private lazy var contentView: UIView = .build() + + private lazy var titleLabel: UILabel = .build { label in + label.text = String(format: .Onboarding.TermsOfService.Title, AppName.shortName.rawValue) + label.font = FXFontStyles.Regular.title1.scaledFont() + label.textAlignment = .center + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true + label.accessibilityIdentifier = AccessibilityIdentifiers.TermsOfService.title + } + + private lazy var logoImage: UIImageView = .build { logoImage in + logoImage.image = UIImage(named: ImageIdentifiers.logo) + logoImage.accessibilityIdentifier = AccessibilityIdentifiers.TermsOfService.logo + } + + private lazy var confirmationButton: PrimaryRoundedButton = .build { [weak self] button in + let viewModel = PrimaryRoundedButtonViewModel( + title: .Onboarding.TermsOfService.AgreementButtonTitle, + a11yIdentifier: AccessibilityIdentifiers.TermsOfService.agreeAndContinueButton) + button.configure(viewModel: viewModel) + } + + private lazy var agreementContent: UIStackView = .build { stackView in + stackView.axis = .vertical + stackView.spacing = UX.agreementContentSpacing + } + + // MARK: - Initializers + init( + windowUUID: WindowUUID, + themeManager: ThemeManager = AppContainer.shared.resolve(), + notificationCenter: NotificationProtocol = NotificationCenter.default + ) { + self.windowUUID = windowUUID + self.themeManager = themeManager + self.notificationCenter = notificationCenter + super.init(nibName: nil, bundle: nil) + + setupLayout() + applyTheme() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View cycles + override func viewDidLoad() { + super.viewDidLoad() + listenForThemeChange(view) + } + + // MARK: - View setup + private func configure() { + agreementContent.removeAllArrangedViews() + let termsOfServiceLink = String(format: .Onboarding.TermsOfService.TermsOfServiceLink, AppName.shortName.rawValue) + let termsOfServiceAgreement = String(format: .Onboarding.TermsOfService.TermsOfServiceAgreement, termsOfServiceLink) + setupAgreementTextView(with: termsOfServiceAgreement, + linkTitle: termsOfServiceLink, + and: AccessibilityIdentifiers.TermsOfService.termsOfServiceAgreement) + + let privacyNoticeLink = String.Onboarding.TermsOfService.PrivacyNoticeLink + let privacyNoticeText = String.Onboarding.TermsOfService.PrivacyNoticeAgreement + let privacyAgreement = String(format: privacyNoticeText, AppName.shortName.rawValue, privacyNoticeLink) + setupAgreementTextView(with: privacyAgreement, + linkTitle: privacyNoticeLink, + and: AccessibilityIdentifiers.TermsOfService.privacyNoticeAgreement) + + let manageLink = String.Onboarding.TermsOfService.ManageLink + let manageText = String.Onboarding.TermsOfService.ManagePreferenceAgreement + let manageAgreement = String(format: manageText, AppName.shortName.rawValue, manageLink) + setupAgreementTextView(with: manageAgreement, + linkTitle: manageLink, + and: AccessibilityIdentifiers.TermsOfService.manageDataCollectionAgreement) + } + + private func setupLayout() { + view.addSubview(contentScrollView) + contentScrollView.addSubview(contentView) + contentView.addSubview(titleLabel) + contentView.addSubview(logoImage) + contentView.addSubview(confirmationButton) + contentView.addSubview(agreementContent) + + let topMargin = view.frame.size.height / 3 - UX.logoIconSize - UX.margin + + NSLayoutConstraint.activate([ + contentScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + contentScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + contentScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + contentScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + contentView.topAnchor.constraint(equalTo: contentScrollView.topAnchor), + contentView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor), + contentView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor), + contentView.heightAnchor.constraint(equalTo: contentScrollView.heightAnchor).priority(.defaultLow), + + logoImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + logoImage.heightAnchor.constraint(equalToConstant: UX.logoIconSize), + logoImage.widthAnchor.constraint(equalToConstant: UX.logoIconSize), + logoImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: topMargin), + + titleLabel.topAnchor.constraint(equalTo: logoImage.bottomAnchor, constant: UX.margin), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: UX.horizontalMargin), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -UX.horizontalMargin), + + agreementContent.topAnchor.constraint( + greaterThanOrEqualTo: titleLabel.bottomAnchor, + constant: UX.distanceBetweenViews + ), + agreementContent.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: UX.horizontalMargin), + agreementContent.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -UX.horizontalMargin), + + confirmationButton.topAnchor.constraint( + equalTo: agreementContent.bottomAnchor, + constant: UX.distanceBetweenViews + ), + confirmationButton.bottomAnchor.constraint( + equalTo: contentView.bottomAnchor, + constant: -UX.distanceBetweenViews + ), + confirmationButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: UX.horizontalMargin), + confirmationButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -UX.horizontalMargin) + ]) + } + + // TODO: FXIOS-10347 Firefox iOS: Manage Privacy Preferences during Onboarding + private func setupAgreementTextView(with title: String, linkTitle: String, and a11yId: String) { + let agreementLabel: UILabel = .build() + agreementLabel.accessibilityIdentifier = a11yId + agreementLabel.numberOfLines = 0 + agreementLabel.textAlignment = .center + agreementLabel.adjustsFontForContentSizeCategory = true + + let linkedAgreementDescription = NSMutableAttributedString(string: title) + let linkedText = (title as NSString).range(of: linkTitle) + linkedAgreementDescription.addAttribute(.font, + value: FXFontStyles.Regular.caption1.scaledFont(), + range: NSRange(location: 0, length: title.count)) + linkedAgreementDescription.addAttribute(.foregroundColor, + value: themeManager.getCurrentTheme(for: windowUUID).colors.textSecondary, + range: NSRange(location: 0, length: title.count)) + linkedAgreementDescription.addAttribute(.foregroundColor, + value: themeManager.getCurrentTheme(for: windowUUID).colors.textAccent, + range: linkedText) + + agreementLabel.attributedText = linkedAgreementDescription + agreementContent.addArrangedSubview(agreementLabel) + } + + // MARK: - Themable + func applyTheme() { + let theme = themeManager.getCurrentTheme(for: windowUUID) + view.backgroundColor = theme.colors.layer2 + titleLabel.textColor = theme.colors.textPrimary + confirmationButton.applyTheme(theme: theme) + configure() + } +} diff --git a/firefox-ios/Client/Frontend/Strings.swift b/firefox-ios/Client/Frontend/Strings.swift index a981194ed7e3..672c8db133ae 100644 --- a/firefox-ios/Client/Frontend/Strings.swift +++ b/firefox-ios/Client/Frontend/Strings.swift @@ -1646,6 +1646,49 @@ extension String { value: "Skip", comment: "Describes an action on some of the Onboarding screen, including the wallpaper onboarding screen. This string will be on a button so user can skip that onboarding page.") + public struct TermsOfService { + public static let Title = MZLocalizedString( + key: "", // Onboarding.TermsOfService.Title.v135 + tableName: "Onboarding", + value: "Welcome to %@", + comment: "Title for the Terms of Service screen in the onboarding process. Placeholder is for app name.") + public static let AgreementButtonTitle = MZLocalizedString( + key: "", // Onboarding.TermsOfService.AgreementButtonTitle.v135 + tableName: "Onboarding", + value: "Agree and continue", + comment: "Title for the confirmation button for Terms of Service agreement, in the Terms of Service screen.") + public static let TermsOfServiceAgreement = MZLocalizedString( + key: "", // Onboarding.TermsOfService.TermsOfServiceAgreement.v135 + tableName: "Onboarding", + value: "By continuing, you agree to the %@", + comment: "Agreement text for Terms of Service in the Terms of Service screen. Placeholder is for the Terms of Service link button that redirect the user to the Terms of Service page") + public static let PrivacyNoticeAgreement = MZLocalizedString( + key: "", // Onboarding.TermsOfService.PrivacyNoticeAgreement.v135 + tableName: "Onboarding", + value: "%@ cares about your privacy. Read more in our %@", + comment: "Agreement text for Privacy Notice in the Terms of Service screen. First placeholder is for the app name. The second placeholder is for the Privacy Notice link button that redirect the user to the Privacy Notice page") + public static let ManagePreferenceAgreement = MZLocalizedString( + key: "", // Onboarding.TermsOfService.ManagePreferenceAgreement.v135 + tableName: "Onboarding", + value: "To help improve the browser, %@ sends diagnostic and interaction data to Mozilla. %@", + comment: "Agreement text for sends diagnostic and interaction data to Mozilla in the Terms of Service screen. First placeholder is for the app name. The last placeholder is for for the Manage link button which redirect the user to another screen in order to manage the data collection preferences.") + public static let TermsOfServiceLink = MZLocalizedString( + key: "", // Onboarding.TermsOfService.TermsOfServiceLink.v135 + tableName: "Onboarding", + value: "%@ Terms of Service.", + comment: "Title for the Terms of Service button link, in the Terms of Service screen for redirecting the user to the Terms of Service page. Placeholder is for the app name.") + public static let PrivacyNoticeLink = MZLocalizedString( + key: "", // Onboarding.TermsOfService.PrivacyNoticeLink.v135 + tableName: "Onboarding", + value: "Privacy Notice.", + comment: "Title for the Privacy Notice button link, in the Terms of Service screen for redirecting the user to the Privacy Notice page.") + public static let ManageLink = MZLocalizedString( + key: "", // Onboarding.TermsOfService.ManageLink.v135 + tableName: "Onboarding", + value: "Manage", + comment: "Title for the Manage button link, in the Terms of Service screen for redirecting the user to the Manage data collection preferences screen.") + } + public struct Intro { public static let DescriptionPart1 = MZLocalizedString( key: "Onboarding.IntroDescriptionPart1.v114",