diff --git a/MobileWallet.xcodeproj/project.pbxproj b/MobileWallet.xcodeproj/project.pbxproj index 310ff82d..018dc1c1 100644 --- a/MobileWallet.xcodeproj/project.pbxproj +++ b/MobileWallet.xcodeproj/project.pbxproj @@ -422,6 +422,9 @@ 37B48A8324B3968F00F8A8D2 /* BPKeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B48A8224B3968F00F8A8D2 /* BPKeychainWrapper.swift */; }; 37B48A8424B3968F00F8A8D2 /* BPKeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B48A8224B3968F00F8A8D2 /* BPKeychainWrapper.swift */; }; 37B48A8524B3968F00F8A8D2 /* BPKeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B48A8224B3968F00F8A8D2 /* BPKeychainWrapper.swift */; }; + 37B5289424CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B5289324CB1E16008C80EB /* NetworkSpeedProvider.swift */; }; + 37B5289524CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B5289324CB1E16008C80EB /* NetworkSpeedProvider.swift */; }; + 37B5289624CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B5289324CB1E16008C80EB /* NetworkSpeedProvider.swift */; }; 37BB696B245705D20013AC4D /* RadialGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BB696A245705D20013AC4D /* RadialGradientView.swift */; }; 37BB696C245705E60013AC4D /* RadialGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BB696A245705D20013AC4D /* RadialGradientView.swift */; }; 37BB696D245705E70013AC4D /* RadialGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BB696A245705D20013AC4D /* RadialGradientView.swift */; }; @@ -769,6 +772,7 @@ 37B444A8248949B800592D92 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 37B48A7E24B3312000F8A8D2 /* PasswordVerificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordVerificationViewController.swift; sourceTree = ""; }; 37B48A8224B3968F00F8A8D2 /* BPKeychainWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPKeychainWrapper.swift; sourceTree = ""; }; + 37B5289324CB1E16008C80EB /* NetworkSpeedProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSpeedProvider.swift; sourceTree = ""; }; 37BB696A245705D20013AC4D /* RadialGradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialGradientView.swift; sourceTree = ""; }; 37BB6973245726FB0013AC4D /* CopyableLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabel.swift; sourceTree = ""; }; 37C8BA342480F59B005BBC05 /* WaveEmojiAnimation.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = WaveEmojiAnimation.json; sourceTree = ""; }; @@ -972,6 +976,7 @@ 37765D2524A35BA20091AE2A /* AESEncryption.swift */, 37B48A8224B3968F00F8A8D2 /* BPKeychainWrapper.swift */, 372AECAA24B4A293005CBA0F /* UserDefaultsWrapper.swift */, + 37B5289324CB1E16008C80EB /* NetworkSpeedProvider.swift */, ); path = Common; sourceTree = ""; @@ -2250,6 +2255,7 @@ BFF1ED9D2408111000CC9EF6 /* SendingTariViewController.swift in Sources */, 3776080924B73500008167E8 /* Backup.swift in Sources */, 00369D7D24221ACB00BAB95B /* DeepLinkManager.swift in Sources */, + 37B5289424CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */, 001F6CEC2381A39300FA7002 /* CompletedTransaction.swift in Sources */, 371A0818247290C000F97713 /* TransitionLabel.swift in Sources */, 00E2BCB2236B455900C2A105 /* HomeViewFloatingPanelDelegates.swift in Sources */, @@ -2287,6 +2293,7 @@ 0053873124065F6C00901A68 /* SlideView.swift in Sources */, BFE61E4924226A7E003DC99D /* NotificationManager.swift in Sources */, 37049271247EA0770034EE5D /* RestoreWalletViewController.swift in Sources */, + 37B5289524CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */, 0091D4AC23ED87BE004BF7F7 /* KeyServer.swift in Sources */, 004997BC2382F5ED000A0B7D /* WalletTestDataExtension.swift in Sources */, 3723A7AC24ACD03E003382EB /* PasswordField.swift in Sources */, @@ -2530,6 +2537,7 @@ 0074619D239A57B000F00966 /* UIBarButtonItem.swift in Sources */, 3776080B24B75900008167E8 /* Backup.swift in Sources */, 00434A9F2477D0B000C0104F /* AppContainerLock.swift in Sources */, + 37B5289624CB1E16008C80EB /* NetworkSpeedProvider.swift in Sources */, 3708D758247FF81900807D72 /* SettingsParentViewController.swift in Sources */, 37547D5824601BF600EB59CC /* UIView+GlobalFrame.swift in Sources */, 375894C724865A6A00B58816 /* VerifyPhraseViewController.swift in Sources */, diff --git a/MobileWallet/Backup/ICloudBackup.swift b/MobileWallet/Backup/ICloudBackup.swift index 35721ad6..7a54e971 100644 --- a/MobileWallet/Backup/ICloudBackup.swift +++ b/MobileWallet/Backup/ICloudBackup.swift @@ -232,7 +232,7 @@ class ICloudBackup: NSObject { } try FileManager.default.copyItem(at: fileURL, to: walletFolderURL.appendingPathComponent(fileURL.lastPathComponent)) - + isLastBackupFailed = false inProgress = true progressValue = 0.0 BackupScheduler.shared.removeSchedule() @@ -338,14 +338,15 @@ extension ICloudBackup { if let fileUploaded = fileItem?.value(forAttribute: NSMetadataUbiquitousItemIsUploadedKey) as? Bool, fileUploaded == true, fileValues.ubiquitousItemIsUploading == false { progressValue = 0.0 inProgress = false - isLastBackupFailed = false notifyObservers(percent: 100, completed: true, error: nil) try cleanTempDirectory() + query.disableUpdates() } else if let error = fileValues.ubiquitousItemUploadingError { progressValue = 0.0 inProgress = false isLastBackupFailed = true notifyObservers(percent: 0, completed: false, error: error) + query.disableUpdates() } else { if let fileProgress = fileItem?.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double { progressValue = fileProgress @@ -355,17 +356,20 @@ extension ICloudBackup { } catch { isLastBackupFailed = true inProgress = false - notifyObservers(percent: 0, completed: false, error: ICloudBackupError.uploadToICloudFailure) + notifyObservers(percent: 0, completed: false, error: ICloudBackupError.noInternetConnection) } } private func notifyObservers(percent: Double, started: Bool = false, completed: Bool, error: Error?) { - observers.allObjects.forEach { - if let object = $0 as? ICloudBackupObserver { - object.onUploadProgress(percent: percent, started: started, completed: completed, error: error) - } - if completed { - self.endBackgroundBackupTask() + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.observers.allObjects.forEach { + if let object = $0 as? ICloudBackupObserver { + object.onUploadProgress(percent: percent, started: started, completed: completed, error: error) + } + if completed { + self.endBackgroundBackupTask() + } } } } @@ -419,28 +423,35 @@ extension ICloudBackup { // MARK: - private methods extension ICloudBackup { private func syncWithICloud() { - if isInternetConnected() { - query.operationQueue?.addOperation({ [weak self] in - _ = self?.query.start() - self?.query.enableUpdates() - }) - } else { - if !inProgress { return } - query.stop() - inProgress = false - if isValidBackupExists() { - notifyObservers(percent: 100, completed: true, error: ICloudBackupError.uploadToICloudFailure) + checkNetworkConnection { [weak self] (connected) in + guard let self = self else { return } + if connected { + self.query.operationQueue?.addOperation({ [weak self] in + _ = self?.query.start() + self?.query.enableUpdates() + }) + } else { + if !self.inProgress { return } + self.query.stop() + self.inProgress = false + self.isLastBackupFailed = true + self.notifyObservers(percent: 0, completed: false, error: ICloudBackupError.noInternetConnection) } } } - private func isInternetConnected() -> Bool { - guard let reachability = self.reachability else { return false } - switch reachability.connection { - case .wifi, .cellular: - return true - case .unavailable, .none: - return false + private func checkNetworkConnection(completion: @escaping (_ connected: Bool) -> Void) { + let speedTest = NetworkSpeedProvider() + speedTest.testSpeed { (_ speed: Float, _ error: Error?) in + if speed <= 0 || error != nil { completion(false) } + + guard let reachability = self.reachability else { completion(false); return } + switch reachability.connection { + case .wifi, .cellular: + completion(true) + case .unavailable, .none: + completion(false) + } } } @@ -505,20 +516,26 @@ extension ICloudBackup { let folderPath = backup.folderPath // if the last path component contains the “.icloud” extension. If yes the file is not on the device else the file is already downloaded. if lastPathComponent.contains(".icloud") { - if !isInternetConnected() { completion(nil, ICloudBackupError.noInternetConnection) } - - lastPathComponent.removeFirst() - let downloadedFilePath = folderPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "") - try FileManager.default.startDownloadingUbiquitousItem(at: backup.url) - - Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in - if FileManager.default.fileExists(atPath: downloadedFilePath) { - timer.invalidate() - do { - let backup = try Backup(url: URL(fileURLWithPath: downloadedFilePath)) - completion(backup, nil) - } catch { - completion(nil, error) + checkNetworkConnection { (connected) in + if !connected { completion(nil, ICloudBackupError.noInternetConnection) } + + lastPathComponent.removeFirst() + let downloadedFilePath = folderPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "") + do { + try FileManager.default.startDownloadingUbiquitousItem(at: backup.url) + } catch { + completion(nil, ICloudBackupError.noInternetConnection) + } + + Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in + if FileManager.default.fileExists(atPath: downloadedFilePath) { + timer.invalidate() + do { + let backup = try Backup(url: URL(fileURLWithPath: downloadedFilePath)) + completion(backup, nil) + } catch { + completion(nil, error) + } } } } diff --git a/MobileWallet/Common/Extensions/LAContext.swift b/MobileWallet/Common/Extensions/LAContext.swift index 69c4a488..1cca0fac 100644 --- a/MobileWallet/Common/Extensions/LAContext.swift +++ b/MobileWallet/Common/Extensions/LAContext.swift @@ -76,7 +76,7 @@ extension LAContext { } } - func authenticateUser(reason: AuthenticateUserReason = .logIn, onSuccess: @escaping () -> Void) { + func authenticateUser(reason: AuthenticateUserReason = .logIn, showFailedDialog: Bool = true, onSuccess: @escaping () -> Void) { #if targetEnvironment(simulator) //Skip auth on simulator, quicker for development onSuccess() @@ -91,15 +91,14 @@ extension LAContext { [weak self] success, error in DispatchQueue.main.async { [weak self] in + guard let self = self else { return } if success { onSuccess() } else { + if !showFailedDialog { return } let localizedReason = error?.localizedDescription ?? NSLocalizedString("authentication.fail.description", comment: "Authentication") TariLogger.error("Biometrics auth failed", error: error) - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.authenticationFailedAlertOptions(reason: localizedReason, onSuccess: onSuccess) - } + self.authenticationFailedAlertOptions(reason: localizedReason, onSuccess: onSuccess) } } } @@ -125,14 +124,16 @@ extension LAContext { private func authenticationFailedAlertOptions(reason: String, onSuccess: @escaping () -> Void) { let alert = UIAlertController(title: NSLocalizedString("authentication.fail.title", comment: "Authentication"), message: reason, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("Try again", comment: "Authentication"), style: .default, handler: { [weak self] _ in + alert.addAction(UIAlertAction(title: NSLocalizedString("authentication.try_again", comment: "Authentication"), style: .default, handler: { [weak self] _ in guard let self = self else { return } self.authenticateUser(onSuccess: onSuccess) })) alert.addAction(UIAlertAction(title: NSLocalizedString("authentication.action.open_settings", comment: "Authentication"), style: .default, handler: { [weak self] _ in guard let self = self else { return } - self.openAppSettings() + if self.openAppSettings() { + self.authenticationFailedAlertOptions(reason: reason, onSuccess: onSuccess) + } })) if let topController = UIApplication.shared.topController() { @@ -140,9 +141,11 @@ extension LAContext { } } - private func openAppSettings() { + private func openAppSettings() -> Bool { if let appSettings = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(appSettings) + return true } + return false } } diff --git a/MobileWallet/Common/Extensions/UIApplication+KeyWindow.swift b/MobileWallet/Common/Extensions/UIApplication+KeyWindow.swift index d2724370..e6d24c50 100644 --- a/MobileWallet/Common/Extensions/UIApplication+KeyWindow.swift +++ b/MobileWallet/Common/Extensions/UIApplication+KeyWindow.swift @@ -42,16 +42,11 @@ import UIKit extension UIApplication { var keyWindow: UIWindow? { - return UIApplication.shared.connectedScenes - .filter({$0.activationState == .foregroundActive}) - .map({$0 as? UIWindowScene}) - .compactMap({$0}) - .first?.windows - .filter({$0.isKeyWindow}).first + return UIApplication.shared.windows.filter({$0.isKeyWindow}).first } func topController() -> UIViewController? { - if var topController = UIApplication.shared.windows.filter({$0.isKeyWindow}).first?.rootViewController { + if var topController = keyWindow?.rootViewController { while let presentedViewController = topController.presentedViewController { topController = presentedViewController } diff --git a/MobileWallet/Common/Extensions/UIScrollView+RefreshControl.swift b/MobileWallet/Common/Extensions/UIScrollView+RefreshControl.swift index f5eed79c..9a4edc10 100644 --- a/MobileWallet/Common/Extensions/UIScrollView+RefreshControl.swift +++ b/MobileWallet/Common/Extensions/UIScrollView+RefreshControl.swift @@ -44,7 +44,7 @@ extension UIScrollView { func beginRefreshing() { guard let refreshControl = refreshControl, !refreshControl.isRefreshing else { return } - let refreshControlHeight: CGFloat = 60.0 // static because if fast drag tableView refreshControl height will not correct + let refreshControlHeight: CGFloat = 70.0 // static because if fast drag tableView refreshControl height will not correct let contentOffset = CGPoint(x: 0, y: -refreshControlHeight - contentInset.top) refreshControl.beginRefreshing() refreshControl.sendActions(for: .valueChanged) diff --git a/MobileWallet/Common/NetworkSpeedProvider.swift b/MobileWallet/Common/NetworkSpeedProvider.swift new file mode 100644 index 00000000..397a0770 --- /dev/null +++ b/MobileWallet/Common/NetworkSpeedProvider.swift @@ -0,0 +1,88 @@ +// NetworkSpeedProvider.swift + +/* + Package MobileWallet + Created by S.Shovkoplyas on 24.07.2020 + Using Swift 5.0 + Running on macOS 10.15 + + Copyright 2019 The Tari Project + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Foundation + +class NetworkSpeedProvider: NSObject { + + typealias SpeedTestCompletion = (_ megabytesPerSecond: Float, _ error: Error?) -> Void + + private var startTime = CFAbsoluteTime() + private var stopTime = CFAbsoluteTime() + private var bytesReceived: Float = 0 + private var speedTestCompletionHandler: SpeedTestCompletion? + + func testSpeed(completion: @escaping SpeedTestCompletion) { + testDownloadSpeed(withTimout: 5.0, completionHandler: completion) + } +} + +extension NetworkSpeedProvider: URLSessionDataDelegate, URLSessionDelegate { + func testDownloadSpeed(withTimout timeout: TimeInterval, completionHandler: @escaping SpeedTestCompletion) { + let urlForSpeedTest = URL(string: "https://images.apple.com/v/imac-with-retina/a/images/overview/5k_image.jpg") + startTime = CFAbsoluteTimeGetCurrent() + stopTime = startTime + bytesReceived = 0 + speedTestCompletionHandler = completionHandler + let configuration = URLSessionConfiguration.ephemeral + configuration.timeoutIntervalForResource = timeout + let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + + guard let checkedUrl = urlForSpeedTest else { return } + + session.dataTask(with: checkedUrl).resume() + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + bytesReceived += Float(data.count) + stopTime = CFAbsoluteTimeGetCurrent() + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + let elapsed = (stopTime - startTime) //as? CFAbsoluteTime + let speed: Float = elapsed != 0 ? bytesReceived / (Float(CFAbsoluteTimeGetCurrent() - startTime)) / 1024.0 / 1024.0 : -1.0 + // treat timeout as no error (as we're testing speed, not worried about whether we got entire resource or not + if error == nil || ((((error as NSError?)?.domain) == NSURLErrorDomain) && (error as NSError?)?.code == NSURLErrorTimedOut) { + speedTestCompletionHandler?(speed, nil) + } else { + speedTestCompletionHandler?(speed, error) + } + } +} diff --git a/MobileWallet/Screens/AppEntry/Splash/SplashViewController.swift b/MobileWallet/Screens/AppEntry/Splash/SplashViewController.swift index b6e83387..2ee2e5b2 100644 --- a/MobileWallet/Screens/AppEntry/Splash/SplashViewController.swift +++ b/MobileWallet/Screens/AppEntry/Splash/SplashViewController.swift @@ -290,7 +290,7 @@ class SplashViewController: UIViewController, UITextViewDelegate { let homeViewController = HomeViewController() nav.setViewControllers([homeViewController], animated: false) - if let window = UIApplication.shared.windows.first { + if let window = UIApplication.shared.keyWindow { let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false) homeViewController.view.addSubview(overlayView) window.rootViewController = nav diff --git a/MobileWallet/Screens/AppEntry/Splash/WalletCreationViewController.swift b/MobileWallet/Screens/AppEntry/Splash/WalletCreationViewController.swift index e6046c70..81a193f5 100644 --- a/MobileWallet/Screens/AppEntry/Splash/WalletCreationViewController.swift +++ b/MobileWallet/Screens/AppEntry/Splash/WalletCreationViewController.swift @@ -83,6 +83,8 @@ class WalletCreationViewController: UIViewController { private var continueButtonSecondShowConstraint: NSLayoutConstraint? private var continueButtonShowConstraint: NSLayoutConstraint? + private let localAuth = LAContext() + private let radialGradient: RadialGradientView = RadialGradientView(insideColor: Theme.shared.colors.accessAnimationViewShadow!, outsideColor: Theme.shared.colors.creatingWalletBackground!) // MARK: - Override functions @@ -212,14 +214,14 @@ class WalletCreationViewController: UIViewController { DispatchQueue.main.async { Tracker.shared.track("/onboarding/enable_push_notif", "Onboarding - Enable Push Notifications") - let newNavigationController = AlwaysPoppableNavigationController() + let nav = AlwaysPoppableNavigationController() let homeViewController = HomeViewController() - newNavigationController.setViewControllers([homeViewController], animated: false) + nav.setViewControllers([homeViewController], animated: false) - if let window = UIApplication.shared.windows.first { + if let window = UIApplication.shared.keyWindow { let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false) homeViewController.view.addSubview(overlayView) - window.rootViewController = newNavigationController + window.rootViewController = nav UIView.animate(withDuration: 0.4, delay: 0, options: .transitionCrossDissolve, animations: { overlayView.alpha = 0 @@ -232,8 +234,7 @@ class WalletCreationViewController: UIViewController { } private func runAuth() { - let context = LAContext() - context.authenticateUser(onSuccess: successAuth) + localAuth.authenticateUser(onSuccess: successAuth) } private func successAuth() { diff --git a/MobileWallet/Screens/Home/TransactionHistory/TransactionsTableViewController.swift b/MobileWallet/Screens/Home/TransactionHistory/TransactionsTableViewController.swift index f2038db2..df25da82 100644 --- a/MobileWallet/Screens/Home/TransactionHistory/TransactionsTableViewController.swift +++ b/MobileWallet/Screens/Home/TransactionHistory/TransactionsTableViewController.swift @@ -181,11 +181,14 @@ class TransactionsTableViewController: UITableViewController { @objc private func unregisterEvents() { animatedRefresher.animateOut() + animatedRefresher.stateType = .none TariEventBus.unregister(self) } private func beginRefreshing() { tableView.reloadData() + if animatedRefresher.stateType != .none { return } + animatedRefresher.stateType = .updateData animatedRefresher.updateState(.loading) animatedRefresher.animateIn() @@ -206,16 +209,16 @@ class TransactionsTableViewController: UITableViewController { } private func endRefreshingWithSuccess() { + if animatedRefresher.stateType != .updateData { return } guard let refreshControl = tableView.refreshControl, refreshControl.isRefreshing else { return } animatedRefresher.updateState(.success) DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { [weak self] in guard let self = self else { return } self.animatedRefresher.animateOut { [weak self] in - self?.safeRefreshTable() - if !ICloudBackup.shared.inProgress && !BackupScheduler.shared.isBackupScheduled { + self?.safeRefreshTable({ [weak self] in self?.tableView.endRefreshing() - } + }) } }) } @@ -225,9 +228,10 @@ class TransactionsTableViewController: UITableViewController { beginRefreshing() } - func safeRefreshTable() { + func safeRefreshTable(_ completion:(() -> Void)? = nil) { TariLib.shared.waitIfWalletIsRestarting { [weak self] _ in self?.refreshTable() + completion?() } } @@ -372,7 +376,7 @@ extension TransactionsTableViewController: ICloudBackupObserver { private func observeBackupState() { ICloudBackup.shared.addObserver(self) - updateRefreshView() + updateRefreshView(initialUpdate: true) if kvoBackupScheduleToken != nil { return } kvoBackupScheduleToken = BackupScheduler.shared.observe(\.isBackupScheduled, options: .new) { [weak self] (_, _) in self?.updateRefreshView() @@ -385,30 +389,34 @@ extension TransactionsTableViewController: ICloudBackupObserver { kvoBackupScheduleToken = nil } - private func updateRefreshView() { - DispatchQueue.main.async { [weak self] in + private func updateRefreshView(initialUpdate: Bool = false) { + DispatchQueue.main.asyncAfter(deadline: .now() + CATransaction.animationDuration()) { [weak self] in guard let self = self else { return } if BackupScheduler.shared.isBackupScheduled || ICloudBackup.shared.inProgress { + self.animatedRefresher.stateType = .backup self.tableView.beginRefreshing() if BackupScheduler.shared.isBackupScheduled { self.animatedRefresher.updateState(.backupScheduled) } else if ICloudBackup.shared.inProgress { self.animatedRefresher.updateState(.backupInProgress) } - self.animatedRefresher.animateIn() - } else { + } else if !initialUpdate { if !ICloudBackup.shared.isLastBackupFailed { self.animatedRefresher.updateState(.backupSuccess) } DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in guard let self = self else { return } - self.animatedRefresher.setupView(.loading) self.animatedRefresher.animateOut { [weak self] in - self?.tableView.endRefreshing() + self?.animatedRefresher.stateType = .none + self?.safeRefreshTable({ [weak self] in + self?.tableView.endRefreshing() + }) } } } + self.animatedRefresher.animateIn() + self.tableView.layoutSubviews() } } } diff --git a/MobileWallet/Screens/RestoreWallet/RestoreWalletViewController.swift b/MobileWallet/Screens/RestoreWallet/RestoreWalletViewController.swift index 1ff28b48..16757bae 100644 --- a/MobileWallet/Screens/RestoreWallet/RestoreWalletViewController.swift +++ b/MobileWallet/Screens/RestoreWallet/RestoreWalletViewController.swift @@ -42,6 +42,8 @@ import UIKit import LocalAuthentication class RestoreWalletViewController: SettingsParentTableViewController { + private let localAuth = LAContext() + private let pendingView = PendingView(title: NSLocalizedString("restore_pending_view.title", comment: "RestorePending view"), definition: NSLocalizedString("restore_pending_view.description", comment: "RestorePending view")) private let items: [SystemMenuTableViewCellItem] = [ SystemMenuTableViewCellItem(title: RestoreCellTitle.iCloudRestore.rawValue)] @@ -103,8 +105,7 @@ extension RestoreWalletViewController: UITableViewDelegate, UITableViewDataSourc } private func oniCloudRestoreAction() { - let locatAuth = LAContext() - locatAuth.authenticateUser(reason: .userVerification) { [weak self] in + localAuth.authenticateUser(reason: .userVerification) { [weak self] in if self?.iCloudBackup.lastBackup?.isEncrypted == true { self?.navigationController?.pushViewController(PasswordVerificationViewController(variation: .restore, restoreWalletAction: self?.restoreWallet(password:)), animated: true) } else { diff --git a/MobileWallet/Screens/Settings/BackUpSettings/BackupWalletSettingsViewController.swift b/MobileWallet/Screens/Settings/BackUpSettings/BackupWalletSettingsViewController.swift index 6ccd4d5e..586576c4 100644 --- a/MobileWallet/Screens/Settings/BackUpSettings/BackupWalletSettingsViewController.swift +++ b/MobileWallet/Screens/Settings/BackUpSettings/BackupWalletSettingsViewController.swift @@ -47,6 +47,14 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { case backupNow } + private enum BackupSender { + case none + case uiSwitch + case button + } + + private var backupSender: BackupSender = .none + private lazy var settingsSectionItems: [SystemMenuTableViewCellItem] = [ SystemMenuTableViewCellItem(title: BackupWalletSettingsItem.iCloudBackups.rawValue, hasArrow: false, hasSwitch: true, switchIsOn: iCloudBackup.iCloudBackupsIsOn), SystemMenuTableViewCellItem(title: BackupWalletSettingsItem.setupPassword.rawValue) @@ -59,6 +67,8 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { private var iCloudBackupsItem: SystemMenuTableViewCellItem? private var kvoiCloudBackupsToken: NSKeyValueObservation? + private var backupNowButtonWalletBackup: Bool = false + private enum BackupWalletSettingsItem: CaseIterable { case iCloudBackups case setupPassword @@ -91,6 +101,7 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { } private func onBackupNowAction() { + backupSender = .button createWalletBackup() } @@ -112,6 +123,7 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { if change.newValue == change.oldValue { return } self?.iCloudBackup.iCloudBackupsIsOn = item.isSwitchIsOn if item.isSwitchIsOn { + self?.backupSender = .uiSwitch self?.createWalletBackup() } else { BPKeychainWrapper.removeBackupPasswordFromKeychain() @@ -135,7 +147,7 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { override func failedToCreateBackup(error: Error) { super.failedToCreateBackup(error: error) - if iCloudBackup.isLastBackupFailed && !iCloudBackup.isValidBackupExists() { + if iCloudBackup.isLastBackupFailed && !iCloudBackup.isValidBackupExists() && backupSender == .uiSwitch { iCloudBackupsItem?.isSwitchIsOn = false } } @@ -163,6 +175,11 @@ class BackupWalletSettingsViewController: SettingsParentTableViewController { } } + override func onUploadProgress(percent: Double, started: Bool, completed: Bool, error: Error?) { + super.onUploadProgress(percent: percent, started: started, completed: completed, error: error) + if completed { backupSender = .none } + } + deinit { kvoiCloudBackupsToken?.invalidate() } @@ -250,7 +267,9 @@ extension BackupWalletSettingsViewController: UITableViewDelegate, UITableViewDa lastBackupLabel.translatesAutoresizingMaskIntoConstraints = false lastBackupLabel.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: 25).isActive = true + lastBackupLabel.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -25).isActive = true lastBackupLabel.topAnchor.constraint(equalTo: footer.topAnchor, constant: 8).isActive = true + lastBackupLabel.lineBreakMode = .byTruncatingMiddle return footer } else { diff --git a/MobileWallet/Screens/Settings/SettingsViewController.swift b/MobileWallet/Screens/Settings/SettingsViewController.swift index 525b0840..bd4e7563 100644 --- a/MobileWallet/Screens/Settings/SettingsViewController.swift +++ b/MobileWallet/Screens/Settings/SettingsViewController.swift @@ -42,6 +42,7 @@ import UIKit import LocalAuthentication class SettingsViewController: SettingsParentTableViewController { + private let localAuth = LAContext() private enum Section: Int, CaseIterable { case security @@ -118,8 +119,7 @@ class SettingsViewController: SettingsParentTableViewController { } func onBackupWalletAction() { - let localAuth = LAContext() - localAuth.authenticateUser(reason: .userVerification) { [weak self] in + localAuth.authenticateUser(reason: .userVerification, showFailedDialog: false) { [weak self] in self?.navigationController?.pushViewController(BackupWalletSettingsViewController(), animated: true) } } @@ -183,7 +183,9 @@ extension SettingsViewController: UITableViewDelegate, UITableViewDataSource { lastBackupLabel.translatesAutoresizingMaskIntoConstraints = false lastBackupLabel.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 25).isActive = true + lastBackupLabel.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -25).isActive = true lastBackupLabel.topAnchor.constraint(equalTo: header.topAnchor, constant: 8).isActive = true + lastBackupLabel.lineBreakMode = .byTruncatingMiddle } else { header.heightAnchor.constraint(equalToConstant: 70).isActive = true } diff --git a/MobileWallet/UIElements/AnimatedRefreshingView.swift b/MobileWallet/UIElements/AnimatedRefreshingView.swift index a952050a..beedb832 100644 --- a/MobileWallet/UIElements/AnimatedRefreshingView.swift +++ b/MobileWallet/UIElements/AnimatedRefreshingView.swift @@ -71,6 +71,10 @@ private class RefreshingInnerView: UIView { } func setupView(_ type: AnimatedRefreshingViewState) { + emojiLabel.removeFromSuperview() + statusLabel.removeFromSuperview() + spinner.removeFromSuperview() + emojiLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(emojiLabel) emojiLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Theme.shared.sizes.appSidePadding).isActive = true @@ -148,6 +152,15 @@ class AnimatedRefreshingView: UIView { private var currentInnerViewBottomAnchor = NSLayoutConstraint() private var currentState: AnimatedRefreshingViewState = .loading + enum StateType { + case none + case updateData + case backup + case txtView + } + + var stateType: StateType = .none + override init(frame: CGRect) { super.init(frame: frame) } @@ -180,6 +193,7 @@ class AnimatedRefreshingView: UIView { } func animateIn(delay: TimeInterval = 0.25, withDuration: TimeInterval = 0.5) { + if alpha == 1 { return } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { UIView.animate(withDuration: withDuration, delay: 0, options: .curveEaseInOut, animations: { [weak self] in guard let self = self else { return } @@ -190,6 +204,7 @@ class AnimatedRefreshingView: UIView { } func animateOut(_ onComplete: (() -> Void)? = nil) { + if alpha == 0 { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { [weak self] in guard let self = self else { return } diff --git a/MobileWallet/en.lproj/Localizable.strings b/MobileWallet/en.lproj/Localizable.strings index fb2b733e..12d524dc 100644 --- a/MobileWallet/en.lproj/Localizable.strings +++ b/MobileWallet/en.lproj/Localizable.strings @@ -95,7 +95,7 @@ "backup_wallet_settings.item.secure_your_backup" = "Secure your iCloud backup"; "backup_wallet_settings.item.backup_now" = "Back up now"; "backup_wallet_settings.item.with_recovery_phrase" = "Back up with recovery phrase"; -"backup_wallet_settings.header.title" = "Back Up Wallet"; +"backup_wallet_settings.header.title" = "Wallet Backups"; "backup_wallet_settings.header.description" = "By backing up your wallet, you’ll ensure that you don’t lose your tXTR if your phone is lost or broken."; /* SecureBackup view */