diff --git a/BraveShared/BraveStrings.swift b/BraveShared/BraveStrings.swift index 9f284aea5a0..f33f2fb1d25 100644 --- a/BraveShared/BraveStrings.swift +++ b/BraveShared/BraveStrings.swift @@ -629,6 +629,11 @@ extension Strings { public static let OBErrorTitle = NSLocalizedString("OBErrorTitle", bundle: Bundle.braveShared, value: "Sorry", comment: "A generic error title for onboarding") public static let OBErrorDetails = NSLocalizedString("OBErrorDetails", bundle: Bundle.braveShared, value: "Something went wrong while creating your wallet. Please try again", comment: "A generic error body for onboarding") public static let OBErrorOkay = NSLocalizedString("OBErrorOkay", bundle: Bundle.braveShared, value: "Okay", comment: "") + public static let OBPrivacyConsentTitle = NSLocalizedString("OBPrivacyConsentTitle", bundle: .braveShared, value: "Anonymous referral code check", comment: "") + public static let OBPrivacyConsentDetail = NSLocalizedString("OBPrivacyConsentDetail", bundle: .braveShared, value: "You may have downloaded Brave in support of your referrer. To detect your referrer, Brave performs a one-time check of your clipboard for the matching referral code. This check is limited to the code only and no other personal data will be transmitted. If you opt out, your referrer won’t receive rewards from Brave.", comment: "") + public static let OBPrivacyConsentClipboardPermission = NSLocalizedString("OBPrivacyConsentClipboardPermission", bundle: .braveShared, value: "Allow Brave to check my clipboard for a matching referral code", comment: "") + public static let OBPrivacyConsentYesButton = NSLocalizedString("OBPrivacyConsentYesButton", bundle: .braveShared, value: "Allow one-time clipboard check", comment: "") + public static let OBPrivacyConsentNoButton = NSLocalizedString("OBPrivacyConsentNoButton", bundle: .braveShared, value: "Do not allow this check", comment: "") } // MARK: - Ads Notifications diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 470a675f9c2..71631bc6acf 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -157,6 +157,8 @@ 0ADCD45B231973650078CC67 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADCD45A231973650078CC67 /* UserAgentBuilderTests.swift */; }; 0ADCD45F2319799F0078CC67 /* RollingFileLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61453BD1B750A1700C3F9D7 /* RollingFileLoggerTests.swift */; }; 0AE5C09922CAA01E00DFF3EE /* RewardsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE5C09822CAA01E00DFF3EE /* RewardsButton.swift */; }; + 0AE5C69124F0059D004CBC9B /* OnboardingPrivacyConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE5C69024F0059D004CBC9B /* OnboardingPrivacyConsentViewController.swift */; }; + 0AE5C69424F005F9004CBC9B /* OnboardingPrivacyConsentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE5C69324F005F9004CBC9B /* OnboardingPrivacyConsentView.swift */; }; 0AEF16C723CDE4B800158AD9 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEF16C523CDE4B800158AD9 /* BottomSheetViewController.swift */; }; 0AEF99A922E22C5E00294C76 /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEF99A822E22C5E00294C76 /* DownloadsViewController.swift */; }; 0AEF99B222E22D2200294C76 /* DateGroupedTableData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEF99B122E22D2200294C76 /* DateGroupedTableData.swift */; }; @@ -1515,6 +1517,8 @@ 0ADCD4512319640F0078CC67 /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; 0ADCD45A231973650078CC67 /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = ""; }; 0AE5C09822CAA01E00DFF3EE /* RewardsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardsButton.swift; sourceTree = ""; }; + 0AE5C69024F0059D004CBC9B /* OnboardingPrivacyConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPrivacyConsentViewController.swift; sourceTree = ""; }; + 0AE5C69324F005F9004CBC9B /* OnboardingPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPrivacyConsentView.swift; sourceTree = ""; }; 0AE885BC23571EE1006951A8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Storage.strings; sourceTree = ""; }; 0AEF16C523CDE4B800158AD9 /* BottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; 0AEF99A822E22C5E00294C76 /* DownloadsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewController.swift; sourceTree = ""; }; @@ -2933,6 +2937,8 @@ children = ( 0A6112BC230B4306001BBC45 /* OnboardingViewController.swift */, 0A6112AB230B00E7001BBC45 /* OnboardingNavigationController.swift */, + 0AE5C69024F0059D004CBC9B /* OnboardingPrivacyConsentViewController.swift */, + 0AE5C69324F005F9004CBC9B /* OnboardingPrivacyConsentView.swift */, 0A3C789C23055C4A0022F6D8 /* OnboardingSearchEnginesViewController.swift */, 0A3C789E23056C910022F6D8 /* OnboardingSearchEnginesView.swift */, 0A3C78A0230597DA0022F6D8 /* OnboardingShieldsViewController.swift */, @@ -6719,6 +6725,7 @@ 27FD2CAD2146C31C00A5A779 /* AddToFavoritesActivity.swift in Sources */, 4422D4E121BFFB7600BF1855 /* filter_block.cc in Sources */, 0A8C69BE225E350300988715 /* IndentedImageTableViewCell.swift in Sources */, + 0AE5C69124F0059D004CBC9B /* OnboardingPrivacyConsentViewController.swift in Sources */, 0A1E843D2190A57F0042F782 /* SyncSettingsTableViewController.swift in Sources */, 4422D56C21BFFB7F00BF1855 /* bitstate.cc in Sources */, E68E7ADE1CAC208A00FDCA76 /* RemovePasscodeViewController.swift in Sources */, @@ -6772,6 +6779,7 @@ C4EFEECF1CEBB6F2009762A4 /* BackForwardTableViewCell.swift in Sources */, 2C49854E206173C800893DAE /* photon-colors.swift in Sources */, E689C7301E0C7617008BAADB /* NSAttributedStringExtensions.swift in Sources */, + 0AE5C69424F005F9004CBC9B /* OnboardingPrivacyConsentView.swift in Sources */, 0A16E2D723D4E1FB00B0BF94 /* ClaimRewardsNTPNotificationViewController.swift in Sources */, D0C95E0E200FD3B200E4E51C /* PrintHelper.swift in Sources */, 4422D55821BFFB7F00BF1855 /* unicode_casefold.cc in Sources */, diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index 3a979173ab1..c9006cddf24 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -295,54 +295,65 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati Preferences.URP.referralLookupOutstanding.value = isFirstLaunch } - if Preferences.URP.referralLookupOutstanding.value == true { - var refCode: String? - - let savedRefCode: String? = ProcessInfo().operatingSystemVersion.majorVersion > 13 - ? nil : UIPasteboard.general.string - - if Preferences.URP.referralCode.value == nil { - UrpLog.log("No ref code exists on launch, attempting clipboard retrieval") - refCode = UserReferralProgram.sanitize(input: savedRefCode) - } + handleReferralLookup(urp, checkClipboard: false) + } else { + log.error("Failed to initialize user referral program") + UrpLog.log("Failed to initialize user referral program") + } + + AdblockResourceDownloader.shared.startLoading() + + return shouldPerformAdditionalDelegateHandling + } + + func handleReferralLookup(_ urp: UserReferralProgram, checkClipboard: Bool) { + let initialOnboarding = + Preferences.General.basicOnboardingProgress.value == OnboardingProgress.none.rawValue + + // FIXME: Update to iOS14 clipboard api once ready (#2838) + if initialOnboarding && UIPasteboard.general.hasStrings { + log.debug("Skipping URP call at app launch, this is handled in privacy consent onboarding screen") + return + } + + if Preferences.URP.referralLookupOutstanding.value == true { + var refCode: String? + + if Preferences.URP.referralCode.value == nil { + UrpLog.log("No ref code exists on launch, attempting clipboard retrieval") + let savedRefCode = checkClipboard ? UIPasteboard.general.string : nil + refCode = UserReferralProgram.sanitize(input: savedRefCode) if refCode != nil { UrpLog.log("Clipboard ref code found: " + (savedRefCode ?? "!Clipboard Empty!")) UrpLog.log("Clearing clipboard.") UIPasteboard.general.clearPasteboard() } - - urp.referralLookup(refCode: refCode) { referralCode, offerUrl in - // Attempting to send ping after first urp lookup. - // This way we can grab the referral code if it exists, see issue #2586. - self.dau.sendPingToServer() - if let code = referralCode { - let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes - let retryDeadline = Date() + retryTime - - Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline - - self.browserViewController.backgroundDataSource - .fetchSpecificResource(.superReferral(code: code)) - } else { - self.browserViewController.backgroundDataSource.startFetching() - } + } + + urp.referralLookup(refCode: refCode) { referralCode, offerUrl in + // Attempting to send ping after first urp lookup. + // This way we can grab the referral code if it exists, see issue #2586. + self.dau.sendPingToServer() + if let code = referralCode { + let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes + let retryDeadline = Date() + retryTime - guard let url = offerUrl?.asURL else { return } - self.browserViewController.openReferralLink(url: url) + Preferences.NewTabPage.superReferrerThemeRetryDeadline.value = retryDeadline + + self.browserViewController.backgroundDataSource + .fetchSpecificResource(.superReferral(code: code)) + } else { + self.browserViewController.backgroundDataSource.startFetching() } - } else { - urp.pingIfEnoughTimePassed() - browserViewController.backgroundDataSource.startFetching() + + guard let url = offerUrl?.asURL else { return } + self.browserViewController.openReferralLink(url: url) } } else { - log.error("Failed to initialize user referral program") - UrpLog.log("Failed to initialize user referral program") + urp.pingIfEnoughTimePassed() + browserViewController.backgroundDataSource.startFetching() } - - AdblockResourceDownloader.shared.startLoading() - - return shouldPerformAdditionalDelegateHandling } func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { diff --git a/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift b/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift index 6b3196c2a30..9aa3cb4c382 100644 --- a/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift +++ b/Client/Frontend/Browser/Onboarding/OnboardingNavigationController.swift @@ -4,6 +4,7 @@ import UIKit import Shared +import BraveShared import pop import Lottie import BraveRewards @@ -55,7 +56,19 @@ class OnboardingNavigationController: UINavigationController { if progress == .rewards || progress == .ads { return BraveAds.isCurrentLocaleSupported() ? [.adsCountdown] : [.rewardsAgreement] } - return BraveAds.isCurrentLocaleSupported() ? [.searchEnginePicker, .shieldsInfo, .rewardsAgreement, .adsCountdown] : [.searchEnginePicker, .shieldsInfo, .rewardsAgreement] + + var newUserScreens: [Screens] = [.searchEnginePicker, .shieldsInfo, .rewardsAgreement] + + if BraveAds.isCurrentLocaleSupported() { + newUserScreens.append(.adsCountdown) + } + + // FIXME: Update to iOS14 clipboard api once ready (#2838) + if Preferences.URP.referralCode.value == nil && UIPasteboard.general.hasStrings { + newUserScreens.insert(.privacyConsent, at: 0) + } + + return newUserScreens case .existingUserRewardsOff(let progress): //The user already made it to rewards and agreed so they should only see ads countdown if progress == .rewards || progress == .ads { @@ -79,6 +92,7 @@ class OnboardingNavigationController: UINavigationController { } fileprivate enum Screens { + case privacyConsent case searchEnginePicker case shieldsInfo case existingRewardsTurnOnAds @@ -88,6 +102,7 @@ class OnboardingNavigationController: UINavigationController { /// Returns new ViewController associated with the screen type func viewController(with profile: Profile, rewards: BraveRewards?, theme: Theme) -> OnboardingViewController { switch self { + case .privacyConsent: return OnboardingPrivacyConsentViewController(profile: profile, rewards: rewards, theme: theme) case .searchEnginePicker: return OnboardingSearchEnginesViewController(profile: profile, rewards: rewards, theme: theme) case .shieldsInfo: @@ -103,6 +118,7 @@ class OnboardingNavigationController: UINavigationController { var type: OnboardingViewController.Type { switch self { + case .privacyConsent: return OnboardingPrivacyConsentViewController.self case .searchEnginePicker: return OnboardingSearchEnginesViewController.self case .shieldsInfo: return OnboardingShieldsViewController.self case .existingRewardsTurnOnAds: return OnboardingAdsAvailableController.self diff --git a/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentView.swift b/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentView.swift new file mode 100644 index 00000000000..c01fba3c851 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentView.swift @@ -0,0 +1,87 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import Shared +import BraveShared +import pop +import SnapKit + +extension OnboardingPrivacyConsentViewController { + + class View: UIView { + + let yesConsentButton = CommonViews.primaryButton(text: Strings.OBPrivacyConsentYesButton).then { + $0.accessibilityIdentifier = "OnboardingPrivacyConsentViewController.YesButton" + $0.titleLabel?.adjustsFontSizeToFitWidth = true + } + + let noConsentButton = CommonViews.secondaryButton(text: Strings.OBPrivacyConsentNoButton).then { + $0.accessibilityIdentifier = "OnboardingPrivacyConsentViewController.NoButton" + $0.titleLabel?.adjustsFontSizeToFitWidth = true + } + + private let mainStackView = UIStackView().then { + $0.axis = .vertical + $0.distribution = .equalSpacing + } + + private let braveLogo = UIImageView(image: #imageLiteral(resourceName: "browser_lock_popup")).then { + $0.contentMode = .scaleAspectFit + } + + private let titleLabel = CommonViews.primaryText(Strings.OBPrivacyConsentTitle).then { + $0.font = .systemFont(ofSize: 20, weight: .semibold) + $0.numberOfLines = 0 + } + + private let refProgramLabel = UILabel().then { + $0.text = Strings.OBPrivacyConsentDetail + $0.font = .systemFont(ofSize: 16, weight: .regular) + $0.numberOfLines = 0 + $0.textAlignment = .left + $0.minimumScaleFactor = 0.7 + } + + init(theme: Theme) { + super.init(frame: .zero) + + mainStackView.tag = OnboardingViewAnimationID.detailsContent.rawValue + braveLogo.tag = OnboardingViewAnimationID.background.rawValue + + applyTheme(theme) + + mainStackView.addStackViewItems( + .view(.spacer(.vertical, amount: 1)), + .view(braveLogo), + .view( + UIStackView(arrangedSubviews: [titleLabel, refProgramLabel]).then { + $0.axis = .vertical + $0.spacing = 24 + }), + .view(UIStackView(arrangedSubviews: [yesConsentButton, noConsentButton]).then { + $0.axis = .vertical + $0.spacing = 8 + }) + ) + + addSubview(mainStackView) + + mainStackView.snp.makeConstraints { + $0.top.equalTo(safeArea.top).inset(24) + $0.bottom.equalTo(safeArea.bottom).inset(16) + $0.leading.trailing.equalToSuperview().inset(25) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { fatalError() } + + func applyTheme(_ theme: Theme) { + backgroundColor = OnboardingViewController.colorForTheme(theme) + titleLabel.appearanceTextColor = theme.colors.tints.home + refProgramLabel.appearanceTextColor = theme.colors.tints.home + } + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentViewController.swift b/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentViewController.swift new file mode 100644 index 00000000000..3e344e87dd8 --- /dev/null +++ b/Client/Frontend/Browser/Onboarding/OnboardingPrivacyConsentViewController.swift @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import BraveShared +import Shared + +private let log = Logger.browserLogger + +class OnboardingPrivacyConsentViewController: OnboardingViewController { + + private var contentView: View { + return view as! View // swiftlint:disable:this force_cast + } + + override func loadView() { + view = View(theme: theme) + } + + override func viewDidLoad() { + super.viewDidLoad() + + contentView.yesConsentButton.addTarget(self, action: #selector(yesConsentTapped), for: .touchUpInside) + contentView.noConsentButton.addTarget(self, action: #selector(noConsentTaapped), for: .touchUpInside) + } + + @objc func yesConsentTapped() { + presentNextScreen(withPrivacyConsent: true) + } + + @objc func noConsentTaapped() { + presentNextScreen(withPrivacyConsent: false) + } + + private func presentNextScreen(withPrivacyConsent: Bool) { + Preferences.General.basicOnboardingProgress.value = OnboardingProgress.privacyConsent.rawValue + if let urp = UserReferralProgram.shared { + (UIApplication.shared.delegate as? AppDelegate)? + .handleReferralLookup(urp, checkClipboard: withPrivacyConsent) + } + + delegate?.presentNextScreen(current: self) + } + + override func applyTheme(_ theme: Theme) { + styleChildren(theme: theme) + contentView.applyTheme(theme) + } +} diff --git a/Client/Frontend/Browser/Onboarding/OnboardingState.swift b/Client/Frontend/Browser/Onboarding/OnboardingState.swift index 0f1ec139d74..6333938651f 100644 --- a/Client/Frontend/Browser/Onboarding/OnboardingState.swift +++ b/Client/Frontend/Browser/Onboarding/OnboardingState.swift @@ -19,6 +19,8 @@ enum OnboardingState: Int { enum OnboardingProgress: Int { /// The user has never started any onboarding. case none + /// The user has completed the privacy consent + case privacyConsent /// The user has completed the search engine onboarding. case searchEngine /// The user has completed the rewards onboarding.