Эх сурвалжийг харах

basic UI for iroh-share backup transfer

B. Petersen 2 жил өмнө
parent
commit
57ee543109

+ 8 - 0
DcCore/DcCore.xcodeproj/project.pbxproj

@@ -30,6 +30,8 @@
 		30E8F2482449C98600CE2C90 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2472449C98600CE2C90 /* UIView+Extensions.swift */; };
 		30E8F24B2449CF6500CE2C90 /* InitialsBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F24A2449CF6500CE2C90 /* InitialsBadge.swift */; };
 		30E8F24D2449D30200CE2C90 /* DcColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F24C2449D30200CE2C90 /* DcColors.swift */; };
+		78A8733F287766FA00690A0B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 78A8733E287766FA00690A0B /* libc++.tbd */; };
+		78A873412877679B00690A0B /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78A873402877679B00690A0B /* SystemConfiguration.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -69,6 +71,8 @@
 		30E8F2472449C98600CE2C90 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
 		30E8F24A2449CF6500CE2C90 /* InitialsBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialsBadge.swift; sourceTree = "<group>"; };
 		30E8F24C2449D30200CE2C90 /* DcColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DcColors.swift; sourceTree = "<group>"; };
+		78A8733E287766FA00690A0B /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libc++.tbd"; sourceTree = DEVELOPER_DIR; };
+		78A873402877679B00690A0B /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -76,6 +80,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				78A873412877679B00690A0B /* SystemConfiguration.framework in Frameworks */,
+				78A8733F287766FA00690A0B /* libc++.tbd in Frameworks */,
 				30421959243DE6AD00516852 /* libdeltachat.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -149,6 +155,8 @@
 		30421957243DE61400516852 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				78A873402877679B00690A0B /* SystemConfiguration.framework */,
+				78A8733E287766FA00690A0B /* libc++.tbd */,
 				30421958243DE61400516852 /* libdeltachat.a */,
 			);
 			name = Frameworks;

+ 32 - 0
DcCore/DcCore/DC/Wrapper.swift

@@ -554,6 +554,15 @@ public class DcContext {
     public func imex(what: Int32, directory: String, passphrase: String? = nil) {
         dc_imex(contextPointer, what, directory, passphrase)
     }
+  
+    public func send_backup(directory: String, passphrase: String? = nil) -> DcBackupSender? {
+      guard let dcBackupSenderPointer = dc_send_backup(contextPointer, directory, passphrase) else { return nil }
+      return DcBackupSender(dcBackupSenderPointer)
+    }
+
+    public func receiveBackup(qrCode: String, passphrase: String? = nil) {
+        dc_receive_backup(contextPointer, qrCode, passphrase)
+    }
 
     public func imexHasBackup(filePath: String) -> String? {
         var file: String?
@@ -1402,6 +1411,29 @@ public class DcLot {
     }
 }
 
+public class DcBackupSender {
+  private var dcBackupSenderPointer: OpaquePointer?
+  
+  // takes ownership of specified pointer
+  public init(_ dcBackupSenderPointer: OpaquePointer) {
+      print(">>>> 💙 init DcBackupSender")
+      self.dcBackupSenderPointer = dcBackupSenderPointer
+  }
+
+  deinit {
+      print(">>>> 💙 deinit DcBackupSender")
+      dc_backup_sender_unref(dcBackupSenderPointer)
+  }
+
+  public func qr_code(context: DcContext) -> String? {
+    guard let cString = dc_backup_sender_qr(context.contextPointer, dcBackupSenderPointer) else { return nil }
+    let swiftString = String(cString: cString)
+    dc_str_unref(cString)
+    return swiftString
+    
+  }
+}
+
 public class DcProvider {
     private var dcProviderPointer: OpaquePointer?
 

+ 1 - 0
DcCore/DcCore/DC/wrapper.h

@@ -12,5 +12,6 @@ typedef dc_msg_t dc_msg_t;
 typedef dc_lot_t dc_lot_t;
 typedef dc_array_t dc_array_t;
 typedef dc_chatlist_t dc_chatlist_t;
+typedef dc_backup_sender_t dc_backup_sender_t;
 
 #endif /* wrapper_h */

+ 4 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -132,6 +132,7 @@
 		7837B64021E54DC600CDE126 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 7837B63F21E54DC600CDE126 /* .swiftlint.yml */; };
 		785BE16821E247F1003BE98C /* MessageInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785BE16721E247F1003BE98C /* MessageInfoViewController.swift */; };
 		789E879621D6CB58003ED1C5 /* QrCodeReaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789E879521D6CB58003ED1C5 /* QrCodeReaderController.swift */; };
+		78A8733D2877608200690A0B /* QrBackupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A8733C2877608200690A0B /* QrBackupViewController.swift */; };
 		78E45E3A21D3CFBC00D4B15E /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E3921D3CFBC00D4B15E /* SettingsController.swift */; };
 		78E45E4421D3F14A00D4B15E /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E4321D3F14A00D4B15E /* UIImage+Extension.swift */; };
 		78ED838321D5379000243125 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78ED838221D5379000243125 /* TextFieldCell.swift */; };
@@ -409,6 +410,7 @@
 		785BE16721E247F1003BE98C /* MessageInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInfoViewController.swift; sourceTree = "<group>"; };
 		787D6699229F2237000A7A9D /* libdeltachat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libdeltachat.a; path = "deltachat-ios/libraries/deltachat-core-rust/target/universal/debug/libdeltachat.a"; sourceTree = "<group>"; };
 		789E879521D6CB58003ED1C5 /* QrCodeReaderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeReaderController.swift; sourceTree = "<group>"; };
+		78A8733C2877608200690A0B /* QrBackupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrBackupViewController.swift; sourceTree = "<group>"; };
 		78C7036A21D46752005D4525 /* deltachat-ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "deltachat-ios.entitlements"; sourceTree = "<group>"; };
 		78E45E3921D3CFBC00D4B15E /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
 		78E45E4321D3F14A00D4B15E /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
@@ -941,6 +943,7 @@
 				789E879521D6CB58003ED1C5 /* QrCodeReaderController.swift */,
 				AECEF03D244F2D55006C90DA /* QrPageController.swift */,
 				30149D9222F21129003C12B5 /* QrViewController.swift */,
+				78A8733C2877608200690A0B /* QrBackupViewController.swift */,
 				B21005DA23383664004C70C5 /* SettingsClassicViewController.swift */,
 				30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */,
 				78E45E3921D3CFBC00D4B15E /* SettingsController.swift */,
@@ -1497,6 +1500,7 @@
 				30653081254358B10093E196 /* QuoteView.swift in Sources */,
 				3067AAC62667F3FE00525036 /* ImageFormat.swift in Sources */,
 				30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */,
+				78A8733D2877608200690A0B /* QrBackupViewController.swift in Sources */,
 				30EF7308252F6A3300E2C54A /* PaddingTextView.swift in Sources */,
 				30E348E124F53772005C93D1 /* ImageTextCell.swift in Sources */,
 				3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */,

+ 239 - 0
deltachat-ios/Controller/QrBackupViewController.swift

@@ -0,0 +1,239 @@
+import Foundation
+import UIKit
+import DcCore
+import SDWebImageSVGKitPlugin
+
+protocol QrBackupViewControllerDelegate: NSObject {
+    func onBackupFinished(sender: QrBackupViewController)
+    func onBackupStarted()
+    func isPreviousBackupRunning() -> Bool
+    func onDismissed()
+}
+
+class QrBackupViewController: UIViewController {
+
+    private let dcContext: DcContext
+    private let dcAccounts: DcAccounts
+    private var dcBackupSender: DcBackupSender?
+    // this delegate is not weak on purpose: we manually remove the referece once the ViewController has
+    // been dismissed if no backup is in progress OR if the ongoing backup finished and the
+    // ViewController has been dismissed in the past
+    public var delegate: QrBackupViewControllerDelegate?
+
+    private lazy var qrContentView: UIImageView = {
+        let view = UIImageView()
+        view.contentMode = .scaleAspectFit
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.accessibilityHint = String.localized("scan_to_transfer")
+        return view
+    }()
+
+    private lazy var progressContainer: UIView = {
+        let view = UIView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.layer.cornerRadius = 20
+        view.clipsToBounds = true
+        view.layer.borderColor = DcColors.grey50.cgColor
+        view.layer.borderWidth = 1
+        view.backgroundColor = DcColors.defaultInverseColor.withAlphaComponent(0.5)
+        return view
+    }()
+
+    private lazy var progress: UIActivityIndicatorView = {
+        let progress = UIActivityIndicatorView(style: .white)
+        progress.translatesAutoresizingMaskIntoConstraints = false
+        return progress
+    }()
+
+    private lazy var blurView: UIVisualEffectView = {
+        let blurEffect = UIBlurEffect(style: .light)
+        let view = UIVisualEffectView(effect: blurEffect)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    private var isBackupRunning = false
+    private var progressObserver: NSObjectProtocol?
+
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
+        super.init(nibName: nil, bundle: nil)
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        logger.debug(">>>>>>>>>>>>>>>>> viewDidLoad")
+        title = String.localized("setup_new_device")
+        setupSubviews()
+        view.backgroundColor = DcColors.defaultBackgroundColor
+        if let delegate = delegate, !delegate.isPreviousBackupRunning() {
+            logger.debug(">>>> viewDidLoad no previous Backup Running starting Backup")
+            startBackup()
+        }
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        logger.debug(">>>> onViewDidDisappear")
+        delegate?.onDismissed()
+        if isBackupRunning {
+            logger.debug(">>>> stop ongoingProcess")
+            self.dcContext.stopOngoingProcess()
+        } else {
+            delegate = nil
+            dcBackupSender = nil
+            logger.debug(">>>> nil delegate")
+        }
+    }
+
+    public func startBackup() {
+        let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+        if !documents.isEmpty {
+            self.isBackupRunning = true
+            logger.debug(">>>> startBackup isBackupRunning: \(isBackupRunning)")
+            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+                guard let self = self else { return }
+                self.delegate?.onBackupStarted()
+                self.dcAccounts.stopIo()
+                self.dcBackupSender = self.dcContext.send_backup(directory: documents[0], passphrase: nil)
+                self.dcAccounts.startIo()
+                self.delegate?.onBackupFinished(sender: self)
+                DispatchQueue.main.async {
+                    self.isBackupRunning = false
+                    logger.debug(">>>> startBackup finishing - isBackupRunning: \(self.isBackupRunning)")
+                    logger.debug(">>>> startBackup has parent: \(self.parent != nil)")
+                    if self.parent == nil {
+                        logger.debug(">>>> nil delegate")
+                        self.delegate = nil
+                        self.dcBackupSender = nil
+                    } else {
+                        let image = self.getQrImage(svg: self.dcBackupSender?.qr_code(context: self.dcContext))
+                        self.qrContentView.image = image
+                        self.progress.stopAnimating()
+                        self.progressContainer.isHidden = true
+                    }
+                }
+            }
+        } else {
+            logger.error("document directory not found")
+            self.dcBackupSender = nil
+        }
+    }
+
+    // MARK: - setup
+    private func setupSubviews() {
+        view.addSubview(qrContentView)
+        view.addSubview(progressContainer)
+        progressContainer.addSubview(blurView)
+        progressContainer.addSubview(progress)
+        let qrDefaultWidth = qrContentView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.75)
+        qrDefaultWidth.priority = UILayoutPriority(500)
+        qrDefaultWidth.isActive = true
+        let qrMinWidth = qrContentView.widthAnchor.constraint(lessThanOrEqualToConstant: 260)
+        qrMinWidth.priority = UILayoutPriority(999)
+        qrMinWidth.isActive = true
+        view.addConstraints([
+            qrContentView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 1.05),
+            qrContentView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
+            qrContentView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
+            progressContainer.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
+            progressContainer.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
+            progressContainer.constraintHeightTo(100),
+            progressContainer.constraintWidthTo(100),
+            progress.constraintCenterXTo(progressContainer),
+            progress.constraintCenterYTo(progressContainer)
+        ])
+        blurView.fillSuperview()
+        progressContainer.isHidden = false
+        progress.startAnimating()
+    }
+    
+    private func getQrImage(svg: String?) -> UIImage? {
+        if let svg = svg {
+            let svgData = svg.data(using: .utf8)
+            return SDImageSVGKCoder.shared.decodedImage(with: svgData, options: [:])
+        }
+        return nil
+    }
+
+}
+
+
+/// Does a best effort attempt to trigger the local network privacy alert.
+///
+/// It works by sending a UDP datagram to the discard service (port 9) of every
+/// IP address associated with a broadcast-capable interface. This should
+/// trigger the local network privacy alert, assuming the alert hasn’t already
+/// been displayed for this app.
+///
+/// This code takes a ‘best effort’. It handles errors by ignoring them. As
+/// such, there’s guarantee that it’ll actually trigger the alert.
+///
+/// - note: iOS devices don’t actually run the discard service. I’m using it
+/// here because I need a port to send the UDP datagram to and port 9 is
+/// always going to be safe (either the discard service is running, in which
+/// case it will discard the datagram, or it’s not, in which case the TCP/IP
+/// stack will discard it).
+///
+/// There should be a proper API for this (r. 69157424).
+///
+/// For more background on this, see [Triggering the Local Network Privacy Alert](https://developer.apple.com/forums/thread/663768).
+/// [via https://developer.apple.com/forums/thread/663768 ]
+func triggerLocalNetworkPrivacyAlert() {
+    let sock4 = socket(AF_INET, SOCK_DGRAM, 0)
+    guard sock4 >= 0 else { return }
+    defer { close(sock4) }
+    let sock6 = socket(AF_INET6, SOCK_DGRAM, 0)
+    guard sock6 >= 0 else { return }
+    defer { close(sock6) }
+
+    let addresses = addressesOfDiscardServiceOnBroadcastCapableInterfaces()
+    var message = [UInt8]("!".utf8)
+    for address in addresses {
+        address.withUnsafeBytes { buf in
+            let sa = buf.baseAddress!.assumingMemoryBound(to: sockaddr.self)
+            let saLen = socklen_t(buf.count)
+            let sock = sa.pointee.sa_family == AF_INET ? sock4 : sock6
+            _ = sendto(sock, &message, message.count, MSG_DONTWAIT, sa, saLen)
+        }
+    }
+}
+
+/// Returns the addresses of the discard service (port 9) on every
+/// broadcast-capable interface.
+///
+/// Each array entry is contains either a `sockaddr_in` or `sockaddr_in6`.
+private func addressesOfDiscardServiceOnBroadcastCapableInterfaces() -> [Data] {
+    var addrList: UnsafeMutablePointer<ifaddrs>? = nil
+    let err = getifaddrs(&addrList)
+    guard err == 0, let start = addrList else { return [] }
+    defer { freeifaddrs(start) }
+    return sequence(first: start, next: { $0.pointee.ifa_next })
+        .compactMap { i -> Data? in
+            guard
+                (i.pointee.ifa_flags & UInt32(bitPattern: IFF_BROADCAST)) != 0,
+                let sa = i.pointee.ifa_addr
+            else { return nil }
+            var result = Data(UnsafeRawBufferPointer(start: sa, count: Int(sa.pointee.sa_len)))
+            switch CInt(sa.pointee.sa_family) {
+            case AF_INET:
+                result.withUnsafeMutableBytes { buf in
+                    let sin = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in.self)
+                    sin.pointee.sin_port = UInt16(9).bigEndian
+                }
+            case AF_INET6:
+                result.withUnsafeMutableBytes { buf in
+                    let sin6 = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in6.self)
+                    sin6.pointee.sin6_port = UInt16(9).bigEndian
+                }
+            default:
+                return nil
+            }
+            return result
+        }
+}

+ 3 - 1
deltachat-ios/Controller/QrPageController.swift

@@ -131,6 +131,7 @@ extension QrPageController: UIPageViewControllerDataSource, UIPageViewController
         if viewController is QrViewController {
             return nil
         }
+        
         return QrViewController(dcContext: dcContext, qrCodeHint: qrCodeHint)
     }
 
@@ -138,6 +139,7 @@ extension QrPageController: UIPageViewControllerDataSource, UIPageViewController
         if viewController is QrViewController {
             return makeQRReader()
         }
+
         return nil
     }
 
@@ -211,7 +213,7 @@ extension QrPageController: QrCodeReaderDelegate {
             }))
             present(alert, animated: true, completion: nil)
 
-        case DC_QR_ACCOUNT:
+        case DC_QR_ACCOUNT, DC_QR_BACKUP:
             if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
                 appDelegate.appCoordinator.presentWelcomeController(accountCode: code)
             }

+ 52 - 2
deltachat-ios/Controller/SettingsController.swift

@@ -30,6 +30,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         case videoChat = 15
         case connectivity = 16
         case selectBackground = 17
+        case qrCodeBackup = 18
     }
 
     private var dcContext: DcContext
@@ -188,6 +189,13 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         return cell
     }()
 
+    private lazy var qrCodeBackupCell: ActionCell = {
+        let cell = ActionCell()
+        cell.tag = CellTags.qrCodeBackup.rawValue
+        cell.actionTitle = String.localized("setup_new_device")
+        return cell
+    }()
+
     private lazy var advancedCell: ActionCell = {
         let cell = ActionCell()
         cell.tag = CellTags.advanced.rawValue
@@ -226,6 +234,9 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         return cell
     }()
 
+    private var qrCodeBackupRunning = false
+    private var qrBackupViewController: QrBackupViewController?
+
     private lazy var sections: [SectionConfigs] = {
         var appNameAndVersion = "Delta Chat"
         if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
@@ -233,8 +244,8 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         }
         let profileSection = SectionConfigs(
             headerTitle: String.localized("pref_profile_info_headline"),
-            footerTitle: nil,
-            cells: [profileCell, switchAccountCell]
+            footerTitle: "Link another device to this account",
+            cells: [profileCell, switchAccountCell, qrCodeBackupCell]
         )
         let preferencesSection = SectionConfigs(
             headerTitle: String.localized("pref_chats_and_media"),
@@ -367,6 +378,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         case .autocryptPreferences: break
         case .sendAutocryptMessage: sendAutocryptSetupMessage()
         case .exportBackup: createBackup()
+        case .qrCodeBackup: showQRBackupViewController()
         case .advanced: showAdvancedDialog()
         case .switchAccount: showSwitchAccountMenu()
         case .help: showHelp()
@@ -395,6 +407,15 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         present(alert, animated: true, completion: nil)
     }
 
+    private func showQRBackupViewController() {
+        qrBackupViewController = QrBackupViewController(dcAccounts: dcAccounts)
+        guard let qrBackupViewController = qrBackupViewController else {
+            return
+        }
+        qrBackupViewController.delegate = self
+        navigationController?.pushViewController(qrBackupViewController, animated: true)
+    }
+
     @objc private func handleNotificationToggle(_ sender: UISwitch) {
         UserDefaults.standard.set(!sender.isOn, forKey: "notifications_disabled")
         if sender.isOn {
@@ -737,3 +758,32 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         DBDebugToolkit.showMenu()
     }
 }
+
+extension SettingsViewController: QrBackupViewControllerDelegate {
+    func onBackupFinished(sender: QrBackupViewController) {
+        qrCodeBackupRunning = false
+        logger.debug(">>>> onBackupFinished qrCodeBackupRunning \(qrCodeBackupRunning)")
+        if let qrBackupViewController = qrBackupViewController,
+        sender !== qrBackupViewController {
+            logger.debug(">>>> onBackupFinished starting new Backup for different VC")
+            qrBackupViewController.startBackup()
+        } else {
+            logger.debug(">>>> onBackupFinished no new qrBackup")
+        }
+    }
+
+    func onBackupStarted() {
+        qrCodeBackupRunning = true
+        logger.debug(">>>> onBackupStarted qrCodeBackupRunning \(qrCodeBackupRunning)")
+    }
+
+    func isPreviousBackupRunning() -> Bool {
+        logger.debug(">>>> isPreviousBackupRunning \(qrCodeBackupRunning)")
+        return qrCodeBackupRunning
+    }
+
+    func onDismissed() {
+        logger.debug(">>>> onDismissed: nil qrBackupViewController")
+        qrBackupViewController = nil
+    }
+}

+ 30 - 1
deltachat-ios/Controller/WelcomeViewController.swift

@@ -316,6 +316,8 @@ extension WelcomeViewController: QrCodeReaderDelegate {
         let lot = dcContext.checkQR(qrCode: code)
         if let domain = lot.text1, lot.state == DC_QR_ACCOUNT {
             confirmAccountCreationAlert(accountDomain: domain, qrCode: code)
+        } else if lot.state == DC_QR_BACKUP {
+            confirmSetupNewDeviceAlert(qrCode: code)
         } else {
             qrErrorAlert()
         }
@@ -360,6 +362,33 @@ extension WelcomeViewController: QrCodeReaderDelegate {
         }
     }
 
+    private func confirmSetupNewDeviceAlert(qrCode: String) {
+        triggerLocalNetworkPrivacyAlert()
+        let alert = UIAlertController(title: String.localized("setup_new_device"),
+                                      message: "Set up the account from the other device here? Data on the other device are not altered.",
+                                      preferredStyle: .alert)
+        alert.addAction(UIAlertAction(
+            title: String.localized("ok"),
+            style: .default,
+            handler: { [weak self] _ in
+                guard let self = self else { return }
+                self.dismissQRReader()
+                self.addProgressHudBackupListener()
+                self.showProgressAlert(title: String.localized("setup_new_device"), dcContext: self.dcContext)
+                self.dcAccounts.stopIo()
+                self.dcContext.receiveBackup(qrCode: qrCode)
+            }
+        ))
+        alert.addAction(UIAlertAction(
+            title: String.localized("cancel"),
+            style: .cancel,
+            handler: { [weak self] _ in
+                self?.dismissQRReader()
+            }
+        ))
+        qrCodeReader?.present(alert, animated: true)
+    }
+
     private func qrErrorAlert() {
         let title = String.localized("qraccount_qr_code_cannot_be_used")
         let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
@@ -451,7 +480,7 @@ class WelcomeContentView: UIView {
 
     private lazy var qrCodeButton: UIButton = {
         let button = UIButton()
-        let title = String.localized("scan_invitation_code")
+        let title = String.localized("qrscan_title")
         button.setTitleColor(UIColor.systemBlue, for: .normal)
         button.setTitle(title, for: .normal)
         button.addTarget(self, action: #selector(qrCodeButtonPressed(_:)), for: .touchUpInside)

+ 2 - 0
deltachat-ios/en.lproj/Localizable.strings

@@ -896,3 +896,5 @@
 "webxdc_empty_hint" = "Received or sent apps will appear here. Tap \"Files\" to select downloaded apps.";
 "shortcut_share_btn" = "Click the share button";
 "shortcut_add_to_home_description" = "Select - Add to Home Screen - to add the app to your home screen.";
+"setup_new_device" = "Link Device";
+"scan_to_transfer" = "Scan this QR code to transfer your account to a new device.";

+ 2 - 0
scripts/untranslated.xml

@@ -6,4 +6,6 @@
     <string name ="webxdc_empty_hint">Received or sent apps will appear here. Tap \"Files\" to select downloaded apps.</string>
     <string name ="shortcut_share_btn">Click the share button</string>
     <string name ="shortcut_add_to_home_description">Select - Add to Home Screen - to add the app to your home screen.</string>
+    <string name="setup_new_device">Link Device</string>
+    <string name="scan_to_transfer">Scan this QR code to transfer your account to a new device.</string>
 </resources>