diff --git a/BraveShared/BraveStrings.swift b/BraveShared/BraveStrings.swift index 9d54d1fdc69..170fdfdd386 100644 --- a/BraveShared/BraveStrings.swift +++ b/BraveShared/BraveStrings.swift @@ -520,3 +520,13 @@ extension Strings { public static let QuickActionNewPrivateTab = NSLocalizedString("ShortcutItemTitleNewPrivateTab", tableName: "BraveShared", bundle: Bundle.braveShared, value: "New Private Tab", comment: "Quick Action for 3D-Touch on the Application Icon") public static let QuickActionScanQRCode = NSLocalizedString("ShortcutItemTitleQRCode", tableName: "BraveShared", bundle: Bundle.braveShared, value: "Scan QR Code", comment: "Quick Action for 3D-Touch on the Application Icon") } + +// MARK: - Onboarding +extension Strings { + public static let OBContinueButton = NSLocalizedString("OnboardingContinueButton", bundle: Bundle.shared, value: "Continue", comment: "Continue button to navigate to next onboarding screen.") + public static let OBSkipButton = NSLocalizedString("OnboardingSkipButton", bundle: Bundle.shared, value: "Skip", comment: "Skip button to skip onboarding and start using the app.") + public static let OBSearchEngineTitle = NSLocalizedString("OBSearchEngineTitle", bundle: Bundle.shared, value: "Welcome to Brave Browser", comment: "Title for search engine onboarding screen") + public static let OBSearchEngineDetail = NSLocalizedString("OBSearchEngineDetail", bundle: Bundle.shared, value: "Select your default search engine", comment: "Detail text for search engine onboarding screen") + public static let OBShieldsTitle = NSLocalizedString("OBShieldsTitle", bundle: Bundle.shared, value: "Brave Shields", comment: "Title for shields onboarding screen") + public static let OBShieldsDetail = NSLocalizedString("OBShieldsDetail", bundle: Bundle.shared, value: "Block privacy-invading trackers so you can browse without being followed around the web", comment: "Detail text for shields onboarding screen") +} diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 35a99233c75..6df29161a98 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -30,6 +30,10 @@ 0A1E84462190A57F0042F782 /* SyncAddDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1E843C2190A57F0042F782 /* SyncAddDeviceViewController.swift */; }; 0A2F921C22146B7700304249 /* FrecencyQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2F921B22146B7700304249 /* FrecencyQuery.swift */; }; 0A39D56D21930B89008B2772 /* ScriptOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A39D56C21930B89008B2772 /* ScriptOpener.swift */; }; + 0A3C789D23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C789C23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift */; }; + 0A3C789F23056C910022F6D8 /* OnboardingSearchEnginesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C789E23056C910022F6D8 /* OnboardingSearchEnginesView.swift */; }; + 0A3C78A1230597DA0022F6D8 /* OnboardingShieldsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C78A0230597DA0022F6D8 /* OnboardingShieldsViewController.swift */; }; + 0A3C78A3230597F10022F6D8 /* OnboardingShieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C78A2230597F10022F6D8 /* OnboardingShieldsView.swift */; }; 0A4214E921A6EBCF006B8E39 /* SafeBrowsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4214E821A6EBCF006B8E39 /* SafeBrowsingTests.swift */; }; 0A4214FF21AC0D6C006B8E39 /* TimeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4214F621AC0AFF006B8E39 /* TimeExtensions.swift */; }; 0A42150121AC0E8E006B8E39 /* TimeExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A42150021AC0E8E006B8E39 /* TimeExtensionTests.swift */; }; @@ -48,6 +52,8 @@ 0A4BEFDA221EF3360005551A /* NetworkResourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4BEFD9221EF3360005551A /* NetworkResourceType.swift */; }; 0A4BEFDE221F03C80005551A /* NetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4BEFDD221F03C80005551A /* NetworkManagerTests.swift */; }; 0A53F3E721E6560A0086E80C /* InMemoryDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A53F3E621E6560A0086E80C /* InMemoryDataController.swift */; }; + 0A6112AC230B00E7001BBC45 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6112AB230B00E7001BBC45 /* OnboardingNavigationController.swift */; }; + 0A6112BD230B4306001BBC45 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6112BC230B4306001BBC45 /* OnboardingViewController.swift */; }; 0A771D35218C8FDC00336E0D /* Bookmark+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A771D34218C8FDC00336E0D /* Bookmark+Sync.swift */; }; 0A7B5D6722689C5D00AADF22 /* AddEditHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7B5D6622689C5D00AADF22 /* AddEditHeaderView.swift */; }; 0A7B5D702269E72C00AADF22 /* BookmarkSaveLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7B5D6F2269E72C00AADF22 /* BookmarkSaveLocation.swift */; }; @@ -1171,6 +1177,10 @@ 0A2F921B22146B7700304249 /* FrecencyQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrecencyQuery.swift; sourceTree = ""; }; 0A38EA7F216532DB00142710 /* CRUDProtocolsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CRUDProtocolsTests.swift; sourceTree = ""; }; 0A39D56C21930B89008B2772 /* ScriptOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptOpener.swift; sourceTree = ""; }; + 0A3C789C23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSearchEnginesViewController.swift; sourceTree = ""; }; + 0A3C789E23056C910022F6D8 /* OnboardingSearchEnginesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSearchEnginesView.swift; sourceTree = ""; }; + 0A3C78A0230597DA0022F6D8 /* OnboardingShieldsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingShieldsViewController.swift; sourceTree = ""; }; + 0A3C78A2230597F10022F6D8 /* OnboardingShieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingShieldsView.swift; sourceTree = ""; }; 0A4214E821A6EBCF006B8E39 /* SafeBrowsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeBrowsingTests.swift; sourceTree = ""; }; 0A4214F621AC0AFF006B8E39 /* TimeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeExtensions.swift; sourceTree = ""; }; 0A42150021AC0E8E006B8E39 /* TimeExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeExtensionTests.swift; sourceTree = ""; }; @@ -1189,6 +1199,8 @@ 0A4BEFD9221EF3360005551A /* NetworkResourceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResourceType.swift; sourceTree = ""; }; 0A4BEFDD221F03C80005551A /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = ""; }; 0A53F3E621E6560A0086E80C /* InMemoryDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryDataController.swift; sourceTree = ""; }; + 0A6112AB230B00E7001BBC45 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; + 0A6112BC230B4306001BBC45 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 0A6231E32121F761007B429B /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0A771D34218C8FDC00336E0D /* Bookmark+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+Sync.swift"; sourceTree = ""; }; 0A7B5D6622689C5D00AADF22 /* AddEditHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditHeaderView.swift; sourceTree = ""; }; @@ -2483,6 +2495,19 @@ path = Sync; sourceTree = ""; }; + 0A3C789423055AC80022F6D8 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 0A6112BC230B4306001BBC45 /* OnboardingViewController.swift */, + 0A6112AB230B00E7001BBC45 /* OnboardingNavigationController.swift */, + 0A3C789C23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift */, + 0A3C789E23056C910022F6D8 /* OnboardingSearchEnginesView.swift */, + 0A3C78A0230597DA0022F6D8 /* OnboardingShieldsViewController.swift */, + 0A3C78A2230597F10022F6D8 /* OnboardingShieldsView.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; 0A4319B321B1C4D60041625B /* Adblock */ = { isa = PBXGroup; children = ( @@ -3750,6 +3775,7 @@ D3A994941A368691008AD1AC /* Browser */ = { isa = PBXGroup; children = ( + 0A3C789423055AC80022F6D8 /* Onboarding */, C6D267612137E45D00465DFA /* PrivacyProtection */, C6D267592137E39A00465DFA /* ImageCache */, 0AADC4BB20D2A4F700FDE368 /* HomePanel */, @@ -5689,6 +5715,7 @@ E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */, 0A1E843E2190A57F0042F782 /* SyncCodewordsView.swift in Sources */, E68F36981EA694000048CF44 /* PanelDataObservers.swift in Sources */, + 0A3C789F23056C910022F6D8 /* OnboardingSearchEnginesView.swift in Sources */, 31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */, 0A4B012020D02EC4004D4011 /* TabsBarViewController.swift in Sources */, 4422D4D921BFFB7600BF1855 /* merger.cc in Sources */, @@ -5726,6 +5753,7 @@ C615FAD9212A1E2600A8168C /* WebImageCache.swift in Sources */, C615FAED212ACAD200A8168C /* BraveWebView.swift in Sources */, C817B34D1FC609500086018E /* UIScrollViewSwizzled.swift in Sources */, + 0A3C78A3230597F10022F6D8 /* OnboardingShieldsView.swift in Sources */, 4422D4E221BFFB7600BF1855 /* format.cc in Sources */, 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */, 4422D50B21BFFB7600BF1855 /* memenv.cc in Sources */, @@ -5772,6 +5800,7 @@ 0A0D3D5221A565C300BEE65B /* SafeBrowsingHandler.swift in Sources */, D0C95E36200FDC5500E4E51C /* MetadataParserHelper.swift in Sources */, 4422D55B21BFFB7F00BF1855 /* onepass.cc in Sources */, + 0A3C78A1230597DA0022F6D8 /* OnboardingShieldsViewController.swift in Sources */, A1CDF22D20BDDB66005C6E58 /* BasicAnimationController.swift in Sources */, 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */, D0C95EF6201A55A800E4E51C /* BrowserViewController+UIDropInteractionDelegate.swift in Sources */, @@ -5870,6 +5899,7 @@ D03F8EB22004014E003C2224 /* FaviconHandler.swift in Sources */, 0AB2442C22AA789B00B4D9DD /* ReaderModeButton.swift in Sources */, 4422D55E21BFFB7F00BF1855 /* mimics_pcre.cc in Sources */, + 0A6112AC230B00E7001BBC45 /* OnboardingNavigationController.swift in Sources */, 4422D57321BFFB7F00BF1855 /* stringpiece.cc in Sources */, 27187808216526090006036E /* AlertPopupView.swift in Sources */, 4422D56421BFFB7F00BF1855 /* re2.cc in Sources */, @@ -5892,6 +5922,7 @@ D02816C21ECA5E2A00240CAA /* HistoryStateHelper.swift in Sources */, 0AEFB84922244135007AF600 /* AdblockDebugMenuTableViewController.swift in Sources */, E68E7ACB1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift in Sources */, + 0A6112BD230B4306001BBC45 /* OnboardingViewController.swift in Sources */, D04D1B92209790B60074B35F /* Toast.swift in Sources */, D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */, 0A93F1892264C72000A3571B /* BookmarkFormFieldsProtocol.swift in Sources */, @@ -5914,6 +5945,7 @@ 4422D56521BFFB7F00BF1855 /* filtered_re2.cc in Sources */, 279C756B219DDE3B001CD1CB /* FingerprintingProtection.swift in Sources */, E650755F1E37F756006961AC /* Try.m in Sources */, + 0A3C789D23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift in Sources */, 3B6889C51D66950E002AC85E /* UIImageColors.swift in Sources */, 0A1E84402190A57F0042F782 /* SyncSelectDeviceTypeViewController.swift in Sources */, 392ED7E41D0AEF56009D9B62 /* NewTabAccessors.swift in Sources */, diff --git a/Client/Application/ClientPreferences.swift b/Client/Application/ClientPreferences.swift index f4df808e4c0..4c47c3bf917 100644 --- a/Client/Application/ClientPreferences.swift +++ b/Client/Application/ClientPreferences.swift @@ -50,6 +50,9 @@ extension Preferences { /// /// Currently unused. static let showClipboardBar = Option(key: "general.show-clipboard-bar", default: false) + /// Whether or not user onboarding has completed. + /// User skipping onboarding counts as completed too. + static let onboardingCompleted = Option(key: "general.onboarding-completed", default: false) } final class Search { /// Whether or not to show suggestions while the user types diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index a1f619ee446..4c12fbfc34c 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -596,6 +596,8 @@ class BrowserViewController: UIViewController { } override func viewDidAppear(_ animated: Bool) { + presentOnboardingIntro() + screenshotHelper.viewIsVisible = true screenshotHelper.takePendingScreenshots(tabManager.allTabs) @@ -623,6 +625,16 @@ class BrowserViewController: UIViewController { } } } + + func presentOnboardingIntro() { + if Preferences.General.onboardingCompleted.value { return } + + guard let onboarding = OnboardingNavigationController(profile: profile) else { return } + onboarding.onboardingDelegate = self + + + present(onboarding, animated: true) + } // THe logic for shouldShowWhatsNewTab is as follows: If we do not have the LatestAppVersionProfileKey in // the profile, that means that this is a fresh install and we do not show the What's New. If we do have @@ -3105,3 +3117,10 @@ extension BrowserViewController { } } } + +extension BrowserViewController: OnboardingControllerDelegate { + func onboardingCompleted(_ onboardingController: OnboardingNavigationController) { + Preferences.General.onboardingCompleted.value = true + onboardingController.dismiss(animated: true) + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift b/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift new file mode 100644 index 00000000000..35cde21fdda --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift @@ -0,0 +1,121 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import Shared + +private let log = Logger.browserLogger + +protocol Onboardable: class { + /// Show next on boarding screen if possible. + /// If last screen is currently presenting, the view is dimissed instead(onboarding finished). + func presentNextScreen(current: OnboardingViewController) + /// Skip all onboarding screens, onboarding is considered as completed. + func skip() +} + +protocol OnboardingControllerDelegate: class { + func onboardingCompleted(_ onboardingController: OnboardingNavigationController) +} + +class OnboardingNavigationController: UINavigationController { + + private struct UX { + /// The onboarding screens are showing as a modal on iPads. + static let preferredModalSize = CGSize(width: 375, height: 667) + } + + weak var onboardingDelegate: OnboardingControllerDelegate? + + enum Screens: CaseIterable { + case searchEnginePicker + case shieldsInfo + + func viewController(with profile: Profile) -> OnboardingViewController { + switch self { + case .searchEnginePicker: + return OnboardingSearchEnginesViewController(profile: profile) + case .shieldsInfo: + return OnboardingShieldsViewController(profile: profile) + } + } + + var type: AnyClass { + switch self { + case .searchEnginePicker: return OnboardingSearchEnginesViewController.self + case .shieldsInfo: return OnboardingShieldsViewController.self + } + } + } + + init?(profile: Profile) { + guard let firstScreen = Screens.allCases.first else { return nil } + + let firstViewController = firstScreen.viewController(with: profile) + super.init(rootViewController: firstViewController) + firstViewController.delegate = self + + isNavigationBarHidden = true + if UIDevice.current.userInterfaceIdiom == .phone { + modalPresentationStyle = .fullScreen + } else { + modalPresentationStyle = .formSheet + } + + if #available(iOS 13.0, *) { + // Prevent dismissing the modal by swipe + isModalInPresentation = true + } + preferredContentSize = UX.preferredModalSize + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } +} + +extension OnboardingNavigationController: Onboardable { + + func presentNextScreen(current: OnboardingViewController) { + let allScreens = Screens.allCases + let index = allScreens.map { $0.type }.firstIndex(where: { $0 == type(of: current) }) + + guard let nextIndex = index?.advanced(by: 1), + let nextScreen = allScreens[safe: nextIndex]?.viewController(with: current.profile) else { + log.info("Last screen reached, onboarding is complete") + onboardingDelegate?.onboardingCompleted(self) + return + } + + nextScreen.delegate = self + + pushViewController(nextScreen, animated: true) + } + + func skip() { + onboardingDelegate?.onboardingCompleted(self) + } +} + +// Disabling orientation changes +extension OnboardingNavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { + return .default + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + + override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { + return .portrait + } + + override var shouldAutorotate: Bool { + return false + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesView.swift b/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesView.swift new file mode 100644 index 00000000000..30bace46981 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesView.swift @@ -0,0 +1,127 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import Shared +import BraveShared + +extension OnboardingSearchEnginesViewController { + + private struct UX { + static let topInset: CGFloat = 64 + static let contentInset: CGFloat = 16 + + struct SearchEngineCell { + static let rowHeight: CGFloat = 64 + static let imageSize: CGFloat = 32 + static let cornerRadius: CGFloat = 15 + static let selectedBackgroundColor = #colorLiteral(red: 0.8431372549, green: 0.8431372549, blue: 0.8965459466, alpha: 1) + static let deselectedBackgroundColor: UIColor = .white + } + } + + class View: UIView { + + let searchEnginesTable = UITableView().then { + $0.separatorStyle = .none + $0.allowsMultipleSelection = false + $0.alwaysBounceVertical = false + } + + let continueButton = CommonViews.primaryButton().then { + $0.accessibilityIdentifier = "OnboardingSearchEnginesViewController.ContinueButton" + } + + let skipButton = CommonViews.secondaryButton().then { + $0.accessibilityIdentifier = "OnboardingSearchEnginesViewController.SkipButton" + } + + private let mainStackView = UIStackView().then { + $0.axis = .vertical + $0.alignment = .fill + $0.spacing = 16 + $0.translatesAutoresizingMaskIntoConstraints = false + } + + private let braveLogo = UIImageView(image: #imageLiteral(resourceName: "browser_lock_popup")).then { + $0.contentMode = .scaleAspectFit + } + + private let titleStackView = UIStackView().then { stackView in + stackView.axis = .vertical + + let titlePrimary = CommonViews.primaryText(Strings.OBSearchEngineTitle) + let titleSecondary = CommonViews.secondaryText(Strings.OBSearchEngineDetail) + + [titlePrimary, titleSecondary].forEach(stackView.addArrangedSubview(_:)) + } + + private let buttonsStackView = UIStackView().then { + $0.distribution = .equalCentering + } + + init() { + super.init(frame: .zero) + backgroundColor = .white + + [skipButton, continueButton, UIView.spacer(.horizontal, amount: 0)] + .forEach(buttonsStackView.addArrangedSubview(_:)) + + [braveLogo, titleStackView, searchEnginesTable, buttonsStackView] + .forEach(mainStackView.addArrangedSubview(_:)) + + addSubview(mainStackView) + + mainStackView.snp.makeConstraints { + $0.top.equalTo(self.safeArea.top).inset(UX.topInset) + + $0.leading.equalTo(self.safeArea.leading).inset(UX.contentInset) + $0.trailing.equalTo(self.safeArea.trailing).inset(UX.contentInset) + $0.bottom.equalTo(self.safeArea.bottom).inset(UX.contentInset) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + } + + class SearchEngineCell: UITableViewCell { + + static let preferredHeight = UX.SearchEngineCell.rowHeight + + var searchEngineName: String? { + set { textLabel?.text = newValue } + get { return textLabel?.text } + } + + var searchEngineImage: UIImage? { + set { imageView?.image = newValue } + get { return imageView?.image } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + imageView?.contentMode = .scaleAspectFit + layer.cornerRadius = UX.SearchEngineCell.cornerRadius + selectionStyle = .none + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + backgroundColor = selected ? + UX.SearchEngineCell.selectedBackgroundColor : UX.SearchEngineCell.deselectedBackgroundColor + } + + override func layoutSubviews() { + super.layoutSubviews() + let size = UX.SearchEngineCell.imageSize + imageView?.bounds = CGRect(x: 0, y: 0, width: size, height: size) + } + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesViewController.swift b/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesViewController.swift new file mode 100644 index 00000000000..a9fab51d731 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingSearchEnginesViewController.swift @@ -0,0 +1,87 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveShared +import Shared + +private let log = Logger.browserLogger + +class OnboardingSearchEnginesViewController: OnboardingViewController { + + let searchEngines: SearchEngines + + private var contentView: View { + return view as! View // swiftlint:disable:this force_cast + } + + override func loadView() { + view = View() + } + + override init(profile: Profile) { + self.searchEngines = profile.searchEngines + super.init(profile: profile) + + //super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + + contentView.searchEnginesTable.dataSource = self + contentView.searchEnginesTable.delegate = self + + contentView.continueButton.addTarget(self, action: #selector(continueTapped), for: .touchDown) + contentView.skipButton.addTarget(self, action: #selector(skipTapped), for: .touchDown) + } + + @objc override func continueTapped() { + guard let selectedRow = contentView.searchEnginesTable.indexPathForSelectedRow?.row, + let selectedEngine = searchEngines.orderedEngines[safe: selectedRow]?.shortName else { + return + log.error("Failed to unwrap selected row or selected engine.") + } + + searchEngines.setDefaultEngine(selectedEngine, forType: .standard) + searchEngines.setDefaultEngine(selectedEngine, forType: .privateMode) + + delegate?.presentNextScreen(current: self) + } + +} + +extension OnboardingSearchEnginesViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return SearchEngineCell.preferredHeight + } +} + +extension OnboardingSearchEnginesViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return searchEngines.orderedEngines.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = SearchEngineCell() + + guard let searchEngine = searchEngines.orderedEngines[safe: indexPath.row] else { + log.error("Can't find search engine at index: \(indexPath.row)") + assertionFailure() + return cell + } + + let defaultEngine = searchEngines.defaultEngine() + + cell.searchEngineName = searchEngine.shortName + cell.searchEngineImage = searchEngine.image + + if searchEngine == defaultEngine { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) + } + + return cell + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingShieldsView.swift b/Client/Frontend/Browser/Onboarding/OnboardingShieldsView.swift new file mode 100644 index 00000000000..9f6038f00ea --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingShieldsView.swift @@ -0,0 +1,111 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import Shared +import BraveShared + +extension OnboardingShieldsViewController { + + private struct UX { + /// A negative spacing is needed to make rounded corners for details view visible. + static let negativeSpacing: CGFloat = -16 + static let descriptionContentInset: CGFloat = 32 + } + + class View: UIView { + + let continueButton = CommonViews.primaryButton().then { + $0.accessibilityIdentifier = "OnboardingShieldsViewController.ContinueButton" + } + + let skipButton = CommonViews.secondaryButton().then { + $0.accessibilityIdentifier = "OnboardingShieldsViewController.SkipButton" + } + + private let mainStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = UX.negativeSpacing + } + + private let imageView = UIImageView().then { + $0.backgroundColor = #colorLiteral(red: 0.1176470588, green: 0.1176470588, blue: 0.1568627451, alpha: 1) + } + + private let descriptionView = UIView().then { + $0.backgroundColor = .white + $0.layer.cornerRadius = 20 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + + private let descriptionStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 32 + } + + private let textStackView = UIStackView().then { stackView in + stackView.axis = .vertical + stackView.spacing = 8 + + let titleLabel = CommonViews.primaryText(Strings.OBShieldsTitle) + + let descriptionLabel = CommonViews.secondaryText("").then { + $0.attributedText = Strings.OBShieldsDetail.makeFirstWordBold(with: $0.font) + } + + [titleLabel, descriptionLabel].forEach { + stackView.addArrangedSubview($0) + } + } + + private let buttonsStackView = UIStackView().then { + $0.distribution = .equalCentering + } + + init() { + super.init(frame: .zero) + + [imageView, descriptionView].forEach(mainStackView.addArrangedSubview(_:)) + + [skipButton, continueButton, UIView.spacer(.horizontal, amount: 0)] + .forEach(buttonsStackView.addArrangedSubview(_:)) + + [textStackView, buttonsStackView].forEach(descriptionStackView.addArrangedSubview(_:)) + + addSubview(mainStackView) + descriptionView.addSubview(descriptionStackView) + + mainStackView.snp.makeConstraints { + $0.leading.equalTo(self.safeArea.leading) + $0.trailing.equalTo(self.safeArea.trailing) + $0.bottom.equalTo(self.safeArea.bottom) + $0.top.equalTo(self) // extend the view undeneath the safe area/notch + } + + descriptionStackView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UX.descriptionContentInset) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + } +} + +private extension String { + func makeFirstWordBold(with font: UIFont) -> NSMutableAttributedString { + let mutableDescriptionText = NSMutableAttributedString(string: self) + + if let firstWord = self.components(separatedBy: " ").first { + if let range = self.range(of: firstWord) { + let nsRange = NSRange(range, in: self) + let font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.semibold) + + mutableDescriptionText.addAttribute(NSAttributedString.Key.font, value: font, range: nsRange) + } + } + + return mutableDescriptionText + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingShieldsViewController.swift b/Client/Frontend/Browser/Onboarding/OnboardingShieldsViewController.swift new file mode 100644 index 00000000000..74f850b6435 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingShieldsViewController.swift @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit + +class OnboardingShieldsViewController: OnboardingViewController { + + private var contentView: View { + return view as! View // swiftlint:disable:this force_cast + } + + override func loadView() { + view = View() + } + + override func viewDidLoad() { + super.viewDidLoad() + + contentView.continueButton.addTarget(self, action: #selector(continueTapped), for: .touchDown) + contentView.skipButton.addTarget(self, action: #selector(skipTapped), for: .touchDown) + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingViewController.swift b/Client/Frontend/Browser/Onboarding/OnboardingViewController.swift new file mode 100644 index 00000000000..b0bf492bd46 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingViewController.swift @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveShared +import Shared + +/// A base class to provide common implementations needed for user onboarding screens. +class OnboardingViewController: UIViewController { + weak var delegate: Onboardable? + var profile: Profile + + init(profile: Profile) { + self.profile = profile + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + + /// Default behavior to present next onboarding screen. + /// Override it to add custom behavior. + @objc func continueTapped() { + delegate?.presentNextScreen(current: self) + } + + /// Default behavior if skip onboarding is tapped. + /// Override it to add custom behavior. + @objc func skipTapped() { + delegate?.skip() + } + + struct CommonViews { + + static func primaryButton(text: String = Strings.OBContinueButton) -> UIButton { + let button = RoundInterfaceButton().then { + $0.setTitle(text, for: .normal) + $0.backgroundColor = BraveUX.BraveOrange + $0.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) + } + + return button + } + + static func secondaryButton(text: String = Strings.OBSkipButton) -> UIButton { + let button = UIButton().then { + $0.setTitle(text, for: .normal) + $0.setTitleColor(.gray, for: .normal) + } + + return button + } + + static func primaryText(_ text: String) -> UILabel { + let label = UILabel().then { + $0.text = text + $0.font = UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.semibold) + $0.textAlignment = .center + } + + return label + } + + static func secondaryText(_ text: String) -> UILabel { + let label = UILabel().then { + $0.text = text + $0.font = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.regular) + $0.textAlignment = .center + $0.numberOfLines = 0 + } + + return label + } + } +}