diff --git a/BraveShared/Preferences.swift b/BraveShared/Preferences.swift index 559edf1a3f3..b11dce5455f 100644 --- a/BraveShared/Preferences.swift +++ b/BraveShared/Preferences.swift @@ -55,6 +55,7 @@ extension Preferences { public static let isEnabled = Option(key: "brave-today.enabled", default: true) public static let languageChecked = Option(key: "brave-today.language-checked", default: false) public static let isShowingIntroCard = Option(key: "brave-today.showing-intro-card", default: true) + public static let debugEnvironment = Option(key: "brave-today.debug.environment", default: nil) } public final class Review { diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 4d6d3bf3505..ede163d6678 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -250,6 +250,7 @@ 272FCAA0225CF8F00091E645 /* OnePasswordExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 272FCA98225CF8F00091E645 /* OnePasswordExtension.framework */; }; 27384CAE254360120086922F /* OnboardingRewardsAgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1D8C6C232BF9C200BDE662 /* OnboardingRewardsAgreementView.swift */; }; 273EB3A72422AB24002A8AAF /* PaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9616E6A240EE43F00667C2D /* PaymentRequest.swift */; }; + 273FCB9A25A7BC5500F279B5 /* BraveTodayDebugSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273FCB9925A7BC5500F279B5 /* BraveTodayDebugSettingsController.swift */; }; 274398E224E4827800E79605 /* FeedCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274398E124E4827800E79605 /* FeedCard.swift */; }; 274398E524E4829900E79605 /* FeedFillStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274398E424E4829900E79605 /* FeedFillStrategy.swift */; }; 274398E724E483AD00E79605 /* FeedItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274398E624E483AD00E79605 /* FeedItemMenu.swift */; }; @@ -1512,6 +1513,7 @@ 2726637224981B5F0056CFE1 /* FeedSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSectionHeaderView.swift; sourceTree = ""; }; 2727369A24A65F650096DCB9 /* UIActionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActionExtensions.swift; sourceTree = ""; }; 272FCA98225CF8F00091E645 /* OnePasswordExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OnePasswordExtension.framework; path = Carthage/Build/iOS/OnePasswordExtension.framework; sourceTree = ""; }; + 273FCB9925A7BC5500F279B5 /* BraveTodayDebugSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraveTodayDebugSettingsController.swift; sourceTree = ""; }; 274398E124E4827800E79605 /* FeedCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCard.swift; sourceTree = ""; }; 274398E424E4829900E79605 /* FeedFillStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedFillStrategy.swift; sourceTree = ""; }; 274398E624E483AD00E79605 /* FeedItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemMenu.swift; sourceTree = ""; }; @@ -3426,6 +3428,7 @@ 27D114D32358FBBF00166534 /* BraveRewardsSettingsViewController.swift */, 27D114D52358FCA400166534 /* SettingsRowViews.swift */, 27D67CEC24D07EB800066D83 /* BraveTodaySettingsViewController.swift */, + 273FCB9925A7BC5500F279B5 /* BraveTodayDebugSettingsController.swift */, 278C700924F96D7000A246C8 /* BraveShieldsAndPrivacySettingsController.swift */, ); path = Settings; @@ -6215,6 +6218,7 @@ E63ED8E11BFD25580097D08E /* LoginListViewController.swift in Sources */, D0625CA8208FC47A0081F3B2 /* BrowserViewController+DownloadQueueDelegate.swift in Sources */, 2F44FCC71A9E8CF500FD20CC /* SearchSettingsTableViewController.swift in Sources */, + 273FCB9A25A7BC5500F279B5 /* BraveTodayDebugSettingsController.swift in Sources */, 39A359E41BFCCE94006B9E87 /* UserActivityHandler.swift in Sources */, 0AF6B1EA24A0EE19005417FC /* InstallVPNView.swift in Sources */, 27201F0424589B9800C19DD1 /* NewTabPageNotifications.swift in Sources */, diff --git a/Client/Frontend/Brave Today/Composer/FeedDataSource.swift b/Client/Frontend/Brave Today/Composer/FeedDataSource.swift index ee4c818b3e1..df9ca8df43d 100644 --- a/Client/Frontend/Brave Today/Composer/FeedDataSource.swift +++ b/Client/Frontend/Brave Today/Composer/FeedDataSource.swift @@ -70,6 +70,11 @@ class FeedDataSource { init() { restoreCachedSources() + if !AppConstants.buildChannel.isPublic, + let savedEnvironment = Preferences.BraveToday.debugEnvironment.value, + let environment = Environment(rawValue: savedEnvironment) { + self.environment = environment + } } // MARK: - Resource Managment @@ -85,19 +90,39 @@ class FeedDataSource { return decoder }() + /// A Brave Today environment + enum Environment: String, CaseIterable { + case dev = "brave.software" + case staging = "bravesoftware.com" + case production = "brave.com" + } + + /// The current Brave Today environment. + /// + /// Updating the environment automatically clears the current cached items if any exist. + /// + /// - warning: Should only be changed in non-public releases + var environment: Environment = .production { + didSet { + if oldValue == environment { return } + assert(!AppConstants.buildChannel.isPublic, + "Environment cannot be changed on non-public build channels") + Preferences.BraveToday.debugEnvironment.value = environment.rawValue + clearCachedFiles() + } + } + private struct TodayBucket { var name: String var path: String = "" - - var url: URL { - var components = URLComponents() - components.scheme = "https" - // TODO: At the moment these files are only available on the dev servers, eventually we will - // change `brave.software` to `bravesoftware.com` or `brave.com` based on staging/prod - components.host = "\(name).brave.com" - components.path = "/\(path)" - return components.url! - } + } + + private func resourceUrl(for bucket: TodayBucket) -> URL? { + var components = URLComponents() + components.scheme = "https" + components.host = "\(bucket.name).\(environment.rawValue)" + components.path = "/\(bucket.path)" + return components.url } private struct TodayResource { @@ -180,8 +205,11 @@ class FeedDataSource { if let data = data { return .init(value: .success(data), defaultQueue: .main) } + guard let url = self.resourceUrl(for: resource.bucket) else { + fatalError("Incorrect URL generated for the given resource: \(resource)") + } let deferred = Deferred>(value: nil, defaultQueue: .main) - self.session.dataRequest(with: resource.bucket.url.appendingPathComponent(filename)) { data, response, error in + self.session.dataRequest(with: url.appendingPathComponent(filename)) { data, response, error in if let error = error { deferred.fill(.failure(error)) return diff --git a/Client/Frontend/Browser/New Tab Page/NewTabPageViewController.swift b/Client/Frontend/Browser/New Tab Page/NewTabPageViewController.swift index 5cc524bf210..aebc5563ce4 100644 --- a/Client/Frontend/Browser/New Tab Page/NewTabPageViewController.swift +++ b/Client/Frontend/Browser/New Tab Page/NewTabPageViewController.swift @@ -184,6 +184,9 @@ class NewTabPageViewController: UIViewController, Themeable { collectionView.backgroundView = backgroundButtonsView feedOverlayView.headerView.settingsButton.addTarget(self, action: #selector(tappedBraveTodaySettings), for: .touchUpInside) + if !AppConstants.buildChannel.isPublic { + feedOverlayView.headerView.settingsButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressedBraveTodaySettingsButton))) + } feedOverlayView.newContentAvailableButton.addTarget(self, action: #selector(tappedNewContentAvailable), for: .touchUpInside) backgroundButtonsView.tappedActiveButton = { [weak self] sender in @@ -697,6 +700,14 @@ class NewTabPageViewController: UIViewController, Themeable { UIImpactFeedbackGenerator(style: .medium).bzzt() present(alert, animated: true, completion: nil) } + + @objc private func longPressedBraveTodaySettingsButton() { + assert(!AppConstants.buildChannel.isPublic, + "Debug settings are not accessible on public builds") + let settings = BraveTodayDebugSettingsController(dataSource: feedDataSource) + let container = UINavigationController(rootViewController: settings) + present(container, animated: true) + } } extension NewTabPageViewController: PreferencesObserver { diff --git a/Client/Frontend/Settings/BraveTodayDebugSettingsController.swift b/Client/Frontend/Settings/BraveTodayDebugSettingsController.swift new file mode 100644 index 00000000000..81ede28ee99 --- /dev/null +++ b/Client/Frontend/Settings/BraveTodayDebugSettingsController.swift @@ -0,0 +1,145 @@ +// Copyright 2020 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 Static + +extension FeedDataSource.Environment { + fileprivate var name: String { + switch self { + case .dev: return "Dev" + case .staging: return "Staging" + case .production: return "Production" + } + } +} + +extension FeedDataSource { + fileprivate func description(of state: State) -> String { + switch state { + case .initial: + return "—" + case .loading: + return "Loading" + case .success: + return "Success" + case .failure(let error): + return "Error: \(error.localizedDescription)" + } + } + fileprivate func detailRows(for state: State) -> [Row] { + switch state { + case .initial: + return [] + case .loading(let previousState): + return [Row(text: "Previous State", detailText: description(of: previousState))] + case .success(let cards): + return [ + Row(text: "Sources", detailText: "\(sources.count)"), + Row(text: "Cards Generated", detailText: "\(cards.count)") + ] + case .failure(let error): + return [ + Row(text: "Error Details", detailText: error.localizedDescription, cellClass: MultilineSubtitleCell.self) + ] + } + } +} + +class BraveTodayDebugSettingsController: TableViewController { + private let feedDataSource: FeedDataSource + + init(dataSource: FeedDataSource) { + feedDataSource = dataSource + if #available(iOS 13.0, *) { + super.init(style: .insetGrouped) + } else { + super.init(style: .grouped) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Brave Today QA Settings" + + reloadData() + + if navigationController?.viewControllers.first === self { + navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, target: self, action: #selector(tappedDone)) + } + } + + func reloadData() { + dataSource.sections = [ + .init( + rows: [ + Row(text: "Environment", detailText: feedDataSource.environment.name, selection: { [unowned self] in + let picker = TodayEnvironmentPicker(selectedEnvironment: feedDataSource.environment) { [unowned self] newEnvironment in + feedDataSource.environment = newEnvironment + self.reloadData() + self.navigationController?.popViewController(animated: true) + } + navigationController?.pushViewController(picker, animated: true) + }, accessory: .disclosureIndicator) + ], + footer: .title("Changing the environment will purge all cached resources immediately.") + ), + .init( + rows: [ + Row(text: "State", detailText: feedDataSource.description(of: feedDataSource.state)), + ] + feedDataSource.detailRows(for: feedDataSource.state) + ) + ] + } + + @objc private func tappedDone() { + dismiss(animated: true) + } +} + +private class TodayEnvironmentPicker: TableViewController { + let selectedEnvironment: FeedDataSource.Environment + let environmentUpdated: (FeedDataSource.Environment) -> Void + init(selectedEnvironment: FeedDataSource.Environment, + environmentUpdated: @escaping (FeedDataSource.Environment) -> Void) { + self.selectedEnvironment = selectedEnvironment + self.environmentUpdated = environmentUpdated + if #available(iOS 13.0, *) { + super.init(style: .insetGrouped) + } else { + super.init(style: .grouped) + } + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError() + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Environment" + navigationItem.leftBarButtonItem = .init(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPicker)) + + dataSource.sections = [ + .init(rows: FeedDataSource.Environment.allCases.map { environment in + Row(text: environment.name, selection: { [unowned self] in + self.environmentUpdated(environment) + }, accessory: environment == selectedEnvironment ? .checkmark : .none) + }) + ] + } + + @objc private func cancelPicker() { + navigationController?.popViewController(animated: true) + } +} diff --git a/Client/Frontend/Settings/SettingsViewController.swift b/Client/Frontend/Settings/SettingsViewController.swift index acfa772a023..9f1dd3fb7dd 100644 --- a/Client/Frontend/Settings/SettingsViewController.swift +++ b/Client/Frontend/Settings/SettingsViewController.swift @@ -106,6 +106,11 @@ class SettingsViewController: TableViewController { navigationController?.pushViewController(settings, animated: true) } + private func displayBraveTodayDebugMenu() { + let settings = BraveTodayDebugSettingsController(dataSource: feedDataSource) + navigationController?.pushViewController(settings, animated: true) + } + private var theme: Theme { Theme.of(tabManager.selectedTab) } @@ -480,6 +485,9 @@ class SettingsViewController: TableViewController { Row(text: "View Rewards Debug Menu", selection: { [unowned self] in self.displayRewardsDebugMenu() }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), + Row(text: "View Brave Today Debug Menu", selection: { [unowned self] in + self.displayBraveTodayDebugMenu() + }, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row(text: "Load all QA Links", selection: { [unowned self] in let url = URL(string: "https://raw.githubusercontent.com/brave/qa-resources/master/testlinks.json")! let string = try? String(contentsOf: url)