Skip to content

Commit

Permalink
[WIP] Copy and paste QR-Code-Handling
Browse files Browse the repository at this point in the history
  • Loading branch information
zeitschlag committed May 16, 2024
1 parent ac5ccb7 commit cfe0e18
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import DcCore

class InstantOnboardingViewController: UIViewController, ProgressAlertHandler {

private let dcContext: DcContext
private var dcContext: DcContext
private let dcAccounts: DcAccounts
weak var progressAlert: UIAlertController?
var progressObserver: NSObjectProtocol?
private var qrCodeReader: QrCodeReaderController?
private var securityScopedResource: NSURL?
private var backupProgressObserver: NSObjectProtocol?
private lazy var canCancel: Bool = {
// "cancel" removes selected unconfigured account, so there needs to be at least one other account
return dcAccounts.getAll().count >= 2
}()

var contentView: InstantOnboardingView { view as! InstantOnboardingView }

Check warning on line 19 in deltachat-ios/Controller/AccountSetup/Instand Onboarding/InstantOnboardingViewController.swift

View workflow job for this annotation

GitHub Actions / build

Force Cast Violation: Force casts should be avoided (force_cast)

Expand Down Expand Up @@ -133,7 +140,16 @@ class InstantOnboardingViewController: UIViewController, ProgressAlertHandler {
}

@objc private func scanQRCode(_ sender: UIButton) {
// TODO: Implement
let qrReader = QrCodeReaderController(title: String.localized("multidevice_receiver_title"),
addHints: "" + String.localized("multidevice_same_network_hint") + "\n\n"
+ "" + String.localized("multidevice_open_settings_on_other_device") + "\n\n"
+ String.localized("multidevice_experimental_hint"),
showTroubleshooting: true)
qrReader.delegate = self

navigationController?.pushViewController(qrReader, animated: true)

self.qrCodeReader = qrReader
}

@objc
Expand Down Expand Up @@ -163,7 +179,7 @@ class InstantOnboardingViewController: UIViewController, ProgressAlertHandler {
contentView.spacer.isHidden = true
contentView.bottomSpacer.isHidden = false
}

@objc private func keyboardWillHide(_ notification: Notification) {
contentView.spacer.isHidden = false
contentView.bottomSpacer.isHidden = true
Expand Down Expand Up @@ -202,9 +218,277 @@ class InstantOnboardingViewController: UIViewController, ProgressAlertHandler {
}
}

// MARK: - MediaPickerDelegate
extension InstantOnboardingViewController: MediaPickerDelegate {
func onImageSelected(image: UIImage) {
AvatarHelper.saveSelfAvatarImage(dcContext: dcContext, image: image)
contentView.imageButton.setImage(image, for: .normal)
}
}

// MARK: - QrCodeReaderDelegate
extension InstantOnboardingViewController: QrCodeReaderDelegate {
func handleQrCode(_ code: String) {
let lot = dcContext.checkQR(qrCode: code)
if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
let title = String.localizedStringWithFormat(
String.localized(dcAccounts.getAll().count > 1 ? "qraccount_ask_create_and_login_another" : "qraccount_ask_create_and_login"),
domain)
confirmQrAccountAlert(title: title, qrCode: code)
} else if let email = lot.text1, lot.state == DC_QR_LOGIN {
let title = String.localizedStringWithFormat(
String.localized(dcAccounts.getAll().count > 1 ? "qrlogin_ask_login_another" : "qrlogin_ask_login"),
email)
confirmQrAccountAlert(title: title, qrCode: code)
} else if lot.state == DC_QR_BACKUP {
confirmSetupNewDevice(qrCode: code)
} else {
qrErrorAlert()
}
}

private func confirmQrAccountAlert(title: String, qrCode: String) {
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)

let okAction = UIAlertAction(
title: String.localized("ok"),
style: .default,
handler: { [weak self] _ in
guard let self else { return }
self.dismissQRReader()
self.createAccountFromQRCode(qrCode: qrCode)
}
)

let qrCancelAction = UIAlertAction(
title: String.localized("cancel"),
style: .cancel,
handler: { [weak self] _ in
guard let self else { return }
self.dismissQRReader()
// if an injected qrCodeData exists, the InstantOnboardingViewController was only opened to handle that
// cancelling the action should also dismiss the whole controller
if self.qrCodeData != nil {
self.cancelAccountCreation()
}
}
)

alert.addAction(okAction)
alert.addAction(qrCancelAction)
if qrCodeReader != nil {
qrCodeReader?.present(alert, animated: true)
} else {
self.present(alert, animated: true)
}
}

private func confirmSetupNewDevice(qrCode: String) {
triggerLocalNetworkPrivacyAlert()
let alert = UIAlertController(title: String.localized("multidevice_receiver_title"),
message: String.localized("multidevice_receiver_scanning_ask"),
preferredStyle: .alert)
alert.addAction(UIAlertAction(
title: String.localized("ok"),
style: .default,
handler: { [weak self] _ in
guard let self else { return }
if self.dcAccounts.getSelected().isConfigured() {
UserDefaults.standard.setValue(self.dcAccounts.getSelected().id, forKey: Constants.Keys.lastSelectedAccountKey)
_ = self.dcAccounts.add()
}
let accountId = self.dcAccounts.getSelected().id
if accountId != 0 {
self.dcContext = self.dcAccounts.get(id: accountId)
self.dismissQRReader()
self.addProgressHudBackupListener(importByFile: false)
self.showProgressAlert(title: String.localized("multidevice_receiver_title"), dcContext: self.dcContext)
self.dcAccounts.stopIo()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self else { return }
logger.info("##### receiveBackup() with qr: \(qrCode)")
let res = self.dcContext.receiveBackup(qrCode: qrCode)
logger.info("##### receiveBackup() done with result: \(res)")
}
}
}
))
alert.addAction(UIAlertAction(
title: String.localized("cancel"),
style: .cancel,
handler: { [weak self] _ in
self?.dcContext.stopOngoingProcess()
self?.dismissQRReader()
}
))
if let qrCodeReader {
qrCodeReader.present(alert, animated: true)
} else {
self.present(alert, animated: true)
}
}

private func qrErrorAlert() {
let title = String.localized("qraccount_qr_code_cannot_be_used")
let alert = UIAlertController(title: title, message: dcContext.lastErrorString, preferredStyle: .alert)
let okAction = UIAlertAction(
title: String.localized("ok"),
style: .default,
handler: { [weak self] _ in
guard let self else { return }
if self.qrCodeData != nil {
// if an injected qrCodeData exists, the WelcomeViewController was only opened to handle that
// if the action failed the whole controller should be dismissed
self.cancelAccountCreation()
} else {
self.qrCodeReader?.startSession()
}
}
)
alert.addAction(okAction)
qrCodeReader?.present(alert, animated: true, completion: nil)
}

private func dismissQRReader() {
self.navigationController?.popViewController(animated: true)
self.qrCodeReader = nil
}

private func stopAccessingSecurityScopedResource() {
self.securityScopedResource?.stopAccessingSecurityScopedResource()
self.securityScopedResource = nil
}

private func createAccountFromQRCode(qrCode: String) {
if dcAccounts.getSelected().isConfigured() {
UserDefaults.standard.setValue(dcAccounts.getSelected().id, forKey: Constants.Keys.lastSelectedAccountKey)
_ = dcAccounts.add()
}
let accountId = dcAccounts.getSelected().id

if accountId != 0 {
dcContext = dcAccounts.get(id: accountId)
addProgressAlertListener(dcAccounts: self.dcAccounts,
progressName: eventConfigureProgress,
onSuccess: self.handleLoginSuccess)
showProgressAlert(title: String.localized("login_header"), dcContext: self.dcContext)
DispatchQueue.global().async { [weak self] in
guard let self else { return }
let success = self.dcContext.setConfigFromQR(qrCode: qrCode)
DispatchQueue.main.async {
if success {
self.dcAccounts.stopIo()
self.dcContext.configure()
} else {
self.updateProgressAlert(error: self.dcContext.lastErrorString,
completion: self.qrCodeData != nil ? self.cancelAccountCreation : nil)
}
}
}
}
}

@objc private func cancelAccountCreation() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
// take a bit care on account removal:
// remove only openend and unconfigured and make sure, there is another account
// (normally, both checks are not needed, however, some resilience wrt future program-flow-changes seems to be reasonable here)
let selectedAccount = dcAccounts.getSelected()
if selectedAccount.isOpen() && !selectedAccount.isConfigured() {
_ = dcAccounts.remove(id: selectedAccount.id)
KeychainManager.deleteAccountSecret(id: selectedAccount.id)
if self.dcAccounts.getAll().isEmpty {
_ = self.dcAccounts.add()
}
}

let lastSelectedAccountId = UserDefaults.standard.integer(forKey: Constants.Keys.lastSelectedAccountKey)
if lastSelectedAccountId != 0 {
_ = dcAccounts.select(id: lastSelectedAccountId)
dcAccounts.startIo()
}

appDelegate.reloadDcContext()
}

private func handleLoginSuccess() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
appDelegate.registerForNotifications()
}

let profileInfoController = ProfileInfoViewController(context: self.dcContext)
profileInfoController.onClose = {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.reloadDcContext()
}
}

navigationController?.setViewControllers([profileInfoController], animated: true)
}

private func addProgressHudBackupListener(importByFile: Bool) {
UIApplication.shared.isIdleTimerDisabled = true
backupProgressObserver = NotificationCenter.default.addObserver(
forName: eventImexProgress,
object: nil,
queue: nil
) { [weak self] notification in
guard let self else { return }
if let ui = notification.userInfo {
if let error = ui["error"] as? Bool, error {
UIApplication.shared.isIdleTimerDisabled = false
if self.dcContext.isConfigured() {
let accountId = self.dcContext.id
_ = self.dcAccounts.remove(id: accountId)
KeychainManager.deleteAccountSecret(id: accountId)
_ = self.dcAccounts.add()
self.dcContext = self.dcAccounts.getSelected()
self.navigationItem.title = String.localized(self.canCancel ? "add_account" : "welcome_desktop")
}
self.updateProgressAlert(error: ui["errorMessage"] as? String)
self.stopAccessingSecurityScopedResource()
self.removeBackupProgressObserver()
} else if let done = ui["done"] as? Bool, done {
UIApplication.shared.isIdleTimerDisabled = false
self.dcAccounts.startIo()
self.updateProgressAlertSuccess(completion: self.handleBackupRestoreSuccess)
self.stopAccessingSecurityScopedResource()
} else if importByFile {
self.updateProgressAlertValue(value: ui["progress"] as? Int)
} else {
guard let permille = ui["progress"] as? Int else { return }
var statusLineText = ""
if permille <= 100 {
statusLineText = String.localized("preparing_account")
} else if permille <= 950 {
let percent = ((permille-100)*100)/850
statusLineText = String.localized("transferring") + " \(percent)%"
} else {
statusLineText = "Finishing..." // range not used, should not happen
}
self.updateProgressAlert(message: statusLineText)
}
}
}
}

private func removeBackupProgressObserver() {
if let backupProgressObserver {
NotificationCenter.default.removeObserver(backupProgressObserver)
self.backupProgressObserver = nil
}
}

private func handleBackupRestoreSuccess() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
appDelegate.registerForNotifications()
}

if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.reloadDcContext()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
if canCancel {
navigationItem.leftBarButtonItem = cancelButton
}
if let accountCode = accountCode {
if let accountCode {
handleQrCode(accountCode)
}
}
Expand Down

0 comments on commit cfe0e18

Please sign in to comment.