From b78981de9615b04e155d8faeeb99e1d685f829f8 Mon Sep 17 00:00:00 2001 From: ant013 Date: Mon, 9 Oct 2023 16:57:34 +0600 Subject: [PATCH] Add restore from file logic for 'import wallet' screen - Fix 'init adapters' when calls many times from different thread --- .../project.pbxproj | 24 ++- .../Core/Managers/AccountManager.swift | 11 + .../Core/Managers/AdapterManager.swift | 8 +- .../Backup/ICloud/AppBackupProvider.swift | 62 +++--- .../RestoreFileConfigurationModule.swift | 7 - .../RestoreFileConfigurationService.swift | 48 ----- .../RestoreFileConfigurationViewModel.swift | 4 - .../RestoreFileConfigurationModule.swift | 16 ++ ...storeFileConfigurationViewController.swift | 169 ++++++++++++++++ .../RestoreFileConfigurationViewModel.swift | 70 +++++++ .../RestoreFile/RestoreFileHelper.swift | 27 +++ .../RestorePassphraseService.swift | 2 +- .../RestorePassphraseViewController.swift | 11 +- .../RestorePassphraseViewModel.swift | 7 +- .../RestoreType/RestoreTypeModule.swift | 9 - .../RestoreTypeViewController.swift | 189 +++++++++++------- .../RestoreType/RestoreTypeViewModel.swift | 21 +- .../UserInterface/Extensions/HudHelper.swift | 5 +- .../en.lproj/Localizable.strings | 6 + 19 files changed, 519 insertions(+), 177 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index b16b709e92..07fb6f7fff 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2001,6 +2001,7 @@ ABC9A0B58626A1E0C4248162 /* SendEip1155Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC6A0B950C0AABD5A93E /* SendEip1155Service.swift */; }; ABC9A0B5A5577704AC99F47B /* ChartIndicatorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3AF18834CE9E569C89E /* ChartIndicatorsViewModel.swift */; }; ABC9A0BAB439DEB0BC7495C3 /* ContactBookAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A12529DC8DE5D46D9776 /* ContactBookAddressViewModel.swift */; }; + ABC9A0C5DE01B3C50D4C7FF2 /* RestoreFileConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9CB516D0B925DE22C1E /* RestoreFileConfigurationViewController.swift */; }; ABC9A0C6E37779D3F3602EEC /* SendEip1155AvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB612DE3C8AA3A1EEAC7 /* SendEip1155AvailableBalanceViewModel.swift */; }; ABC9A0CE0155F89F12350DFC /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4D1C7AE5723851A53EB /* BackupListView.swift */; }; ABC9A0CEBC41CCE5AB205B3C /* NftAssetOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A90781302D793E0773CB /* NftAssetOverviewModule.swift */; }; @@ -2045,7 +2046,6 @@ ABC9A295A99F39EFAAF8FCDA /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF26FDCB363793BF66E1 /* Integer.swift */; }; ABC9A29A23C043A3FD65AF1C /* SendBinanceFeeWarningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2C2D1331E91D666AB3E /* SendBinanceFeeWarningViewModel.swift */; }; ABC9A2A249A94B271F56EBD0 /* BackupCryptoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD5CB1911A698718213F /* BackupCryptoHelper.swift */; }; - ABC9A2A327AF3D72F9842DCA /* RestoreFileConfigurationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD9A0F91C825D149B289 /* RestoreFileConfigurationService.swift */; }; ABC9A2A6C3A1EFDD33D53287 /* WalletTokenBalanceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1833D302B63028A3966 /* WalletTokenBalanceModule.swift */; }; ABC9A2A9540003916929DC77 /* ContactBookSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A24CBB826A2D2F88EC61 /* ContactBookSettingsModule.swift */; }; ABC9A2AA80535822D8731DA4 /* ContactBookViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2D87362E00FD9FB5688 /* ContactBookViewController.swift */; }; @@ -2069,6 +2069,7 @@ ABC9A3160546BCE6ECD32669 /* IntegerAmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6D56EBB7FFAD68CFD66 /* IntegerAmountInputViewModel.swift */; }; ABC9A3187D032F44CD4E8986 /* MarketCardTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC7983E4A81E421AB639 /* MarketCardTitleView.swift */; }; ABC9A32176DC914BBB4E9BFF /* WalletConnectListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A52AC277ED7563F2707F /* WalletConnectListViewModel.swift */; }; + ABC9A3231731F39ECA5B90ED /* RestoreFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61774389A4773BE18C /* RestoreFileHelper.swift */; }; ABC9A324BB7E7FF8758A92C3 /* RestoreCloudViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */; }; ABC9A32D8EFFA6779886A27A /* ProChartFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAB6BA03FFE92F247FF6 /* ProChartFetcher.swift */; }; ABC9A338C63FEB1DF42D3D6E /* WalletTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A38082BD2EBE1BC8E11E /* WalletTokenService.swift */; }; @@ -2081,7 +2082,6 @@ ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.swift */; }; ABC9A372F53F1F1D59BF8969 /* RestoreFileConfigurationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */; }; ABC9A37FB71FA7DA14553EFC /* RawFullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A819E6708797C571CA0B /* RawFullBackup.swift */; }; - ABC9A394D1D0D165160F5F43 /* RestoreFileConfigurationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD9A0F91C825D149B289 /* RestoreFileConfigurationService.swift */; }; ABC9A395A96C1F7C30F21940 /* WalletConnectPendingRequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */; }; ABC9A3B155B3F6E7E0F2CB07 /* HudHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A381CB4C09FF7CB62A94 /* HudHelper.swift */; }; ABC9A3BC9A18F74818EF5C17 /* MetadataMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */; }; @@ -2157,6 +2157,7 @@ ABC9A66D7B34C6547C2469E9 /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5CDF9153AECED3DE50C /* BackupTypeView.swift */; }; ABC9A66E5775762856F8927D /* NftAssetOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A90781302D793E0773CB /* NftAssetOverviewModule.swift */; }; ABC9A67A87DFB11102AB607A /* SendBitcoinFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3DC5DA5B7BFDBF72B5D /* SendBitcoinFactory.swift */; }; + ABC9A67C2D782AD0DFDF0C3C /* RestoreFileConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9CB516D0B925DE22C1E /* RestoreFileConfigurationViewController.swift */; }; ABC9A6887B716464A5813EE9 /* BackupCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */; }; ABC9A69264C2086E4B3B09D2 /* WalletTokenBalanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A352F3EAA38107897CEF /* WalletTokenBalanceService.swift */; }; ABC9A69A1A01DBD07CAAC9CD /* ContactBookAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A55B0E99C1DD25839EDB /* ContactBookAddressViewController.swift */; }; @@ -2336,6 +2337,7 @@ ABC9ACDFA2F5F3BD9517723D /* NftAssetOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A806FD17A129212E3F7C /* NftAssetOverviewViewController.swift */; }; ABC9ACE1EDEA27A054EDC2C4 /* ContactBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC4A19838CA08603E17B /* ContactBookService.swift */; }; ABC9ACE255480B2D6E340611 /* ChartIndicatorsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2F3E5147E0E92258FBB /* ChartIndicatorsService.swift */; }; + ABC9ACEB81BCB00435B35F64 /* RestoreFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61774389A4773BE18C /* RestoreFileHelper.swift */; }; ABC9ACEE45E455BA098231EE /* SendMemoInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3C103C1DE359184D944 /* SendMemoInputCell.swift */; }; ABC9AD05E7B986179310D6D7 /* SwapInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A56611CF5E7B3F25CD5C /* SwapInputAccessoryView.swift */; }; ABC9AD1C8D0CE88A604D5250 /* SendBinanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0DD32AB4B9BAB79F11 /* SendBinanceFactory.swift */; }; @@ -3910,6 +3912,7 @@ ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookSettingsViewController.swift; sourceTree = ""; }; ABC9A9B35C58F6525F3B2D5C /* FullCoin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullCoin.swift; sourceTree = ""; }; ABC9A9C09ECB9B0CCBAD8C21 /* SendEip1155ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155ViewController.swift; sourceTree = ""; }; + ABC9A9CB516D0B925DE22C1E /* RestoreFileConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileConfigurationViewController.swift; sourceTree = ""; }; ABC9A9E2C039C005650491D2 /* WalletConnectAppShowModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowModule.swift; sourceTree = ""; }; ABC9A9F6635146BEBFB432D1 /* ChartCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartCell.swift; sourceTree = ""; }; ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataMonitor.swift; sourceTree = ""; }; @@ -3935,6 +3938,7 @@ ABC9AB2ED4E48D4FCEDBE769 /* ContactBookContactModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactModule.swift; sourceTree = ""; }; ABC9AB3EC7A1FB0D6C9F7F89 /* ChartIndicatorsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsModule.swift; sourceTree = ""; }; ABC9AB612DE3C8AA3A1EEAC7 /* SendEip1155AvailableBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155AvailableBalanceViewModel.swift; sourceTree = ""; }; + ABC9AB61774389A4773BE18C /* RestoreFileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileHelper.swift; sourceTree = ""; }; ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppBackupProvider.swift; sourceTree = ""; }; ABC9AB69D8053840476C26FA /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; ABC9AB785128005F6C2C9F9A /* ProFeaturesStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProFeaturesStorage.swift; sourceTree = ""; }; @@ -3969,7 +3973,6 @@ ABC9AD448DC071D8800C6B12 /* WalletTokenBalanceCustomAmountCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceCustomAmountCell.swift; sourceTree = ""; }; ABC9AD5CB1911A698718213F /* BackupCryptoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCryptoHelper.swift; sourceTree = ""; }; ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManagerModule.swift; sourceTree = ""; }; - ABC9AD9A0F91C825D149B289 /* RestoreFileConfigurationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileConfigurationService.swift; sourceTree = ""; }; ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileConfigurationModule.swift; sourceTree = ""; }; ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendFeeService.swift; sourceTree = ""; }; ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryMarketCapFetcher.swift; sourceTree = ""; }; @@ -7253,7 +7256,8 @@ isa = PBXGroup; children = ( ABC9A781F6F6806A9DCE4C9E /* RestorePassphrase */, - ABC9A6EF8CB7A0B2D477F2C5 /* BakcupFileConfiguration */, + ABC9A6EF8CB7A0B2D477F2C5 /* RestoreFileConfiguration */, + ABC9AB61774389A4773BE18C /* RestoreFileHelper.swift */, ); path = RestoreFile; sourceTree = ""; @@ -7268,14 +7272,14 @@ path = Zcash; sourceTree = ""; }; - ABC9A6EF8CB7A0B2D477F2C5 /* BakcupFileConfiguration */ = { + ABC9A6EF8CB7A0B2D477F2C5 /* RestoreFileConfiguration */ = { isa = PBXGroup; children = ( ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */, - ABC9AD9A0F91C825D149B289 /* RestoreFileConfigurationService.swift */, ABC9AF6C15800AF8C37C3516 /* RestoreFileConfigurationViewModel.swift */, + ABC9A9CB516D0B925DE22C1E /* RestoreFileConfigurationViewController.swift */, ); - path = BakcupFileConfiguration; + path = RestoreFileConfiguration; sourceTree = ""; }; ABC9A71A64E11AD3709A1174 /* Workers */ = { @@ -9461,9 +9465,10 @@ ABC9AA016413C37F4CC95080 /* RestorePassphraseViewController.swift in Sources */, ABC9A453F337BA22A5698DCC /* RestorePassphraseModule.swift in Sources */, ABC9A904FCE6BFE793C944AE /* RestoreFileConfigurationModule.swift in Sources */, - ABC9A394D1D0D165160F5F43 /* RestoreFileConfigurationService.swift in Sources */, ABC9A481F1C13DBAAD3F632B /* RestoreFileConfigurationViewModel.swift in Sources */, ABC9AF309AAE5C54D2020B23 /* RawFullBackup.swift in Sources */, + ABC9A67C2D782AD0DFDF0C3C /* RestoreFileConfigurationViewController.swift in Sources */, + ABC9ACEB81BCB00435B35F64 /* RestoreFileHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10788,9 +10793,10 @@ ABC9AE2131780654A7139081 /* RestorePassphraseViewController.swift in Sources */, ABC9A414F0F0AEA6E4DD4E9D /* RestorePassphraseModule.swift in Sources */, ABC9A372F53F1F1D59BF8969 /* RestoreFileConfigurationModule.swift in Sources */, - ABC9A2A327AF3D72F9842DCA /* RestoreFileConfigurationService.swift in Sources */, ABC9A2F6D2A2AAFA31C64BAB /* RestoreFileConfigurationViewModel.swift in Sources */, ABC9A37FB71FA7DA14553EFC /* RawFullBackup.swift in Sources */, + ABC9A0C5DE01B3C50D4C7FF2 /* RestoreFileConfigurationViewController.swift in Sources */, + ABC9A3231731F39ECA5B90ED /* RestoreFileHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift index e397b42a91..77de29848f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift @@ -128,6 +128,17 @@ extension AccountManager { set(activeAccountId: account.id) } + func save(accounts: [Account]) { + accounts.forEach { account in + storage.save(account: account) + } + + accountsRelay.accept(storage.accounts) + if let first = accounts.first { + set(activeAccountId: first.id) + } + } + func delete(account: Account) { storage.delete(account: account) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AdapterManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AdapterManager.swift index 4eac5cec74..424d76b6f8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AdapterManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AdapterManager.swift @@ -14,6 +14,7 @@ class AdapterManager { private let adapterDataReadyRelay = PublishRelay() private let queue = DispatchQueue(label: "\(AppConfig.label).adapter_manager", qos: .userInitiated) + private let initAdaptersQueue = DispatchQueue(label: "\(AppConfig.label).adapter_manager.init_adapters", qos: .userInitiated) private var _adapterData = AdapterData(adapterMap: [:], account: nil) init(adapterFactory: AdapterFactory, walletManager: WalletManager, evmBlockchainManager: EvmBlockchainManager, @@ -37,13 +38,18 @@ class AdapterManager { } private func initAdapters(wallets: [Wallet], account: Account?) { + initAdaptersQueue.async { + self._initAdapters(wallets: wallets, account: account) + } + } + + private func _initAdapters(wallets: [Wallet], account: Account?) { var newAdapterMap = queue.sync { _adapterData.adapterMap } for wallet in wallets { guard newAdapterMap[wallet] == nil else { continue } - if let adapter = adapterFactory.adapter(wallet: wallet) { newAdapterMap[wallet] = adapter adapter.start() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index 14ff7a2f2c..ace803fe26 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -136,40 +136,52 @@ class AppBackupProvider { } extension AppBackupProvider { - func restore(raw: RawWalletBackup) { - switch raw.account.type { - case .cex: - accountManager.save(account: raw.account) - default: - accountManager.save(account: raw.account) - - let wallets = raw.enabledWallets.map { - if !$0.settings.isEmpty { - var restoreSettings = [RestoreSettingType: String]() - $0.settings.forEach { key, value in - if let key = RestoreSettingType(rawValue: key) { - restoreSettings[key] = value + func restore(raws: [RawWalletBackup]) { + let updated = raws.map { raw in + let account = accountFactory.account( + type: raw.account.type, + origin: raw.account.origin, + backedUp: raw.account.backedUp, + fileBackedUp: raw.account.fileBackedUp, + name: raw.account.name + ) + return RawWalletBackup(account: account, enabledWallets: raw.enabledWallets) + } + + accountManager.save(accounts: updated.map { $0.account }) + + updated.forEach { raw in + switch raw.account.type { + case .cex: () + default: + let wallets = raw.enabledWallets.map { + if !$0.settings.isEmpty { + var restoreSettings = [RestoreSettingType: String]() + $0.settings.forEach { key, value in + if let key = RestoreSettingType(rawValue: key) { + restoreSettings[key] = value + } + } + if let tokenQuery = TokenQuery(id: $0.tokenQueryId) { + restoreSettingsManager.save(settings: restoreSettings, account: raw.account, blockchainType: tokenQuery.blockchainType) } } - if let tokenQuery = TokenQuery(id: $0.tokenQueryId) { - restoreSettingsManager.save(settings: restoreSettings, account: raw.account, blockchainType: tokenQuery.blockchainType) - } + return EnabledWallet( + tokenQueryId: $0.tokenQueryId, + accountId: raw.account.id, + coinName: $0.coinName, + coinCode: $0.coinCode, + tokenDecimals: $0.tokenDecimals + ) } - return EnabledWallet( - tokenQueryId: $0.tokenQueryId, - accountId: raw.account.id, - coinName: $0.coinName, - coinCode: $0.coinCode, - tokenDecimals: $0.tokenDecimals - ) + walletManager.save(enabledWallets: wallets) } - walletManager.save(enabledWallets: wallets) } } func restore(raw: RawFullBackup) { raw.accounts.forEach { wallet in - restore(raw: wallet) + restore(raws: [wallet]) } favoritesManager.add(coinUids: raw.watchlistIds) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift deleted file mode 100644 index f965264f49..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -class RestoreFileConfigurationModule { - static func viewController(fullBackup: FullBackup) -> UIViewController { - return UIViewController() - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift deleted file mode 100644 index 3eef61fed6..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -class RestoreFileConfigurationService { - private let contactBookManager: ContactBookManager - private let fullBackup: FullBackup - private let passphrase: String - - init(contactBookManager: ContactBookManager, fullBackup: FullBackup, passphrase: String) { - self.contactBookManager = contactBookManager - self.fullBackup = fullBackup - self.passphrase = passphrase - } -} - -extension RestoreFileConfigurationService { - var accountItems: [BackupAppModule.AccountItem] { - [] -// let wallets = fullBackup -// .wallets -// .filter { !$0.walletBackup.type.isWatch } -// -// wallets.map { wallet in -// BackupAppModule.AccountItem( -// accountId: wallet.walletBackup.id, -// name: <#T##String##Swift.String#>, -// description: <#T##String##Swift.String#>, -// cautionType: <#T##CautionType?##Unstoppable_Dev.CautionType?#> -// ) -// } - } - - var otherItems: [BackupAppModule.Item] { - let watchAccountCount = fullBackup - .wallets - .filter { $0.walletBackup.type.isWatch } - .count - - let contacts = fullBackup.contacts.flatMap { try? ContactBookManager.decrypt(crypto: $0, passphrase: passphrase) } - let contactAddressCount = (contacts ?? []).reduce(into: 0) { $0 += $1.addresses.count } - - return BackupAppModule.items( - watchAccountCount: watchAccountCount, - watchlistCount: fullBackup.watchlistIds.count, - contactAddressCount: contactAddressCount, - blockchainSourcesCount: fullBackup.settings.evmSyncSources.custom.count - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift deleted file mode 100644 index cfa9b242a5..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Foundation - -class BackupFileConfigurationViewModel { -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift new file mode 100644 index 0000000000..2f0430fd91 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift @@ -0,0 +1,16 @@ +import UIKit + +class RestoreFileConfigurationModule { + static func viewController(rawBackup: RawFullBackup, returnViewController: UIViewController?) -> UIViewController { + let viewModel = RestoreFileConfigurationViewModel( + cloudBackupManager: App.shared.cloudBackupManager, + appBackupProvider: App.shared.appBackupProvider, + rawBackup: rawBackup + ) + + return RestoreFileConfigurationViewController( + viewModel: viewModel, + returnViewController: returnViewController + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift new file mode 100644 index 0000000000..993903037a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift @@ -0,0 +1,169 @@ +import Combine +import ComponentKit +import Foundation +import SectionsTableView +import ThemeKit +import UIKit + +class RestoreFileConfigurationViewController: KeyboardAwareViewController { + private let viewModel: RestoreFileConfigurationViewModel + private var cancellables = Set() + + private weak var returnViewController: UIViewController? + + private let tableView = SectionsTableView(style: .grouped) + + private let gradientWrapperView = BottomGradientHolder() + private let restoreButton = PrimaryButton() + + init(viewModel: RestoreFileConfigurationViewModel, returnViewController: UIViewController?) { + self.viewModel = viewModel + self.returnViewController = returnViewController + + super.init(scrollViews: [tableView], accessoryView: gradientWrapperView) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "backup_app.backup_list.title".localized + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.cancel".localized, style: .plain, target: self, action: #selector(onCancel)) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + navigationItem.largeTitleDisplayMode = .never + + view.addSubview(tableView) + tableView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + + tableView.sectionDataSource = self + + gradientWrapperView.add(to: self) + gradientWrapperView.addSubview(restoreButton) + + restoreButton.setTitle("button.restore".localized, for: .normal) + restoreButton.addTarget(self, action: #selector(onTapRestore), for: .touchUpInside) + restoreButton.set(style: .yellow) + + viewModel.finishedPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] success in + self?.finish(success: success) + } + .store(in: &cancellables) + + tableView.buildSections() + } + + @objc private func onCancel() { + (returnViewController ?? self)?.dismiss(animated: true) + } + + private func finish(success: Bool) { + UIApplication.shared.windows.first { $0.isKeyWindow }?.set(newRootController: MainModule.instance(presetTab: .balance)) + + if success { + HudHelper.instance.show(banner: .done) + } + } + + @objc private func onTapRestore() { + let viewController = BottomSheetModule.viewController( + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), + title: "alert.notice".localized, + items: [ + .highlightedDescription(text: "backup_app.restore.notice.description".localized), + ], + buttons: [ + .init(style: .yellow, title: "backup_app.restore.notice.merge".localized, actionType: .afterClose) { [weak self] in + self?.viewModel.onTapRestore() + }, + .init(style: .transparent, title: "button.cancel".localized, actionType: .afterClose), + ] + ) + + present(viewController, animated: true) + } + + private func row(accountItem: BackupAppModule.AccountItem, rowInfo: RowInfo) -> RowProtocol { + let subtitleColor: UIColor = accountItem.cautionType?.labelColor ?? .themeGray + + return tableView.universalRow62( + id: accountItem.id, + title: .body(accountItem.name), + description: .subhead2(accountItem.description, color: subtitleColor), + isFirst: rowInfo.isFirst, + isLast: rowInfo.isLast + ) + } + + private func row(item: BackupAppModule.Item, rowInfo: RowInfo) -> RowProtocol { + tableView.universalRow62( + id: item.title, + title: .body(item.title), + description: .subhead2(item.description), + isFirst: rowInfo.isFirst, + isLast: rowInfo.isLast + ) + } + + private var descriptionSection: SectionProtocol { + Section( + id: "description", + headerState: .margin(height: .margin12), + footerState: .margin(height: .margin32), + rows: [ + tableView.descriptionRow( + id: "description", + text: "backup_app.backup_list.description.restore".localized, + font: .subhead2, + textColor: .themeGray, + ignoreBottomMargin: true + ), + ] + ) + } +} + +extension RestoreFileConfigurationViewController: SectionsDataSource { + func buildSections() -> [SectionProtocol] { + var sections: [SectionProtocol] = [ + descriptionSection, + ] + + if !viewModel.accountItems.isEmpty { + sections.append( + Section( + id: "wallets-section", + headerState: tableView.sectionHeader(text: "backup_app.backup_list.header.wallets".localized), + footerState: .margin(height: .margin24), + rows: viewModel.accountItems + .enumerated() + .map { index, item in row(accountItem: item, rowInfo: RowInfo(index: index, count: viewModel.accountItems.count)) } + ) + ) + } + + if !viewModel.otherItems.isEmpty { + sections.append( + Section( + id: "other-section", + headerState: tableView.sectionHeader(text: "backup_app.backup_list.header.other".localized), + footerState: .margin(height: .margin32), + rows: viewModel.otherItems + .enumerated() + .map { index, item in row(item: item, rowInfo: RowInfo(index: index, count: viewModel.otherItems.count)) } + ) + ) + } + + return sections + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift new file mode 100644 index 0000000000..1686634c30 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift @@ -0,0 +1,70 @@ +import Foundation +import Combine + +class RestoreFileConfigurationViewModel { + private let cloudBackupManager: CloudBackupManager + private let appBackupProvider: AppBackupProvider + private let rawBackup: RawFullBackup + + private let finishedSubject = PassthroughSubject() + + init(cloudBackupManager: CloudBackupManager, appBackupProvider: AppBackupProvider, rawBackup: RawFullBackup) { + self.cloudBackupManager = cloudBackupManager + self.appBackupProvider = appBackupProvider + self.rawBackup = rawBackup + } + + private func item(account: Account) -> BackupAppModule.AccountItem { + var alertSubtitle: String? + let hasAlertDescription = !(account.backedUp || cloudBackupManager.backedUp(uniqueId: account.type.uniqueId())) + if account.nonStandard { + alertSubtitle = "manage_accounts.migration_required".localized + } else if hasAlertDescription { + alertSubtitle = "manage_accounts.backup_required".localized + } + + let showAlert = alertSubtitle != nil || account.nonRecommended + + let cautionType: CautionType? = showAlert ? .error : .none + let description = alertSubtitle ?? account.type.detailedDescription + + return BackupAppModule.AccountItem( + accountId: account.id, + name: account.name, + description: description, + cautionType: cautionType + ) + } +} + +extension RestoreFileConfigurationViewModel { + var accountItems: [BackupAppModule.AccountItem] { + rawBackup + .accounts + .filter { !$0.account.watchAccount } + .map { item(account: $0.account) } + } + + var otherItems: [BackupAppModule.Item] { + let contactAddressCount = rawBackup.contacts.reduce(into: 0) { $0 += $1.addresses.count } + let watchAccounts = rawBackup + .accounts + .filter { $0.account.watchAccount } + + return BackupAppModule.items( + watchAccountCount: watchAccounts.count, + watchlistCount: rawBackup.watchlistIds.count, + contactAddressCount: contactAddressCount, + blockchainSourcesCount: rawBackup.customSyncSources.count + ) + } + + func onTapRestore() { + appBackupProvider.restore(raw: rawBackup) + finishedSubject.send(true) + } + + var finishedPublisher: AnyPublisher { + finishedSubject.eraseToAnyPublisher() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift new file mode 100644 index 0000000000..8f3ea1cc79 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift @@ -0,0 +1,27 @@ +import Foundation + +struct RestoreFileHelper { + static func parse(url: URL) throws -> BackupModule.NamedSource { + let data = try FileManager.default.contentsOfFile(coordinatingAccessAt: url) + let filename = NSString(string: url.lastPathComponent).deletingPathExtension + + if let oneWallet = try? JSONDecoder().decode(WalletBackup.self, from: data) { + + return .init(name: filename, source: .wallet(oneWallet)) + } + + if let fullBackup = try? JSONDecoder().decode(FullBackup.self, from: data) { + + return .init(name: filename, source: .full(fullBackup)) + } + + throw ParseError.wrongFile + } + +} + +extension RestoreFileHelper { + enum ParseError: Error { + case wrongFile + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift index 40ff63bced..fb4bb50045 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift @@ -32,7 +32,7 @@ extension RestorePassphraseService { switch restoredBackup.source { case let .wallet(walletBackup): let rawBackup = try appBackupProvider.decrypt(walletBackup: walletBackup, name: restoredBackup.name, passphrase: passphrase) - appBackupProvider.restore(raw: rawBackup) + appBackupProvider.restore(raws: [rawBackup]) switch rawBackup.account.type { case .cex: return .success diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift index cffa763a92..48b7a9fa23 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift @@ -30,7 +30,6 @@ class RestorePassphraseViewController: KeyboardAwareViewController { super.init(scrollViews: [tableView], accessoryView: gradientWrapperView) } - @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -109,6 +108,11 @@ class RestorePassphraseViewController: KeyboardAwareViewController { } .store(in: &cancellables) + viewModel.openConfigurationPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.openConfiguration(rawBackup: $0) } + .store(in: &cancellables) + viewModel.successPublisher .receive(on: DispatchQueue.main) .sink { [weak self] in @@ -174,6 +178,11 @@ class RestorePassphraseViewController: KeyboardAwareViewController { ) navigationController?.pushViewController(viewController, animated: true) } + + private func openConfiguration(rawBackup: RawFullBackup) { + let viewController = RestoreFileConfigurationModule.viewController(rawBackup: rawBackup, returnViewController: returnViewController) + navigationController?.pushViewController(viewController, animated: true) + } } extension RestorePassphraseViewController: SectionsDataSource { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift index 374adadebf..919c3be59c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift @@ -12,6 +12,7 @@ class RestorePassphraseViewModel { private let clearInputsSubject = PassthroughSubject() private let showErrorSubject = PassthroughSubject() private let openSelectCoinsSubject = PassthroughSubject() + private let openConfigurationSubject = PassthroughSubject() private let successSubject = PassthroughSubject() init(service: RestorePassphraseService) { @@ -38,6 +39,10 @@ extension RestorePassphraseViewModel { openSelectCoinsSubject.eraseToAnyPublisher() } + var openConfigurationPublisher: AnyPublisher { + openConfigurationSubject.eraseToAnyPublisher() + } + var successPublisher: AnyPublisher { successSubject.eraseToAnyPublisher() } @@ -74,7 +79,7 @@ extension RestorePassphraseViewModel { self?.successSubject.send() } case let .restoredFullBackup(rawBackup): - print("Result: \(rawBackup)") + self?.openConfigurationSubject.send(rawBackup) } } catch { switch error as? RestoreCloudModule.RestoreError { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift index 7ffbad74cb..1276faf387 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift @@ -13,15 +13,6 @@ struct RestoreTypeModule { return TermsModule.viewController(sourceViewController: sourceViewController, moduleToOpen: module) } } - - static func destination(restoreType: RestoreType, sourceViewController: UIViewController? = nil, returnViewController: UIViewController? = nil) -> UIViewController? { - switch restoreType { - case .recoveryOrPrivateKey: return RestoreModule.viewController(sourceViewController: sourceViewController, returnViewController: returnViewController) - case .cloudRestore: return RestoreCloudModule.viewController(returnViewController: returnViewController) - case .fileRestore: return nil - case .cex: return RestoreCexViewController(returnViewController: returnViewController) - } - } } extension RestoreTypeModule { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift index e88eaeda4e..66ff5eeb7f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift @@ -1,9 +1,10 @@ import Combine -import UIKit -import ThemeKit -import SnapKit -import SectionsTableView import ComponentKit +import SectionsTableView +import SnapKit +import ThemeKit +import UIKit +import UniformTypeIdentifiers class RestoreTypeViewController: ThemeViewController { private let viewModel: RestoreTypeViewModel @@ -19,7 +20,8 @@ class RestoreTypeViewController: ThemeViewController { super.init() } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,18 +48,32 @@ class RestoreTypeViewController: ThemeViewController { tableView.sectionDataSource = self viewModel.showModulePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.show(type: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.show(type: $0) + } + .store(in: &cancellables) viewModel.showCloudNotAvailablePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.showNotCloudAvailable() - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.showNotCloudAvailable() + } + .store(in: &cancellables) + + viewModel.showWrongFilePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.showWrongFile() + } + .store(in: &cancellables) + + viewModel.showRestoreBackupPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] namedSource in + self?.show(source: namedSource) + } + .store(in: &cancellables) tableView.buildSections() } @@ -76,65 +92,90 @@ class RestoreTypeViewController: ThemeViewController { let description = viewModel.description(type: type) return CellBuilderNew.row( - rootElement: .hStack([ - .image24 { (component: ImageComponent) -> () in - return component.imageView.image = UIImage(named: icon) + rootElement: .hStack([ + .image24 { (component: ImageComponent) in + component.imageView.image = UIImage(named: icon) + }, + .vStackCentered([ + .text { (component: TextComponent) in + component.font = titleFont + component.textColor = .themeLeah + component.text = title + component.numberOfLines = 0 + }, + .margin4, + .text { (component: TextComponent) in + component.font = valueFont + component.textColor = .themeGray + component.text = description + component.numberOfLines = 0 }, - .vStackCentered([ - .text { (component: TextComponent) -> () in - component.font = titleFont - component.textColor = .themeLeah - component.text = title - component.numberOfLines = 0 - }, - .margin4, - .text { (component: TextComponent) -> () in - component.font = valueFont - component.textColor = .themeGray - component.text = description - component.numberOfLines = 0 - } - ]) ]), - tableView: tableView, - id: description, - autoDeselect: true, - dynamicHeight: { containerWidth in - let size = CellBuilderNew.height( - containerWidth: containerWidth, - backgroundStyle: backgroundStyle, - text: title, - font: titleFont, - verticalPadding: .margin24, - elements: [.fixed(width: .iconSize24), .multiline] - ) + .margin4 + + ]), + tableView: tableView, + id: description, + autoDeselect: true, + dynamicHeight: { containerWidth in + let size = CellBuilderNew.height( + containerWidth: containerWidth, + backgroundStyle: backgroundStyle, + text: title, + font: titleFont, + verticalPadding: .margin24, + elements: [.fixed(width: .iconSize24), .multiline] + ) + .margin4 + CellBuilderNew.height( - containerWidth: containerWidth, - backgroundStyle: backgroundStyle, - text: description, - font: valueFont, - verticalPadding: 0, - elements: [.fixed(width: .iconSize24), .multiline] + containerWidth: containerWidth, + backgroundStyle: backgroundStyle, + text: description, + font: valueFont, + verticalPadding: 0, + elements: [.fixed(width: .iconSize24), .multiline] ) - return max(106, size) // usually cells will have 3 lines - }, - bind: { cell in - cell.set(backgroundStyle: backgroundStyle, isFirst: true, isLast: true) - }, - action: { [weak self] in - self?.viewModel.onTap(type: type) - } + return max(106, size) // usually cells will have 3 lines + }, + bind: { cell in + cell.set(backgroundStyle: backgroundStyle, isFirst: true, isLast: true) + }, + action: { [weak self] in + self?.viewModel.onTap(type: type) + } ) } private func show(type: RestoreTypeModule.RestoreType) { - guard let viewController = RestoreTypeModule.destination( - restoreType: type, - sourceViewController: self, - returnViewController: returnViewController) else { - return + let viewController: UIViewController + var viaPush = true + switch type { + case .recoveryOrPrivateKey: viewController = RestoreModule.viewController(sourceViewController: self, returnViewController: returnViewController) + case .cloudRestore: viewController = RestoreCloudModule.viewController(returnViewController: returnViewController) + case .fileRestore: + let documentPicker: UIDocumentPickerViewController + if #available(iOS 14.0, *) { + let types = UTType.types(tag: "json", tagClass: UTTagClass.filenameExtension, conformingTo: nil) + documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: types) + } else { + documentPicker = UIDocumentPickerViewController(documentTypes: ["*.json"], in: .import) + } + + documentPicker.delegate = self + documentPicker.allowsMultipleSelection = false + + viaPush = false + viewController = documentPicker + case .cex: viewController = RestoreCexViewController(returnViewController: returnViewController) + } + + if viaPush { + navigationController?.pushViewController(viewController, animated: true) + } else { + present(viewController, animated: true) } + } + + private func show(source: BackupModule.NamedSource) { + let viewController = RestorePassphraseModule.viewController(item: source, returnViewController: returnViewController) navigationController?.pushViewController(viewController, animated: true) } @@ -143,18 +184,28 @@ class RestoreTypeViewController: ThemeViewController { present(viewController, animated: true) } + private func showWrongFile() { + HudHelper.instance.show(banner: .error(string: "alert.cant_recognize".localized)) + } } extension RestoreTypeViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { viewModel.items.enumerated().map { index, item in Section( - id: "restore_type", - headerState: index == 0 ? .margin(height: .margin12) : .margin(height: 0), - footerState: index == viewModel.items.count - 1 ? .margin(height: .margin32) : .margin(height: .margin12), - rows: [row(item)]) + id: "restore_type", + headerState: index == 0 ? .margin(height: .margin12) : .margin(height: 0), + footerState: index == viewModel.items.count - 1 ? .margin(height: .margin32) : .margin(height: .margin12), + rows: [row(item)] + ) } } +} +extension RestoreTypeViewController: UIDocumentPickerDelegate { + func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + if let jsonUrl = urls.first { + viewModel.didPick(url: jsonUrl) + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift index 542ebbebce..534b321441 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift @@ -6,7 +6,9 @@ class RestoreTypeViewModel { let sourceType: BackupModule.Source.Abstract private let showCloudNotAvailableSubject = PassthroughSubject() + private let showWrongFileSubject = PassthroughSubject() private let showModuleSubject = PassthroughSubject() + private let showRestoreBackupSubject = PassthroughSubject() init(cloudAccountBackupManager: CloudBackupManager, sourceType: BackupModule.Source.Abstract) { self.cloudAccountBackupManager = cloudAccountBackupManager @@ -19,10 +21,18 @@ extension RestoreTypeViewModel { showCloudNotAvailableSubject.eraseToAnyPublisher() } + var showWrongFilePublisher: AnyPublisher { + showWrongFileSubject.eraseToAnyPublisher() + } + var showModulePublisher: AnyPublisher { showModuleSubject.eraseToAnyPublisher() } + var showRestoreBackupPublisher: AnyPublisher { + showRestoreBackupSubject.eraseToAnyPublisher() + } + func onTap(type: RestoreTypeModule.RestoreType) { switch type { case .recoveryOrPrivateKey, .cex, .fileRestore: showModuleSubject.send(type) @@ -34,6 +44,15 @@ extension RestoreTypeViewModel { } } } + + func didPick(url: URL) { + do { + let namedSource = try RestoreFileHelper.parse(url: url) + showRestoreBackupSubject.send(namedSource) + } catch { + showWrongFileSubject.send() + } + } } extension RestoreTypeViewModel { @@ -55,7 +74,7 @@ extension RestoreTypeViewModel { switch type { case .recoveryOrPrivateKey: return "restore_type.recovery.title".localized case .cloudRestore: return "restore_type.cloud.title".localized - case .fileRestore: return "restore_type.cloud.title".localized + case .fileRestore: return "restore_type.file.title".localized case .cex: return "restore_type.cex.title".localized } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift index 56df90caf6..53fc39bb2b 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift @@ -16,6 +16,7 @@ extension HudHelper { case saved case savedToCloud case done + case restored case created case imported case walletAdded @@ -51,6 +52,7 @@ extension HudHelper { case .saved: image = UIImage(named: "download_24") case .savedToCloud: image = UIImage(named: "icloud_24") case .done: image = UIImage(named: "circle_check_24") + case .restored: image = UIImage(named: "download_24") case .created: image = UIImage(named: "add_to_wallet_24") case .imported: image = UIImage(named: "add_to_wallet_2_24") case .walletAdded: image = UIImage(named: "binocule_24") @@ -74,7 +76,7 @@ extension HudHelper { switch self { case .addedToWatchlist, .alreadyAddedToWallet, .notSupportedYet, .sent, .swapped, .approved, .revoked, .attention: return .themeJacob case .removedFromWallet, .removedFromWatchlist, .deleted, .noInternet, .disconnectedWalletConnect, .error: return .themeLucian - case .addedToWallet, .copied, .saved, .savedToCloud, .done, .created, .imported, .walletAdded, .enabled, .success: return .themeRemus + case .addedToWallet, .copied, .saved, .savedToCloud, .done, .restored, .created, .imported, .walletAdded, .enabled, .success: return .themeRemus case .waitingForSession, .disconnectingWalletConnect, .enabling, .sending, .swapping, .approving, .revoking: return .themeGray } } @@ -91,6 +93,7 @@ extension HudHelper { case .saved: return "alert.saved".localized case .savedToCloud: return "alert.saved_to_icloud".localized case .done: return "alert.success_action".localized + case .restored: return "alert.restored".localized case .created: return "alert.created".localized case .imported: return "alert.imported".localized case .walletAdded: return "alert.wallet_added".localized diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index bbf3f7aefb..3193f2733b 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -36,9 +36,11 @@ "alert.wrong_amount" = "Wrong Amount"; "alert.no_fee" = "Wrong Fee"; "alert.warning" = "Warning"; +"alert.notice" = "Notice"; "alert.error" = "Error"; "alert.unknown_error" = "Unknown Error"; "alert.success_action" = "Done"; +"alert.restored" = "Restored"; "alert.success" = "Success"; "alert.added_to_watchlist" = "Added to Watchlist"; @@ -1129,6 +1131,10 @@ Go to Settings - > %@ and allow access to the camera."; "backup_app.backup.password.description" = "Set unlock password for your backup. It must consist of at least 8 symbols and include at least one lowercase letter, uppercase letter, number and a special character."; "backup_app.backup.password.highlighted_description" = "This password is used to encrypt backup file of your wallet. It can't be recovered or reset if lost or forgotten."; +"backup_app.restore.notice.description" = "As a result of this action, wallets and contacts from a backup file will be added to the existing wallets and contacts in the app."; +"backup_app.restore.notice.merge" = "Merge"; + + // Settings -> Security "settings_security.title" = "Security";