WelcomeViewController.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. import UIKit
  2. import DcCore
  3. class WelcomeViewController: UIViewController, ProgressAlertHandler {
  4. private var dcContext: DcContext
  5. private let dcAccounts: DcAccounts
  6. private let accountCode: String?
  7. private var backupProgressObserver: NSObjectProtocol?
  8. var progressObserver: NSObjectProtocol?
  9. var onProgressSuccess: VoidFunction?
  10. private var securityScopedResource: NSURL?
  11. private lazy var scrollView: UIScrollView = {
  12. let scrollView = UIScrollView()
  13. scrollView.showsVerticalScrollIndicator = false
  14. return scrollView
  15. }()
  16. private lazy var welcomeView: WelcomeContentView = {
  17. let view = WelcomeContentView()
  18. view.onLogin = { [weak self] in
  19. guard let self = self else { return }
  20. self.showAccountSetupController()
  21. }
  22. view.onScanQRCode = { [weak self] in
  23. guard let self = self else { return }
  24. let qrReader = QrCodeReaderController()
  25. qrReader.delegate = self
  26. self.qrCodeReader = qrReader
  27. self.navigationController?.pushViewController(qrReader, animated: true)
  28. }
  29. view.onImportBackup = { [weak self] in
  30. guard let self = self else { return }
  31. self.restoreBackup()
  32. }
  33. view.translatesAutoresizingMaskIntoConstraints = false
  34. return view
  35. }()
  36. private lazy var canCancel: Bool = {
  37. // "cancel" removes selected unconfigured account, so there needs to be at least one other account
  38. return dcAccounts.getAll().count >= 2
  39. }()
  40. private lazy var cancelButton: UIBarButtonItem = {
  41. return UIBarButtonItem(title: String.localized("cancel"), style: .plain, target: self, action: #selector(cancelAccountCreation))
  42. }()
  43. private lazy var moreButton: UIBarButtonItem = {
  44. let image: UIImage?
  45. if #available(iOS 13.0, *) {
  46. image = UIImage(systemName: "ellipsis.circle")
  47. } else {
  48. image = UIImage(named: "ic_more")
  49. }
  50. return UIBarButtonItem(image: image,
  51. style: .plain,
  52. target: self,
  53. action: #selector(moreButtonPressed))
  54. }()
  55. private lazy var mediaPicker: MediaPicker? = {
  56. let mediaPicker = MediaPicker(navigationController: navigationController)
  57. mediaPicker.delegate = self
  58. return mediaPicker
  59. }()
  60. private var qrCodeReader: QrCodeReaderController?
  61. weak var progressAlert: UIAlertController?
  62. init(dcAccounts: DcAccounts, accountCode: String? = nil) {
  63. self.dcAccounts = dcAccounts
  64. self.dcContext = dcAccounts.getSelected()
  65. self.accountCode = accountCode
  66. super.init(nibName: nil, bundle: nil)
  67. self.navigationItem.title = String.localized(canCancel ? "add_account" : "welcome_desktop")
  68. onProgressSuccess = { [weak self] in
  69. guard let self = self else { return }
  70. let profileInfoController = ProfileInfoViewController(context: self.dcContext)
  71. profileInfoController.onClose = {
  72. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  73. appDelegate.reloadDcContext()
  74. }
  75. }
  76. self.navigationController?.setViewControllers([profileInfoController], animated: true)
  77. }
  78. }
  79. required init?(coder: NSCoder) {
  80. fatalError("init(coder:) has not been implemented")
  81. }
  82. // MARK: - lifecycle
  83. override func viewDidLoad() {
  84. super.viewDidLoad()
  85. setupSubviews()
  86. if canCancel {
  87. navigationItem.leftBarButtonItem = cancelButton
  88. }
  89. navigationItem.rightBarButtonItem = moreButton
  90. if let accountCode = accountCode {
  91. handleQrCode(accountCode)
  92. }
  93. }
  94. override func viewDidLayoutSubviews() {
  95. super.viewDidLayoutSubviews()
  96. welcomeView.minContainerHeight = view.frame.height - view.safeAreaInsets.top
  97. }
  98. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  99. super.viewWillTransition(to: size, with: coordinator)
  100. welcomeView.minContainerHeight = size.height - view.safeAreaInsets.top
  101. }
  102. override func viewDidDisappear(_ animated: Bool) {
  103. let nc = NotificationCenter.default
  104. if let observer = self.progressObserver {
  105. nc.removeObserver(observer)
  106. self.progressObserver = nil
  107. }
  108. removeBackupProgressObserver()
  109. }
  110. private func removeBackupProgressObserver() {
  111. if let backupProgressObserver = self.backupProgressObserver {
  112. NotificationCenter.default.removeObserver(backupProgressObserver)
  113. self.backupProgressObserver = nil
  114. }
  115. }
  116. // MARK: - setup
  117. private func setupSubviews() {
  118. view.addSubview(scrollView)
  119. scrollView.translatesAutoresizingMaskIntoConstraints = false
  120. scrollView.addSubview(welcomeView)
  121. let frameGuide = scrollView.frameLayoutGuide
  122. let contentGuide = scrollView.contentLayoutGuide
  123. frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
  124. frameGuide.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
  125. frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
  126. frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
  127. contentGuide.leadingAnchor.constraint(equalTo: welcomeView.leadingAnchor).isActive = true
  128. contentGuide.topAnchor.constraint(equalTo: welcomeView.topAnchor).isActive = true
  129. contentGuide.trailingAnchor.constraint(equalTo: welcomeView.trailingAnchor).isActive = true
  130. contentGuide.bottomAnchor.constraint(equalTo: welcomeView.bottomAnchor).isActive = true
  131. // this enables vertical scrolling
  132. frameGuide.widthAnchor.constraint(equalTo: contentGuide.widthAnchor).isActive = true
  133. }
  134. // MARK: - actions
  135. private func createAccountFromQRCode(qrCode: String) {
  136. if dcAccounts.getSelected().isConfigured() {
  137. UserDefaults.standard.setValue(dcAccounts.getSelected().id, forKey: Constants.Keys.lastSelectedAccountKey)
  138. // FIXME: what do we want to do with QR-Code created accounts? For now: adding an unencrypted account
  139. // ensure we're configuring on an empty new account
  140. _ = dcAccounts.add()
  141. }
  142. let accountId = dcAccounts.getSelected().id
  143. if accountId != 0 {
  144. dcContext = dcAccounts.get(id: accountId)
  145. addProgressAlertListener(dcAccounts: self.dcAccounts,
  146. progressName: dcNotificationConfigureProgress,
  147. onSuccess: self.handleLoginSuccess)
  148. showProgressAlert(title: String.localized("login_header"), dcContext: self.dcContext)
  149. DispatchQueue.global().async { [weak self] in
  150. guard let self = self else { return }
  151. let success = self.dcContext.setConfigFromQR(qrCode: qrCode)
  152. DispatchQueue.main.async {
  153. if success {
  154. self.dcAccounts.stopIo()
  155. self.dcContext.configure()
  156. } else {
  157. self.updateProgressAlert(error: self.dcContext.lastErrorString,
  158. completion: self.accountCode != nil ? self.cancelAccountCreation : nil)
  159. }
  160. }
  161. }
  162. }
  163. }
  164. private func handleLoginSuccess() {
  165. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  166. if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
  167. appDelegate.registerForNotifications()
  168. }
  169. onProgressSuccess?()
  170. }
  171. private func handleBackupRestoreSuccess() {
  172. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  173. if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
  174. appDelegate.registerForNotifications()
  175. }
  176. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  177. appDelegate.reloadDcContext()
  178. }
  179. }
  180. @objc private func moreButtonPressed() {
  181. let alert = UIAlertController(title: "Encrypted Account (experimental)",
  182. message: "Do you want to encrypt your account database? This cannot be undone.",
  183. preferredStyle: .safeActionSheet)
  184. let encryptedAccountAction = UIAlertAction(title: "Create encrypted account", style: .default, handler: switchToEncrypted(_:))
  185. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .destructive, handler: nil)
  186. alert.addAction(encryptedAccountAction)
  187. alert.addAction(cancelAction)
  188. self.present(alert, animated: true, completion: nil)
  189. }
  190. private func switchToEncrypted(_ action: UIAlertAction) {
  191. let lastContextId = dcAccounts.getSelected().id
  192. let newContextId = dcAccounts.addClosedAccount()
  193. _ = dcAccounts.remove(id: lastContextId)
  194. KeychainManager.deleteAccountSecret(id: lastContextId)
  195. _ = dcAccounts.select(id: newContextId)
  196. dcContext = dcAccounts.getSelected()
  197. do {
  198. let secret = try KeychainManager.getAccountSecret(accountID: dcContext.id)
  199. guard dcContext.open(passphrase: secret) else {
  200. logger.error("Failed to open account database for account \(dcContext.id)")
  201. return
  202. }
  203. self.navigationItem.title = String.localized("add_encrypted_account")
  204. } catch KeychainError.unhandledError(let message, let status) {
  205. logger.error("Keychain error. Failed to create encrypted account. \(message). Error status: \(status)")
  206. } catch {
  207. logger.error("Keychain error. Failed to create encrypted account.")
  208. }
  209. }
  210. private func showAccountSetupController() {
  211. let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
  212. accountSetupController.onLoginSuccess = {
  213. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  214. appDelegate.reloadDcContext()
  215. }
  216. }
  217. self.navigationController?.pushViewController(accountSetupController, animated: true)
  218. }
  219. @objc private func cancelAccountCreation() {
  220. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  221. // take a bit care on account removal:
  222. // remove only openend and unconfigured and make sure, there is another account
  223. // (normally, both checks are not needed, however, some resilience wrt future program-flow-changes seems to be reasonable here)
  224. let selectedAccount = dcAccounts.getSelected()
  225. if selectedAccount.isOpen() && !selectedAccount.isConfigured() {
  226. _ = dcAccounts.remove(id: selectedAccount.id)
  227. KeychainManager.deleteAccountSecret(id: selectedAccount.id)
  228. if self.dcAccounts.getAll().isEmpty {
  229. _ = self.dcAccounts.add()
  230. }
  231. }
  232. let lastSelectedAccountId = UserDefaults.standard.integer(forKey: Constants.Keys.lastSelectedAccountKey)
  233. if lastSelectedAccountId != 0 {
  234. _ = dcAccounts.select(id: lastSelectedAccountId)
  235. dcAccounts.startIo()
  236. }
  237. appDelegate.reloadDcContext()
  238. }
  239. private func restoreBackup() {
  240. logger.info("restoring backup")
  241. if dcContext.isConfigured() {
  242. return
  243. }
  244. mediaPicker?.showDocumentLibrary(selectFolder: true)
  245. }
  246. private func importBackup(at filepath: String) {
  247. logger.info("restoring backup: \(filepath)")
  248. showProgressAlert(title: String.localized("import_backup_title"), dcContext: dcContext)
  249. dcAccounts.stopIo()
  250. dcContext.imex(what: DC_IMEX_IMPORT_BACKUP, directory: filepath)
  251. }
  252. private func addProgressHudBackupListener() {
  253. let nc = NotificationCenter.default
  254. UIApplication.shared.isIdleTimerDisabled = true
  255. backupProgressObserver = nc.addObserver(
  256. forName: dcNotificationImexProgress,
  257. object: nil,
  258. queue: nil
  259. ) { [weak self] notification in
  260. guard let self = self else { return }
  261. if let ui = notification.userInfo {
  262. if let error = ui["error"] as? Bool, error {
  263. UIApplication.shared.isIdleTimerDisabled = false
  264. if self.dcContext.isConfigured() {
  265. let accountId = self.dcContext.id
  266. _ = self.dcAccounts.remove(id: accountId)
  267. KeychainManager.deleteAccountSecret(id: accountId)
  268. _ = self.dcAccounts.add()
  269. self.dcContext = self.dcAccounts.getSelected()
  270. self.navigationItem.title = String.localized(self.canCancel ? "add_account" : "welcome_desktop")
  271. }
  272. var error = ui["errorMessage"] as? String ?? ""
  273. if error.isEmpty {
  274. error = self.dcContext.lastErrorString
  275. }
  276. self.updateProgressAlert(error: error)
  277. self.stopAccessingSecurityScopedResource()
  278. self.removeBackupProgressObserver()
  279. } else if let done = ui["done"] as? Bool, done {
  280. UIApplication.shared.isIdleTimerDisabled = false
  281. self.dcAccounts.startIo()
  282. self.updateProgressAlertSuccess(completion: self.handleBackupRestoreSuccess)
  283. self.stopAccessingSecurityScopedResource()
  284. } else {
  285. self.updateProgressAlertValue(value: ui["progress"] as? Int)
  286. }
  287. }
  288. }
  289. }
  290. }
  291. extension WelcomeViewController: QrCodeReaderDelegate {
  292. func handleQrCode(_ code: String) {
  293. let lot = dcContext.checkQR(qrCode: code)
  294. if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
  295. let title = String.localizedStringWithFormat(
  296. String.localized(dcAccounts.getAll().count > 1 ? "qraccount_ask_create_and_login_another" : "qraccount_ask_create_and_login"),
  297. domain)
  298. confirmQrAccountAlert(title: title, qrCode: code)
  299. } else if let email = lot.text1, lot.state == DC_QR_LOGIN {
  300. let title = String.localizedStringWithFormat(
  301. String.localized(dcAccounts.getAll().count > 1 ? "qrlogin_ask_login_another" : "qrlogin_ask_login"),
  302. email)
  303. confirmQrAccountAlert(title: title, qrCode: code)
  304. } else if lot.state == DC_QR_BACKUP {
  305. confirmSetupNewDevice(qrCode: code)
  306. } else {
  307. qrErrorAlert()
  308. }
  309. }
  310. private func confirmQrAccountAlert(title: String, qrCode: String) {
  311. let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
  312. let okAction = UIAlertAction(
  313. title: String.localized("ok"),
  314. style: .default,
  315. handler: { [weak self] _ in
  316. guard let self = self else { return }
  317. self.dismissQRReader()
  318. self.createAccountFromQRCode(qrCode: qrCode)
  319. }
  320. )
  321. let qrCancelAction = UIAlertAction(
  322. title: String.localized("cancel"),
  323. style: .cancel,
  324. handler: { [weak self] _ in
  325. guard let self = self else { return }
  326. self.dismissQRReader()
  327. // if an injected accountCode exists, the WelcomeViewController was only opened to handle that
  328. // cancelling the action should also dismiss the whole controller
  329. if self.accountCode != nil {
  330. self.cancelAccountCreation()
  331. }
  332. }
  333. )
  334. alert.addAction(okAction)
  335. alert.addAction(qrCancelAction)
  336. if qrCodeReader != nil {
  337. qrCodeReader?.present(alert, animated: true)
  338. } else {
  339. self.present(alert, animated: true)
  340. }
  341. }
  342. private func confirmSetupNewDevice(qrCode: String) {
  343. // triggerLocalNetworkPrivacyAlert() // TODO: is that needed with new iroh?
  344. let alert = UIAlertController(title: String.localized("add_another_device"),
  345. message: String.localized("scan_other_device_explain"),
  346. preferredStyle: .alert)
  347. alert.addAction(UIAlertAction(
  348. title: String.localized("ok"),
  349. style: .default,
  350. handler: { [weak self] _ in
  351. guard let self = self else { return }
  352. self.dismissQRReader()
  353. self.addProgressHudBackupListener()
  354. self.showProgressAlert(title: String.localized("add_another_device"), dcContext: self.dcContext)
  355. self.dcAccounts.stopIo() // TODO: is this needed?
  356. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  357. guard let self = self else { return }
  358. self.dcContext.logger?.info("##### receiveBackup() with qr: \(qrCode)")
  359. let res = self.dcContext.receiveBackup(qrCode: qrCode)
  360. self.dcContext.logger?.info("##### receiveBackup() done with result: \(res)")
  361. }
  362. }
  363. ))
  364. alert.addAction(UIAlertAction(
  365. title: String.localized("cancel"),
  366. style: .cancel,
  367. handler: { [weak self] _ in
  368. self?.dcContext.stopOngoingProcess()
  369. self?.dismissQRReader()
  370. }
  371. ))
  372. qrCodeReader?.present(alert, animated: true)
  373. }
  374. private func qrErrorAlert() {
  375. let title = String.localized("qraccount_qr_code_cannot_be_used")
  376. let alert = UIAlertController(title: title, message: dcContext.lastErrorString, preferredStyle: .alert)
  377. let okAction = UIAlertAction(
  378. title: String.localized("ok"),
  379. style: .default,
  380. handler: { [weak self] _ in
  381. guard let self = self else { return }
  382. if self.accountCode != nil {
  383. // if an injected accountCode exists, the WelcomeViewController was only opened to handle that
  384. // if the action failed the whole controller should be dismissed
  385. self.cancelAccountCreation()
  386. } else {
  387. self.qrCodeReader?.startSession()
  388. }
  389. }
  390. )
  391. alert.addAction(okAction)
  392. qrCodeReader?.present(alert, animated: true, completion: nil)
  393. }
  394. private func dismissQRReader() {
  395. self.navigationController?.popViewController(animated: true)
  396. self.qrCodeReader = nil
  397. }
  398. private func stopAccessingSecurityScopedResource() {
  399. self.securityScopedResource?.stopAccessingSecurityScopedResource()
  400. self.securityScopedResource = nil
  401. }
  402. }
  403. // MARK: - WelcomeContentView
  404. class WelcomeContentView: UIView {
  405. var onLogin: VoidFunction?
  406. var onScanQRCode: VoidFunction?
  407. var onImportBackup: VoidFunction?
  408. var minContainerHeight: CGFloat = 0 {
  409. didSet {
  410. containerMinHeightConstraint.constant = minContainerHeight
  411. logoHeightConstraint.constant = calculateLogoHeight()
  412. }
  413. }
  414. private lazy var containerMinHeightConstraint: NSLayoutConstraint = {
  415. return container.heightAnchor.constraint(greaterThanOrEqualToConstant: 0)
  416. }()
  417. private lazy var logoHeightConstraint: NSLayoutConstraint = {
  418. return logoView.heightAnchor.constraint(equalToConstant: 0)
  419. }()
  420. private var container = UIView()
  421. private var logoView: UIImageView = {
  422. let image = #imageLiteral(resourceName: "background_intro")
  423. let view = UIImageView(image: image)
  424. return view
  425. }()
  426. private lazy var titleLabel: UILabel = {
  427. let label = UILabel()
  428. label.text = String.localized("welcome_chat_over_email")
  429. label.textColor = DcColors.grayTextColor
  430. label.textAlignment = .center
  431. label.numberOfLines = 0
  432. label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
  433. return label
  434. }()
  435. private lazy var buttonStack: UIStackView = {
  436. let stack = UIStackView(arrangedSubviews: [loginButton, qrCodeButton, importBackupButton])
  437. stack.axis = .vertical
  438. stack.spacing = 15
  439. return stack
  440. }()
  441. private lazy var loginButton: UIButton = {
  442. let button = UIButton(type: .roundedRect)
  443. let title = String.localized("login_header").uppercased()
  444. button.setTitle(title, for: .normal)
  445. button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .regular)
  446. button.setTitleColor(.white, for: .normal)
  447. button.backgroundColor = DcColors.primary
  448. let insets = button.contentEdgeInsets
  449. button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
  450. button.layer.cornerRadius = 5
  451. button.clipsToBounds = true
  452. button.addTarget(self, action: #selector(loginButtonPressed(_:)), for: .touchUpInside)
  453. return button
  454. }()
  455. private lazy var qrCodeButton: UIButton = {
  456. let button = UIButton()
  457. let title = String.localized("qrscan_title")
  458. button.setTitleColor(UIColor.systemBlue, for: .normal)
  459. button.setTitle(title, for: .normal)
  460. button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)
  461. return button
  462. }()
  463. private lazy var importBackupButton: UIButton = {
  464. let button = UIButton()
  465. let title = String.localized("import_backup_title")
  466. button.setTitleColor(UIColor.systemBlue, for: .normal)
  467. button.setTitle(title, for: .normal)
  468. button.addTarget(self, action: #selector(importBackupButtonPressed(_:)), for: .touchUpInside)
  469. return button
  470. }()
  471. private let defaultSpacing: CGFloat = 20
  472. init() {
  473. super.init(frame: .zero)
  474. setupSubviews()
  475. backgroundColor = DcColors.defaultBackgroundColor
  476. }
  477. required init?(coder: NSCoder) {
  478. fatalError("init(coder:) has not been implemented")
  479. }
  480. // MARK: - setup
  481. private func setupSubviews() {
  482. addSubview(container)
  483. container.translatesAutoresizingMaskIntoConstraints = false
  484. container.topAnchor.constraint(equalTo: topAnchor).isActive = true
  485. container.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
  486. container.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75).isActive = true
  487. container.centerXAnchor.constraint(equalTo: centerXAnchor, constant: 0).isActive = true
  488. containerMinHeightConstraint.isActive = true
  489. _ = [logoView, titleLabel].map {
  490. addSubview($0)
  491. $0.translatesAutoresizingMaskIntoConstraints = false
  492. }
  493. let bottomLayoutGuide = UILayoutGuide()
  494. container.addLayoutGuide(bottomLayoutGuide)
  495. bottomLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  496. bottomLayoutGuide.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.45).isActive = true
  497. titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
  498. titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
  499. titleLabel.topAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
  500. titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
  501. logoView.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -defaultSpacing).isActive = true
  502. logoView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  503. logoHeightConstraint.constant = calculateLogoHeight()
  504. logoHeightConstraint.isActive = true
  505. logoView.widthAnchor.constraint(equalTo: logoView.heightAnchor).isActive = true
  506. let logoTopAnchor = logoView.topAnchor.constraint(equalTo: container.topAnchor, constant: 20) // this will allow the container to grow in height
  507. logoTopAnchor.priority = .defaultLow
  508. logoTopAnchor.isActive = true
  509. let buttonContainerGuide = UILayoutGuide()
  510. container.addLayoutGuide(buttonContainerGuide)
  511. buttonContainerGuide.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
  512. buttonContainerGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
  513. loginButton.setContentHuggingPriority(.defaultHigh, for: .vertical)
  514. container.addSubview(buttonStack)
  515. buttonStack.translatesAutoresizingMaskIntoConstraints = false
  516. buttonStack.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
  517. buttonStack.centerYAnchor.constraint(equalTo: buttonContainerGuide.centerYAnchor).isActive = true
  518. let buttonStackTopAnchor = buttonStack.topAnchor.constraint(equalTo: buttonContainerGuide.topAnchor, constant: defaultSpacing)
  519. // this will allow the container to grow in height
  520. let buttonStackBottomAnchor = buttonStack.bottomAnchor.constraint(equalTo: buttonContainerGuide.bottomAnchor, constant: -50)
  521. _ = [buttonStackTopAnchor, buttonStackBottomAnchor].map {
  522. $0.priority = .defaultLow
  523. $0.isActive = true
  524. }
  525. }
  526. private func calculateLogoHeight() -> CGFloat {
  527. if UIDevice.current.userInterfaceIdiom == .phone {
  528. return UIApplication.shared.statusBarOrientation.isLandscape ? UIScreen.main.bounds.height * 0.5 : UIScreen.main.bounds.width * 0.75
  529. } else {
  530. return 275
  531. }
  532. }
  533. // MARK: - actions
  534. @objc private func loginButtonPressed(_ sender: UIButton) {
  535. onLogin?()
  536. }
  537. @objc private func qrCodeButtonPressed(_ sender: UIButton) {
  538. onScanQRCode?()
  539. }
  540. @objc private func importBackupButtonPressed(_ sender: UIButton) {
  541. onImportBackup?()
  542. }
  543. }
  544. extension WelcomeViewController: MediaPickerDelegate {
  545. func onDocumentSelected(url: NSURL) {
  546. // ensure we can access folders outside of the app's sandbox
  547. let isSecurityScopedResource = url.startAccessingSecurityScopedResource()
  548. if isSecurityScopedResource {
  549. securityScopedResource = url
  550. }
  551. if let selectedBackupFilePath = url.relativePath {
  552. addProgressHudBackupListener()
  553. importBackup(at: selectedBackupFilePath)
  554. } else {
  555. stopAccessingSecurityScopedResource()
  556. }
  557. }
  558. }