mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-30 19:28:40 +09:00
1092 lines
45 KiB
Swift
1092 lines
45 KiB
Swift
/* 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 won’t 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()
|
||
}
|
||
}
|