WelcomeViewController.swift 15 KB

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