WelcomeViewController.swift 15 KB

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