WelcomeViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import UIKit
  2. import DcCore
  3. class WelcomeViewController: UIViewController, ProgressAlertHandler {
  4. weak var coordinator: WelcomeCoordinator?
  5. private let dcContext: DcContext
  6. private var scannedQrCode: String?
  7. var configureProgressObserver: Any?
  8. var onProgressSuccess: VoidFunction?
  9. private lazy var scrollView: UIScrollView = {
  10. let scrollView = UIScrollView()
  11. scrollView.showsVerticalScrollIndicator = false
  12. return scrollView
  13. }()
  14. private lazy var welcomeView: WelcomeContentView = {
  15. let view = WelcomeContentView()
  16. view.onLogin = {
  17. [unowned self] in
  18. self.coordinator?.showLogin()
  19. }
  20. view.onScanQRCode = {
  21. [unowned self] in
  22. self.showQRReader()
  23. }
  24. view.translatesAutoresizingMaskIntoConstraints = false
  25. return view
  26. }()
  27. private lazy var qrCordeReader: QrCodeReaderController = {
  28. let controller = QrCodeReaderController()
  29. controller.delegate = self
  30. return controller
  31. }()
  32. private lazy var qrCodeReaderNav: UINavigationController = {
  33. let nav = UINavigationController(rootViewController: qrCordeReader)
  34. nav.modalPresentationStyle = .fullScreen
  35. return nav
  36. }()
  37. lazy var progressAlert: UIAlertController = {
  38. let alert = UIAlertController(title: "", message: "", preferredStyle: .alert)
  39. alert.addAction(UIAlertAction(
  40. title: String.localized("cancel"),
  41. style: .cancel,
  42. handler: { _ in
  43. self.dcContext.stopOngoingProcess()
  44. }))
  45. return alert
  46. }()
  47. init(dcContext: DcContext) {
  48. self.dcContext = dcContext
  49. super.init(nibName: nil, bundle: nil)
  50. onProgressSuccess = {[unowned self] in
  51. self.coordinator?.handleQRAccountCreationSuccess()
  52. }
  53. }
  54. required init?(coder: NSCoder) {
  55. fatalError("init(coder:) has not been implemented")
  56. }
  57. // MARK: - lifecycle
  58. override func viewDidLoad() {
  59. super.viewDidLoad()
  60. setupSubviews()
  61. }
  62. override func viewDidLayoutSubviews() {
  63. super.viewDidLayoutSubviews()
  64. welcomeView.minContainerHeight = view.frame.height
  65. }
  66. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  67. super.viewWillTransition(to: size, with: coordinator)
  68. welcomeView.minContainerHeight = size.height
  69. scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
  70. }
  71. override func viewDidDisappear(_ animated: Bool) {
  72. let nc = NotificationCenter.default
  73. if let configureProgressObserver = self.configureProgressObserver {
  74. nc.removeObserver(configureProgressObserver)
  75. }
  76. }
  77. // MARK: - setup
  78. private func setupSubviews() {
  79. view.addSubview(scrollView)
  80. scrollView.translatesAutoresizingMaskIntoConstraints = false
  81. scrollView.addSubview(welcomeView)
  82. let frameGuide = scrollView.frameLayoutGuide
  83. let contentGuide = scrollView.contentLayoutGuide
  84. frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
  85. frameGuide.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
  86. frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
  87. frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
  88. contentGuide.leadingAnchor.constraint(equalTo: welcomeView.leadingAnchor).isActive = true
  89. contentGuide.topAnchor.constraint(equalTo: welcomeView.topAnchor).isActive = true
  90. contentGuide.trailingAnchor.constraint(equalTo: welcomeView.trailingAnchor).isActive = true
  91. contentGuide.bottomAnchor.constraint(equalTo: welcomeView.bottomAnchor).isActive = true
  92. // this enables vertical scrolling
  93. frameGuide.widthAnchor.constraint(equalTo: contentGuide.widthAnchor).isActive = true
  94. }
  95. /// if active the welcomeViewController will show nothing but a centered activity indicator
  96. func activateSpinner(_ active: Bool) {
  97. welcomeView.showSpinner(active)
  98. }
  99. // MARK: - actions
  100. private func showQRReader(completion onComplete: VoidFunction? = nil) {
  101. present(qrCodeReaderNav, animated: true) {
  102. onComplete?()
  103. }
  104. }
  105. private func createAccountFromQRCode() {
  106. guard let code = scannedQrCode else {
  107. return
  108. }
  109. let success = dcContext.configureAccountFromQR(qrCode: code)
  110. scannedQrCode = nil
  111. if success {
  112. if let loginCompletion = self.onProgressSuccess {
  113. addProgressAlertListener(onSuccess: loginCompletion)
  114. showProgressAlert(title: String.localized("qraccount_use_on_new_install"))
  115. }
  116. dcContext.configure()
  117. } else {
  118. accountCreationErrorAlert()
  119. }
  120. }
  121. private func accountCreationErrorAlert() {
  122. func handleRepeat() {
  123. showQRReader(completion: { [unowned self] in
  124. self.activateSpinner(false)
  125. })
  126. }
  127. let title = String.localized("qraccount_creation_failed")
  128. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  129. let okAction = UIAlertAction(
  130. title: String.localized("ok"),
  131. style: .default,
  132. handler: { [unowned self] _ in
  133. self.activateSpinner(false)
  134. }
  135. )
  136. let repeatAction = UIAlertAction(
  137. title: String.localized("global_menu_edit_redo_desktop"),
  138. style: .default,
  139. handler: { _ in
  140. handleRepeat()
  141. }
  142. )
  143. alert.addAction(okAction)
  144. alert.addAction(repeatAction)
  145. present(alert, animated: true)
  146. }
  147. }
  148. extension WelcomeViewController: QrCodeReaderDelegate {
  149. func handleQrCode(_ code: String) {
  150. let lot = dcContext.checkQR(qrCode: code)
  151. if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
  152. self.scannedQrCode = code
  153. confirmAccountCreationAlert(accountDomain: domain)
  154. } else {
  155. qrErrorAlert()
  156. }
  157. }
  158. private func confirmAccountCreationAlert(accountDomain domain: String) {
  159. let title = String.localizedStringWithFormat(NSLocalizedString("qraccount_ask_create_and_login", comment: ""), domain)
  160. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  161. let okAction = UIAlertAction(
  162. title: String.localized("ok"),
  163. style: .default,
  164. handler: { [unowned self] _ in
  165. self.activateSpinner(true)
  166. self.qrCodeReaderNav.dismiss(animated: true) {
  167. self.createAccountFromQRCode()
  168. }
  169. }
  170. )
  171. let qrCancelAction = UIAlertAction(
  172. title: String.localized("cancel"),
  173. style: .cancel,
  174. handler: { [unowned self] _ in
  175. self.qrCodeReaderNav.dismiss(animated: true) {
  176. self.scannedQrCode = nil
  177. }
  178. }
  179. )
  180. alert.addAction(okAction)
  181. alert.addAction(qrCancelAction)
  182. qrCodeReaderNav.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: { [unowned self] _ in
  191. self.qrCordeReader.startSession()
  192. }
  193. )
  194. let qrCancelAction = UIAlertAction(
  195. title: String.localized("cancel"),
  196. style: .cancel,
  197. handler: { [unowned self] _ in
  198. self.qrCodeReaderNav.dismiss(animated: true) {
  199. self.scannedQrCode = nil
  200. }
  201. }
  202. )
  203. alert.addAction(okAction)
  204. alert.addAction(qrCancelAction)
  205. qrCodeReaderNav.present(alert, animated: true, completion: nil)
  206. }
  207. }
  208. // MARK: - WelcomeContentView
  209. class WelcomeContentView: UIView {
  210. var onLogin: VoidFunction?
  211. var onScanQRCode: VoidFunction?
  212. var onImportBackup: VoidFunction?
  213. var minContainerHeight: CGFloat = 0 {
  214. didSet {
  215. containerMinHeightConstraint.constant = minContainerHeight
  216. logoHeightConstraint.constant = calculateLogoHeight()
  217. }
  218. }
  219. private lazy var containerMinHeightConstraint: NSLayoutConstraint = {
  220. return container.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
  221. }()
  222. private lazy var logoHeightConstraint: NSLayoutConstraint = {
  223. return logoView.heightAnchor.constraint(equalToConstant: 0)
  224. }()
  225. private var container = UIView()
  226. private var logoView: UIImageView = {
  227. let image = #imageLiteral(resourceName: "dc_logo")
  228. let view = UIImageView(image: image)
  229. return view
  230. }()
  231. private lazy var titleLabel: UILabel = {
  232. let label = UILabel()
  233. label.text = String.localized("welcome_desktop")
  234. label.textColor = DcColors.grayTextColor
  235. label.textAlignment = .center
  236. label.numberOfLines = 0
  237. label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
  238. return label
  239. }()
  240. private lazy var subtitleLabel: UILabel = {
  241. let label = UILabel()
  242. label.text = String.localized("welcome_intro1_message")
  243. label.font = UIFont.systemFont(ofSize: 22, weight: .regular)
  244. label.textColor = DcColors.grayTextColor
  245. label.numberOfLines = 0
  246. label.textAlignment = .center
  247. return label
  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 buttonStack: UIStackView = {
  264. let stack = UIStackView(arrangedSubviews: [loginButton, qrCodeButton /*, importBackupButton */])
  265. stack.axis = .vertical
  266. stack.spacing = 15
  267. return stack
  268. }()
  269. private lazy var qrCodeButton: UIButton = {
  270. let button = UIButton()
  271. let title = String.localized("qrscan_title")
  272. button.setTitleColor(UIColor.systemBlue, for: .normal)
  273. button.setTitle(title, for: .normal)
  274. button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)
  275. return button
  276. }()
  277. private lazy var importBackupButton: UIButton = {
  278. let button = UIButton()
  279. let title = String.localized("import_backup_title")
  280. button.setTitleColor(UIColor.systemBlue, for: .normal)
  281. button.setTitle(title, for: .normal)
  282. button.addTarget(self, action: #selector(importBackupButtonPressed(_:)), for: .touchUpInside)
  283. return button
  284. }()
  285. private var activityIndicator: UIActivityIndicatorView = {
  286. let view: UIActivityIndicatorView
  287. if #available(iOS 13, *) {
  288. view = UIActivityIndicatorView(style: .large)
  289. } else {
  290. view = UIActivityIndicatorView(style: .whiteLarge)
  291. view.color = UIColor.gray
  292. }
  293. view.isHidden = true
  294. return view
  295. }()
  296. private let defaultSpacing: CGFloat = 20
  297. init() {
  298. super.init(frame: .zero)
  299. setupSubviews()
  300. backgroundColor = DcColors.defaultBackgroundColor
  301. }
  302. required init?(coder: NSCoder) {
  303. fatalError("init(coder:) has not been implemented")
  304. }
  305. // MARK: - setup
  306. private func setupSubviews() {
  307. addSubview(container)
  308. container.translatesAutoresizingMaskIntoConstraints = false
  309. container.topAnchor.constraint(equalTo: topAnchor).isActive = true
  310. container.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  311. container.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75).isActive = true
  312. container.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0).isActive = true
  313. containerMinHeightConstraint.isActive = true
  314. _ = [logoView, titleLabel, subtitleLabel].map {
  315. addSubview($0)
  316. $0.translatesAutoresizingMaskIntoConstraints = false
  317. }
  318. let bottomLayoutGuide = UILayoutGuide()
  319. container.addLayoutGuide(bottomLayoutGuide)
  320. bottomLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  321. bottomLayoutGuide.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.55).isActive = true
  322. subtitleLabel.topAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
  323. subtitleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  324. subtitleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  325. subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  326. titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  327. titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  328. titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  329. titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  330. logoView.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  331. logoView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  332. logoHeightConstraint.constant = calculateLogoHeight()
  333. logoHeightConstraint.isActive = true
  334. logoView.widthAnchor.constraint(equalTo: logoView.heightAnchor).isActive = true
  335. let logoTopAnchor = logoView.topAnchor.constraint(equalTo: container.topAnchor, constant: 20) // this will allow the container to grow in height
  336. logoTopAnchor.priority = .defaultLow
  337. logoTopAnchor.isActive = true
  338. let buttonContainerGuide = UILayoutGuide()
  339. container.addLayoutGuide(buttonContainerGuide)
  340. buttonContainerGuide.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor).isActive = true
  341. buttonContainerGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  342. loginButton.setContentHuggingPriority(.defaultHigh, for: .vertical)
  343. container.addSubview(buttonStack)
  344. buttonStack.translatesAutoresizingMaskIntoConstraints = false
  345. buttonStack.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  346. buttonStack.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  347. let buttonStackTopAnchor = buttonStack.topAnchor.constraint(equalTo: buttonContainerGuide.topAnchor, constant: defaultSpacing)
  348. // this will allow the container to grow in height
  349. let buttonStackBottomAnchor = buttonStack.bottomAnchor.constraint(equalTo: buttonContainerGuide.bottomAnchor, constant: -50)
  350. _ = [buttonStackTopAnchor, buttonStackBottomAnchor].map {
  351. $0.priority = .defaultLow
  352. $0.isActive = true
  353. }
  354. addSubview(activityIndicator)
  355. activityIndicator.translatesAutoresizingMaskIntoConstraints = false
  356. activityIndicator.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  357. activityIndicator.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  358. }
  359. private func calculateLogoHeight() -> CGFloat {
  360. let titleHeight = titleLabel.intrinsicContentSize.height
  361. let subtitleHeight = subtitleLabel.intrinsicContentSize.height
  362. let intrinsicHeight = subtitleHeight + titleHeight
  363. let maxHeight: CGFloat = 100
  364. return intrinsicHeight > maxHeight ? maxHeight : intrinsicHeight
  365. }
  366. // MARK: - actions
  367. @objc private func loginButtonPressed(_ sender: UIButton) {
  368. onLogin?()
  369. }
  370. @objc private func qrCodeButtonPressed(_ sender: UIButton) {
  371. onScanQRCode?()
  372. }
  373. @objc private func importBackupButtonPressed(_ sender: UIButton) {
  374. onImportBackup?()
  375. }
  376. func showSpinner(_ show: Bool) {
  377. if show {
  378. activityIndicator.startAnimating()
  379. } else {
  380. activityIndicator.stopAnimating()
  381. }
  382. activityIndicator.isHidden = !show
  383. buttonStack.isHidden = show
  384. }
  385. }