WelcomeViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import UIKit
  2. import DcCore
  3. class WelcomeViewController: UIViewController, ProgressAlertHandler {
  4. private var dcContext: DcContext
  5. private let dcAccounts: DcAccounts
  6. var progressObserver: NSObjectProtocol?
  7. var onProgressSuccess: VoidFunction?
  8. private lazy var scrollView: UIScrollView = {
  9. let scrollView = UIScrollView()
  10. scrollView.showsVerticalScrollIndicator = false
  11. return scrollView
  12. }()
  13. private lazy var welcomeView: WelcomeContentView = {
  14. let view = WelcomeContentView()
  15. view.onLogin = { [weak self] in
  16. guard let self = self else { return }
  17. let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
  18. accountSetupController.onLoginSuccess = {
  19. [weak self] in
  20. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  21. appDelegate.appCoordinator.presentTabBarController()
  22. appDelegate.appCoordinator.popTabsToRootViewControllers()
  23. }
  24. }
  25. self.navigationController?.pushViewController(accountSetupController, animated: true)
  26. }
  27. view.onScanQRCode = { [weak self] in
  28. guard let self = self else { return }
  29. let qrReader = QrCodeReaderController()
  30. qrReader.delegate = self
  31. self.qrCodeReader = qrReader
  32. self.navigationController?.pushViewController(qrReader, animated: true)
  33. }
  34. view.translatesAutoresizingMaskIntoConstraints = false
  35. return view
  36. }()
  37. private lazy var canCancel: Bool = {
  38. // "cancel" removes selected unconfigured account, so there needs to be at least one other account
  39. return dcAccounts.getAll().count >= 2
  40. }()
  41. private lazy var cancelButton: UIBarButtonItem = {
  42. return UIBarButtonItem(title: String.localized("cancel"), style: .plain, target: self, action: #selector(cancelButtonPressed))
  43. }()
  44. private var qrCodeReader: QrCodeReaderController?
  45. weak var progressAlert: UIAlertController?
  46. init(dcAccounts: DcAccounts) {
  47. self.dcAccounts = dcAccounts
  48. self.dcContext = dcAccounts.getSelected()
  49. super.init(nibName: nil, bundle: nil)
  50. self.navigationItem.title = String.localized(canCancel ? "add_account" : "welcome_desktop")
  51. onProgressSuccess = { [weak self] in
  52. guard let self = self else { return }
  53. let profileInfoController = ProfileInfoViewController(context: self.dcContext)
  54. profileInfoController.onClose = {
  55. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  56. appDelegate.appCoordinator.presentTabBarController()
  57. }
  58. }
  59. self.navigationController?.setViewControllers([profileInfoController], animated: true)
  60. }
  61. }
  62. required init?(coder: NSCoder) {
  63. fatalError("init(coder:) has not been implemented")
  64. }
  65. // MARK: - lifecycle
  66. override func viewDidLoad() {
  67. super.viewDidLoad()
  68. setupSubviews()
  69. if canCancel {
  70. navigationItem.leftBarButtonItem = cancelButton
  71. }
  72. }
  73. override func viewDidLayoutSubviews() {
  74. super.viewDidLayoutSubviews()
  75. welcomeView.minContainerHeight = view.frame.height - view.safeAreaInsets.top
  76. }
  77. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  78. super.viewWillTransition(to: size, with: coordinator)
  79. welcomeView.minContainerHeight = size.height - view.safeAreaInsets.top
  80. scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
  81. }
  82. override func viewDidDisappear(_ animated: Bool) {
  83. let nc = NotificationCenter.default
  84. if let observer = self.progressObserver {
  85. nc.removeObserver(observer)
  86. self.progressObserver = nil
  87. }
  88. }
  89. // MARK: - setup
  90. private func setupSubviews() {
  91. view.addSubview(scrollView)
  92. scrollView.translatesAutoresizingMaskIntoConstraints = false
  93. scrollView.addSubview(welcomeView)
  94. let frameGuide = scrollView.frameLayoutGuide
  95. let contentGuide = scrollView.contentLayoutGuide
  96. frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
  97. frameGuide.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
  98. frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
  99. frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
  100. contentGuide.leadingAnchor.constraint(equalTo: welcomeView.leadingAnchor).isActive = true
  101. contentGuide.topAnchor.constraint(equalTo: welcomeView.topAnchor).isActive = true
  102. contentGuide.trailingAnchor.constraint(equalTo: welcomeView.trailingAnchor).isActive = true
  103. contentGuide.bottomAnchor.constraint(equalTo: welcomeView.bottomAnchor).isActive = true
  104. // this enables vertical scrolling
  105. frameGuide.widthAnchor.constraint(equalTo: contentGuide.widthAnchor).isActive = true
  106. }
  107. // MARK: - actions
  108. private func createAccountFromQRCode(qrCode: String) {
  109. let success = dcContext.setConfigFromQR(qrCode: qrCode)
  110. if success {
  111. addProgressAlertListener(dcAccounts: dcAccounts, progressName: dcNotificationConfigureProgress, onSuccess: handleLoginSuccess)
  112. showProgressAlert(title: String.localized("login_header"), dcContext: dcContext)
  113. dcAccounts.stopIo()
  114. let accountId = dcAccounts.add()
  115. if accountId != 0 {
  116. self.dcContext = dcAccounts.get(id: accountId)
  117. }
  118. dcContext.configure()
  119. } else {
  120. accountCreationErrorAlert()
  121. }
  122. }
  123. private func handleLoginSuccess() {
  124. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  125. if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
  126. appDelegate.registerForNotifications()
  127. }
  128. onProgressSuccess?()
  129. }
  130. private func accountCreationErrorAlert() {
  131. let title = dcContext.lastErrorString ?? String.localized("error")
  132. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  133. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default))
  134. present(alert, animated: true)
  135. }
  136. @objc private func cancelButtonPressed() {
  137. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  138. // take a bit care on account removal:
  139. // remove only unconfigured and make sure, there is another account
  140. // (normally, both checks are not needed, however, some resilience wrt future program-flow-changes seems to be reasonable here)
  141. let selectedAccount = dcAccounts.getSelected()
  142. if !selectedAccount.isConfigured() {
  143. _ = dcAccounts.remove(id: selectedAccount.id)
  144. if self.dcAccounts.getAll().isEmpty {
  145. _ = self.dcAccounts.add()
  146. }
  147. }
  148. // do not just pop view controller as the stack is empty on restarts
  149. appDelegate.reloadDcContext()
  150. }
  151. }
  152. extension WelcomeViewController: QrCodeReaderDelegate {
  153. func handleQrCode(_ code: String) {
  154. let lot = dcContext.checkQR(qrCode: code)
  155. if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
  156. confirmAccountCreationAlert(accountDomain: domain, qrCode: code)
  157. } else {
  158. qrErrorAlert()
  159. }
  160. }
  161. private func confirmAccountCreationAlert(accountDomain domain: String, qrCode: String) {
  162. let title = String.localizedStringWithFormat(String.localized("qraccount_ask_create_and_login"), domain)
  163. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  164. let okAction = UIAlertAction(
  165. title: String.localized("ok"),
  166. style: .default,
  167. handler: { [weak self] _ in
  168. guard let self = self else { return }
  169. self.dismissQRReader()
  170. self.createAccountFromQRCode(qrCode: qrCode)
  171. }
  172. )
  173. let qrCancelAction = UIAlertAction(
  174. title: String.localized("cancel"),
  175. style: .cancel,
  176. handler: { [weak self] _ in
  177. self?.dismissQRReader()
  178. }
  179. )
  180. alert.addAction(okAction)
  181. alert.addAction(qrCancelAction)
  182. qrCodeReader?.present(alert, animated: true)
  183. }
  184. private func qrErrorAlert() {
  185. let title = String.localized("qraccount_qr_code_cannot_be_used")
  186. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  187. let okAction = UIAlertAction(
  188. title: String.localized("ok"),
  189. style: .default,
  190. handler: { [weak self] _ in
  191. self?.qrCodeReader?.startSession()
  192. }
  193. )
  194. alert.addAction(okAction)
  195. qrCodeReader?.present(alert, animated: true, completion: nil)
  196. }
  197. private func dismissQRReader() {
  198. self.navigationController?.popViewController(animated: true)
  199. self.qrCodeReader = nil
  200. }
  201. }
  202. // MARK: - WelcomeContentView
  203. class WelcomeContentView: UIView {
  204. var onLogin: VoidFunction?
  205. var onScanQRCode: VoidFunction?
  206. var onImportBackup: VoidFunction?
  207. var minContainerHeight: CGFloat = 0 {
  208. didSet {
  209. containerMinHeightConstraint.constant = minContainerHeight
  210. logoHeightConstraint.constant = calculateLogoHeight()
  211. }
  212. }
  213. private lazy var containerMinHeightConstraint: NSLayoutConstraint = {
  214. return container.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
  215. }()
  216. private lazy var logoHeightConstraint: NSLayoutConstraint = {
  217. return logoView.heightAnchor.constraint(equalToConstant: 0)
  218. }()
  219. private var container = UIView()
  220. private var logoView: UIImageView = {
  221. let image = #imageLiteral(resourceName: "dc_logo")
  222. let view = UIImageView(image: image)
  223. return view
  224. }()
  225. private lazy var titleLabel: UILabel = {
  226. let label = UILabel()
  227. label.text = String.localized("welcome_desktop")
  228. label.textColor = DcColors.grayTextColor
  229. label.textAlignment = .center
  230. label.numberOfLines = 0
  231. label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
  232. return label
  233. }()
  234. private lazy var subtitleLabel: UILabel = {
  235. let label = UILabel()
  236. label.text = String.localized("welcome_intro1_message")
  237. label.font = UIFont.systemFont(ofSize: 22, weight: .regular)
  238. label.textColor = DcColors.grayTextColor
  239. label.numberOfLines = 0
  240. label.textAlignment = .center
  241. return label
  242. }()
  243. private lazy var buttonStack: UIStackView = {
  244. let stack = UIStackView(arrangedSubviews: [loginButton, qrCodeButton /*, importBackupButton */])
  245. stack.axis = .vertical
  246. stack.spacing = 15
  247. return stack
  248. }()
  249. private lazy var loginButton: UIButton = {
  250. let button = UIButton(type: .roundedRect)
  251. let title = String.localized("login_header").uppercased()
  252. button.setTitle(title, for: .normal)
  253. button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .regular)
  254. button.setTitleColor(.white, for: .normal)
  255. button.backgroundColor = DcColors.primary
  256. let insets = button.contentEdgeInsets
  257. button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
  258. button.layer.cornerRadius = 5
  259. button.clipsToBounds = true
  260. button.addTarget(self, action: #selector(loginButtonPressed(_:)), for: .touchUpInside)
  261. return button
  262. }()
  263. private lazy var qrCodeButton: UIButton = {
  264. let button = UIButton()
  265. let title = String.localized("qrscan_title")
  266. button.setTitleColor(UIColor.systemBlue, for: .normal)
  267. button.setTitle(title, for: .normal)
  268. button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)
  269. return button
  270. }()
  271. private lazy var importBackupButton: UIButton = {
  272. let button = UIButton()
  273. let title = String.localized("import_backup_title")
  274. button.setTitleColor(UIColor.systemBlue, for: .normal)
  275. button.setTitle(title, for: .normal)
  276. button.addTarget(self, action: #selector(importBackupButtonPressed(_:)), for: .touchUpInside)
  277. return button
  278. }()
  279. private let defaultSpacing: CGFloat = 20
  280. init() {
  281. super.init(frame: .zero)
  282. setupSubviews()
  283. backgroundColor = DcColors.defaultBackgroundColor
  284. }
  285. required init?(coder: NSCoder) {
  286. fatalError("init(coder:) has not been implemented")
  287. }
  288. // MARK: - setup
  289. private func setupSubviews() {
  290. addSubview(container)
  291. container.translatesAutoresizingMaskIntoConstraints = false
  292. container.topAnchor.constraint(equalTo: topAnchor).isActive = true
  293. container.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  294. container.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75).isActive = true
  295. container.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0).isActive = true
  296. containerMinHeightConstraint.isActive = true
  297. _ = [logoView, titleLabel, subtitleLabel].map {
  298. addSubview($0)
  299. $0.translatesAutoresizingMaskIntoConstraints = false
  300. }
  301. let bottomLayoutGuide = UILayoutGuide()
  302. container.addLayoutGuide(bottomLayoutGuide)
  303. bottomLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  304. bottomLayoutGuide.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.55).isActive = true
  305. subtitleLabel.topAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
  306. subtitleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  307. subtitleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  308. subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  309. titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  310. titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  311. titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  312. titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  313. logoView.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  314. logoView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  315. logoHeightConstraint.constant = calculateLogoHeight()
  316. logoHeightConstraint.isActive = true
  317. logoView.widthAnchor.constraint(equalTo: logoView.heightAnchor).isActive = true
  318. let logoTopAnchor = logoView.topAnchor.constraint(equalTo: container.topAnchor, constant: 20) // this will allow the container to grow in height
  319. logoTopAnchor.priority = .defaultLow
  320. logoTopAnchor.isActive = true
  321. let buttonContainerGuide = UILayoutGuide()
  322. container.addLayoutGuide(buttonContainerGuide)
  323. buttonContainerGuide.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor).isActive = true
  324. buttonContainerGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  325. loginButton.setContentHuggingPriority(.defaultHigh, for: .vertical)
  326. container.addSubview(buttonStack)
  327. buttonStack.translatesAutoresizingMaskIntoConstraints = false
  328. buttonStack.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  329. buttonStack.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  330. let buttonStackTopAnchor = buttonStack.topAnchor.constraint(equalTo: buttonContainerGuide.topAnchor, constant: defaultSpacing)
  331. // this will allow the container to grow in height
  332. let buttonStackBottomAnchor = buttonStack.bottomAnchor.constraint(equalTo: buttonContainerGuide.bottomAnchor, constant: -50)
  333. _ = [buttonStackTopAnchor, buttonStackBottomAnchor].map {
  334. $0.priority = .defaultLow
  335. $0.isActive = true
  336. }
  337. }
  338. private func calculateLogoHeight() -> CGFloat {
  339. let titleHeight = titleLabel.intrinsicContentSize.height
  340. let subtitleHeight = subtitleLabel.intrinsicContentSize.height
  341. let intrinsicHeight = subtitleHeight + titleHeight
  342. let maxHeight: CGFloat = 100
  343. return intrinsicHeight > maxHeight ? maxHeight : intrinsicHeight
  344. }
  345. // MARK: - actions
  346. @objc private func loginButtonPressed(_ sender: UIButton) {
  347. onLogin?()
  348. }
  349. @objc private func qrCodeButtonPressed(_ sender: UIButton) {
  350. onScanQRCode?()
  351. }
  352. @objc private func importBackupButtonPressed(_ sender: UIButton) {
  353. onImportBackup?()
  354. }
  355. }