WelcomeViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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. self.showAccountSetupController()
  18. }
  19. view.onScanQRCode = { [weak self] in
  20. guard let self = self else { return }
  21. let qrReader = QrCodeReaderController()
  22. qrReader.delegate = self
  23. self.qrCodeReader = qrReader
  24. self.navigationController?.pushViewController(qrReader, animated: true)
  25. }
  26. view.translatesAutoresizingMaskIntoConstraints = false
  27. return view
  28. }()
  29. private lazy var canCancel: Bool = {
  30. // "cancel" removes selected unconfigured account, so there needs to be at least one other account
  31. return dcAccounts.getAll().count >= 2
  32. }()
  33. private lazy var cancelButton: UIBarButtonItem = {
  34. return UIBarButtonItem(title: String.localized("cancel"), style: .plain, target: self, action: #selector(cancelButtonPressed))
  35. }()
  36. private lazy var moreButton: UIBarButtonItem = {
  37. let image: UIImage?
  38. if #available(iOS 13.0, *) {
  39. image = UIImage(systemName: "ellipsis.circle")
  40. } else {
  41. image = UIImage(named: "ic_more")
  42. }
  43. return UIBarButtonItem(image: image,
  44. style: .plain,
  45. target: self,
  46. action: #selector(moreButtonPressed))
  47. }()
  48. private var qrCodeReader: QrCodeReaderController?
  49. weak var progressAlert: UIAlertController?
  50. init(dcAccounts: DcAccounts) {
  51. self.dcAccounts = dcAccounts
  52. self.dcContext = dcAccounts.getSelected()
  53. super.init(nibName: nil, bundle: nil)
  54. self.navigationItem.title = String.localized(canCancel ? "add_account" : "welcome_desktop")
  55. onProgressSuccess = { [weak self] in
  56. guard let self = self else { return }
  57. let profileInfoController = ProfileInfoViewController(context: self.dcContext)
  58. profileInfoController.onClose = {
  59. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  60. appDelegate.reloadDcContext()
  61. }
  62. }
  63. self.navigationController?.setViewControllers([profileInfoController], animated: true)
  64. }
  65. }
  66. required init?(coder: NSCoder) {
  67. fatalError("init(coder:) has not been implemented")
  68. }
  69. // MARK: - lifecycle
  70. override func viewDidLoad() {
  71. super.viewDidLoad()
  72. setupSubviews()
  73. if canCancel {
  74. navigationItem.leftBarButtonItem = cancelButton
  75. }
  76. navigationItem.rightBarButtonItem = moreButton
  77. }
  78. override func viewDidLayoutSubviews() {
  79. super.viewDidLayoutSubviews()
  80. welcomeView.minContainerHeight = view.frame.height - view.safeAreaInsets.top
  81. }
  82. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  83. super.viewWillTransition(to: size, with: coordinator)
  84. welcomeView.minContainerHeight = size.height - view.safeAreaInsets.top
  85. scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
  86. coordinator.animate(alongsideTransition: { [weak self] _ in
  87. self?.scrollView.scrollToBottom(animated: true)
  88. })
  89. }
  90. override func viewDidDisappear(_ animated: Bool) {
  91. let nc = NotificationCenter.default
  92. if let observer = self.progressObserver {
  93. nc.removeObserver(observer)
  94. self.progressObserver = nil
  95. }
  96. }
  97. // MARK: - setup
  98. private func setupSubviews() {
  99. view.addSubview(scrollView)
  100. scrollView.translatesAutoresizingMaskIntoConstraints = false
  101. scrollView.addSubview(welcomeView)
  102. let frameGuide = scrollView.frameLayoutGuide
  103. let contentGuide = scrollView.contentLayoutGuide
  104. frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
  105. frameGuide.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
  106. frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
  107. frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
  108. contentGuide.leadingAnchor.constraint(equalTo: welcomeView.leadingAnchor).isActive = true
  109. contentGuide.topAnchor.constraint(equalTo: welcomeView.topAnchor).isActive = true
  110. contentGuide.trailingAnchor.constraint(equalTo: welcomeView.trailingAnchor).isActive = true
  111. contentGuide.bottomAnchor.constraint(equalTo: welcomeView.bottomAnchor).isActive = true
  112. // this enables vertical scrolling
  113. frameGuide.widthAnchor.constraint(equalTo: contentGuide.widthAnchor).isActive = true
  114. }
  115. // MARK: - actions
  116. private func createAccountFromQRCode(qrCode: String) {
  117. if dcAccounts.getSelected().isConfigured() {
  118. UserDefaults.standard.setValue(dcAccounts.getSelected().id, forKey: Constants.Keys.lastSelectedAccountKey)
  119. // FIXME: what do we want to do with QR-Code created accounts? For now: adding an unencrypted account
  120. // ensure we're configuring on an empty new account
  121. _ = dcAccounts.add()
  122. }
  123. let accountId = dcAccounts.getSelected().id
  124. if accountId != 0 {
  125. self.dcContext = dcAccounts.get(id: accountId)
  126. let success = dcContext.setConfigFromQR(qrCode: qrCode)
  127. if success {
  128. addProgressAlertListener(dcAccounts: dcAccounts, progressName: dcNotificationConfigureProgress, onSuccess: handleLoginSuccess)
  129. showProgressAlert(title: String.localized("login_header"), dcContext: dcContext)
  130. dcAccounts.stopIo()
  131. dcContext.configure()
  132. } else {
  133. accountCreationErrorAlert()
  134. }
  135. }
  136. }
  137. private func handleLoginSuccess() {
  138. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  139. if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
  140. appDelegate.registerForNotifications()
  141. }
  142. onProgressSuccess?()
  143. }
  144. private func accountCreationErrorAlert() {
  145. let title = dcContext.lastErrorString
  146. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  147. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default))
  148. present(alert, animated: true)
  149. }
  150. @objc private func moreButtonPressed() {
  151. let alert = UIAlertController(title: "Encrypted Account (experimental)",
  152. message: "Do you want to encrypt your account database? This cannot be undone.",
  153. preferredStyle: .safeActionSheet)
  154. let encryptedAccountAction = UIAlertAction(title: "Create encrypted account", style: .default, handler: switchToEncrypted(_:))
  155. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .destructive, handler: nil)
  156. alert.addAction(encryptedAccountAction)
  157. alert.addAction(cancelAction)
  158. self.present(alert, animated: true, completion: nil)
  159. }
  160. private func switchToEncrypted(_ action: UIAlertAction) {
  161. let lastContextId = dcAccounts.getSelected().id
  162. let newContextId = dcAccounts.addClosedAccount()
  163. _ = dcAccounts.remove(id: lastContextId)
  164. _ = dcAccounts.select(id: newContextId)
  165. let selected = dcAccounts.getSelected()
  166. do {
  167. let secret = try KeychainManager.getAccountSecret(accountID: selected.id)
  168. guard selected.open(passphrase: secret) else {
  169. logger.error("Failed to open account database for account \(selected.id)")
  170. return
  171. }
  172. showAccountSetupController()
  173. } catch KeychainError.unhandledError(let message, let status) {
  174. logger.error("Keychain error. Failed to create encrypted account. \(message). Error status: \(status)")
  175. } catch {
  176. logger.error("Keychain error. Failed to create encrypted account.")
  177. }
  178. }
  179. private func showAccountSetupController() {
  180. let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
  181. accountSetupController.onLoginSuccess = {
  182. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  183. appDelegate.reloadDcContext()
  184. }
  185. }
  186. self.navigationController?.pushViewController(accountSetupController, animated: true)
  187. }
  188. @objc private func cancelButtonPressed() {
  189. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  190. // take a bit care on account removal:
  191. // remove only unconfigured and make sure, there is another account
  192. // (normally, both checks are not needed, however, some resilience wrt future program-flow-changes seems to be reasonable here)
  193. let selectedAccount = dcAccounts.getSelected()
  194. if !selectedAccount.isConfigured() {
  195. _ = dcAccounts.remove(id: selectedAccount.id)
  196. if self.dcAccounts.getAll().isEmpty {
  197. _ = self.dcAccounts.add()
  198. }
  199. }
  200. let lastSelectedAccountId = UserDefaults.standard.integer(forKey: Constants.Keys.lastSelectedAccountKey)
  201. if lastSelectedAccountId != 0 {
  202. _ = dcAccounts.select(id: lastSelectedAccountId)
  203. }
  204. appDelegate.reloadDcContext()
  205. }
  206. }
  207. extension WelcomeViewController: QrCodeReaderDelegate {
  208. func handleQrCode(_ code: String) {
  209. let lot = dcContext.checkQR(qrCode: code)
  210. if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
  211. confirmAccountCreationAlert(accountDomain: domain, qrCode: code)
  212. } else {
  213. qrErrorAlert()
  214. }
  215. }
  216. private func confirmAccountCreationAlert(accountDomain domain: String, qrCode: String) {
  217. let title = String.localizedStringWithFormat(String.localized("qraccount_ask_create_and_login"), domain)
  218. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  219. let okAction = UIAlertAction(
  220. title: String.localized("ok"),
  221. style: .default,
  222. handler: { [weak self] _ in
  223. guard let self = self else { return }
  224. self.dismissQRReader()
  225. self.createAccountFromQRCode(qrCode: qrCode)
  226. }
  227. )
  228. let qrCancelAction = UIAlertAction(
  229. title: String.localized("cancel"),
  230. style: .cancel,
  231. handler: { [weak self] _ in
  232. self?.dismissQRReader()
  233. }
  234. )
  235. alert.addAction(okAction)
  236. alert.addAction(qrCancelAction)
  237. qrCodeReader?.present(alert, animated: true)
  238. }
  239. private func qrErrorAlert() {
  240. let title = String.localized("qraccount_qr_code_cannot_be_used")
  241. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  242. let okAction = UIAlertAction(
  243. title: String.localized("ok"),
  244. style: .default,
  245. handler: { [weak self] _ in
  246. self?.qrCodeReader?.startSession()
  247. }
  248. )
  249. alert.addAction(okAction)
  250. qrCodeReader?.present(alert, animated: true, completion: nil)
  251. }
  252. private func dismissQRReader() {
  253. self.navigationController?.popViewController(animated: true)
  254. self.qrCodeReader = nil
  255. }
  256. }
  257. // MARK: - WelcomeContentView
  258. class WelcomeContentView: UIView {
  259. var onLogin: VoidFunction?
  260. var onScanQRCode: VoidFunction?
  261. var onImportBackup: VoidFunction?
  262. var minContainerHeight: CGFloat = 0 {
  263. didSet {
  264. containerMinHeightConstraint.constant = minContainerHeight
  265. logoHeightConstraint.constant = calculateLogoHeight()
  266. }
  267. }
  268. private lazy var containerMinHeightConstraint: NSLayoutConstraint = {
  269. return container.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
  270. }()
  271. private lazy var logoHeightConstraint: NSLayoutConstraint = {
  272. return logoView.heightAnchor.constraint(equalToConstant: 0)
  273. }()
  274. private var container = UIView()
  275. private var logoView: UIImageView = {
  276. let image = #imageLiteral(resourceName: "background_intro")
  277. let view = UIImageView(image: image)
  278. return view
  279. }()
  280. private lazy var titleLabel: UILabel = {
  281. let label = UILabel()
  282. label.text = String.localized("welcome_chat_over_email")
  283. label.textColor = DcColors.grayTextColor
  284. label.textAlignment = .center
  285. label.numberOfLines = 0
  286. label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
  287. return label
  288. }()
  289. private lazy var buttonStack: UIStackView = {
  290. let stack = UIStackView(arrangedSubviews: [loginButton, qrCodeButton /*, importBackupButton */])
  291. stack.axis = .vertical
  292. stack.spacing = 15
  293. return stack
  294. }()
  295. private lazy var loginButton: UIButton = {
  296. let button = UIButton(type: .roundedRect)
  297. let title = String.localized("login_header").uppercased()
  298. button.setTitle(title, for: .normal)
  299. button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .regular)
  300. button.setTitleColor(.white, for: .normal)
  301. button.backgroundColor = DcColors.primary
  302. let insets = button.contentEdgeInsets
  303. button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
  304. button.layer.cornerRadius = 5
  305. button.clipsToBounds = true
  306. button.addTarget(self, action: #selector(loginButtonPressed(_:)), for: .touchUpInside)
  307. return button
  308. }()
  309. private lazy var qrCodeButton: UIButton = {
  310. let button = UIButton()
  311. let title = String.localized("scan_invitation_code")
  312. button.setTitleColor(UIColor.systemBlue, for: .normal)
  313. button.setTitle(title, for: .normal)
  314. button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)
  315. return button
  316. }()
  317. private lazy var importBackupButton: UIButton = {
  318. let button = UIButton()
  319. let title = String.localized("import_backup_title")
  320. button.setTitleColor(UIColor.systemBlue, for: .normal)
  321. button.setTitle(title, for: .normal)
  322. button.addTarget(self, action: #selector(importBackupButtonPressed(_:)), for: .touchUpInside)
  323. return button
  324. }()
  325. private let defaultSpacing: CGFloat = 20
  326. init() {
  327. super.init(frame: .zero)
  328. setupSubviews()
  329. backgroundColor = DcColors.defaultBackgroundColor
  330. }
  331. required init?(coder: NSCoder) {
  332. fatalError("init(coder:) has not been implemented")
  333. }
  334. // MARK: - setup
  335. private func setupSubviews() {
  336. addSubview(container)
  337. container.translatesAutoresizingMaskIntoConstraints = false
  338. container.topAnchor.constraint(equalTo: topAnchor).isActive = true
  339. container.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  340. container.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75).isActive = true
  341. container.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0).isActive = true
  342. containerMinHeightConstraint.isActive = true
  343. _ = [logoView, titleLabel].map {
  344. addSubview($0)
  345. $0.translatesAutoresizingMaskIntoConstraints = false
  346. }
  347. let bottomLayoutGuide = UILayoutGuide()
  348. container.addLayoutGuide(bottomLayoutGuide)
  349. bottomLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  350. bottomLayoutGuide.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.45).isActive = true
  351. titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  352. titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  353. titleLabel.topAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
  354. titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  355. logoView.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  356. logoView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  357. logoHeightConstraint.constant = calculateLogoHeight()
  358. logoHeightConstraint.isActive = true
  359. logoView.widthAnchor.constraint(equalTo: logoView.heightAnchor).isActive = true
  360. let logoTopAnchor = logoView.topAnchor.constraint(equalTo: container.topAnchor, constant: 20) // this will allow the container to grow in height
  361. logoTopAnchor.priority = .defaultLow
  362. logoTopAnchor.isActive = true
  363. let buttonContainerGuide = UILayoutGuide()
  364. container.addLayoutGuide(buttonContainerGuide)
  365. buttonContainerGuide.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
  366. buttonContainerGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  367. loginButton.setContentHuggingPriority(.defaultHigh, for: .vertical)
  368. container.addSubview(buttonStack)
  369. buttonStack.translatesAutoresizingMaskIntoConstraints = false
  370. buttonStack.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  371. buttonStack.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  372. let buttonStackTopAnchor = buttonStack.topAnchor.constraint(equalTo: buttonContainerGuide.topAnchor, constant: defaultSpacing)
  373. // this will allow the container to grow in height
  374. let buttonStackBottomAnchor = buttonStack.bottomAnchor.constraint(equalTo: buttonContainerGuide.bottomAnchor, constant: -50)
  375. _ = [buttonStackTopAnchor, buttonStackBottomAnchor].map {
  376. $0.priority = .defaultLow
  377. $0.isActive = true
  378. }
  379. }
  380. private func calculateLogoHeight() -> CGFloat {
  381. if UIDevice.current.userInterfaceIdiom == .phone {
  382. return UIApplication.shared.statusBarOrientation.isLandscape ? UIScreen.main.bounds.height * 0.5 : UIScreen.main.bounds.width * 0.75
  383. } else {
  384. return 275
  385. }
  386. }
  387. // MARK: - actions
  388. @objc private func loginButtonPressed(_ sender: UIButton) {
  389. onLogin?()
  390. }
  391. @objc private func qrCodeButtonPressed(_ sender: UIButton) {
  392. onScanQRCode?()
  393. }
  394. @objc private func importBackupButtonPressed(_ sender: UIButton) {
  395. onImportBackup?()
  396. }
  397. }