// // SettingsViewController.swift // AltStore // // Created by Riley Testut on 8/31/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import SafariServices import MessageUI extension SettingsViewController { fileprivate enum Section: Int, CaseIterable { case signIn case account case patreon case backgroundRefresh case jailbreak case instructions case credits case debug } fileprivate enum CreditsRow: Int, CaseIterable { case developer case designer case softwareLicenses } fileprivate enum DebugRow: Int, CaseIterable { case sendFeedback case refreshAttempts } } class SettingsViewController: UITableViewController { private var activeTeam: Team? private var prototypeHeaderFooterView: SettingsHeaderFooterView! private var debugGestureCounter = 0 private weak var debugGestureTimer: Timer? @IBOutlet private var accountNameLabel: UILabel! @IBOutlet private var accountEmailLabel: UILabel! @IBOutlet private var accountTypeLabel: UILabel! @IBOutlet private var backgroundRefreshSwitch: UISwitch! @IBOutlet private var versionLabel: UILabel! override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) } override func viewDidLoad() { super.viewDidLoad() let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil) self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView") let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:))) debugModeGestureRecognizer.delegate = self debugModeGestureRecognizer.direction = .up debugModeGestureRecognizer.numberOfTouchesRequired = 3 self.tableView.addGestureRecognizer(debugModeGestureRecognizer) if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { self.versionLabel.text = NSLocalizedString(String(format: "AltStore %@", version), comment: "AltStore Version") } else { self.versionLabel.text = NSLocalizedString("AltStore", comment: "") } self.tableView.contentInset.bottom = 20 self.update() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.update() } } private extension SettingsViewController { func update() { if let team = DatabaseManager.shared.activeTeam() { self.accountNameLabel.text = team.name self.accountEmailLabel.text = team.account.appleID self.accountTypeLabel.text = team.type.localizedDescription self.activeTeam = team } else { self.activeTeam = nil } self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled if self.isViewLoaded { self.tableView.reloadData() } } func prepare(_ settingsHeaderFooterView: SettingsHeaderFooterView, for section: Section, isHeader: Bool) { settingsHeaderFooterView.primaryLabel.isHidden = !isHeader settingsHeaderFooterView.secondaryLabel.isHidden = isHeader settingsHeaderFooterView.button.isHidden = true settingsHeaderFooterView.layoutMargins.bottom = isHeader ? 0 : 8 switch section { case .signIn: if isHeader { settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "") } else { settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Sign in with your Apple ID to download apps from AltStore.", comment: "") } case .patreon: if isHeader { settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("PATREON", comment: "") } else { settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Receive access to beta versions of AltStore, Delta, and more by becoming a patron.", comment: "") } case .account: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "") settingsHeaderFooterView.button.setTitle(NSLocalizedString("SIGN OUT", comment: ""), for: .normal) settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered) settingsHeaderFooterView.button.isHidden = false case .backgroundRefresh: settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Automatically refresh apps in the background when connected to the same WiFi as AltServer.", comment: "") case .jailbreak: if isHeader { settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("JAILBREAK", comment: "") } else { settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("AltDaemon allows AltStore to install and refresh apps without a computer. You can install AltDaemon using Filza or another file manager.", comment: "") } case .instructions: break case .credits: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "") case .debug: settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "") } } func preferredHeight(for settingsHeaderFooterView: SettingsHeaderFooterView, in section: Section, isHeader: Bool) -> CGFloat { let widthConstraint = settingsHeaderFooterView.contentView.widthAnchor.constraint(equalToConstant: tableView.bounds.width) NSLayoutConstraint.activate([widthConstraint]) defer { NSLayoutConstraint.deactivate([widthConstraint]) } self.prepare(settingsHeaderFooterView, for: section, isHeader: isHeader) let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) return size.height } } private extension SettingsViewController { func signIn() { AppManager.shared.authenticate(presentingViewController: self) { (result) in DispatchQueue.main.async { switch result { case .failure(OperationError.cancelled): // Ignore break case .failure(let error): let toastView = ToastView(error: error) toastView.show(in: self) case .success: break } self.update() } } } @objc func signOut(_ sender: UIBarButtonItem) { func signOut() { DatabaseManager.shared.signOut { (error) in DispatchQueue.main.async { if let error = error { let toastView = ToastView(error: error) toastView.show(in: self) } self.update() } } } let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer be able to install or refresh apps once you sign out.", comment: ""), preferredStyle: .actionSheet) alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() }) alertController.addAction(.cancel) self.present(alertController, animated: true, completion: nil) } @IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch) { UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn } @IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer) { self.debugGestureCounter += 1 self.debugGestureTimer?.invalidate() if self.debugGestureCounter >= 3 { self.debugGestureCounter = 0 UserDefaults.standard.isDebugModeEnabled.toggle() self.tableView.reloadData() } else { self.debugGestureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { [weak self] (timer) in self?.debugGestureCounter = 0 } } } func openTwitter(username: String) { let twitterAppURL = URL(string: "twitter://user?screen_name=" + username)! UIApplication.shared.open(twitterAppURL, options: [:]) { (success) in if success { if let selectedIndexPath = self.tableView.indexPathForSelectedRow { self.tableView.deselectRow(at: selectedIndexPath, animated: true) } } else { let safariURL = URL(string: "https://twitter.com/" + username)! let safariViewController = SFSafariViewController(url: safariURL) safariViewController.preferredControlTintColor = .altPrimary self.present(safariViewController, animated: true, completion: nil) } } } } private extension SettingsViewController { @objc func openPatreonSettings(_ notification: Notification) { guard self.presentedViewController == nil else { return } UIView.performWithoutAnimation { self.navigationController?.popViewController(animated: false) self.performSegue(withIdentifier: "showPatreon", sender: nil) } } } extension SettingsViewController { override func numberOfSections(in tableView: UITableView) -> Int { var numberOfSections = super.numberOfSections(in: tableView) if !UserDefaults.standard.isDebugModeEnabled { numberOfSections -= 1 } return numberOfSections } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let section = Section.allCases[section] switch section { case .signIn: return (self.activeTeam == nil) ? 1 : 0 case .account: return (self.activeTeam == nil) ? 0 : 3 case .jailbreak: return UIDevice.current.isJailbroken ? 1 : 0 default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue) } } override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let section = Section.allCases[section] switch section { case .signIn where self.activeTeam != nil: return nil case .account where self.activeTeam == nil: return nil case .jailbreak where !UIDevice.current.isJailbroken: return nil case .signIn, .account, .patreon, .jailbreak, .credits, .debug: let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(headerView, for: section, isHeader: true) return headerView case .backgroundRefresh, .instructions: return nil } } override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { let section = Section.allCases[section] switch section { case .signIn where self.activeTeam != nil: return nil case .jailbreak where !UIDevice.current.isJailbroken: return nil case .signIn, .patreon, .backgroundRefresh, .jailbreak: let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView self.prepare(footerView, for: section, isHeader: false) return footerView case .account, .credits, .debug, .instructions: return nil } } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { let section = Section.allCases[section] switch section { case .signIn where self.activeTeam != nil: return 1.0 case .account where self.activeTeam == nil: return 1.0 case .jailbreak where !UIDevice.current.isJailbroken: return 1.0 case .signIn, .account, .patreon, .jailbreak, .credits, .debug: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true) return height case .backgroundRefresh, .instructions: return 0.0 } } override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { let section = Section.allCases[section] switch section { case .signIn where self.activeTeam != nil: return 1.0 case .account where self.activeTeam == nil: return 1.0 case .jailbreak where !UIDevice.current.isJailbroken: return 1.0 case .signIn, .patreon, .backgroundRefresh, .jailbreak: let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false) return height case .account, .credits, .debug, .instructions: return 0.0 } } } extension SettingsViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let section = Section.allCases[indexPath.section] switch section { case .signIn: self.signIn() case .instructions: break case .jailbreak: let fileURL = Bundle.main.url(forResource: "AltDaemon", withExtension: "deb")! let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil) self.present(activityViewController, animated: true, completion: nil) tableView.deselectRow(at: indexPath, animated: true) case .credits: let row = CreditsRow.allCases[indexPath.row] switch row { case .developer: self.openTwitter(username: "rileytestut") case .designer: self.openTwitter(username: "1carolinemoore") case .softwareLicenses: break } case .debug: let row = DebugRow.allCases[indexPath.row] switch row { case .sendFeedback: if MFMailComposeViewController.canSendMail() { let mailViewController = MFMailComposeViewController() mailViewController.mailComposeDelegate = self mailViewController.setToRecipients(["support@altstore.io"]) if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { mailViewController.setSubject("AltStore Beta \(version) Feedback") } else { mailViewController.setSubject("AltStore Beta Feedback") } self.present(mailViewController, animated: true, completion: nil) } else { let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil) toastView.show(in: self) } case .refreshAttempts: break } default: break } } } extension SettingsViewController: MFMailComposeViewControllerDelegate { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { if let error = error { let toastView = ToastView(error: error) toastView.show(in: self) } controller.dismiss(animated: true, completion: nil) } } extension SettingsViewController: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } }