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.qrCodeReaderNav.dismiss(animated: true) {
  166. self.createAccountFromQRCode()
  167. }
  168. }
  169. )
  170. let qrCancelAction = UIAlertAction(
  171. title: String.localized("cancel"),
  172. style: .cancel,
  173. handler: { [unowned self] _ in
  174. self.qrCodeReaderNav.dismiss(animated: true) {
  175. self.scannedQrCode = nil
  176. }
  177. }
  178. )
  179. alert.addAction(okAction)
  180. alert.addAction(qrCancelAction)
  181. qrCodeReaderNav.present(alert, animated: true)
  182. }
  183. private func qrErrorAlert() {
  184. let title = String.localized("qraccount_qr_code_cannot_be_used")
  185. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  186. let okAction = UIAlertAction(
  187. title: String.localized("ok"),
  188. style: .default,
  189. handler: { [unowned self] _ in
  190. self.qrCordeReader.startSession()
  191. }
  192. )
  193. let qrCancelAction = UIAlertAction(
  194. title: String.localized("cancel"),
  195. style: .cancel,
  196. handler: { [unowned self] _ in
  197. self.qrCodeReaderNav.dismiss(animated: true) {
  198. self.scannedQrCode = nil
  199. }
  200. }
  201. )
  202. alert.addAction(okAction)
  203. alert.addAction(qrCancelAction)
  204. qrCodeReaderNav.present(alert, animated: true, completion: nil)
  205. }
  206. }
  207. // MARK: - WelcomeContentView
  208. class WelcomeContentView: UIView {
  209. var onLogin: VoidFunction?
  210. var onScanQRCode: VoidFunction?
  211. var onImportBackup: VoidFunction?
  212. var minContainerHeight: CGFloat = 0 {
  213. didSet {
  214. containerMinHeightConstraint.constant = minContainerHeight
  215. logoHeightConstraint.constant = calculateLogoHeight()
  216. }
  217. }
  218. private lazy var containerMinHeightConstraint: NSLayoutConstraint = {
  219. return container.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
  220. }()
  221. private lazy var logoHeightConstraint: NSLayoutConstraint = {
  222. return logoView.heightAnchor.constraint(equalToConstant: 0)
  223. }()
  224. private var container = UIView()
  225. private var logoView: UIImageView = {
  226. let image = #imageLiteral(resourceName: "dc_logo")
  227. let view = UIImageView(image: image)
  228. return view
  229. }()
  230. private lazy var titleLabel: UILabel = {
  231. let label = UILabel()
  232. label.text = String.localized("welcome_desktop")
  233. label.textColor = DcColors.grayTextColor
  234. label.textAlignment = .center
  235. label.numberOfLines = 0
  236. label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
  237. return label
  238. }()
  239. private lazy var subtitleLabel: UILabel = {
  240. let label = UILabel()
  241. label.text = String.localized("welcome_intro1_message")
  242. label.font = UIFont.systemFont(ofSize: 22, weight: .regular)
  243. label.textColor = DcColors.grayTextColor
  244. label.numberOfLines = 0
  245. label.textAlignment = .center
  246. return label
  247. }()
  248. private lazy var loginButton: UIButton = {
  249. let button = UIButton(type: .roundedRect)
  250. let title = String.localized("login_header").uppercased()
  251. button.setTitle(title, for: .normal)
  252. button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .regular)
  253. button.setTitleColor(.white, for: .normal)
  254. button.backgroundColor = DcColors.primary
  255. let insets = button.contentEdgeInsets
  256. button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
  257. button.layer.cornerRadius = 5
  258. button.clipsToBounds = true
  259. button.addTarget(self, action: #selector(loginButtonPressed(_:)), for: .touchUpInside)
  260. return button
  261. }()
  262. private lazy var buttonStack: UIStackView = {
  263. let stack = UIStackView(arrangedSubviews: [loginButton, qrCodeButton /*, importBackupButton */])
  264. stack.axis = .vertical
  265. stack.spacing = 15
  266. return stack
  267. }()
  268. private lazy var qrCodeButton: UIButton = {
  269. let button = UIButton()
  270. let title = String.localized("qrscan_title")
  271. button.setTitleColor(UIColor.systemBlue, for: .normal)
  272. button.setTitle(title, for: .normal)
  273. button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)
  274. return button
  275. }()
  276. private lazy var importBackupButton: UIButton = {
  277. let button = UIButton()
  278. let title = String.localized("import_backup_title")
  279. button.setTitleColor(UIColor.systemBlue, for: .normal)
  280. button.setTitle(title, for: .normal)
  281. button.addTarget(self, action: #selector(importBackupButtonPressed(_:)), for: .touchUpInside)
  282. return button
  283. }()
  284. private var activityIndicator: UIActivityIndicatorView = {
  285. let view: UIActivityIndicatorView
  286. if #available(iOS 13, *) {
  287. view = UIActivityIndicatorView(style: .large)
  288. } else {
  289. view = UIActivityIndicatorView(style: .whiteLarge)
  290. view.color = UIColor.gray
  291. }
  292. view.isHidden = true
  293. return view
  294. }()
  295. private let defaultSpacing: CGFloat = 20
  296. init() {
  297. super.init(frame: .zero)
  298. setupSubviews()
  299. backgroundColor = DcColors.defaultBackgroundColor
  300. }
  301. required init?(coder: NSCoder) {
  302. fatalError("init(coder:) has not been implemented")
  303. }
  304. // MARK: - setup
  305. private func setupSubviews() {
  306. addSubview(container)
  307. container.translatesAutoresizingMaskIntoConstraints = false
  308. container.topAnchor.constraint(equalTo: topAnchor).isActive = true
  309. container.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  310. container.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75).isActive = true
  311. container.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0).isActive = true
  312. containerMinHeightConstraint.isActive = true
  313. _ = [logoView, titleLabel, subtitleLabel].map {
  314. addSubview($0)
  315. $0.translatesAutoresizingMaskIntoConstraints = false
  316. }
  317. let bottomLayoutGuide = UILayoutGuide()
  318. container.addLayoutGuide(bottomLayoutGuide)
  319. bottomLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  320. bottomLayoutGuide.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.55).isActive = true
  321. subtitleLabel.topAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
  322. subtitleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  323. subtitleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  324. subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  325. titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  326. titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  327. titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  328. titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  329. logoView.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  330. logoView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  331. logoHeightConstraint.constant = calculateLogoHeight()
  332. logoHeightConstraint.isActive = true
  333. logoView.widthAnchor.constraint(equalTo: logoView.heightAnchor).isActive = true
  334. let logoTopAnchor = logoView.topAnchor.constraint(equalTo: container.topAnchor, constant: 20) // this will allow the container to grow in height
  335. logoTopAnchor.priority = .defaultLow
  336. logoTopAnchor.isActive = true
  337. let buttonContainerGuide = UILayoutGuide()
  338. container.addLayoutGuide(buttonContainerGuide)
  339. buttonContainerGuide.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor).isActive = true
  340. buttonContainerGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  341. loginButton.setContentHuggingPriority(.defaultHigh, for: .vertical)
  342. container.addSubview(buttonStack)
  343. buttonStack.translatesAutoresizingMaskIntoConstraints = false
  344. buttonStack.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  345. buttonStack.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  346. let buttonStackTopAnchor = buttonStack.topAnchor.constraint(equalTo: buttonContainerGuide.topAnchor, constant: defaultSpacing)
  347. // this will allow the container to grow in height
  348. let buttonStackBottomAnchor = buttonStack.bottomAnchor.constraint(equalTo: buttonContainerGuide.bottomAnchor, constant: -50)
  349. _ = [buttonStackTopAnchor, buttonStackBottomAnchor].map {
  350. $0.priority = .defaultLow
  351. $0.isActive = true
  352. }
  353. addSubview(activityIndicator)
  354. activityIndicator.translatesAutoresizingMaskIntoConstraints = false
  355. activityIndicator.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  356. activityIndicator.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  357. }
  358. private func calculateLogoHeight() -> CGFloat {
  359. let titleHeight = titleLabel.intrinsicContentSize.height
  360. let subtitleHeight = subtitleLabel.intrinsicContentSize.height
  361. let intrinsicHeight = subtitleHeight + titleHeight
  362. let maxHeight: CGFloat = 100
  363. return intrinsicHeight > maxHeight ? maxHeight : intrinsicHeight
  364. }
  365. // MARK: - actions
  366. @objc private func loginButtonPressed(_ sender: UIButton) {
  367. onLogin?()
  368. }
  369. @objc private func qrCodeButtonPressed(_ sender: UIButton) {
  370. onScanQRCode?()
  371. }
  372. @objc private func importBackupButtonPressed(_ sender: UIButton) {
  373. onImportBackup?()
  374. }
  375. func showSpinner(_ show: Bool) {
  376. if show {
  377. activityIndicator.startAnimating()
  378. } else {
  379. activityIndicator.stopAnimating()
  380. }
  381. activityIndicator.isHidden = !show
  382. buttonStack.isHidden = show
  383. }
  384. }