Dactyloidae/mobile/ios/Client/Frontend/Settings/AppSettingsOptions.swift

1092 lines
45 KiB
Swift
Raw Normal View History

2026-06-26 21:04:09 -07:00
/* 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 Account
import SwiftKeychainWrapper
import LocalAuthentication
// This file contains all of the settings available in the main settings screen of the app.
private var ShowDebugSettings: Bool = false
private var DebugSettingsClickCount: Int = 0
// For great debugging!
class HiddenSetting: Setting {
unowned let settings: SettingsTableViewController
init(settings: SettingsTableViewController) {
self.settings = settings
super.init(title: nil)
}
override var hidden: Bool {
return !ShowDebugSettings
}
}
// Sync setting for connecting a Firefox Account. Shown when we don't have an account.
class ConnectSetting: WithoutAccountSetting {
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var title: NSAttributedString? {
return NSAttributedString(string: Strings.FxASignIntoSync, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var accessibilityIdentifier: String? { return "SignInToSync" }
override func onClick(_ navigationController: UINavigationController?) {
let viewController = FxAContentViewController(profile: profile)
viewController.delegate = self
viewController.url = settings.profile.accountConfiguration.signInURL
navigationController?.pushViewController(viewController, animated: true)
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
if AppConstants.MOZ_SHOW_FXA_AVATAR {
let image = UIImage(named: "FxA-Default")
cell.imageView?.image = image?.createScaled(CGSize(width: 30, height: 30))
cell.imageView?.layer.cornerRadius = (cell.imageView?.frame.size.width)! / 2
cell.imageView?.layer.masksToBounds = true
}
}
}
// Sync setting for disconnecting a Firefox Account. Shown when we have an account.
class DisconnectSetting: WithAccountSetting {
override var accessoryType: UITableViewCellAccessoryType { return .none }
override var textAlignment: NSTextAlignment { return .center }
override var title: NSAttributedString? {
return NSAttributedString(string: Strings.SettingsDisconnectSyncButton, attributes: [NSForegroundColorAttributeName: UIConstants.DestructiveRed])
}
override var accessibilityIdentifier: String? { return "SignOut" }
override func onClick(_ navigationController: UINavigationController?) {
let alertController = UIAlertController(
title: Strings.SettingsDisconnectSyncAlertTitle,
message: NSLocalizedString("Firefox will stop syncing with your account, but wont delete any of your browsing data on this device.", comment: "Text of the 'sign out firefox account' alert"),
preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(
UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel) { (action) in
// Do nothing.
})
alertController.addAction(
UIAlertAction(title: Strings.SettingsDisconnectDestructiveAction, style: .destructive) { (action) in
FxALoginHelper.sharedInstance.applicationDidDisconnect(UIApplication.shared)
self.settings.settings = self.settings.generateSettings()
self.settings.SELfirefoxAccountDidChange()
LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: self.profile.hasAccount()])
})
navigationController?.present(alertController, animated: true, completion: nil)
}
}
class SyncNowSetting: WithAccountSetting {
static let NotificationUserInitiatedSyncManually = "NotificationUserInitiatedSyncManually"
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let syncIconWrapper = UIImage.createWithColor(CGSize(width: 30, height: 30), color: UIColor.clear)
let syncBlueIcon = UIImage(named: "FxA-Sync-Blue")?.createScaled(CGSize(width: 20, height: 20))
let syncIcon = UIImage(named: "FxA-Sync")?.createScaled(CGSize(width: 20, height: 20))
// Animation used to rotate the Sync icon 360 degrees while syncing is in progress.
let continuousRotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
override init(settings: SettingsTableViewController) {
super.init(settings: settings)
NotificationCenter.default.addObserver(self, selector: #selector(SyncNowSetting.stopRotateSyncIcon), name: NotificationProfileDidFinishSyncing, object: nil)
}
fileprivate lazy var timestampFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
fileprivate var syncNowTitle: NSAttributedString {
if !DeviceInfo.hasConnectivity() {
return NSAttributedString(
string: Strings.FxANoInternetConnection,
attributes: [
NSForegroundColorAttributeName: UIColor.red,
NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultMediumFont
]
)
}
return NSAttributedString(
string: NSLocalizedString("Sync Now", comment: "Sync Firefox Account"),
attributes: [
NSForegroundColorAttributeName: self.enabled ? UIConstants.TableViewRowSyncTextColor : UIColor.gray,
NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont
]
)
}
fileprivate let syncingTitle = NSAttributedString(string: Strings.SyncingMessageWithEllipsis, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor, NSFontAttributeName: UIFont.systemFont(ofSize: DynamicFontHelper.defaultHelper.DefaultStandardFontSize, weight: UIFontWeightRegular)])
func startRotateSyncIcon() {
DispatchQueue.main.async {
self.imageView.layer.add(self.continuousRotateAnimation, forKey: "rotateKey")
}
}
func stopRotateSyncIcon() {
DispatchQueue.main.async {
self.imageView.layer.removeAllAnimations()
}
}
override var accessoryType: UITableViewCellAccessoryType { return .none }
override var style: UITableViewCellStyle { return .value1 }
override var image: UIImage? {
if !AppConstants.MOZ_SHOW_FXA_AVATAR {
return nil
}
guard let syncStatus = profile.syncManager.syncDisplayState else {
return syncIcon
}
switch syncStatus {
case .inProgress:
return syncBlueIcon
default:
return syncIcon
}
}
override var title: NSAttributedString? {
guard let syncStatus = profile.syncManager.syncDisplayState else {
return syncNowTitle
}
switch syncStatus {
case .bad(let message):
guard let message = message else { return syncNowTitle }
return NSAttributedString(string: message, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowErrorTextColor, NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont])
case .warning(let message):
return NSAttributedString(string: message, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowWarningTextColor, NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont])
case .inProgress:
return syncingTitle
default:
return syncNowTitle
}
}
override var status: NSAttributedString? {
guard let timestamp = profile.syncManager.lastSyncFinishTime else {
return nil
}
let formattedLabel = timestampFormatter.string(from: Date.fromTimestamp(timestamp))
let attributedString = NSMutableAttributedString(string: formattedLabel)
let attributes = [NSForegroundColorAttributeName: UIColor.gray, NSFontAttributeName: UIFont.systemFont(ofSize: 12, weight: UIFontWeightRegular)]
let range = NSRange(location: 0, length: attributedString.length)
attributedString.setAttributes(attributes, range: range)
return attributedString
}
override var enabled: Bool {
if !DeviceInfo.hasConnectivity() {
return false
}
return profile.hasSyncableAccount()
}
fileprivate lazy var troubleshootButton: UIButton = {
let troubleshootButton = UIButton(type: UIButtonType.roundedRect)
troubleshootButton.setTitle(Strings.FirefoxSyncTroubleshootTitle, for: .normal)
troubleshootButton.addTarget(self, action: #selector(self.troubleshoot), for: .touchUpInside)
troubleshootButton.tintColor = UIConstants.TableViewRowActionAccessoryColor
troubleshootButton.titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultSmallFont
troubleshootButton.sizeToFit()
return troubleshootButton
}()
fileprivate lazy var warningIcon: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "AmberCaution"))
imageView.sizeToFit()
return imageView
}()
fileprivate lazy var errorIcon: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "RedCaution"))
imageView.sizeToFit()
return imageView
}()
fileprivate let syncSUMOURL = SupportUtils.URLForTopic("sync-status-ios")
@objc fileprivate func troubleshoot() {
let viewController = SettingsContentViewController()
viewController.url = syncSUMOURL
settings.navigationController?.pushViewController(viewController, animated: true)
}
override func onConfigureCell(_ cell: UITableViewCell) {
cell.textLabel?.attributedText = title
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = .byWordWrapping
if let syncStatus = profile.syncManager.syncDisplayState {
switch syncStatus {
case .bad(let message):
if let _ = message {
// add the red warning symbol
// add a link to the MANA page
cell.detailTextLabel?.attributedText = nil
cell.accessoryView = troubleshootButton
addIcon(errorIcon, toCell: cell)
} else {
cell.detailTextLabel?.attributedText = status
cell.accessoryView = nil
}
case .warning(_):
// add the amber warning symbol
// add a link to the MANA page
cell.detailTextLabel?.attributedText = nil
cell.accessoryView = troubleshootButton
addIcon(warningIcon, toCell: cell)
case .good:
cell.detailTextLabel?.attributedText = status
fallthrough
default:
cell.accessoryView = nil
}
} else {
cell.accessoryView = nil
}
cell.accessoryType = accessoryType
cell.isUserInteractionEnabled = !profile.syncManager.isSyncing && DeviceInfo.hasConnectivity()
if AppConstants.MOZ_SHOW_FXA_AVATAR {
// Animation that loops continously until stopped
continuousRotateAnimation.fromValue = 0.0
continuousRotateAnimation.toValue = CGFloat(Double.pi)
continuousRotateAnimation.isRemovedOnCompletion = true
continuousRotateAnimation.duration = 0.5
continuousRotateAnimation.repeatCount = Float.infinity
// To ensure sync icon is aligned properly with user's avatar, an image is created with proper
// dimensions and color, then the scaled sync icon is added as a subview.
imageView.contentMode = .center
imageView.image = image
cell.imageView?.subviews.forEach({ $0.removeFromSuperview() })
cell.imageView?.image = syncIconWrapper
cell.imageView?.addSubview(imageView)
if let syncStatus = profile.syncManager.syncDisplayState {
switch syncStatus {
case .inProgress:
self.startRotateSyncIcon()
default:
self.stopRotateSyncIcon()
}
}
}
}
fileprivate func addIcon(_ image: UIImageView, toCell cell: UITableViewCell) {
cell.contentView.addSubview(image)
cell.textLabel?.snp.updateConstraints { make in
make.leading.equalTo(image.snp.trailing).offset(5)
make.trailing.lessThanOrEqualTo(cell.contentView)
make.centerY.equalTo(cell.contentView)
}
image.snp.makeConstraints { make in
make.leading.equalTo(cell.contentView).offset(17)
make.top.equalTo(cell.textLabel!).offset(2)
}
}
override func onClick(_ navigationController: UINavigationController?) {
if !DeviceInfo.hasConnectivity() {
return
}
NotificationCenter.default.post(name: Notification.Name(rawValue: SyncNowSetting.NotificationUserInitiatedSyncManually), object: nil)
profile.syncManager.syncEverything(why: .syncNow)
}
}
// Sync setting that shows the current Firefox Account status.
class AccountStatusSetting: WithAccountSetting {
override init(settings: SettingsTableViewController) {
super.init(settings: settings)
NotificationCenter.default.addObserver(self, selector: #selector(AccountStatusSetting.updateAccount(notification:)), name: NotificationFirefoxAccountProfileChanged, object: nil)
}
func updateAccount(notification: Notification) {
DispatchQueue.main.async {
self.settings.tableView.reloadData()
}
}
override var image: UIImage? {
if !AppConstants.MOZ_SHOW_FXA_AVATAR {
return nil
}
if let image = profile.getAccount()?.fxaProfile?.avatar.image {
return image.createScaled(CGSize(width: 30, height: 30))
}
let image = UIImage(named: "placeholder-avatar")
return image?.createScaled(CGSize(width: 30, height: 30))
}
override var accessoryType: UITableViewCellAccessoryType {
if let account = profile.getAccount() {
switch account.actionNeeded {
case .needsVerification:
// We link to the resend verification email page.
return .disclosureIndicator
case .needsPassword:
// We link to the re-enter password page.
return .disclosureIndicator
case .none:
// We link to FxA web /settings.
return .disclosureIndicator
case .needsUpgrade:
// In future, we'll want to link to an upgrade page.
return .none
}
}
return .disclosureIndicator
}
override var title: NSAttributedString? {
if let account = profile.getAccount() {
if let displayName = account.fxaProfile?.displayName {
return NSAttributedString(string: displayName, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor])
}
if let email = account.fxaProfile?.email {
return NSAttributedString(string: email, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor])
}
return NSAttributedString(string: account.email, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor])
}
return nil
}
override var status: NSAttributedString? {
if let account = profile.getAccount() {
var string: String
switch account.actionNeeded {
case .none:
return nil
case .needsVerification:
string = NSLocalizedString("Verify your email address.", comment: "Text message in the settings table view")
break
case .needsPassword:
string = NSLocalizedString("Enter your password to connect.", comment: "Text message in the settings table view")
break
case .needsUpgrade:
string = NSLocalizedString("Upgrade Firefox to connect.", comment: "Text message in the settings table view")
break
}
let orange = UIColor(red: 255.0 / 255, green: 149.0 / 255, blue: 0.0 / 255, alpha: 1)
let range = NSRange(location: 0, length: string.characters.count)
let attrs = [NSForegroundColorAttributeName: orange]
let res = NSMutableAttributedString(string: string)
res.setAttributes(attrs, range: range)
return res
}
return nil
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = FxAContentViewController(profile: profile)
viewController.delegate = self
if let account = profile.getAccount() {
switch account.actionNeeded {
case .none, .needsVerification:
var cs = URLComponents(url: account.configuration.settingsURL, resolvingAgainstBaseURL: false)
cs?.queryItems?.append(URLQueryItem(name: "email", value: account.email))
if let url = try? cs?.asURL() {
viewController.url = url
}
case .needsPassword:
var cs = URLComponents(url: account.configuration.forceAuthURL, resolvingAgainstBaseURL: false)
cs?.queryItems?.append(URLQueryItem(name: "email", value: account.email))
if let url = try? cs?.asURL() {
viewController.url = url
}
case .needsUpgrade:
// In future, we'll want to link to an upgrade page.
return
}
}
navigationController?.pushViewController(viewController, animated: true)
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
if AppConstants.MOZ_SHOW_FXA_AVATAR {
cell.imageView?.subviews.forEach({ $0.removeFromSuperview() })
cell.imageView?.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
cell.imageView?.layer.cornerRadius = (cell.imageView?.frame.height)! / 2
cell.imageView?.layer.masksToBounds = true
cell.imageView?.image = image
}
}
}
// For great debugging!
class RequirePasswordDebugSetting: WithAccountSetting {
override var hidden: Bool {
if !ShowDebugSettings {
return true
}
if let account = profile.getAccount(), account.actionNeeded != FxAActionNeeded.needsPassword {
return false
}
return true
}
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Debug: require password", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
profile.getAccount()?.makeSeparated()
settings.tableView.reloadData()
}
}
// For great debugging!
class RequireUpgradeDebugSetting: WithAccountSetting {
override var hidden: Bool {
if !ShowDebugSettings {
return true
}
if let account = profile.getAccount(), account.actionNeeded != FxAActionNeeded.needsUpgrade {
return false
}
return true
}
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Debug: require upgrade", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
profile.getAccount()?.makeDoghouse()
settings.tableView.reloadData()
}
}
// For great debugging!
class ForgetSyncAuthStateDebugSetting: WithAccountSetting {
override var hidden: Bool {
if !ShowDebugSettings {
return true
}
if let _ = profile.getAccount() {
return false
}
return true
}
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Debug: forget Sync auth state", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
profile.getAccount()?.syncAuthState.invalidate()
settings.tableView.reloadData()
}
}
class DeleteExportedDataSetting: HiddenSetting {
override var title: NSAttributedString? {
// Not localized for now.
return NSAttributedString(string: "Debug: delete exported databases", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let fileManager = FileManager.default
do {
let files = try fileManager.contentsOfDirectory(atPath: documentsPath)
for file in files {
if file.startsWith("browser.") || file.startsWith("logins.") {
try fileManager.removeItemInDirectory(documentsPath, named: file)
}
}
} catch {
print("Couldn't delete exported data: \(error).")
}
}
}
class ExportBrowserDataSetting: HiddenSetting {
override var title: NSAttributedString? {
// Not localized for now.
return NSAttributedString(string: "Debug: copy databases to app container", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
do {
let log = Logger.syncLogger
try self.settings.profile.files.copyMatching(fromRelativeDirectory: "", toAbsoluteDirectory: documentsPath) { file in
log.debug("Matcher: \(file)")
return file.startsWith("browser.") || file.startsWith("logins.") || file.startsWith("metadata.")
}
} catch {
print("Couldn't export browser data: \(error).")
}
}
}
/*
FeatureSwitchSetting is a boolean switch for features that are enabled via a FeatureSwitch.
These are usually features behind a partial release and not features released to the entire population.
*/
class FeatureSwitchSetting: BoolSetting {
let featureSwitch: FeatureSwitch
let prefs: Prefs
init(prefs: Prefs, featureSwitch: FeatureSwitch, with title: NSAttributedString) {
self.featureSwitch = featureSwitch
self.prefs = prefs
super.init(prefs: prefs, defaultValue: featureSwitch.isMember(prefs), attributedTitleText: title)
}
override var hidden: Bool {
return !ShowDebugSettings
}
override func displayBool(_ control: UISwitch) {
control.isOn = featureSwitch.isMember(prefs)
}
override func writeBool(_ control: UISwitch) {
self.featureSwitch.setMembership(control.isOn, for: self.prefs)
}
}
class EnableBookmarkMergingSetting: HiddenSetting {
override var title: NSAttributedString? {
// Not localized for now.
return NSAttributedString(string: "Enable Bidirectional Bookmark Sync ", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
AppConstants.shouldMergeBookmarks = true
}
}
class ForceCrashSetting: HiddenSetting {
override var title: NSAttributedString? {
return NSAttributedString(string: "Debug: Force Crash", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onClick(_ navigationController: UINavigationController?) {
Sentry.shared.crash()
}
}
// Show the current version of Firefox
class VersionSetting: Setting {
unowned let settings: SettingsTableViewController
init(settings: SettingsTableViewController) {
self.settings = settings
super.init(title: nil)
}
override var title: NSAttributedString? {
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
return NSAttributedString(string: String(format: NSLocalizedString("Version %@ (%@)", comment: "Version number of Firefox shown in settings"), appVersion, buildNumber), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
cell.selectionStyle = .none
}
override func onClick(_ navigationController: UINavigationController?) {
DebugSettingsClickCount += 1
if DebugSettingsClickCount >= 5 {
DebugSettingsClickCount = 0
ShowDebugSettings = !ShowDebugSettings
settings.tableView.reloadData()
}
}
}
// Opens the the license page in a new tab
class LicenseAndAcknowledgementsSetting: Setting {
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Licenses", comment: "Settings item that opens a tab containing the licenses. See http://mzl.la/1NSAWCG"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var url: URL? {
return URL(string: WebServer.sharedInstance.URLForResource("license", module: "about"))
}
override func onClick(_ navigationController: UINavigationController?) {
setUpAndPushSettingsContentViewController(navigationController)
}
}
// Opens about:rights page in the content view controller
class YourRightsSetting: Setting {
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Your Rights", comment: "Your Rights settings section title"), attributes:
[NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var url: URL? {
return URL(string: "https://www.mozilla.org/about/legal/terms/firefox/")
}
override func onClick(_ navigationController: UINavigationController?) {
setUpAndPushSettingsContentViewController(navigationController)
}
}
// Opens the on-boarding screen again
class ShowIntroductionSetting: Setting {
let profile: Profile
override var accessibilityIdentifier: String? { return "ShowTour" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
super.init(title: NSAttributedString(string: NSLocalizedString("Show Tour", comment: "Show the on-boarding screen again from the settings"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
navigationController?.dismiss(animated: true, completion: {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.browserViewController.presentIntroViewController(true)
}
})
}
}
class SendFeedbackSetting: Setting {
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Send Feedback", comment: "Menu item in settings used to open input.mozilla.org where people can submit feedback"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var url: URL? {
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
return URL(string: "https://input.mozilla.org/feedback/fxios/\(appVersion)")
}
override func onClick(_ navigationController: UINavigationController?) {
setUpAndPushSettingsContentViewController(navigationController)
}
}
class SendAnonymousUsageDataSetting: BoolSetting {
init(prefs: Prefs, delegate: SettingsDelegate?) {
super.init(
prefs: prefs, prefKey: AppConstants.PrefSendUsageData, defaultValue: true,
attributedTitleText: NSAttributedString(string: Strings.SendUsageSettingTitle),
attributedStatusText: createStatusText(),
settingDidChange: {
AdjustIntegration.setEnabled($0)
LeanPlumClient.shared.set(attributes: [LPAttributeKey.telemetryOptIn: $0])
LeanPlumClient.shared.set(enabled: $0)
}
)
}
private func createStatusText() -> NSAttributedString {
let statusText = NSMutableAttributedString()
statusText.append(NSAttributedString(string: Strings.SendUsageSettingMessage, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor]))
statusText.append(NSAttributedString(string: " "))
statusText.append(NSAttributedString(string: Strings.SendUsageSettingLink, attributes: [NSForegroundColorAttributeName: UIConstants.HighlightBlue]))
return statusText
}
override var url: URL? {
return SupportUtils.URLForTopic("adjust")
}
override func onClick(_ navigationController: UINavigationController?) {
setUpAndPushSettingsContentViewController(navigationController)
}
}
// Opens the the SUMO page in a new tab
class OpenSupportPageSetting: Setting {
init(delegate: SettingsDelegate?) {
super.init(title: NSAttributedString(string: NSLocalizedString("Help", comment: "Show the SUMO support page from the Support section in the settings. see http://mzl.la/1dmM8tZ"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]),
delegate: delegate)
}
override func onClick(_ navigationController: UINavigationController?) {
navigationController?.dismiss(animated: true) {
if let url = URL(string: "https://support.mozilla.org/products/ios") {
self.delegate?.settingsOpenURLInNewTab(url)
}
}
}
}
// Opens the search settings pane
class SearchSetting: Setting {
let profile: Profile
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var style: UITableViewCellStyle { return .value1 }
override var status: NSAttributedString { return NSAttributedString(string: profile.searchEngines.defaultEngine.shortName) }
override var accessibilityIdentifier: String? { return "Search" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
super.init(title: NSAttributedString(string: NSLocalizedString("Search", comment: "Open search section of settings"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = SearchSettingsTableViewController()
viewController.model = profile.searchEngines
viewController.profile = profile
navigationController?.pushViewController(viewController, animated: true)
}
}
class LoginsSetting: Setting {
let profile: Profile
var tabManager: TabManager!
weak var navigationController: UINavigationController?
weak var settings: AppSettingsTableViewController?
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "Logins" }
init(settings: SettingsTableViewController, delegate: SettingsDelegate?) {
self.profile = settings.profile
self.tabManager = settings.tabManager
self.navigationController = settings.navigationController
self.settings = settings as? AppSettingsTableViewController
let loginsTitle = NSLocalizedString("Logins", comment: "Label used as an item in Settings. When touched, the user will be navigated to the Logins/Password manager.")
super.init(title: NSAttributedString(string: loginsTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]),
delegate: delegate)
}
func deselectRow () {
if let selectedRow = self.settings?.tableView.indexPathForSelectedRow {
self.settings?.tableView.deselectRow(at: selectedRow, animated: true)
}
}
override func onClick(_: UINavigationController?) {
guard let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() else {
settings?.navigateToLoginsList()
LeanPlumClient.shared.track(event: .openedLogins)
return
}
if authInfo.requiresValidation() {
AppAuthenticator.presentAuthenticationUsingInfo(authInfo,
touchIDReason: AuthenticationStrings.loginsTouchReason,
success: {
self.settings?.navigateToLoginsList()
LeanPlumClient.shared.track(event: .openedLogins)
},
cancel: {
self.deselectRow()
},
fallback: {
AppAuthenticator.presentPasscodeAuthentication(self.navigationController, delegate: self.settings)
self.deselectRow()
})
} else {
settings?.navigateToLoginsList()
LeanPlumClient.shared.track(event: .openedLogins)
}
}
}
class TouchIDPasscodeSetting: Setting {
let profile: Profile
var tabManager: TabManager!
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "TouchIDPasscode" }
init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil) {
self.profile = settings.profile
self.tabManager = settings.tabManager
let localAuthContext = LAContext()
let title: String
if localAuthContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
if #available(iOS 11.0, *), localAuthContext.biometryType == .faceID {
title = AuthenticationStrings.faceIDPasscodeSetting
} else {
title = AuthenticationStrings.touchIDPasscodeSetting
}
} else {
title = AuthenticationStrings.passcode
}
super.init(title: NSAttributedString(string: title, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]),
delegate: delegate)
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = AuthenticationSettingsViewController()
viewController.profile = profile
navigationController?.pushViewController(viewController, animated: true)
}
}
@available(iOS 11, *)
class ContentBlockerSetting: Setting {
let profile: Profile
var tabManager: TabManager!
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "TrackingProtection" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
self.tabManager = settings.tabManager
super.init(title: NSAttributedString(string: Strings.SettingsTrackingProtectionSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = ContentBlockerSettingViewController(prefs: profile.prefs)
viewController.profile = profile
viewController.tabManager = tabManager
navigationController?.pushViewController(viewController, animated: true)
}
}
class ClearPrivateDataSetting: Setting {
let profile: Profile
var tabManager: TabManager!
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "ClearPrivateData" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
self.tabManager = settings.tabManager
let clearTitle = Strings.SettingsClearPrivateDataSectionName
super.init(title: NSAttributedString(string: clearTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = ClearPrivateDataTableViewController()
viewController.profile = profile
viewController.tabManager = tabManager
navigationController?.pushViewController(viewController, animated: true)
}
}
class PrivacyPolicySetting: Setting {
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Privacy Policy", comment: "Show Firefox Browser Privacy Policy page from the Privacy section in the settings. See https://www.mozilla.org/privacy/firefox/"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var url: URL? {
return URL(string: "https://www.mozilla.org/privacy/firefox/")
}
override func onClick(_ navigationController: UINavigationController?) {
setUpAndPushSettingsContentViewController(navigationController)
}
}
class ChinaSyncServiceSetting: WithoutAccountSetting {
override var accessoryType: UITableViewCellAccessoryType { return .none }
var prefs: Prefs { return settings.profile.prefs }
let prefKey = "useChinaSyncService"
override var title: NSAttributedString? {
return NSAttributedString(string: "本地同步服务", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var status: NSAttributedString? {
return NSAttributedString(string: "禁用后使用全球服务同步数据", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor])
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
let control = UISwitch()
control.onTintColor = UIConstants.ControlTintColor
control.addTarget(self, action: #selector(ChinaSyncServiceSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged)
control.isOn = prefs.boolForKey(prefKey) ?? self.profile.isChinaEdition
cell.accessoryView = control
cell.selectionStyle = .none
}
@objc func switchValueChanged(_ toggle: UISwitch) {
prefs.setObject(toggle.isOn, forKey: prefKey)
}
}
class StageSyncServiceDebugSetting: WithoutAccountSetting {
override var accessoryType: UITableViewCellAccessoryType { return .none }
var prefs: Prefs { return settings.profile.prefs }
var prefKey: String = "useStageSyncService"
override var hidden: Bool {
if !ShowDebugSettings {
return true
}
if let _ = profile.getAccount() {
return true
}
return false
}
override var title: NSAttributedString? {
return NSAttributedString(string: NSLocalizedString("Debug: use stage servers", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override var status: NSAttributedString? {
// Derive the configuration we display from the profile. Currently, this could be either a custom
// FxA server or FxA stage servers.
let isOn = prefs.boolForKey(prefKey) ?? false
let isCustomSync = prefs.boolForKey(PrefsKeys.KeyUseCustomSyncService) ?? false
var configurationURL = ProductionFirefoxAccountConfiguration().authEndpointURL
if isCustomSync {
configurationURL = CustomFirefoxAccountConfiguration(prefs: profile.prefs).authEndpointURL
} else if isOn {
configurationURL = StageFirefoxAccountConfiguration().authEndpointURL
}
return NSAttributedString(string: configurationURL.absoluteString, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor])
}
override func onConfigureCell(_ cell: UITableViewCell) {
super.onConfigureCell(cell)
let control = UISwitch()
control.onTintColor = UIConstants.ControlTintColor
control.addTarget(self, action: #selector(StageSyncServiceDebugSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged)
control.isOn = prefs.boolForKey(prefKey) ?? false
cell.accessoryView = control
cell.selectionStyle = .none
}
@objc func switchValueChanged(_ toggle: UISwitch) {
prefs.setObject(toggle.isOn, forKey: prefKey)
settings.tableView.reloadData()
}
}
class HomePageSetting: Setting {
let profile: Profile
var tabManager: TabManager!
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "Homepage" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
self.tabManager = settings.tabManager
super.init(title: NSAttributedString(string: Strings.SettingsHomePageSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = HomePageSettingsViewController()
viewController.profile = profile
viewController.tabManager = tabManager
navigationController?.pushViewController(viewController, animated: true)
}
}
class NewTabPageSetting: Setting {
let profile: Profile
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "NewTab" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
super.init(title: NSAttributedString(string: Strings.SettingsNewTabSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = NewTabContentSettingsViewController()
viewController.profile = profile
navigationController?.pushViewController(viewController, animated: true)
}
}
class OpenWithSetting: Setting {
let profile: Profile
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "OpenWith.Setting" }
init(settings: SettingsTableViewController) {
self.profile = settings.profile
super.init(title: NSAttributedString(string: Strings.SettingsOpenWithSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]))
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = OpenWithSettingsViewController(prefs: profile.prefs)
navigationController?.pushViewController(viewController, animated: true)
}
}
class AdvanceAccountSetting: HiddenSetting {
let profile: Profile
override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator }
override var accessibilityIdentifier: String? { return "AdvanceAccount.Setting" }
override var title: NSAttributedString? {
return NSAttributedString(string: Strings.SettingsAdvanceAccountTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])
}
override init(settings: SettingsTableViewController) {
self.profile = settings.profile
super.init(settings: settings)
}
override func onClick(_ navigationController: UINavigationController?) {
let viewController = AdvanceAccountSettingViewController()
viewController.profile = profile
navigationController?.pushViewController(viewController, animated: true)
}
override var hidden: Bool {
return !ShowDebugSettings || profile.hasAccount()
}
}