diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index 1a5f81d514e..845e7f481e9 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -270,8 +270,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Check if user has launched the application before and determine if it is a new retention user - if Preferences.General.isFirstLaunch.value, Preferences.General.isNewRetentionUser.value == nil { - Preferences.General.isNewRetentionUser.value = true + if Preferences.General.isFirstLaunch.value, Preferences.Onboarding.isNewRetentionUser.value == nil { + Preferences.Onboarding.isNewRetentionUser.value = true } if Preferences.DAU.appRetentionLaunchDate.value == nil { @@ -339,6 +339,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { .didFinishLaunching(options: launchOptions ?? [:]) ) + // DAU may not have pinged on the first launch so weekOfInstallation pref may not be set yet + if let weekOfInstall = Preferences.DAU.weekOfInstallation.value ?? + Preferences.DAU.installationDate.value?.mondayOfCurrentWeekFormatted { + braveCore.initializeP3AService( + forChannel: AppConstants.buildChannel.serverChannelParam, + weekOfInstall: weekOfInstall + ) + } + return shouldPerformAdditionalDelegateHandling } diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index 6ccb1240d48..b0d8b21f8d0 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -1120,9 +1120,10 @@ public class BrowserViewController: UIViewController { presentOnboardingIntro() // Full Screen Callout Presentation - // Priority: VPN - Default Browser - Rewards - Sync + // Priority: P3A - VPN - Default Browser - Rewards // TODO: Remove the dispatch after with a proper fix and fix calling present functions before super.viewDidAppear DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.presentP3AScreenCallout() self.presentVPNAlertCallout() self.presentDefaultBrowserScreenCallout() self.presentBraveRewardsScreenCallout() diff --git a/Client/Frontend/Browser/BrowserViewController/BVC+Rewards.swift b/Client/Frontend/Browser/BrowserViewController/BVC+Rewards.swift index ac0d3480141..292e7e551ca 100644 --- a/Client/Frontend/Browser/BrowserViewController/BVC+Rewards.swift +++ b/Client/Frontend/Browser/BrowserViewController/BVC+Rewards.swift @@ -37,7 +37,7 @@ extension BrowserViewController { func showBraveRewardsPanel() { if !Preferences.FullScreenCallout.rewardsCalloutCompleted.value, - Preferences.General.isNewRetentionUser.value == true, + Preferences.Onboarding.isNewRetentionUser.value == true, !Preferences.Rewards.rewardsToggledOnce.value { let controller = OnboardingRewardsAgreementViewController() diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift index 186bfe6cd09..e040538edc7 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift @@ -11,6 +11,7 @@ import SwiftKeychainWrapper import SwiftUI import BraveVPN import Onboarding +import SafariServices // MARK: - Callouts @@ -29,6 +30,51 @@ extension BrowserViewController { present(controller, animated: false) } } + + func presentP3AScreenCallout() { + if Preferences.DebugFlag.skipNTPCallouts == true || isOnboardingOrFullScreenCalloutPresented { return } + + if presentedViewController != nil || !FullScreenCalloutManager.shouldShowDefaultBrowserCallout(calloutType: .p3a) { + return + } + + let onboardingP3ACalloutController = Welcome3PAViewController().then { + $0.isModalInPresentation = true + $0.modalPresentationStyle = .overFullScreen + } + + let state = WelcomeViewCalloutState.p3a( + info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( + title: Strings.Callout.p3aCalloutTitle, + toggleTitle: Strings.Callout.p3aCalloutToggleTitle, + details: Strings.Callout.p3aCalloutDescription, + linkDescription: Strings.Callout.p3aCalloutLinkTitle, + primaryButtonTitle: Strings.done, + toggleAction: { [weak self] isOn in + self?.braveCore.p3aUtils.isP3AEnabled = isOn + }, + linkAction: { url in + let p3aLearnMoreController = SFSafariViewController(url: BraveUX.braveP3ALearnMoreURL, configuration: .init()) + p3aLearnMoreController.modalPresentationStyle = .currentContext + + onboardingP3ACalloutController.present(p3aLearnMoreController, animated: true) + }, + primaryButtonAction: { [weak self] in + Preferences.Onboarding.p3aOnboardingShown.value = true + + self?.isOnboardingOrFullScreenCalloutPresented = true + self?.dismiss(animated: false) + } + ) + ) + + onboardingP3ACalloutController.setLayoutState(state: state) + + if !isOnboardingOrFullScreenCalloutPresented { + braveCore.p3aUtils.isNoticeAcknowledged = true + present(onboardingP3ACalloutController, animated: false) + } + } func presentVPNAlertCallout() { if Preferences.DebugFlag.skipNTPCallouts == true || isOnboardingOrFullScreenCalloutPresented { return } @@ -77,7 +123,7 @@ extension BrowserViewController { details: Strings.Callout.defaultBrowserCalloutDescription, primaryButtonTitle: Strings.Callout.defaultBrowserCalloutPrimaryButtonTitle, secondaryButtonTitle: Strings.Callout.defaultBrowserCalloutSecondaryButtonTitle, - primaryAction: { [weak self] in + primaryButtonAction: { [weak self] in guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } @@ -88,13 +134,13 @@ extension BrowserViewController { UIApplication.shared.open(settingsUrl) self?.dismiss(animated: false) }, - secondaryAction: { [weak self] in + secondaryButtonAction: { [weak self] in self?.isOnboardingOrFullScreenCalloutPresented = true self?.dismiss(animated: false) } ) - ) + ), p3aUtilities: braveCore.p3aUtils ) if !isOnboardingOrFullScreenCalloutPresented { diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+CookieNotificationBlocking.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+CookieNotificationBlocking.swift index 5115fbd2280..56447cf99a8 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+CookieNotificationBlocking.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+CookieNotificationBlocking.swift @@ -8,6 +8,7 @@ import SwiftUI import BraveUI import BraveShared import Data +import Onboarding extension BrowserViewController { func presentCookieNotificationBlockingCalloutIfNeeded() { diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift index 95842d67ee2..fc1a4a7c1a8 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift @@ -32,8 +32,8 @@ extension BrowserViewController { // 1. User is brand new // 2. User hasn't completed onboarding if Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue, - Preferences.General.isNewRetentionUser.value == true { - let onboardingController = WelcomeViewController() + Preferences.Onboarding.isNewRetentionUser.value == true { + let onboardingController = WelcomeViewController(p3aUtilities: braveCore.p3aUtils) onboardingController.modalPresentationStyle = .fullScreen parentController.present(onboardingController, animated: false) isOnboardingOrFullScreenCalloutPresented = true @@ -58,7 +58,7 @@ extension BrowserViewController { Preferences.DebugFlag.skipNTPCallouts != true { if !Preferences.FullScreenCallout.omniboxCalloutCompleted.value, - Preferences.General.isNewRetentionUser.value == true { + Preferences.Onboarding.isNewRetentionUser.value == true { presentOmniBoxOnboarding() addNTPTutorialPage() } diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift index efab21b3944..f905eb06e86 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ProductNotification.swift @@ -43,7 +43,7 @@ extension BrowserViewController { !Preferences.General.onboardingAdblockPopoverShown.value, !benchmarkNotificationPresented, !Preferences.AppState.backgroundedCleanly.value, - Preferences.General.isNewRetentionUser.value == true, + Preferences.Onboarding.isNewRetentionUser.value == true, !topToolbar.inOverlayMode, !isTabTrayActive, selectedTab.webView?.scrollView.isDragging == false, diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift index 1bcdd171ed7..60395ee6bc8 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift @@ -299,7 +299,8 @@ extension BrowserViewController: TopToolbarDelegate { profile: self.profile, tabManager: self.tabManager, feedDataSource: self.feedDataSource, - historyAPI: self.braveCore.historyAPI + historyAPI: self.braveCore.historyAPI, + p3aUtilities: self.braveCore.p3aUtils ) let container = SettingsNavigationController(rootViewController: shieldsAndPrivacy) container.isModalInPresentation = true diff --git a/Client/Frontend/ClientPreferences.swift b/Client/Frontend/ClientPreferences.swift index f494e340c1e..bad8f355dc6 100644 --- a/Client/Frontend/ClientPreferences.swift +++ b/Client/Frontend/ClientPreferences.swift @@ -45,8 +45,6 @@ extension Preferences { final public class General { /// Whether this is the first time user has ever launched Brave after intalling. *Should never be set to `true` manually!* public static let isFirstLaunch = Option(key: "general.first-launch", default: true) - /// Whether this is a new user who installed the application after onboarding retention updates - public static let isNewRetentionUser = Option(key: "general.new-retention", default: nil) /// Whether or not to save logins in Brave static let saveLogins = Option(key: "general.save-logins", default: true) /// Whether or not to block popups from websites automaticaly @@ -95,43 +93,6 @@ extension Preferences { static let isUsingBottomBar = Option(key: "general.bottom-bar", default: false) } - final public class FullScreenCallout { - /// Whether the block cookie consent notices callout is shown. - static let blockCookieConsentNoticesCalloutCompleted = - Option(key: "fullScreenCallout.full-screen-cookie-consent-notices-callout-completed", default: false) - - /// Whether the vpn callout is shown. - static let vpnCalloutCompleted = - Option(key: "fullScreenCallout.full-screen-vpn-callout-completed", default: false) - - /// Whether the rewards callout is shown. - static let rewardsCalloutCompleted = - Option(key: "fullScreenCallout.full-screen-rewards-callout-completed", default: false) - - /// Whether the whats new callout should be shown. - static let whatsNewCalloutOptIn = - Option(key: "fullScreenCallout.full-screen-whats-new-callout-completed", default: false) - - /// Whether the ntp callout is shown. - static let ntpCalloutCompleted = - Option(key: "fullScreenCallout.full-screen-ntp-callout-completed", default: false) - - /// Whether the omnibox callout is shown. - static let omniboxCalloutCompleted = - Option(key: "fullScreenCallout.full-screen-omnibox-callout-completed", default: false) - } - - final public class DefaultBrowserIntro { - /// Whether the default browser onboarding completed. This can happen by opening app settings or after the user - /// dismissed the intro screen enough amount of times. - static let completed = - Option(key: "defaultBrowserIntro.intro-completed", default: false) - - /// Whether system notification showed or not - static let defaultBrowserNotificationScheduled = - Option(key: "general.default-browser-notification-scheduled", default: false) - } - final public class Search { /// Whether or not to show suggestions while the user types static let showSuggestions = Option(key: "search.show-suggestions", default: false) diff --git a/Client/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift b/Client/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift index ca4215a8da0..9a010c8e6a3 100644 --- a/Client/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift +++ b/Client/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift @@ -21,16 +21,25 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { let tabManager: TabManager let feedDataSource: FeedDataSource let historyAPI: BraveHistoryAPI + let p3aUtilities: BraveP3AUtils + private let cookieConsentNoticesRowUUID = UUID() private var filterListsSubscription: AnyCancellable? private var currentCookieConsentNoticeBlockingState: Bool - init(profile: Profile, tabManager: TabManager, feedDataSource: FeedDataSource, historyAPI: BraveHistoryAPI) { + init( + profile: Profile, + tabManager: TabManager, + feedDataSource: FeedDataSource, + historyAPI: BraveHistoryAPI, + p3aUtilities: BraveP3AUtils + ) { self.profile = profile self.tabManager = tabManager self.feedDataSource = feedDataSource self.historyAPI = historyAPI - + self.p3aUtilities = p3aUtilities + self.currentCookieConsentNoticeBlockingState = FilterListResourceDownloader.shared.isEnabled( for: FilterList.cookieConsentNoticesComponentID ) @@ -264,6 +273,16 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { }() private lazy var otherSettingsSection: Section = { + let p3aRow = Row.boolRow( + title: Strings.P3A.settingTitle, + detailText: Strings.P3A.settingSubtitle, + toggleValue: p3aUtilities.isP3AEnabled, + valueChange: { [unowned self] isOn in + p3aUtilities.isP3AEnabled = isOn + }, + cellReuseId: "p3a" + ) + var section = Section( header: .title(Strings.otherPrivacySettingsSection), rows: [ @@ -289,6 +308,7 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { ) ) } + section.rows.append(p3aRow) return section }() diff --git a/Client/Frontend/Settings/RetentionPreferencesDebugMenuViewController.swift b/Client/Frontend/Settings/RetentionPreferencesDebugMenuViewController.swift index 5e5f628fb1f..4e2abb8fc4f 100644 --- a/Client/Frontend/Settings/RetentionPreferencesDebugMenuViewController.swift +++ b/Client/Frontend/Settings/RetentionPreferencesDebugMenuViewController.swift @@ -10,10 +10,13 @@ import BraveShared import UIKit import BraveUI import Onboarding +import BraveCore class RetentionPreferencesDebugMenuViewController: TableViewController { + private let p3aUtilities: BraveP3AUtils - init() { + init(p3aUtilities: BraveP3AUtils) { + self.p3aUtilities = p3aUtilities super.init(style: .insetGrouped) } @@ -52,7 +55,7 @@ class RetentionPreferencesDebugMenuViewController: TableViewController { .init( text: "Start Onboarding", selection: { [unowned self] in - let onboardingController = WelcomeViewController() + let onboardingController = WelcomeViewController(state: .loading, p3aUtilities: self.p3aUtilities) onboardingController.modalPresentationStyle = .fullScreen present(onboardingController, animated: false) @@ -104,11 +107,11 @@ class RetentionPreferencesDebugMenuViewController: TableViewController { .boolRow( title: "Retention User", detailText: "Flag showing if the user installed the application after new onboarding is added.", - toggleValue: Preferences.General.isNewRetentionUser.value ?? false, + toggleValue: Preferences.Onboarding.isNewRetentionUser.value ?? false, valueChange: { if $0 { let status = $0 - Preferences.General.isNewRetentionUser.value = status + Preferences.Onboarding.isNewRetentionUser.value = status } }, cellReuseId: "RetentionUserCell"), diff --git a/Client/Frontend/Settings/SettingsViewController.swift b/Client/Frontend/Settings/SettingsViewController.swift index f4af96483d7..79538a6139a 100644 --- a/Client/Frontend/Settings/SettingsViewController.swift +++ b/Client/Frontend/Settings/SettingsViewController.swift @@ -46,6 +46,7 @@ class SettingsViewController: TableViewController { private let passwordAPI: BravePasswordAPI private let syncAPI: BraveSyncAPI private let syncProfileServices: BraveSyncProfileServiceIOS + private let p3aUtilities: BraveP3AUtils private let keyringStore: KeyringStore? private let cryptoStore: CryptoStore? private let windowProtection: WindowProtection? @@ -74,6 +75,7 @@ class SettingsViewController: TableViewController { self.passwordAPI = braveCore.passwordAPI self.syncAPI = braveCore.syncAPI self.syncProfileServices = braveCore.syncProfileService + self.p3aUtilities = braveCore.p3aUtils self.keyringStore = keyringStore self.cryptoStore = cryptoStore @@ -205,7 +207,9 @@ class SettingsViewController: TableViewController { profile: self.profile, tabManager: self.tabManager, feedDataSource: self.feedDataSource, - historyAPI: self.historyAPI) + historyAPI: self.historyAPI, + p3aUtilities: self.p3aUtilities + ) self.navigationController?.pushViewController(controller, animated: true) }, image: UIImage(named: "settings-shields", in: .module, compatibleWith: nil)!, accessory: .disclosureIndicator) ], @@ -659,6 +663,14 @@ class SettingsViewController: TableViewController { selection: { [unowned self] in self.displayBraveSearchDebugMenu() }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), + Row( + text: "View Brave Histogram (p3a) Logs", + selection: { [unowned self] in + let histogramsController = self.p3aUtilities.histogramsController().then { + $0.title = "Histograms (p3a)" + } + self.navigationController?.pushViewController(histogramsController, animated: true) + }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( text: "VPN Logs", selection: { [unowned self] in @@ -667,7 +679,7 @@ class SettingsViewController: TableViewController { Row( text: "Retention Preferences Debug Menu", selection: { [unowned self] in - self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(), animated: true) + self.navigationController?.pushViewController(RetentionPreferencesDebugMenuViewController(p3aUtilities: p3aUtilities), animated: true) }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( text: "Load all QA Links", diff --git a/Package.swift b/Package.swift index 81b35656b97..4ed136a42b9 100644 --- a/Package.swift +++ b/Package.swift @@ -352,6 +352,7 @@ let package = Package( "BraveCore", "Fuzi", "Storage", + "Growth", .product(name: "Lottie", package: "lottie-ios") ], resources: [ @@ -359,7 +360,7 @@ let package = Package( .copy("LottieAssets/onboarding-rewards.json"), .copy("LottieAssets/onboarding-shields.json"), .copy("Welcome/Resources/disconnect-entitylist.json"), - .copy("ProductNotifications/blocking-summary.json") + .copy("ProductNotifications/Resources/blocking-summary.json") ], plugins: ["LoggerPlugin"] ), diff --git a/Sources/BraveShared/BraveStrings.swift b/Sources/BraveShared/BraveStrings.swift index 0320694881c..0c151670fd2 100644 --- a/Sources/BraveShared/BraveStrings.swift +++ b/Sources/BraveShared/BraveStrings.swift @@ -266,6 +266,30 @@ extension Strings { tableName: "BraveShared", bundle: .module, value: "Tab Received", comment: "Title for 'Tab Received' Callout, This is shown in the message when a Tab information is received from another sync device. ") + public static let p3aCalloutTitle = + NSLocalizedString( + "callout.p3aCalloutTitle", + tableName: "BraveShared", bundle: .module, + value: "Help make Brave better.", + comment: "Title for p3a (Privacy Preserving Analytics) Full Screen Callout") + public static let p3aCalloutDescription = + NSLocalizedString( + "callout.p3aCalloutDescription", + tableName: "BraveShared", bundle: .module, + value: "This helps us learn what Brave features are used most often. Change this at any time in Brave Settings under ‘Brave Shields and Privacy’.", + comment: "Subtitle - Description for p3a (Privacy Preserving Analytics) Full Screen Callout") + public static let p3aCalloutToggleTitle = + NSLocalizedString( + "callout.p3aCalloutToggleTitle", + tableName: "BraveShared", bundle: .module, + value: "Share anonymous, private product insights. ", + comment: "Title for toggle for enabling p3a (Privacy Preserving Analytics) Full Screen Callout") + public static let p3aCalloutLinkTitle = + NSLocalizedString( + "callout.p3aCalloutLinkTitle", + tableName: "BraveShared", bundle: .module, + value: "Learn more about our Privacy Preserving Product Analytics (P3A).", + comment: "Title for the link that navigates to a webpage showing information about p3a (Privacy Preserving Analytics)") } } @@ -980,7 +1004,7 @@ extension Strings { public static let HTTPSEverywhere = NSLocalizedString("HTTPSEverywhere", tableName: "BraveShared", bundle: .module, value: "Upgrade Connections to HTTPS", comment: "") public static let HTTPSEverywhereDescription = NSLocalizedString("HTTPSEverywhereDescription", tableName: "BraveShared", bundle: .module, value: "Opens sites using secure HTTPS instead of HTTP when possible.", comment: "") public static let blockPhishingAndMalware = NSLocalizedString("BlockPhishingAndMalware", tableName: "BraveShared", bundle: .module, value: "Block Phishing and Malware", comment: "") - public static let googleSafeBrowsing = NSLocalizedString("GoogleSafeBrowsing", tableName: "BraveShared", bundle: .module, value: "Block dangerous sites", comment: "") + public static let googleSafeBrowsing = NSLocalizedString("GoogleSafeBrowsing", tableName: "BraveShared", bundle: .module, value: "Block Dangerous Sites", comment: "") public static let googleSafeBrowsingUsingWebKitDescription = NSLocalizedString("GoogleSafeBrowsingUsingWebKitDescription", tableName: "BraveShared", bundle: .module, value: "Sends obfuscated URLs of some pages you visit to the Google Safe Browsing service, when your security is at risk.", comment: "") public static let contentFiltering = NSLocalizedString("ContentFiltering", tableName: "BraveShared", bundle: .module, value: "Content Filtering", comment: "A title to the content filtering page under global shield settings and the title on the Content filtering page") public static let blockMobileAnnoyances = NSLocalizedString("blockMobileAnnoyances", tableName: "BraveShared", bundle: .module, value: "Block 'Switch to App' Notices", comment: "A title for setting which blocks 'switch to app' popups") @@ -4093,6 +4117,21 @@ extension Strings { } } +// P3A +extension Strings { + public struct P3A { + public static let settingTitle = NSLocalizedString( + "p3a.settingTitle", tableName: "BraveShared", bundle: .module, + value: "Product Analytics", + comment: "The title for the setting that will allow a user to toggle sending privacy preserving analytics to Brave.") + + public static let settingSubtitle = NSLocalizedString( + "p3a.settingSubtitle", tableName: "BraveShared", bundle: .module, + value: "Share anonymous, private product insights. This helps us learn what Brave features are used most often.", + comment: "A subtitle shown on the setting that toggles analytics on Brave.") + } +} + // Page Zoom extension Strings { public struct PageZoom { diff --git a/Sources/BraveShared/BraveUX.swift b/Sources/BraveShared/BraveUX.swift index 3efc5f25b19..b23f2db51fd 100644 --- a/Sources/BraveShared/BraveUX.swift +++ b/Sources/BraveShared/BraveUX.swift @@ -24,8 +24,7 @@ public struct BraveUX { public static let ntpTutorialPageURL = URL(string: "https://brave.com/ja/ntp-tutorial") public static let privacyReportsURL = URL(string: "https://brave.com/privacy-features/")! public static let braveWalletNetworkLearnMoreURL = URL(string: "https://support.brave.com")! - - public static let textFieldCornerRadius: CGFloat = 8.0 + public static let braveP3ALearnMoreURL = URL(string: "https://support.brave.com/hc/en-us/articles/9140465918093-What-is-P3A-in-Brave-")! public static let faviconBorderColor = UIColor(white: 0, alpha: 0.2) public static let faviconBorderWidth = 1.0 / UIScreen.main.scale diff --git a/Sources/BraveUI/UIKit/DataSourceExtensions.swift b/Sources/BraveUI/Extensions/DataSourceExtensions.swift similarity index 100% rename from Sources/BraveUI/UIKit/DataSourceExtensions.swift rename to Sources/BraveUI/Extensions/DataSourceExtensions.swift diff --git a/Sources/BraveUI/UIKit/UIViewExtensions.swift b/Sources/BraveUI/Extensions/UIViewExtensions.swift similarity index 100% rename from Sources/BraveUI/UIKit/UIViewExtensions.swift rename to Sources/BraveUI/Extensions/UIViewExtensions.swift diff --git a/Sources/Growth/DAU.swift b/Sources/Growth/DAU.swift index b1f6c3c2210..3386a34741b 100644 --- a/Sources/Growth/DAU.swift +++ b/Sources/Growth/DAU.swift @@ -363,7 +363,7 @@ public class DAU { extension Date { /// Returns date of current week's monday in YYYY-MM-DD formatted String - var mondayOfCurrentWeekFormatted: String? { + public var mondayOfCurrentWeekFormatted: String? { // We look for a previous monday because Sunday is considered a beggining of a new week using default gregorian calendar. // For example if today is Sunday, the next Monday using Calendar would be the day after Sunday which is wrong. // That's why backward search may sound counter intuitive. diff --git a/Sources/Onboarding/OnboardingPreferences.swift b/Sources/Onboarding/OnboardingPreferences.swift index 94ee9159173..eb0239f5ff5 100644 --- a/Sources/Onboarding/OnboardingPreferences.swift +++ b/Sources/Onboarding/OnboardingPreferences.swift @@ -15,11 +15,72 @@ extension Preferences { public static let basicOnboardingCompleted = Option( key: "general.basic-onboarding-completed", default: OnboardingState.undetermined.rawValue) + /// The time until the next on-boarding shows public static let basicOnboardingDefaultBrowserSelected = Option( key: "general.basic-onboarding-default-browser-selected", default: false) + /// The progress the user has made with onboarding - public static let basicOnboardingProgress = Option(key: "general.basic-onboarding-progress", default: OnboardingProgress.none.rawValue) + public static let basicOnboardingProgress = Option( + key: "general.basic-onboarding-progress", + default: OnboardingProgress.none.rawValue) + + /// The bool detemining if p3a infomartion is shown in onboarding to a user so they will not see it again as pop-over + public static let p3aOnboardingShown = Option( + key: "onboarding.basic-onboarding-default-browser-selected", + default: false) + + /// Whether this is a new user who installed the application after onboarding retention updates + public static let isNewRetentionUser = Option(key: "general.new-retention", default: nil) + } +} + +extension Preferences { + public final class FullScreenCallout { + /// Whether the block cookie consent notices callout is shown. + public static let blockCookieConsentNoticesCalloutCompleted = Option( + key: "fullScreenCallout.full-screen-cookie-consent-notices-callout-completed", + default: false) + + /// Whether the vpn callout is shown. + public static let vpnCalloutCompleted = Option( + key: "fullScreenCallout.full-screen-vpn-callout-completed", + default: false) + + /// Whether the rewards callout is shown. + public static let rewardsCalloutCompleted = Option( + key: "fullScreenCallout.full-screen-rewards-callout-completed", + default: false) + + /// Whether the whats new callout should be shown. + public static let whatsNewCalloutOptIn = Option( + key: "fullScreenCallout.full-screen-whats-new-callout-completed", + default: false) + + /// Whether the ntp callout is shown. + public static let ntpCalloutCompleted = Option( + key: "fullScreenCallout.full-screen-ntp-callout-completed", + default: false) + + /// Whether the omnibox callout is shown. + public static let omniboxCalloutCompleted = Option( + key: "fullScreenCallout.full-screen-omnibox-callout-completed", + default: false) + } +} + +extension Preferences { + public final class DefaultBrowserIntro { + /// Whether the default browser onboarding completed. This can happen by opening app settings or after the user + /// dismissed the intro screen enough amount of times. + public static let completed = Option( + key: "defaultBrowserIntro.intro-completed", + default: false) + + /// Whether system notification showed or not + public static let defaultBrowserNotificationScheduled = Option( + key: "general.default-browser-notification-scheduled", + default: false) } } diff --git a/Sources/Onboarding/ProductNotifications/BlockedAdsStackView.swift b/Sources/Onboarding/ProductNotifications/BlockedAdsStackView.swift new file mode 100644 index 00000000000..13b07ecf39e --- /dev/null +++ b/Sources/Onboarding/ProductNotifications/BlockedAdsStackView.swift @@ -0,0 +1,46 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import UIKit +import SnapKit +import BraveUI +import DesignSystem +import BraveShared +import Shared + +class BlockedAdsStackView: UIStackView { + + init(edgeInsets: UIEdgeInsets, spacing: CGFloat? = 0) { + super.init(frame: .zero) + if let spacing = spacing { + self.spacing = spacing + } + + alignment = .center + layoutMargins = edgeInsets + isLayoutMarginsRelativeArrangement = true + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + /// Adds Background to StackView with Color and Corner Radius + public func addBackground(color: UIColor, cornerRadius: CGFloat? = nil) { + let backgroundView = UIView(frame: bounds).then { + $0.backgroundColor = color + $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] + } + + if let radius = cornerRadius { + backgroundView.layer.cornerRadius = radius + backgroundView.layer.cornerCurve = .continuous + } + + insertSubview(backgroundView, at: 0) + } +} diff --git a/Client/Frontend/Browser/FullScreenCalloutManager.swift b/Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift similarity index 60% rename from Client/Frontend/Browser/FullScreenCalloutManager.swift rename to Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift index 4aa7afed1ac..f2f21116b9d 100644 --- a/Client/Frontend/Browser/FullScreenCalloutManager.swift +++ b/Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift @@ -8,36 +8,43 @@ import Shared import BraveShared import Growth -struct FullScreenCalloutManager { +public struct FullScreenCalloutManager { - enum FullScreenCalloutType { - case vpn, rewards, defaultBrowser, blockCookieConsentNotices + public enum FullScreenCalloutType { + case p3a, vpn, rewards, defaultBrowser, blockCookieConsentNotices /// The number of days passed to show certain type of callout var period: Int { switch self { - case .blockCookieConsentNotices: return 0 + case .p3a: return 0 case .vpn: return 4 case .rewards: return 8 case .defaultBrowser: return 10 + case .blockCookieConsentNotices: return 0 } } /// The preference value stored for complete state - var preferenceValue: Preferences.Option { + public var preferenceValue: Preferences.Option { switch self { - case .blockCookieConsentNotices: return Preferences.FullScreenCallout.blockCookieConsentNoticesCalloutCompleted - case .vpn: return Preferences.FullScreenCallout.vpnCalloutCompleted - case .rewards: return Preferences.FullScreenCallout.rewardsCalloutCompleted - case .defaultBrowser: return Preferences.DefaultBrowserIntro.completed + case .p3a: + return Preferences.Onboarding.p3aOnboardingShown + case .vpn: + return Preferences.FullScreenCallout.vpnCalloutCompleted + case .rewards: + return Preferences.FullScreenCallout.rewardsCalloutCompleted + case .defaultBrowser: + return Preferences.DefaultBrowserIntro.completed + case .blockCookieConsentNotices: + return Preferences.FullScreenCallout.blockCookieConsentNoticesCalloutCompleted } } } /// It determines whether we should show show the designated callout or not and sets corresponding preferences accordingly. /// Returns true if the callout should be shown. - static func shouldShowDefaultBrowserCallout(calloutType: FullScreenCalloutType) -> Bool { - guard Preferences.General.isNewRetentionUser.value == true, + public static func shouldShowDefaultBrowserCallout(calloutType: FullScreenCalloutType) -> Bool { + guard Preferences.Onboarding.isNewRetentionUser.value == true, let appRetentionLaunchDate = Preferences.DAU.appRetentionLaunchDate.value, !calloutType.preferenceValue.value else { diff --git a/Sources/Onboarding/ProductNotifications/blocking-summary.json b/Sources/Onboarding/ProductNotifications/Resources/blocking-summary.json similarity index 100% rename from Sources/Onboarding/ProductNotifications/blocking-summary.json rename to Sources/Onboarding/ProductNotifications/Resources/blocking-summary.json diff --git a/Sources/Onboarding/Welcome/Welcome3PAViewController.swift b/Sources/Onboarding/Welcome/Welcome3PAViewController.swift new file mode 100644 index 00000000000..865a91a8693 --- /dev/null +++ b/Sources/Onboarding/Welcome/Welcome3PAViewController.swift @@ -0,0 +1,51 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import Shared +import BraveShared +import BraveUI +import BraveCore + +public class Welcome3PAViewController: UIViewController { + + private let contentView = WelcomeViewCallout().then { + $0.isBottomArrowHidden = true + } + + public override func viewDidLoad() { + super.viewDidLoad() + + let backgroundView = UIView().then { + $0.backgroundColor = UIColor.black.withAlphaComponent(0.3) + } + + view.addSubview(backgroundView) + backgroundView.snp.makeConstraints { $0.edges.equalToSuperview() } + + view.addSubview(contentView) + contentView.snp.makeConstraints { + if traitCollection.verticalSizeClass == .regular + && traitCollection.horizontalSizeClass == .compact { + $0.leading.trailing.greaterThanOrEqualTo(view) + } else { + $0.width.lessThanOrEqualTo(BraveUX.baseDimensionValue) + } + + $0.centerX.centerY.equalToSuperview() + } + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundTapped)) + backgroundView.addGestureRecognizer(tapGesture) + } + + @objc func backgroundTapped() { + dismiss(animated: false) + } + + public func setLayoutState(state: WelcomeViewCalloutState) { + contentView.setState(state: state) + } +} diff --git a/Sources/Onboarding/Welcome/WelcomeActionToggle.swift b/Sources/Onboarding/Welcome/WelcomeActionToggle.swift new file mode 100644 index 00000000000..a53273c7f81 --- /dev/null +++ b/Sources/Onboarding/Welcome/WelcomeActionToggle.swift @@ -0,0 +1,91 @@ +// Copyright 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import UIKit +import SnapKit +import BraveUI +import Shared +import BraveShared + +class WelcomeShareActionToggle: BlockedAdsStackView { + + struct DesignDetails { + let defaultSpacing: CGFloat = 10 + let defaultEdgeInset = UIEdgeInsets(equalInset: 15) + let defaultCorderRadius: CGFloat = 10 + let defaultFont: UIFont = .preferredFont(for: .body, weight: .regular) + } + + var text: String? = "" { + didSet { + descriptionLabel.text = text + } + } + + var font: UIFont { + didSet { + descriptionLabel.font = font + } + } + + var isOn: Bool = true { + didSet { + shareToggle.isOn = isOn + } + } + + var onToggleChanged: ((Bool) -> Void)? + + private let descriptionLabel = UILabel().then { + $0.textColor = .bravePrimary + $0.textAlignment = .left + $0.numberOfLines = 0 + $0.minimumScaleFactor = 0.5 + $0.adjustsFontSizeToFitWidth = true + $0.setContentHuggingPriority(.defaultLow, for: .horizontal) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + $0.setContentHuggingPriority(.defaultLow, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + + private let iconView = UIImageView().then { + $0.contentMode = .scaleAspectFit + $0.image = UIImage(named: "welcome-view-ntp-logo", in: .module, compatibleWith: nil) + $0.snp.makeConstraints { + $0.size.equalTo(40) + } + $0.setContentHuggingPriority(.required, for: .horizontal) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + } + + private(set) lazy var shareToggle = UISwitch().then { + $0.addTarget(self, action: #selector(didToggleShare), for: .valueChanged) + $0.onTintColor = .braveBlurple + $0.setContentHuggingPriority(.required, for: .horizontal) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + } + + init(design: DesignDetails = DesignDetails()) { + self.font = design.defaultFont + super.init(edgeInsets: design.defaultEdgeInset, spacing: design.defaultSpacing) + + addBackground(color: .black.withAlphaComponent(0.025), cornerRadius: design.defaultCorderRadius) + + addStackViewItems( + .view(descriptionLabel), + .view(shareToggle) + ) + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + @objc func didToggleShare(_ toggle: UISwitch) { + onToggleChanged?(toggle.isOn) + } +} diff --git a/Sources/Onboarding/Welcome/WelcomeBraveBlockedAdsController.swift b/Sources/Onboarding/Welcome/WelcomeBraveBlockedAdsController.swift index 9d99d611fe9..7ee7557d1b1 100644 --- a/Sources/Onboarding/Welcome/WelcomeBraveBlockedAdsController.swift +++ b/Sources/Onboarding/Welcome/WelcomeBraveBlockedAdsController.swift @@ -154,42 +154,3 @@ public class WelcomeBraveBlockedAdsController: UIViewController, PopoverContentC ) } } - -// MARK: - BlockedAdsStackView - -extension WelcomeBraveBlockedAdsController { - - class BlockedAdsStackView: UIStackView { - - init(edgeInsets: UIEdgeInsets, spacing: CGFloat? = 0) { - super.init(frame: .zero) - if let spacing = spacing { - self.spacing = spacing - } - - alignment = .center - layoutMargins = edgeInsets - isLayoutMarginsRelativeArrangement = true - } - - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError() - } - - /// Adds Background to StackView with Color and Corner Radius - public func addBackground(color: UIColor, cornerRadius: CGFloat? = nil) { - let backgroundView = UIView(frame: bounds).then { - $0.backgroundColor = color - $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] - } - - if let radius = cornerRadius { - backgroundView.layer.cornerRadius = radius - backgroundView.layer.cornerCurve = .continuous - } - - insertSubview(backgroundView, at: 0) - } - } -} diff --git a/Sources/Onboarding/Welcome/WelcomeViewCallout.swift b/Sources/Onboarding/Welcome/WelcomeViewCallout.swift index 0f364a756c1..f22d2abbfb8 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewCallout.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewCallout.swift @@ -13,25 +13,40 @@ import BraveShared public enum WelcomeViewCalloutState { public struct WelcomeViewDefaultBrowserDetails { var title: String + var toggleTitle: String? + var toggleStatus: Bool var details: String + var linkDescription: String? var primaryButtonTitle: String - var secondaryButtonTitle: String - var primaryAction: (() -> Void) - var secondaryAction: (() -> Void) - + var secondaryButtonTitle: String? + var toggleAction: ((Bool) -> Void)? + var linkAction: ((URL) -> Void)? + var primaryButtonAction: (() -> Void) + var secondaryButtonAction: (() -> Void)? + public init( title: String, + toggleTitle: String? = nil, + toggleStatus: Bool = true, details: String, + linkDescription: String? = nil, primaryButtonTitle: String, - secondaryButtonTitle: String, - primaryAction: @escaping () -> Void, - secondaryAction: @escaping () -> Void) { - self.title = title - self.details = details - self.primaryButtonTitle = primaryButtonTitle - self.secondaryButtonTitle = secondaryButtonTitle - self.primaryAction = primaryAction - self.secondaryAction = secondaryAction + secondaryButtonTitle: String? = nil, + toggleAction: ((Bool) -> Void)? = nil, + linkAction: ((URL) -> Void)? = nil, + primaryButtonAction: @escaping () -> Void, + secondaryButtonAction: (() -> Void)? = nil) { + self.title = title + self.toggleTitle = toggleTitle + self.toggleStatus = toggleStatus + self.details = details + self.linkDescription = linkDescription + self.primaryButtonTitle = primaryButtonTitle + self.secondaryButtonTitle = secondaryButtonTitle + self.toggleAction = toggleAction + self.linkAction = linkAction + self.primaryButtonAction = primaryButtonAction + self.secondaryButtonAction = secondaryButtonAction } } @@ -39,6 +54,7 @@ public enum WelcomeViewCalloutState { case welcome(title: String) case defaultBrowser(info: WelcomeViewDefaultBrowserDetails) case settings(title: String, details: String) + case p3a(info: WelcomeViewDefaultBrowserDetails) case defaultBrowserCallout(info: WelcomeViewDefaultBrowserDetails) } @@ -74,6 +90,8 @@ class WelcomeViewCallout: UIView { $0.setContentHuggingPriority(.required, for: .horizontal) $0.setContentCompressionResistancePriority(.required, for: .horizontal) } + + private let actionToggle = WelcomeShareActionToggle() private let detailsLabel = UILabel().then { $0.textColor = .bravePrimary @@ -85,6 +103,16 @@ class WelcomeViewCallout: UIView { $0.setContentHuggingPriority(.required, for: .horizontal) $0.setContentCompressionResistancePriority(.required, for: .horizontal) } + + private let actionDescriptionLabel = LinkLabel().then { + $0.textColor = .bravePrimary + $0.textAlignment = .left + $0.textContainer.lineFragmentPadding = 0 + $0.textContainerInset = .zero + $0.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + $0.setContentHuggingPriority(.required, for: .horizontal) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + } private let primaryButton = RoundInterfaceButton(type: .custom).then { $0.setTitleColor(.white, for: .normal) @@ -125,14 +153,19 @@ class WelcomeViewCallout: UIView { private var verticalLayoutMargin = UX.verticalLayoutMargin private var horizontalLayoutMargin = UX.horizontalLayoutMargin - // MARK: - State private(set) var state: WelcomeViewCalloutState? + + var isBottomArrowHidden: Bool = false { + didSet { + arrowView.isHidden = isBottomArrowHidden + } + } init() { super.init(frame: .zero) doLayout() - [titleLabel, detailsLabel, primaryButton, secondaryButtonContentView].forEach { + [titleLabel, actionToggle, detailsLabel, actionDescriptionLabel, primaryButton, secondaryButtonContentView].forEach { contentStackView.addArrangedSubview($0) $0.alpha = 0.0 @@ -150,7 +183,7 @@ class WelcomeViewCallout: UIView { secondaryButtonContentView.addArrangedSubview($0) } - [titleLabel, detailsLabel].forEach { + [titleLabel, actionToggle, detailsLabel, actionDescriptionLabel].forEach { $0.contentMode = .top } @@ -303,7 +336,7 @@ class WelcomeViewCallout: UIView { UIAction( identifier: .init(rawValue: "primary.action"), handler: { _ in - info.primaryAction() + info.primaryButtonAction() }), for: .touchUpInside) $0.alpha = 1.0 $0.isHidden = false @@ -321,7 +354,7 @@ class WelcomeViewCallout: UIView { UIAction( identifier: .init(rawValue: "secondary.action"), handler: { _ in - info.secondaryAction() + info.secondaryButtonAction?() }), for: .touchUpInside) $0.alpha = 1.0 $0.isHidden = false @@ -382,6 +415,79 @@ class WelcomeViewCallout: UIView { } contentStackView.setCustomSpacing(20.0, after: titleLabel) + case .p3a(let info): + contentStackView.do { + $0.layoutMargins = UIEdgeInsets(top: 2 * verticalLayoutMargin, left: 30, bottom: verticalLayoutMargin, right: 30) + } + titleLabel.do { + $0.text = info.title + $0.textAlignment = .left + $0.textColor = .bravePrimary + $0.font = .preferredFont(for: .title3, weight: .bold) + $0.alpha = 1.0 + $0.isHidden = false + } + + actionToggle.do { + $0.text = info.toggleTitle + $0.font = .preferredFont(for: .body, weight: .regular) + $0.isOn = info.toggleStatus + $0.onToggleChanged = info.toggleAction + $0.alpha = 1.0 + $0.isHidden = false + } + + detailsLabel.do { + $0.text = info.details + $0.font = .preferredFont(for: .footnote, weight: .regular) + $0.alpha = 1.0 + $0.isHidden = false + } + + actionDescriptionLabel.do { + $0.font = .preferredFont(for: .footnote, weight: .regular) + $0.onLinkedTapped = info.linkAction + if let linkDescription = info.linkDescription { + $0.text = String(format: linkDescription) + $0.setURLInfo([linkDescription: "p3a"]) + } + $0.alpha = 1.0 + $0.isHidden = false + } + + primaryButton.do { + $0.setTitle(info.primaryButtonTitle, for: .normal) + $0.titleLabel?.font = .preferredFont(for: .body, weight: .regular) + $0.addAction( + UIAction( + identifier: .init(rawValue: "primary.action"), + handler: { _ in + info.primaryButtonAction() + }), for: .touchUpInside) + $0.alpha = 1.0 + $0.isHidden = false + } + + secondaryLabel.do { + $0.alpha = 0.0 + $0.isHidden = true + } + + secondaryButton.do { + $0.alpha = 0.0 + $0.isHidden = true + } + + secondaryButtonContentView.do { + $0.alpha = 0.0 + $0.isHidden = true + } + + contentStackView.setCustomSpacing(horizontalLayoutMargin, after: titleLabel) + contentStackView.setCustomSpacing(horizontalLayoutMargin, after: actionToggle) + contentStackView.setCustomSpacing(horizontalLayoutMargin, after: detailsLabel) + contentStackView.setCustomSpacing(3 * horizontalLayoutMargin, after: actionDescriptionLabel) + contentStackView.setCustomSpacing(horizontalLayoutMargin, after: primaryButton) case .defaultBrowserCallout(let info): contentStackView.do { $0.layoutMargins = UIEdgeInsets(top: 2 * UX.verticalLayoutMargin, left: 20, bottom: UX.verticalLayoutMargin, right: 20) @@ -409,7 +515,7 @@ class WelcomeViewCallout: UIView { UIAction( identifier: .init(rawValue: "primary.action"), handler: { _ in - info.primaryAction() + info.primaryButtonAction() }), for: .touchUpInside) $0.alpha = 1.0 $0.isHidden = false @@ -433,7 +539,7 @@ class WelcomeViewCallout: UIView { UIAction( identifier: .init(rawValue: "secondary.action"), handler: { _ in - info.secondaryAction() + info.secondaryButtonAction?() }), for: .touchUpInside) $0.alpha = 1.0 $0.isHidden = false diff --git a/Sources/Onboarding/Welcome/WelcomeViewController.swift b/Sources/Onboarding/Welcome/WelcomeViewController.swift index eeb892dc66b..b9f20c33fe7 100644 --- a/Sources/Onboarding/Welcome/WelcomeViewController.swift +++ b/Sources/Onboarding/Welcome/WelcomeViewController.swift @@ -8,6 +8,9 @@ import UIKit import SnapKit import BraveShared import Shared +import BraveCore +import BraveUI +import SafariServices private enum WelcomeViewID: Int { case background = 1 @@ -22,13 +25,15 @@ private enum WelcomeViewID: Int { public class WelcomeViewController: UIViewController { private var state: WelcomeViewCalloutState? + private let p3aUtilities: BraveP3AUtils - public convenience init() { - self.init(state: .loading) + public convenience init(p3aUtilities: BraveP3AUtils) { + self.init(state: .loading, p3aUtilities: p3aUtilities) } - public init(state: WelcomeViewCalloutState?) { + public init(state: WelcomeViewCalloutState?, p3aUtilities: BraveP3AUtils) { self.state = state + self.p3aUtilities = p3aUtilities super.init(nibName: nil, bundle: nil) self.transitioningDelegate = self @@ -196,7 +201,7 @@ public class WelcomeViewController: UIViewController { } } - private func setLayoutState(state: WelcomeViewCalloutState) { + public func setLayoutState(state: WelcomeViewCalloutState) { self.state = state switch state { @@ -261,7 +266,7 @@ public class WelcomeViewController: UIViewController { $0.height.equalTo(180.0) } calloutView.setState(state: state) - case .settings: + case .p3a, .settings: let topTransform = { () -> CGAffineTransform in var transformation = CGAffineTransform.identity transformation = transformation.scaledBy(x: 1.5, y: 1.5) @@ -284,7 +289,6 @@ public class WelcomeViewController: UIViewController { $0.height.equalTo(180.0) } calloutView.setState(state: state) - case .defaultBrowserCallout: let topTransform = { () -> CGAffineTransform in var transformation = CGAffineTransform.identity @@ -313,25 +317,25 @@ public class WelcomeViewController: UIViewController { } private func animateToWelcomeState() { - let nextController = WelcomeViewController(state: nil).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities).then { $0.setLayoutState(state: WelcomeViewCalloutState.welcome(title: Strings.Onboarding.welcomeScreenTitle)) } present(nextController, animated: true) } private func animateToDefaultBrowserState() { - let nextController = WelcomeViewController(state: nil) + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities) let state = WelcomeViewCalloutState.defaultBrowser( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( title: Strings.Callout.defaultBrowserCalloutTitle, details: Strings.Callout.defaultBrowserCalloutDescription, primaryButtonTitle: Strings.Callout.defaultBrowserCalloutPrimaryButtonTitle, secondaryButtonTitle: Strings.DefaultBrowserCallout.introSkipButtonText, - primaryAction: { + primaryButtonAction: { nextController.animateToDefaultSettingsState() }, - secondaryAction: { - self.close() + secondaryButtonAction: { + nextController.animateToP3aState() } ) ) @@ -340,7 +344,7 @@ public class WelcomeViewController: UIViewController { } private func animateToDefaultSettingsState() { - let nextController = WelcomeViewController(state: nil).then { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities).then { $0.setLayoutState( state: WelcomeViewCalloutState.settings( title: Strings.Onboarding.navigateSettingsOnboardingScreenTitle, @@ -351,13 +355,47 @@ public class WelcomeViewController: UIViewController { Preferences.Onboarding.basicOnboardingDefaultBrowserSelected.value = true } } + + private func animateToP3aState() { + let nextController = WelcomeViewController(state: nil, p3aUtilities: self.p3aUtilities) + let state = WelcomeViewCalloutState.p3a( + info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( + title: Strings.Callout.p3aCalloutTitle, + toggleTitle: Strings.Callout.p3aCalloutToggleTitle, + details: Strings.Callout.p3aCalloutDescription, + linkDescription: Strings.Callout.p3aCalloutLinkTitle, + primaryButtonTitle: Strings.done, + toggleAction: { [weak self] isOn in + self?.p3aUtilities.isP3AEnabled = isOn + }, + linkAction: { url in + let p3aLearnMoreController = SFSafariViewController(url: BraveUX.braveP3ALearnMoreURL, configuration: .init()) + p3aLearnMoreController.modalPresentationStyle = .currentContext + + nextController.present(p3aLearnMoreController, animated: true) + }, + + primaryButtonAction: { [weak self] in + self?.close() + } + ) + ) + + nextController.setLayoutState(state: state) + + present(nextController, animated: true) { [unowned self] in + self.p3aUtilities.isNoticeAcknowledged = true + Preferences.Onboarding.p3aOnboardingShown.value = true + } + } private func onSetDefaultBrowser() { guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingsUrl) - self.close() + + animateToP3aState() } private func close() {