diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index ed5471b5fc87..e734bb3610fc 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -250,6 +250,12 @@ 435D660323D793DF0046EFA2 /* UpdateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D660223D793DF0046EFA2 /* UpdateModel.swift */; }; 435D660523D794B90046EFA2 /* UpdateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D660423D794B90046EFA2 /* UpdateViewModel.swift */; }; 435D660723D7962C0046EFA2 /* UpdateCoverSheetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D660623D7962C0046EFA2 /* UpdateCoverSheetTableViewCell.swift */; }; + 435D7CC02461EFCB0043ACB9 /* IntroScreenWelcomeViewV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D7CBF2461EFCB0043ACB9 /* IntroScreenWelcomeViewV2.swift */; }; + 435D7CC32461EFDD0043ACB9 /* IntroScreenSyncViewV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D7CC22461EFDD0043ACB9 /* IntroScreenSyncViewV2.swift */; }; + 435D7CC5246209AA0043ACB9 /* IntroViewControllerV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435D7CC4246209AA0043ACB9 /* IntroViewControllerV2.swift */; }; + 4390F7F1246C76E700570811 /* IntroWelcomeAndSyncViewV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4390F7F0246C76E700570811 /* IntroWelcomeAndSyncViewV1.swift */; }; + 4390F7F5246CE04300570811 /* IntroViewModelV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4390F7F4246CE04300570811 /* IntroViewModelV2.swift */; }; + 4390F7FF246DAFBE00570811 /* FirefoxColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4390F7FE246DAFBE00570811 /* FirefoxColors.swift */; }; 43A5643823CD1E1C00B6857D /* UpdateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A5643523CD1E1B00B6857D /* UpdateViewController.swift */; }; 43A5643923CD1E1C00B6857D /* Update.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43A5643623CD1E1B00B6857D /* Update.xcassets */; }; 43B137F223A181A200CB7FA0 /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B137F123A181A200CB7FA0 /* NSUserDefaultsPrefs.swift */; }; @@ -524,7 +530,6 @@ E487B24E1AC1C66400F3E86F /* FiraSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7571A793CF20022C5E0 /* FiraSans-SemiBold.ttf */; }; E487B24F1AC1CC9200F3E86F /* FiraSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4ECCDAD1AB131770005E717 /* FiraSans-Medium.ttf */; }; E487B2501AC1CC9800F3E86F /* FiraSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7521A793CF20022C5E0 /* FiraSans-Light.ttf */; }; - E49943F51AE6879C00BF9DE4 /* IntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */; }; E49943F71AE69EDD00BF9DE4 /* Intro.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */; }; E4A888161A95679500CDC337 /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A960051ABB9C450069AD6F /* ReaderModeUtils.swift */; }; @@ -1387,7 +1392,13 @@ 435D660223D793DF0046EFA2 /* UpdateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateModel.swift; sourceTree = ""; }; 435D660423D794B90046EFA2 /* UpdateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateViewModel.swift; sourceTree = ""; }; 435D660623D7962C0046EFA2 /* UpdateCoverSheetTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCoverSheetTableViewCell.swift; sourceTree = ""; }; + 435D7CBF2461EFCB0043ACB9 /* IntroScreenWelcomeViewV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroScreenWelcomeViewV2.swift; sourceTree = ""; }; + 435D7CC22461EFDD0043ACB9 /* IntroScreenSyncViewV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroScreenSyncViewV2.swift; sourceTree = ""; }; + 435D7CC4246209AA0043ACB9 /* IntroViewControllerV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroViewControllerV2.swift; sourceTree = ""; }; 435FAB17242404B400AE9310 /* FullscreenHelper.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = FullscreenHelper.js; path = Client/Frontend/UserContent/UserScripts/AllFrames/AtDocumentStart/FullscreenHelper.js; sourceTree = SOURCE_ROOT; }; + 4390F7F0246C76E700570811 /* IntroWelcomeAndSyncViewV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroWelcomeAndSyncViewV1.swift; sourceTree = ""; }; + 4390F7F4246CE04300570811 /* IntroViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroViewModelV2.swift; sourceTree = ""; }; + 4390F7FE246DAFBE00570811 /* FirefoxColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxColors.swift; sourceTree = ""; }; 43A5643523CD1E1B00B6857D /* UpdateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateViewController.swift; sourceTree = ""; }; 43A5643623CD1E1B00B6857D /* Update.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Update.xcassets; sourceTree = ""; }; 43B137F123A181A200CB7FA0 /* NSUserDefaultsPrefs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsPrefs.swift; sourceTree = ""; }; @@ -1647,7 +1658,6 @@ E4424B3B1AC71FB400F44C38 /* FiraSans-Book.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Book.ttf"; sourceTree = ""; }; E46175F21EBB73A10021AE8A /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = Carthage/Build/iOS/Sentry.framework; sourceTree = ""; }; E47616C61AB74CA600E7DD25 /* ReaderModeBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeBarView.swift; sourceTree = ""; }; - E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroViewController.swift; sourceTree = ""; }; E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Intro.xcassets; sourceTree = ""; }; E4A960051ABB9C450069AD6F /* ReaderModeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeUtils.swift; sourceTree = ""; }; E4A961171AC041C40069AD6F /* ReadabilityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadabilityService.swift; sourceTree = ""; }; @@ -3031,8 +3041,12 @@ E49943F31AE6879C00BF9DE4 /* Intro */ = { isa = PBXGroup; children = ( - E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */, E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */, + 4390F7F4246CE04300570811 /* IntroViewModelV2.swift */, + 435D7CC4246209AA0043ACB9 /* IntroViewControllerV2.swift */, + 435D7CBF2461EFCB0043ACB9 /* IntroScreenWelcomeViewV2.swift */, + 435D7CC22461EFDD0043ACB9 /* IntroScreenSyncViewV2.swift */, + 4390F7F0246C76E700570811 /* IntroWelcomeAndSyncViewV1.swift */, ); path = Intro; sourceTree = ""; @@ -3548,6 +3562,7 @@ children = ( 2816EFFF1B33E05400522243 /* UIConstants.swift */, 2C49854D206173C800893DAE /* photon-colors.swift */, + 4390F7FE246DAFBE00570811 /* FirefoxColors.swift */, 392ED7D51D0AEEEE009D9B62 /* Accessors */, E692E3271C46E62D009D1240 /* AuthenticationManager */, D3A994941A368691008AD1AC /* Browser */, @@ -5039,6 +5054,7 @@ D3B6923F1B9F9A58004B87A4 /* FindInPageHelper.swift in Sources */, EB9A179C20E69A7F00B12184 /* DarkTheme.swift in Sources */, EB1C84BF212EFFBF001489DF /* BrowserViewController+ReaderMode.swift in Sources */, + 4390F7F1246C76E700570811 /* IntroWelcomeAndSyncViewV1.swift in Sources */, EBA3B2CD2268F27500728BDB /* PhotonActionSheetWidgets.swift in Sources */, D3C3696E1CC6B78800348A61 /* LocalRequestHelper.swift in Sources */, E4B423DD1ABA0318007E66C8 /* ReaderModeHandlers.swift in Sources */, @@ -5117,6 +5133,7 @@ EB9854FF2422686F0040F24B /* AppDelegate+PushNotifications.swift in Sources */, E64ED8FA1BC55AE300DAF864 /* UIAlertControllerExtensions.swift in Sources */, 39CE74F821A83105007AE4F2 /* TranslationSettingsController.swift in Sources */, + 435D7CC02461EFCB0043ACB9 /* IntroScreenWelcomeViewV2.swift in Sources */, 282DA4731A68C1E700A406E2 /* OpenSearch.swift in Sources */, 39CE74F421A72309007AE4F2 /* TranslationToastHandler.swift in Sources */, D04CD74B216CF86B004FF5B0 /* DevicePickerViewController.swift in Sources */, @@ -5141,10 +5158,12 @@ C4F3B29A1CFCF93A00966259 /* ButtonToast.swift in Sources */, EBA3B2D02268F40C00728BDB /* SyncMenuButton.swift in Sources */, D31A0FC71A65D6D000DC8C7E /* SearchSuggestClient.swift in Sources */, + 435D7CC5246209AA0043ACB9 /* IntroViewControllerV2.swift in Sources */, A83E5AB71C1D993D0026D912 /* UIPasteboardExtensions.swift in Sources */, D8EFFA0C1FF5B1FA001D3A09 /* NavigationRouter.swift in Sources */, D0E55C4F1FB4FD23006DC274 /* FormPostHelper.swift in Sources */, E650754E1E37F6AE006961AC /* GeometryExtensions.swift in Sources */, + 4390F7F5246CE04300570811 /* IntroViewModelV2.swift in Sources */, D3972BF41C22412B00035B87 /* TitleActivityItemProvider.swift in Sources */, D38A1BEE1A9FA2CA00F6A386 /* SiteTableViewController.swift in Sources */, 7BA0601B1C0F4DE200DFADB6 /* TabPeekViewController.swift in Sources */, @@ -5191,6 +5210,7 @@ EBB89504219398E500EB91A0 /* TrackingProtectionPageStats.swift in Sources */, D31F95E91AC226CB005C9F3B /* ScreenshotHelper.swift in Sources */, 28EADE5D1AFC3A78007FB2FB /* UIImageViewExtensions.swift in Sources */, + 435D7CC32461EFDD0043ACB9 /* IntroScreenSyncViewV2.swift in Sources */, 39CE74F021A6D2B8007AE4F2 /* DocumentServicesHelper.swift in Sources */, D3968F251A38FE8500CEFD3B /* TabManager.swift in Sources */, C4E398601D22C409004E89BA /* TopTabsLayout.swift in Sources */, @@ -5244,8 +5264,8 @@ E40FAB0C1A7ABB77009CB80D /* WebServer.swift in Sources */, 59A68D66379CFA85C4EAF00B /* TwoLineCell.swift in Sources */, D04CD718215EBD85004FF5B0 /* SettingsLoadingView.swift in Sources */, + 4390F7FF246DAFBE00570811 /* FirefoxColors.swift in Sources */, D04CD74C216CF86B004FF5B0 /* InstructionsViewController.swift in Sources */, - E49943F51AE6879C00BF9DE4 /* IntroViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Client/Application/LeanplumIntegration.swift b/Client/Application/LeanplumIntegration.swift index fce632c55471..a6452e4adc18 100644 --- a/Client/Application/LeanplumIntegration.swift +++ b/Client/Application/LeanplumIntegration.swift @@ -81,6 +81,9 @@ struct LPAttributeKey { static let telemetryOptIn = "Telemetry Opt In" static let fxaAccountVerified = "FxA account is verified" static let fxaDeviceCount = "Number of devices in FxA account" + static let experimentName = "Experiment name" + static let experimentId = "Experiment id" + static let experimentVariant = "Experiment variant" } struct MozillaAppSchemes { diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index 4b4d40ae49d6..98c6a6baaa04 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -453,7 +453,7 @@ class BrowserViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(self.appMenuBadgeUpdate), name: .FirefoxAccountStateChange, object: nil) - // Setup onboarding user research for A/A testing + // Setup onboarding user research for A/B testing onboardingUserResearch = OnboardingUserResearch() onboardingUserResearch?.lpVariableObserver() } @@ -1913,35 +1913,15 @@ extension BrowserViewController: UIAdaptivePresentationControllerDelegate { } } -extension BrowserViewController: IntroViewControllerDelegate { - @discardableResult func presentIntroViewController(_ force: Bool = false, animated: Bool = true) -> Bool { - onboardingUserResearchHelper() +extension BrowserViewController { + func presentIntroViewController(_ forcedType: OnboardingScreenType? = nil) { if let deeplink = self.profile.prefs.stringForKey("AdjustDeeplinkKey"), let url = URL(string: deeplink) { self.launchFxAFromDeeplinkURL(url) - return true + return } - - if force || profile.prefs.intForKey(PrefsKeys.IntroSeen) == nil { - let introViewController = IntroViewController() - introViewController.delegate = self - // On iPad we present it modally in a controller - if topTabsVisible { - introViewController.preferredContentSize = CGSize(width: ViewControllerConsts.PreferredSize.IntroViewController.width, height: ViewControllerConsts.PreferredSize.IntroViewController.height) - introViewController.modalPresentationStyle = .formSheet - } else { - introViewController.modalPresentationStyle = .fullScreen - } - present(introViewController, animated: animated) { - // On first run (and forced) open up the homepage in the background. - if let homePageURL = NewTabHomePageAccessors.getHomePage(self.profile.prefs), let tab = self.tabManager.selectedTab, DeviceInfo.hasConnectivity() { - tab.loadRequest(URLRequest(url: homePageURL)) - } - } - - return true + if forcedType != nil || profile.prefs.intForKey(PrefsKeys.IntroSeen) == nil { + onboardingUserResearchHelper(forcedType) } - - return false } func presentETPCoverSheetViewController(_ force: Bool = false) { @@ -2015,10 +1995,59 @@ extension BrowserViewController: IntroViewControllerDelegate { return false } - func onboardingUserResearchHelper() { + private func onboardingUserResearchHelper(_ forcedType: OnboardingScreenType? = nil) { print("lp initial value \(String(describing: onboardingUserResearch?.lpVariable?.boolValue()))") + if forcedType != nil { + showProperIntroVC(forcedType) + return + } + let screenType = onboardingUserResearch?.onboardingScreenType + if screenType == nil && !DeviceInfo.hasConnectivity() { + self.onboardingUserResearch?.updateValue(value: true) + showProperIntroVC() + return + } onboardingUserResearch?.updatedLPVariables = {(lpVariable) -> () in - print("lpVariable \(String(describing: lpVariable?.boolValue()))") + if screenType == nil { + print("lp Variable from server \(String(describing: lpVariable?.boolValue()))") + self.onboardingUserResearch?.updateTelemetry() + self.onboardingUserResearch?.updateValue(value: lpVariable?.boolValue() ?? true) + self.showProperIntroVC() + } + } + } + + private func showProperIntroVC(_ forcedType: OnboardingScreenType? = nil) { + let introViewController = forcedType == nil ? IntroViewControllerV2() : IntroViewControllerV2(onboardingType: forcedType) + + introViewController.didFinishClosure = { controller, fxaLoginFlow in + self.profile.prefs.setInt(1, forKey: PrefsKeys.IntroSeen) + controller.dismiss(animated: true) { + if self.navigationController?.viewControllers.count ?? 0 > 1 { + _ = self.navigationController?.popToRootViewController(animated: true) + } + if let flow = fxaLoginFlow { + let fxaParams = FxALaunchParams(query: ["entrypoint": "firstrun"]) + self.presentSignInViewController(fxaParams, flowType: flow) + } + } + } + self.introVCPresentHelper(introViewController: introViewController) + } + + private func introVCPresentHelper(introViewController: UIViewController) { + // On iPad we present it modally in a controller + if topTabsVisible { + introViewController.preferredContentSize = CGSize(width: ViewControllerConsts.PreferredSize.IntroViewController.width, height: ViewControllerConsts.PreferredSize.IntroViewController.height) + introViewController.modalPresentationStyle = .formSheet + } else { + introViewController.modalPresentationStyle = .fullScreen + } + present(introViewController, animated: true) { + // On first run (and forced) open up the homepage in the background. + if let homePageURL = NewTabHomePageAccessors.getHomePage(self.profile.prefs), let tab = self.tabManager.selectedTab, DeviceInfo.hasConnectivity() { + tab.loadRequest(URLRequest(url: homePageURL)) + } } } @@ -2031,20 +2060,6 @@ extension BrowserViewController: IntroViewControllerDelegate { self.presentSignInViewController(fxaParams) } - func introViewControllerDidFinish(_ introViewController: IntroViewController, fxaLoginFlow: FxAPageType?) { - self.profile.prefs.setInt(1, forKey: PrefsKeys.IntroSeen) - introViewController.dismiss(animated: true) { - if self.navigationController?.viewControllers.count ?? 0 > 1 { - _ = self.navigationController?.popToRootViewController(animated: true) - } - - if let flow = fxaLoginFlow { - let fxaParams = FxALaunchParams(query: ["entrypoint": "firstrun"]) - self.presentSignInViewController(fxaParams, flowType: flow) - } - } - } - func getSignInOrFxASettingsVC(_ fxaOptions: FxALaunchParams? = nil, flowType: FxAPageType) -> UIViewController { // Show the settings page if we have already signed in. If we haven't then show the signin page guard profile.hasSyncableAccount() else { diff --git a/Client/Frontend/FirefoxColors.swift b/Client/Frontend/FirefoxColors.swift new file mode 100644 index 000000000000..a5b6ab2e7aa3 --- /dev/null +++ b/Client/Frontend/FirefoxColors.swift @@ -0,0 +1,11 @@ +/* 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 + +extension UIColor { + struct Firefox { + static let DarkGrey10 = UIColor(rgb: 0x242327) + } +} diff --git a/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/Contents.json b/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/Contents.json new file mode 100644 index 000000000000..ba79f5d3071e --- /dev/null +++ b/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "illustration-syncing-devices.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/illustration-syncing-devices.pdf b/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/illustration-syncing-devices.pdf new file mode 100644 index 000000000000..b113e1e87e6c Binary files /dev/null and b/Client/Frontend/Intro/Intro.xcassets/tour-sync-v2.imageset/illustration-syncing-devices.pdf differ diff --git a/Client/Frontend/Intro/IntroScreenSyncViewV2.swift b/Client/Frontend/Intro/IntroScreenSyncViewV2.swift new file mode 100644 index 000000000000..b0aa7b693044 --- /dev/null +++ b/Client/Frontend/Intro/IntroScreenSyncViewV2.swift @@ -0,0 +1,194 @@ +/* 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 UIKit +import Shared +import SnapKit + +/* The layout for update view controller. + +The whole is divided into two parts. Top container view and Bottom view. +Top container view sits above Sign Up button and its height spans all +the way from sign up button to top safe area. We then add [combined view] +that contains Image, Title and Description inside [Top container view] +to make it center in the top container view. + +|----------------|----------[Top Container View]--------- +| | +| |---------[Combined View] +| | +| Image | [Top View] +| | -- Has title image view +| | +| | [Mid View] +| Title | -- Has title +| | -- Description +| Description | +| |---------[Combined View] +| | +|----------------|----------[Top Container View]--------- +| | Bottom View +| [Sign up] | -- Bottom View +| | -- Start Browsing +| Start Browsing | +| | +|----------------| + +*/ + +class IntroScreenSyncViewV2: UIView { + // Private vars + private var fxTextThemeColour: UIColor { + // For dark theme we want to show light colours and for light we want to show dark colours + return UpdateViewController.theme == .dark ? .white : .black + } + private var fxBackgroundThemeColour: UIColor { + return UpdateViewController.theme == .dark ? UIColor.Firefox.DarkGrey10 : .white + } + private lazy var titleImageView: UIImageView = { + let imgView = UIImageView(image: #imageLiteral(resourceName: "tour-sync-v2")) + imgView.contentMode = .scaleAspectFit + return imgView + }() + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = Strings.CardTitleFxASyncDevices + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: 22, weight: .semibold) + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = Strings.CardDescriptionFxASyncDevices + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: 22, weight: .regular) + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + private var signUpButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.layer.cornerRadius = 10 + button.backgroundColor = UIColor.Photon.Blue50 + button.setTitle(Strings.IntroSignUpButtonTitle, for: .normal) + return button + }() + private lazy var startBrowsingButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.backgroundColor = .clear + button.setTitleColor(UIColor.Photon.Blue50, for: .normal) + button.setTitle(Strings.StartBrowsingButtonTitle, for: .normal) + button.titleLabel?.textAlignment = .center + return button + }() + // Container and combined views + private let topContainerView = UIView() + private let combinedView = UIView() + // Orientation independent screen size + private let screenSize = DeviceInfo.screenSizeOrientationIndependent() + // Closure delegates + var signUp: (() -> Void)? + var startBrowsing: (() -> Void)? + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(frame: CGRect) { + super.init(frame: frame) + initialViewSetup() + topContainerViewSetup() + bottomViewSetup() + } + + // MARK: Initializer + private func initialViewSetup() { + combinedView.addSubview(titleLabel) + combinedView.addSubview(descriptionLabel) + combinedView.addSubview(titleImageView) + topContainerView.addSubview(combinedView) + addSubview(topContainerView) + addSubview(signUpButton) + addSubview(startBrowsingButton) + } + + // MARK: View setup + private func topContainerViewSetup() { + // Background colour setup + backgroundColor = fxBackgroundThemeColour + // Height constants + let titleLabelHeight = 100 + let descriptionLabelHeight = 100 + let titleImageHeight = screenSize.height > 600 ? 300 : 200 + // Title label constraints + titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(24) + make.top.equalTo(titleImageView.snp.bottom) + make.height.equalTo(titleLabelHeight) + } + // Description label constraints + descriptionLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(24) + make.top.equalTo(titleLabel.snp.bottom) + make.height.equalTo(descriptionLabelHeight) + } + // Title image view constraints + titleImageView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalToSuperview() + make.height.equalTo(titleImageHeight) + } + // Top container view constraints + topContainerView.snp.makeConstraints { make in + make.top.equalTo(safeArea.top) + make.bottom.equalTo(signUpButton.snp.top) + make.left.right.equalToSuperview() + } + // Combined view constraints + combinedView.snp.makeConstraints { make in + make.height.equalTo(titleLabelHeight + descriptionLabelHeight + titleImageHeight) + make.centerY.equalToSuperview() + make.left.right.equalToSuperview() + } + } + + private func bottomViewSetup() { + // Sign-up button constraints + signUpButton.snp.makeConstraints { make in + make.bottom.equalTo(startBrowsingButton.snp.top).offset(-20) + make.left.right.equalToSuperview().inset(24) + make.height.equalTo(46) + + } + // Start browsing button constraints + startBrowsingButton.snp.makeConstraints { make in + make.bottom.equalTo(safeArea.bottom) + make.left.right.equalToSuperview().inset(80) + make.height.equalTo(46) + } + // Sign-up and start browsing button action + signUpButton.addTarget(self, action: #selector(signUpAction), for: .touchUpInside) + startBrowsingButton.addTarget(self, action: #selector(startBrowsingAction), for: .touchUpInside) + } + + // MARK: Button Actions + @objc private func signUpAction() { + LeanPlumClient.shared.track(event: .dismissedOnboardingShowSignUp, withParameters: ["dismissedOnSlide": "1"]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboardingSignUp, extras: ["slide-num": 1]) + print("Sign up") + signUp?() + } + + @objc private func startBrowsingAction() { + LeanPlumClient.shared.track(event: .dismissedOnboarding, withParameters: ["dismissedOnSlide": "1"]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboarding, extras: ["slide-num": 1]) + print("Start Browsing") + startBrowsing?() + } +} + diff --git a/Client/Frontend/Intro/IntroScreenWelcomeViewV2.swift b/Client/Frontend/Intro/IntroScreenWelcomeViewV2.swift new file mode 100644 index 000000000000..47256ca16a02 --- /dev/null +++ b/Client/Frontend/Intro/IntroScreenWelcomeViewV2.swift @@ -0,0 +1,278 @@ +/* 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 UIKit +import Shared +import SnapKit + +/* The layout for update view controller. + +[Top View] and [Stack View] are put together in another +uiview called combined view mainly to put the whole thing +in the middle of the screen. + +|----------------| +| Cross| Cross button is on top right corner +| | +|----------------|----------[Combined View]-------------- +| | +| Image | [Top View] +| | -- Has title image view +|Title Multiline | -- Title label view +|----------------| +| | [Stack View] - Fixed height and +| | contains subviews with title and description +| Title | -- automaticPrivacyView +| Description | -- Title & Description label uiviews +| | +| Title | -- fastSearchView +| Description | -- Title & Description label uiviews +| | +| Title | -- safeSyncView +| Description | -- Title & Description label uiviews +| | +|----------------|----------[Combined View]-------------- +| | +| [Next] | Bottom View - Only Has next button +|----------------| + +*/ + +class IntroScreenWelcomeViewV2: UIView { + // Private vars + private var fxTextThemeColour: UIColor { + // For dark theme we want to show light colours and for light we want to show dark colours + return UpdateViewController.theme == .dark ? .white : .black + } + private var fxBackgroundThemeColour: UIColor { + return UpdateViewController.theme == .dark ? UIColor.Firefox.DarkGrey10 : .white + } + private lazy var titleImageView: UIImageView = { + let imgView = UIImageView(image: #imageLiteral(resourceName: "splash")) + imgView.contentMode = .scaleAspectFit + return imgView + }() + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = Strings.CardTitleWelcome + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: 26, weight: .semibold) + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + private var closeButton: UIButton = { + let closeButton = UIButton() + closeButton.setImage(UIImage(named: "close-large"), for: .normal) + if #available(iOS 13, *) { + closeButton.tintColor = .secondaryLabel + } else { + closeButton.tintColor = .black + } + return closeButton + }() + private lazy var nextButton: UIButton = { + let button = UIButton() + button.setTitle(Strings.IntroNextButtonTitle, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.setTitleColor(UIColor.Photon.Blue50, for: .normal) + button.titleLabel?.textAlignment = .center + return button + }() + // Welcome card items share same type of label hence combining them into a + // struct so we can reuse it + private struct WelcomeUICardItem { + var title: String + var description: String + var titleColour: UIColor + var descriptionColour: UIColor + lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = title + label.textColor = titleColour + label.font = UIFont.systemFont(ofSize: 19, weight: .semibold) + label.textAlignment = .left + label.numberOfLines = 1 + label.adjustsFontSizeToFitWidth = true + return label + }() + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = description + label.textColor = descriptionColour + label.font = UIFont.systemFont(ofSize: 19, weight: .regular) + label.textAlignment = .left + label.numberOfLines = 2 + label.adjustsFontSizeToFitWidth = true + return label + }() + } + private lazy var welcomeCardItems: [WelcomeUICardItem] = { + var cardItems = [WelcomeUICardItem]() + // Automatic Privacy + let automaticPrivacy = WelcomeUICardItem(title: Strings.CardTitleAutomaticPrivacy, description: Strings.CardDescriptionAutomaticPrivacy, titleColour: fxTextThemeColour, descriptionColour: fxTextThemeColour) + cardItems.append(automaticPrivacy) + // Fast Search + let fastSearch = WelcomeUICardItem(title: Strings.CardTitleFastSearch, description: Strings.CardDescriptionFastSearch, titleColour: fxTextThemeColour, descriptionColour: fxTextThemeColour) + cardItems.append(fastSearch) + // Safe Sync + let safeSync = WelcomeUICardItem(title: Strings.CardTitleSafeSync, description: Strings.CardDescriptionSafeSync, titleColour: fxTextThemeColour, descriptionColour: fxTextThemeColour) + cardItems.append(safeSync) + return cardItems + }() + // See above for explanation of each of these views + private var topView = UIView() + private var automaticPrivacyView = UIView() + private var fastSearchView = UIView() + private var safeSyncView = UIView() + private var itemStackView = UIStackView() + private var combinedView = UIView() + // Orientation independent screen size + private let screenSize = DeviceInfo.screenSizeOrientationIndependent() + // Closure delegates + var nextClosure: (() -> Void)? + var closeClosure: (() -> Void)? + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Initializer + override init(frame: CGRect) { + super.init(frame: frame) + initialViewSetup() + topViewSetup() + stackViewSetup() + combinedViewSetup() + } + + // MARK: View setup + private func initialViewSetup() { + // Adding close button + addSubview(closeButton) + // Top view + topView.addSubview(titleImageView) + topView.addSubview(titleLabel) + // Stack View + // Automatic Privacy + automaticPrivacyView.addSubview(welcomeCardItems[0].titleLabel) + automaticPrivacyView.addSubview(welcomeCardItems[0].descriptionLabel) + // Fast Search + fastSearchView.addSubview(welcomeCardItems[1].titleLabel) + fastSearchView.addSubview(welcomeCardItems[1].descriptionLabel) + // Safe Sync + safeSyncView.addSubview(welcomeCardItems[2].titleLabel) + safeSyncView.addSubview(welcomeCardItems[2].descriptionLabel) + // Adding all three items to tem stack view + // Automatic Privacy + Fast Search + Safe Sync + itemStackView.axis = .vertical + itemStackView.distribution = .fillProportionally + itemStackView.addArrangedSubview(automaticPrivacyView) + itemStackView.addArrangedSubview(fastSearchView) + itemStackView.addArrangedSubview(safeSyncView) + // Adding [Top View] and [Stack View] together put in a combined view + combinedView.addSubview(topView) + combinedView.addSubview(itemStackView) + addSubview(combinedView) + // Adding next button + addSubview(nextButton) + } + + private func topViewSetup() { + // Background colour setup + backgroundColor = fxBackgroundThemeColour + // Close button target and constraints + closeButton.addTarget(self, action: #selector(dismissAnimated), for: .touchUpInside) + closeButton.snp.makeConstraints { make in + make.top.equalToSuperview().offset(30) + make.right.equalToSuperview().inset(10) + } + // Top view constraints + topView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(-20) + make.left.right.equalToSuperview() + make.height.equalToSuperview().dividedBy(2.4) + } + // Title image constraints + titleImageView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + // changing offset for smaller screen Eg. iPhone 5 + let offsetValue = screenSize.height > 570 ? 40 : 10 + make.top.equalToSuperview().offset(offsetValue) + make.height.equalToSuperview().dividedBy(2) + } + // Title label constraints + titleLabel.snp.makeConstraints { make in + make.top.equalTo(titleImageView.snp.bottom).offset(23) + make.left.right.equalToSuperview() + make.height.equalTo(30) + } + } + + private func stackViewSetup() { + // Item stack view constraints + itemStackView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.height.equalTo(320) + // changing inset for smaller screen Eg. iPhone 5 + let insetValue = screenSize.height > 570 ? -10 : 4 + make.top.equalTo(topView.snp.bottom).inset(insetValue) + } + // Automatic privacy + welcomeCardItems[0].titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalToSuperview() + } + welcomeCardItems[0].descriptionLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(welcomeCardItems[0].titleLabel.snp.bottom).offset(2) + } + // Fast Search + welcomeCardItems[1].titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalToSuperview() + } + welcomeCardItems[1].descriptionLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(welcomeCardItems[1].titleLabel.snp.bottom).offset(2) + } + // Safe Sync + welcomeCardItems[2].titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalToSuperview() + } + welcomeCardItems[2].descriptionLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(welcomeCardItems[2].titleLabel.snp.bottom).offset(2) + } + } + + private func combinedViewSetup() { + // Combined top view and stack view constraints + combinedView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalToSuperview().dividedBy(1.3) + } + // Next Button bottom action and constraints + nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside) + nextButton.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalTo(safeArea.bottom).inset(10) + make.height.equalTo(30) + } + } + + // MARK: Button Actions + @objc private func dismissAnimated() { + LeanPlumClient.shared.track(event: .dismissedOnboarding, withParameters: ["dismissedOnSlide": "0"]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboarding, extras: ["slide-num": 0]) + closeClosure?() + } + + @objc private func nextAction() { + nextClosure?() + } +} diff --git a/Client/Frontend/Intro/IntroViewController.swift b/Client/Frontend/Intro/IntroViewController.swift deleted file mode 100644 index b6eae42663d3..000000000000 --- a/Client/Frontend/Intro/IntroViewController.swift +++ /dev/null @@ -1,250 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import UIKit -import SnapKit -import Shared - -protocol IntroViewControllerDelegate: AnyObject { - func introViewControllerDidFinish(_ introViewController: IntroViewController, fxaLoginFlow: FxAPageType?) -} - -struct ViewControllerConsts { - struct PreferredSize { - static let IntroViewController = CGSize(width: 375, height: 667) - static let UpdateViewController = CGSize(width: 375, height: 667) - } -} -class IntroViewController: UIViewController { - - weak var delegate: IntroViewControllerDelegate? - - let imagePage1 = UIImageView() - let imagePage2 = UIImageView() - let subtitlePage1 = UILabel() - let subtitlePage2 = UILabel() - let heading = UILabel() - let nextButton = UIButton() - let startBrowsingButton = UIButton() - - var currentPage = 0 - - /* The layout is a top level equal-split StackView, with the image on top - and the bottom containing `bottomHolder` UIView. The bottom UIView - has the text and buttons. The buttons are anchored to the bottom, - the text is anchored to the middle of the screen, and the image will - center in the top half of the screen. This should handle all screen sizes. - - |----------------| - | | - | image | - | | - |----------------| - | | - | | - | | - |----------------| - - */ - - override func viewDidLoad() { - super.viewDidLoad() - - if #available(iOS 13, *) { - view.backgroundColor = .systemBackground - } else { - view.backgroundColor = .white - } - - let main2panel = UIStackView() - main2panel.axis = .vertical - main2panel.distribution = .fillEqually - - view.addSubview(main2panel) - main2panel.snp.makeConstraints { make in - make.left.right.equalToSuperview() - make.top.equalTo(view.safeArea.top) - make.bottom.equalTo(view.safeArea.bottom) - } - - let imageHolder = UIView() - main2panel.addArrangedSubview(imageHolder) - [imagePage1, imagePage2].forEach { - imageHolder.addSubview($0) - $0.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } - - let bottomHolder = UIView() - main2panel.addArrangedSubview(bottomHolder) - - imagePage1.image = UIImage(named: "tour-Welcome") - imagePage1.contentMode = .center - - imagePage2.image = UIImage(named: "tour-Sync") - imagePage2.isHidden = true - imagePage2.contentMode = .center - - let signUp = UIButton() - signUp.accessibilityIdentifier = "signUpOnboardingButton" - let signIn = UIButton() - signIn.accessibilityIdentifier = "signInOnboardingButton" - - [heading, subtitlePage1, subtitlePage2, signUp, signIn, nextButton, startBrowsingButton].forEach { - bottomHolder.addSubview($0) - } - - heading.text = Strings.CardTitleWelcome - heading.font = UIFont.systemFont(ofSize: 32, weight: .bold) - heading.adjustsFontSizeToFitWidth = true - heading.textAlignment = .center - heading.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(10) - make.top.equalToSuperview() - } - - subtitlePage1.text = Strings.CardTextWelcome - subtitlePage2.text = Strings.CardTextSync - subtitlePage2.isHidden = true - subtitlePage1.numberOfLines = 2 - subtitlePage2.numberOfLines = 3 - [subtitlePage1, subtitlePage2].forEach { - $0.textAlignment = .center - $0.adjustsFontSizeToFitWidth = true - // Shrink the font for the smallest screen size - let fontSize: CGFloat = view.frame.size.width <= 320 ? 16 : 20 - $0.font = UIFont.systemFont(ofSize: fontSize) - $0.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(35) - make.top.equalTo(heading.snp.bottom) - } - } - - let buttonEdgeInset = 15 - let buttonHeight = 46 - let buttonSpacing = 16 - let buttonBlue = UIColor.Photon.Blue50 - - [signUp, signIn, nextButton, startBrowsingButton].forEach { - $0.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold) - $0.layer.cornerRadius = 10 - } - - signUp.backgroundColor = buttonBlue - signUp.setTitle(Strings.IntroSignUpButtonTitle, for: .normal) - signUp.addTarget(self, action: #selector(showSignUpFlow), for: .touchUpInside) - signUp.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(buttonEdgeInset) - make.bottom.equalTo(signIn.snp.top).offset(-buttonSpacing) - make.height.equalTo(buttonHeight) - } - - signIn.backgroundColor = .clear - signIn.setTitleColor(buttonBlue, for: .normal) - signIn.setTitle(Strings.IntroSignInButtonTitle, for: .normal) - signIn.addTarget(self, action: #selector(showEmailLoginFlow), for: .touchUpInside) - signIn.layer.borderWidth = 1 - signIn.layer.borderColor = UIColor.gray.cgColor - signIn.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(buttonEdgeInset) - make.bottom.equalTo(nextButton.snp.top).offset(-buttonSpacing) - make.height.equalTo(buttonHeight) - } - - nextButton.setTitle(Strings.IntroNextButtonTitle, for: .normal) - nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside) - nextButton.accessibilityIdentifier = "nextOnboardingButton" - - [nextButton, startBrowsingButton].forEach { - $0.setTitleColor(buttonBlue, for: .normal) - $0.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(buttonEdgeInset) - let h = view.frame.height - // On large iPhone screens, bump this up from the bottom - let offset: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 20 : (h > 800 ? 60 : 20) - make.bottom.equalToSuperview().inset(offset) - make.height.equalTo(buttonHeight) - } - } - - startBrowsingButton.setTitle(Strings.StartBrowsingButtonTitle, for: .normal) - startBrowsingButton.isHidden = true - startBrowsingButton.addTarget(self, action: #selector(startBrowsing), for: .touchUpInside) - startBrowsingButton.accessibilityIdentifier = "startBrowsingOnboardingButton" - - // Add 'X' to upper right - let closeButton = UIButton() - view.addSubview(closeButton) - closeButton.setImage(UIImage(named: "close-large"), for: .normal) - closeButton.addTarget(self, action: #selector(startBrowsing), for: .touchUpInside) - closeButton.snp.makeConstraints { make in - make.top.equalToSuperview().offset(buttonEdgeInset) - make.right.equalToSuperview().inset(buttonEdgeInset) - } - if #available(iOS 13, *) { - closeButton.tintColor = .secondaryLabel - } else { - closeButton.tintColor = .black - } - } - - @objc func nextTapped() { - currentPage = 1 - - [imagePage2, startBrowsingButton, subtitlePage2].forEach { - $0.alpha = 0 - $0.isHidden = false - } - - UIView.animate(withDuration: 0.3, animations: { - self.imagePage1.alpha = 0 - self.imagePage2.alpha = 1 - - self.nextButton.alpha = 0 - self.startBrowsingButton.alpha = 1 - - self.heading.alpha = 0 - self.subtitlePage1.alpha = 0 - self.subtitlePage2.alpha = 1 - }) { _ in - self.nextButton.isHidden = true - } - } - - @objc func startBrowsing() { - delegate?.introViewControllerDidFinish(self, fxaLoginFlow: nil) - LeanPlumClient.shared.track(event: .dismissedOnboarding, withParameters: ["dismissedOnSlide": String(currentPage)]) - UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboarding, extras: ["slide-num": currentPage]) - } - - @objc func showEmailLoginFlow() { - delegate?.introViewControllerDidFinish(self, fxaLoginFlow: .emailLoginFlow) - LeanPlumClient.shared.track(event: .dismissedOnboardingShowLogin, withParameters: ["dismissedOnSlide": String(currentPage)]) - UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboardingEmailLogin, extras: ["slide-num": currentPage]) - } - - @objc func showSignUpFlow() { - delegate?.introViewControllerDidFinish(self, fxaLoginFlow: .signUpFlow) - LeanPlumClient.shared.track(event: .dismissedOnboardingShowSignUp, withParameters: ["dismissedOnSlide": String(currentPage)]) - UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboardingSignUp, extras: ["slide-num": currentPage]) - } -} - -// UIViewController setup -extension IntroViewController { - override var prefersStatusBarHidden: Bool { - return true - } - - override var shouldAutorotate: Bool { - return false - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - // This actually does the right thing on iPad where the modally - // presented version happily rotates with the iPad orientation. - return .portrait - } -} diff --git a/Client/Frontend/Intro/IntroViewControllerV2.swift b/Client/Frontend/Intro/IntroViewControllerV2.swift new file mode 100644 index 000000000000..c59dcaa9417e --- /dev/null +++ b/Client/Frontend/Intro/IntroViewControllerV2.swift @@ -0,0 +1,149 @@ +/* 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 UIKit +import SnapKit +import Shared +import Leanplum + +class IntroViewControllerV2: UIViewController { + // Public constants + let viewModel:IntroViewModelV2 = IntroViewModelV2() + // private var + private var onboardingType: OnboardingScreenType? + // Private views + private lazy var welcomeCard: IntroScreenWelcomeViewV2 = { + let welcomeCardView = IntroScreenWelcomeViewV2() + welcomeCardView.clipsToBounds = true + return welcomeCardView + }() + private lazy var syncCard: IntroScreenSyncViewV2 = { + let syncCardView = IntroScreenSyncViewV2() + syncCardView.clipsToBounds = true + return syncCardView + }() + private lazy var introWelcomeSyncV1Views: IntroWelcomeAndSyncViewV1 = { + let syncCardView = IntroWelcomeAndSyncViewV1() + syncCardView.clipsToBounds = true + return syncCardView + }() + // Closure delegate + var didFinishClosure: ((IntroViewControllerV2, FxAPageType?) -> Void)? + + // MARK: Initializer + init() { + super.init(nibName: nil, bundle: nil) + } + + + convenience init(onboardingType: OnboardingScreenType?) { + self.init() + self.onboardingType = onboardingType + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + initialViewSetup() + } + + // MARK: View setup + private func initialViewSetup() { + let screenType = onboardingType == nil ? viewModel.screenType : onboardingType + switch screenType { + case .versionV1: + setupIntroViewV1() + case .versionV2: + setupIntroViewV2() + case .none: + setupIntroViewV1() + } + } + + // V1 of onboarding intro view + func setupIntroViewV1() { + view.addSubview(introWelcomeSyncV1Views) + // Constraints + introWelcomeSyncV1Views.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + // Close button closure + introWelcomeSyncV1Views.closeClosure = { + self.didFinishClosure?(self, nil) + } + // Sign in button closure + introWelcomeSyncV1Views.signInClosure = { + self.didFinishClosure?(self, .emailLoginFlow) + } + // Sign up button closure + introWelcomeSyncV1Views.signUpClosure = { + self.didFinishClosure?(self, .signUpFlow) + } + } + + // V2 of onboarding intro view + private func setupIntroViewV2() { + // Initialize + view.addSubview(syncCard) + view.addSubview(welcomeCard) + // Constraints + setupWelcomeCard() + setupSyncCard() + } + + private func setupWelcomeCard() { + // Constraints + welcomeCard.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + // Buton action closures + // Next button action + welcomeCard.nextClosure = { + UIView.animate(withDuration: 0.3, animations: { + self.welcomeCard.alpha = 0 + }) { _ in + self.welcomeCard.isHidden = true + } + } + // Close button action + welcomeCard.closeClosure = { + self.didFinishClosure?(self, nil) + } + } + + private func setupSyncCard() { + syncCard.snp.makeConstraints() { make in + make.edges.equalToSuperview() + } + // Start browsing button action + syncCard.startBrowsing = { + self.didFinishClosure?(self, nil) + } + // Sign-up browsing button action + syncCard.signUp = { + self.didFinishClosure?(self, .signUpFlow) + } + } +} + +// MARK: UIViewController setup +extension IntroViewControllerV2 { + override var prefersStatusBarHidden: Bool { + return true + } + + override var shouldAutorotate: Bool { + return false + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + // This actually does the right thing on iPad where the modally + // presented version happily rotates with the iPad orientation. + return .portrait + } +} diff --git a/Client/Frontend/Intro/IntroViewModelV2.swift b/Client/Frontend/Intro/IntroViewModelV2.swift new file mode 100644 index 000000000000..4dd9b0489563 --- /dev/null +++ b/Client/Frontend/Intro/IntroViewModelV2.swift @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation + +struct ViewControllerConsts { + struct PreferredSize { + static let IntroViewController = CGSize(width: 375, height: 667) + static let UpdateViewController = CGSize(width: 375, height: 667) + } +} + +// MARK: Requires Work (Currently part of A/B test) +// Intro View Model V2 - This is suppose to be the main view model for the +// IntroView V2 however since we are running an onboarding A/B test +// and there might be a chance that some of the work related to views +// might be throw away hence keeping it super simple for IntroViewControllerV2 +// This is another reason why we don't have a proper model either. + +class IntroViewModelV2 { + // Internal vars + var screenType: OnboardingScreenType? + // private vars + private var onboardingResearch: OnboardingUserResearch? + + // Initializer + init() { + onboardingResearch = OnboardingUserResearch() + screenType = onboardingResearch?.onboardingScreenType + } +} diff --git a/Client/Frontend/Intro/IntroWelcomeAndSyncViewV1.swift b/Client/Frontend/Intro/IntroWelcomeAndSyncViewV1.swift new file mode 100644 index 000000000000..9d58fe1a64e9 --- /dev/null +++ b/Client/Frontend/Intro/IntroWelcomeAndSyncViewV1.swift @@ -0,0 +1,304 @@ +/* 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 UIKit +import SnapKit +import Shared + +/* The layout is a top level equal-split StackView, with the image on top + and the bottom containing `bottomHolder` UIView. The bottom UIView + has the text and buttons. The buttons are anchored to the bottom, + the text is anchored to the middle of the screen, and the image will + center in the top half of the screen. This should handle all screen sizes. + + |----------------| + | | + | image | + | | + |----------------| + | | + | | + | | + |----------------| + + */ + +class IntroWelcomeAndSyncViewV1: UIView { + // Private vars + private var fxTextThemeColour: UIColor { + // For dark theme we want to show light colours and for light we want to show dark colours + return UpdateViewController.theme == .dark ? .white : .black + } + private var fxBackgroundThemeColour: UIColor { + return UpdateViewController.theme == .dark ? .black : .white + } + // Screen constants + private let screenHeight = UIScreen.main.bounds.size.height + private let screenWidth = UIScreen.main.bounds.width + // Views + private lazy var titleImageViewPage1: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "tour-Welcome")) + imgView.contentMode = .center + imgView.clipsToBounds = true + return imgView + }() + private lazy var titleImageViewPage2: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "tour-Sync")) + imgView.contentMode = .center + imgView.clipsToBounds = true + return imgView + }() + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = Strings.CardTitleWelcome + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: 32, weight: .bold) + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = true + return label + }() + private lazy var subTitleLabelPage1: UILabel = { + let fontSize: CGFloat = screenWidth <= 320 ? 16 : 20 + let label = UILabel() + label.text = Strings.CardTextWelcome + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: fontSize) + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = true + label.numberOfLines = 2 + return label + }() + private lazy var subTitleLabelPage2: UILabel = { + let fontSize: CGFloat = screenWidth <= 320 ? 16 : 20 + let label = UILabel() + label.text = Strings.CardTextSync + label.textColor = fxTextThemeColour + label.font = UIFont.systemFont(ofSize: fontSize) + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = true + label.numberOfLines = 3 + return label + }() + private var closeButton: UIButton = { + let closeButton = UIButton() + closeButton.setImage(UIImage(named: "close-large"), for: .normal) + if #available(iOS 13, *) { + closeButton.tintColor = .secondaryLabel + } else { + closeButton.tintColor = .black + } + return closeButton + }() + private lazy var signUpButton: UIButton = { + let button = UIButton() + button.accessibilityIdentifier = "signUpOnboardingButton" + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold) + button.layer.cornerRadius = 10 + button.backgroundColor = UIColor.Photon.Blue50 + button.setTitle(Strings.IntroSignUpButtonTitle, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.textAlignment = .center + return button + }() + private lazy var signInButton: UIButton = { + let button = UIButton() + button.accessibilityIdentifier = "signInOnboardingButton" + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold) + button.layer.cornerRadius = 10 + button.layer.borderWidth = 1 + button.layer.borderColor = UIColor.gray.cgColor + button.backgroundColor = .clear + button.setTitle(Strings.IntroSignInButtonTitle, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.setTitleColor(UIColor.Photon.Blue50, for: .normal) + button.titleLabel?.textAlignment = .center + return button + }() + private lazy var nextButton: UIButton = { + let button = UIButton() + button.setTitle(Strings.IntroNextButtonTitle, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.setTitleColor(UIColor.Photon.Blue50, for: .normal) + button.titleLabel?.textAlignment = .center + button.accessibilityIdentifier = "nextOnboardingButton" + return button + }() + private lazy var startBrowsingButton: UIButton = { + let button = UIButton() + button.setTitle(Strings.StartBrowsingButtonTitle, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.setTitleColor(UIColor.Photon.Blue50, for: .normal) + button.titleLabel?.textAlignment = .center + button.accessibilityIdentifier = "startBrowsingOnboardingButton" + return button + }() + // Helper views + let main2panel = UIStackView() + let imageHolder = UIView() + let bottomHolder = UIView() + // Closure delegates + var closeClosure: (() -> Void)? + var nextClosure: (() -> Void)? + var signUpClosure: (() -> Void)? + var signInClosure: (() -> Void)? + // Basic variables + private var currentPage = 0 + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Initializer + override init(frame: CGRect) { + super.init(frame: frame) + initialViewSetup() + } + + // MARK: View setup + private func initialViewSetup() { + if #available(iOS 13, *) { + backgroundColor = .systemBackground + } else { + backgroundColor = .white + } + + main2panel.axis = .vertical + main2panel.distribution = .fillEqually + + addSubview(main2panel) + main2panel.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(safeArea.top) + make.bottom.equalTo(safeArea.bottom) + } + + main2panel.addArrangedSubview(imageHolder) + [titleImageViewPage1, titleImageViewPage2].forEach { + imageHolder.addSubview($0) + $0.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + main2panel.addArrangedSubview(bottomHolder) + [titleLabel, subTitleLabelPage1, subTitleLabelPage2, signUpButton, signInButton, nextButton, startBrowsingButton].forEach { + bottomHolder.addSubview($0) + } + + titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.top.equalToSuperview() + } + + [subTitleLabelPage1, subTitleLabelPage2].forEach { + $0.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(35) + make.top.equalTo(titleLabel.snp.bottom) + } + } + + let buttonEdgeInset = 15 + let buttonHeight = 46 + let buttonSpacing = 16 + + signUpButton.addTarget(self, action: #selector(showSignUpFlow), for: .touchUpInside) + signUpButton.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(buttonEdgeInset) + make.bottom.equalTo(signInButton.snp.top).offset(-buttonSpacing) + make.height.equalTo(buttonHeight) + } + signInButton.addTarget(self, action: #selector(showEmailLoginFlow), for: .touchUpInside) + signInButton.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(buttonEdgeInset) + make.bottom.equalTo(nextButton.snp.top).offset(-buttonSpacing) + make.height.equalTo(buttonHeight) + } + nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside) + startBrowsingButton.addTarget(self, action: #selector(startBrowsing), for: .touchUpInside) + [nextButton, startBrowsingButton].forEach { + $0.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(buttonEdgeInset) + // On large iPhone screens, bump this up from the bottom + let offset: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 20 : (screenHeight > 800 ? 60 : 20) + make.bottom.equalToSuperview().inset(offset) + make.height.equalTo(buttonHeight) + } + } + + addSubview(closeButton) + closeButton.addTarget(self, action: #selector(startBrowsing), for: .touchUpInside) + closeButton.snp.makeConstraints { make in + make.top.equalToSuperview().offset(buttonEdgeInset) + make.right.equalToSuperview().inset(buttonEdgeInset) + } + if #available(iOS 13, *) { + closeButton.tintColor = .secondaryLabel + } else { + closeButton.tintColor = .black + } + + // Initially hide page1 + hidePage1() + } + + private func hidePage1() { + [titleImageViewPage2, startBrowsingButton, subTitleLabelPage2].forEach { + $0.isHidden = true + } + } + + private func showPage2() { + currentPage = 1 + + [titleImageViewPage2, startBrowsingButton, subTitleLabelPage2].forEach { + $0.alpha = 0 + $0.isHidden = false + } + + UIView.animate(withDuration: 0.3, animations: { + self.titleImageViewPage1.alpha = 0 + self.titleImageViewPage2.alpha = 1 + + self.nextButton.alpha = 0 + self.startBrowsingButton.alpha = 1 + + self.titleLabel.alpha = 0 + self.subTitleLabelPage1.alpha = 0 + self.subTitleLabelPage2.alpha = 1 + }) { _ in + self.nextButton.isHidden = true + } + } + + // MARK: Button Actions + @objc func startBrowsing() { + LeanPlumClient.shared.track(event: .dismissedOnboarding, withParameters: ["dismissedOnSlide": String(currentPage)]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboarding, extras: ["slide-num": currentPage]) + closeClosure?() + } + + @objc func showEmailLoginFlow() { + LeanPlumClient.shared.track(event: .dismissedOnboardingShowLogin, withParameters: ["dismissedOnSlide": String(currentPage)]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboardingEmailLogin, extras: ["slide-num": currentPage]) + signInClosure?() + } + + @objc func showSignUpFlow() { + LeanPlumClient.shared.track(event: .dismissedOnboardingShowSignUp, withParameters: ["dismissedOnSlide": String(currentPage)]) + UnifiedTelemetry.recordEvent(category: .action, method: .press, object: .dismissedOnboardingSignUp, extras: ["slide-num": currentPage]) + signUpClosure?() + } + + @objc private func nextAction() { + showPage2() + nextClosure?() + } + + @objc private func dismissAnimated() { + startBrowsing() + closeClosure?() + } +} diff --git a/Client/Frontend/Settings/AppSettingsOptions.swift b/Client/Frontend/Settings/AppSettingsOptions.swift index b40f32374477..4b0989775032 100644 --- a/Client/Frontend/Settings/AppSettingsOptions.swift +++ b/Client/Frontend/Settings/AppSettingsOptions.swift @@ -519,6 +519,34 @@ class SentryIDSetting: HiddenSetting { } } +class ToggleOnboarding: HiddenSetting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: Toggle onboarding type", comment: "Debug option"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.theme.tableView.rowText]) + } + + override func onClick(_ navigationController: UINavigationController?) { + let onboardingResearch = OnboardingUserResearch() + let type = onboardingResearch.onboardingScreenType + var newOnboardingType: OnboardingScreenType = .versionV2 + if type == nil { + newOnboardingType = .versionV1 + } else if type == .versionV2 { + newOnboardingType = .versionV1 + } + OnboardingUserResearch().onboardingScreenType = newOnboardingType + } +} + +class SetOnboardingV2: HiddenSetting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: Set onboarding type to v2", comment: "Debug option"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.theme.tableView.rowText]) + } + + override func onClick(_ navigationController: UINavigationController?) { + OnboardingUserResearch().onboardingScreenType = .versionV2 + } +} + // Show the current version of Firefox class VersionSetting: Setting { unowned let settings: SettingsTableViewController @@ -623,7 +651,11 @@ class ShowIntroductionSetting: Setting { override func onClick(_ navigationController: UINavigationController?) { navigationController?.dismiss(animated: true, completion: { - BrowserViewController.foregroundBVC().presentIntroViewController(true) + let userResearch = OnboardingUserResearch() + var screenType = userResearch.onboardingScreenType + // We default to version V1 of onboarding if user research is nil + screenType = screenType == nil ? .versionV1 : screenType + BrowserViewController.foregroundBVC().presentIntroViewController(screenType) }) } } diff --git a/Client/Frontend/Settings/AppSettingsTableViewController.swift b/Client/Frontend/Settings/AppSettingsTableViewController.swift index 1db63ca485c7..fe7bd7ff975d 100644 --- a/Client/Frontend/Settings/AppSettingsTableViewController.swift +++ b/Client/Frontend/Settings/AppSettingsTableViewController.swift @@ -136,7 +136,8 @@ class AppSettingsTableViewController: SettingsTableViewController { SlowTheDatabase(settings: self), ForgetSyncAuthStateDebugSetting(settings: self), SentryIDSetting(settings: self), - ChangeToChinaSetting(settings: self) + ChangeToChinaSetting(settings: self), + ToggleOnboarding(settings: self) ])] return settings diff --git a/Client/Telemetry/UnifiedTelemetry.swift b/Client/Telemetry/UnifiedTelemetry.swift index 3d9a0e01a472..4efdd2bad4ff 100644 --- a/Client/Telemetry/UnifiedTelemetry.swift +++ b/Client/Telemetry/UnifiedTelemetry.swift @@ -151,6 +151,7 @@ extension UnifiedTelemetry { case action = "action" case appExtensionAction = "app-extension-action" case prompt = "prompt" + case enrollment = "enrollment" } public enum EventMethod: String { @@ -209,6 +210,7 @@ extension UnifiedTelemetry { case removeUnVerifiedAccountButton = "remove-unverified-account-button" case tabSearch = "tab-search" case tabToolbar = "tab-toolbar" + case experimentEnrollment = "experiment-enrollment" } public enum EventValue: String { diff --git a/Client/UserResearch/OnboardingUserResearch.swift b/Client/UserResearch/OnboardingUserResearch.swift index 6367f2d62bd9..69872ec2352d 100644 --- a/Client/UserResearch/OnboardingUserResearch.swift +++ b/Client/UserResearch/OnboardingUserResearch.swift @@ -4,25 +4,79 @@ import Foundation import Leanplum +import Shared struct LPVariables { static var showOnboardingScreen = LPVar.define("showOnboardingScreen", with: true) } +enum OnboardingScreenType: String { + case versionV1 // Default + case versionV2 // New version 2 +} + class OnboardingUserResearch { - // Delegate closure + // Closure delegate var updatedLPVariables: ((LPVar?) -> Void)? // variable var lpVariable: LPVar? + // Constants + private let onboardingScreenTypeKey = "onboardingScreenTypeKey" + // Saving user defaults + private let defaults = UserDefaults.standard + // Publicly accessible onboarding screen type + var onboardingScreenType:OnboardingScreenType? { + set(value) { + if value == nil { + defaults.removeObject(forKey: onboardingScreenTypeKey) + } else { + defaults.set(value?.rawValue, forKey: onboardingScreenTypeKey) + } + } + get { + guard let value = defaults.value(forKey: onboardingScreenTypeKey) as? String else { + return nil + } + return OnboardingScreenType(rawValue: value) + } + } - // Initializer + // MARK: Initializer init(lpVariable: LPVar? = LPVariables.showOnboardingScreen) { self.lpVariable = lpVariable } + // MARK: public func lpVariableObserver() { Leanplum.onVariablesChanged { self.updatedLPVariables?(self.lpVariable) } } + + func updateValue(value: Bool) { + // For LP variable below is the convention + // we are going to follow + // True = Current Onboarding Screen + // False = New Onboarding Screen + onboardingScreenType = value ? .versionV1 : .versionV2 + } + + func updateTelemetry() { + // Printing variant is good to know all details of A/B test fields + print("lp variant \(String(describing: Leanplum.variants()))") + guard let variants = Leanplum.variants(), let lpData = variants.first as? Dictionary else { + return + } + var abTestId = "" + if let value = lpData["abTestId"] as? Int64 { + abTestId = "\(value)" + } + let abTestName = lpData["abTestName"] as? String ?? "" + let abTestVariant = lpData["name"] as? String ?? "" + let attributesExtras = [LPAttributeKey.experimentId: abTestId, LPAttributeKey.experimentName: abTestName, LPAttributeKey.experimentVariant: abTestVariant] + // Leanplum telemetry + LeanPlumClient.shared.set(attributes: attributesExtras) + // Legacy telemetry + UnifiedTelemetry.recordEvent(category: .enrollment, method: .add, object: .experimentEnrollment, extras: attributesExtras) + } }