From 5afac69404f8d5c18d51ad1b28cb06990ad78801 Mon Sep 17 00:00:00 2001 From: Esenbek Date: Thu, 14 Sep 2023 11:28:00 +0600 Subject: [PATCH 01/63] Update app version to 0.36 --- UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index c98d7a71ac..3f1d9daa63 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -10486,7 +10486,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.35; + MARKETING_VERSION = 0.36; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OfficeMode = true; @@ -10558,7 +10558,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.35; + MARKETING_VERSION = 0.36; MTL_ENABLE_DEBUG_INFO = NO; OfficeMode = false; SDKROOT = iphoneos; From 32f5b7131b6a30e881b2b88e10ad749809ec79c5 Mon Sep 17 00:00:00 2001 From: Esenbek Date: Thu, 14 Sep 2023 12:29:34 +0600 Subject: [PATCH 02/63] Disable swap function --- .../WalletTokenBalanceViewItemFactory.swift | 6 +----- .../Modules/Wallet/WalletViewItemFactory.swift | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index 7776682656..399158f2d1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -14,7 +14,7 @@ class WalletTokenBalanceViewItemFactory { var buttons = [WalletModule.Button: ButtonState]() switch item.element { - case .wallet(let wallet): + case .wallet: if item.watchAccount { buttons[.address] = .enabled } else { @@ -22,10 +22,6 @@ class WalletTokenBalanceViewItemFactory { buttons[.send] = sendButtonState buttons[.receive] = .enabled - - if wallet.token.swappable { - buttons[.swap] = sendButtonState - } } case .cexAsset(let cexAsset): buttons[.withdraw] = cexAsset.withdrawEnabled ? .enabled : .disabled diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index da6e27e4a6..cd654bdb2c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -143,8 +143,7 @@ class WalletViewItemFactory { case .evmPrivateKey, .hdExtendedKey, .mnemonic: return [ .send: .enabled, - .receive: .enabled, - .swap: .enabled + .receive: .enabled ] case .evmAddress, .tronAddress: return [:] } From 804203e942c49266ca14440bb02460186bc72ee3 Mon Sep 17 00:00:00 2001 From: Esenbek Date: Thu, 14 Sep 2023 12:39:55 +0600 Subject: [PATCH 03/63] Revert "Disable swap function" This reverts commit 32f5b7131b6a30e881b2b88e10ad749809ec79c5. --- .../WalletTokenBalanceViewItemFactory.swift | 6 +++++- .../Modules/Wallet/WalletViewItemFactory.swift | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index 399158f2d1..7776682656 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -14,7 +14,7 @@ class WalletTokenBalanceViewItemFactory { var buttons = [WalletModule.Button: ButtonState]() switch item.element { - case .wallet: + case .wallet(let wallet): if item.watchAccount { buttons[.address] = .enabled } else { @@ -22,6 +22,10 @@ class WalletTokenBalanceViewItemFactory { buttons[.send] = sendButtonState buttons[.receive] = .enabled + + if wallet.token.swappable { + buttons[.swap] = sendButtonState + } } case .cexAsset(let cexAsset): buttons[.withdraw] = cexAsset.withdrawEnabled ? .enabled : .disabled diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index cd654bdb2c..da6e27e4a6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -143,7 +143,8 @@ class WalletViewItemFactory { case .evmPrivateKey, .hdExtendedKey, .mnemonic: return [ .send: .enabled, - .receive: .enabled + .receive: .enabled, + .swap: .enabled ] case .evmAddress, .tronAddress: return [:] } From d7fab6b35a5bc5b65ecbbb3aef86a230cd039ad0 Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 14 Sep 2023 14:03:27 +0600 Subject: [PATCH 04/63] Add enabled wallets to WalletBackup --- .../Core/Crypto/WalletBackup.swift | 55 +++++++++++++++- .../Managers/CloudAccountBackupManager.swift | 4 +- .../Core/Storage/LocalStorage.swift | 63 ++++++++++++++++++- .../Backup/ICloud/BackupCloudModule.swift | 2 +- .../BackupCloudPassphraseService.swift | 7 ++- .../Backup/ICloud/WalletBackupConverter.swift | 62 +++++++++--------- .../RestoreCloud/RestoreCloudModule.swift | 1 + .../RestoreCloudPassphraseModule.swift | 1 + .../RestoreCloudPassphraseService.swift | 26 +++++++- .../RestoreCloudPassphraseViewModel.swift | 10 ++- 10 files changed, 185 insertions(+), 46 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift index 375f14e63f..63807ee6bf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift @@ -7,9 +7,11 @@ class WalletBackup: Codable { let isManualBackedUp: Bool let version: Int let timestamp: TimeInterval? + let enabledWallets: [EnabledWallet] enum CodingKeys: String, CodingKey { case crypto + case enabledWallets = "enabled_wallets" case id case type case isManualBackedUp = "manual_backup" @@ -17,8 +19,9 @@ class WalletBackup: Codable { case timestamp } - init(crypto: WalletBackupCrypto, id: String, type: AccountType.Abstract, isManualBackedUp: Bool, version: Int, timestamp: TimeInterval) { + init(crypto: WalletBackupCrypto, enabledWallets: [EnabledWallet], id: String, type: AccountType.Abstract, isManualBackedUp: Bool, version: Int, timestamp: TimeInterval) { self.crypto = crypto + self.enabledWallets = enabledWallets self.id = id self.type = type self.isManualBackedUp = isManualBackedUp @@ -29,6 +32,7 @@ class WalletBackup: Codable { required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) crypto = try container.decode(WalletBackupCrypto.self, forKey: .crypto) + enabledWallets = (try? container.decode([EnabledWallet].self, forKey: .enabledWallets)) ?? [] id = try container.decode(String.self, forKey: .id) type = try container.decode(AccountType.Abstract.self, forKey: .type) let isManualBackedUp = try? container.decode(Bool.self, forKey: .isManualBackedUp) @@ -40,12 +44,59 @@ class WalletBackup: Codable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(crypto, forKey: .crypto) + try container.encode(enabledWallets, forKey: .enabledWallets) try container.encode(id, forKey: .id) try container.encode(type, forKey: .type) try container.encode(isManualBackedUp, forKey: .isManualBackedUp) try container.encode(version, forKey: .version) try container.encode(timestamp, forKey: .timestamp) } - } +extension WalletBackup { + struct EnabledWallet: Codable { + let tokenQueryId: String + let coinName: String? + let coinCode: String? + let tokenDecimals: Int? + + enum CodingKeys: String, CodingKey { + case tokenQueryId = "token_query_id" + case coinName = "coin_name" + case coinCode = "coin_code" + case tokenDecimals = "decimals" + } + + init(tokenQueryId: String, coinName: String?, coinCode: String?, tokenDecimals: Int?) { + self.tokenQueryId = tokenQueryId + self.coinName = coinName + self.coinCode = coinCode + self.tokenDecimals = tokenDecimals + } + + init(_ wallet: Wallet) { + tokenQueryId = wallet.token.tokenQuery.id + coinName = wallet.coin.name + coinCode = wallet.coin.code + tokenDecimals = wallet.decimals + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let tokenQueryId = try container.decode(String.self, forKey: .tokenQueryId) + let coinName = try? container.decode(String.self, forKey: .coinName) + let coinCode = try container.decode(String.self, forKey: .coinCode) + let tokenDecimals = try container.decode(Int.self, forKey: .tokenDecimals) + + self.init(tokenQueryId: tokenQueryId, coinName: coinName, coinCode: coinCode, tokenDecimals: tokenDecimals) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(tokenQueryId, forKey: .tokenQueryId) + try container.encode(coinName, forKey: .coinName) + try container.encode(coinCode, forKey: .coinCode) + try container.encode(tokenDecimals, forKey: .tokenDecimals) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift index d88fcc347f..748dc386d6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift @@ -134,14 +134,14 @@ extension CloudAccountBackupManager { iCloudUrl != nil } - func save(accountType: AccountType, isManualBackedUp: Bool, passphrase: String, name: String) throws { + func save(accountType: AccountType, wallets: [Wallet], isManualBackedUp: Bool, passphrase: String, name: String) throws { guard let iCloudUrl else { throw BackupError.urlNotAvailable } do { let name = name + Self.fileExtension - let encoded = try WalletBackupConverter.encode(accountType: accountType, isManualBackedUp: isManualBackedUp, passphrase: passphrase) + let encoded = try WalletBackupConverter.encode(accountType: accountType, wallets: wallets.map { WalletBackup.EnabledWallet($0) }, isManualBackedUp: isManualBackedUp, passphrase: passphrase) try fileStorage.write(directoryUrl: iCloudUrl, filename: name, data: encoded) logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, save \(name)") diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift index 36dafbf00d..e7549ad6cc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift @@ -1,6 +1,6 @@ import Foundation -import StorageKit import MarketKit +import StorageKit class LocalStorage { private let agreementAcceptedKey = "i_understand_key" @@ -25,11 +25,9 @@ class LocalStorage { init(storage: StorageKit.ILocalStorage) { self.storage = storage } - } extension LocalStorage { - var debugLog: String? { get { storage.value(for: debugLogKey) } set { storage.set(value: newValue, for: debugLogKey) } @@ -100,5 +98,64 @@ extension LocalStorage { get { storage.value(for: keyTelegramSupportRequested) ?? false } set { storage.set(value: newValue, for: keyTelegramSupportRequested) } } +} +extension LocalStorage { + var backup: [String: Any] { + var fields: [String: Any] = [ + "lock_time_enabled": lockTimeEnabled.description, + "contacts_sync": remoteContactsSync.description, + "show_indicators": indicatorsShown.description, + ] + + if let chartIndicators { + fields["chart_indicators"] = chartIndicators.hs.hexString + } + + let providers: [String: String] = BlockchainType + .supported + .filter { !$0.allowedProviders.isEmpty } + .map { ($0.uid, defaultProvider(blockchainType: $0).rawValue) } + .reduce(into: [:]) { $0[$1.0] = $1.1 } + + fields["swap_providers"] = providers + + return fields + } + + private func bool(value: Any?) -> Bool? { + if let string = value as? String, + let enabled = Bool(string) { + return enabled + } + return nil + } + + func restore(backup _: [String: Any]) { + if let lockTime = bool(value: backup["lock_time_enabled"]) { + lockTimeEnabled = lockTime + } + if let contactsSync = bool(value: backup["contacts_sync"]) { + remoteContactsSync = contactsSync + } + if let showIndicators = bool(value: backup["show_indicators"]) { + indicatorsShown = showIndicators + } + + if let chartIndicators = backup["chart_indicators"] as? String, + let data = chartIndicators.hs.hexData { + self.chartIndicators = data + } + + if let providers = backup["swap_providers"] as? [String: String] { + providers.forEach { key, value in + let type = BlockchainType(uid: key) + if let provider = SwapModule.Dex.Provider(rawValue: value), + !type.allowedProviders.isEmpty { + + setDefaultProvider(blockchainType: type, provider: provider) + } + } + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift index a82d186a2b..dabc1a6829 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift @@ -22,7 +22,7 @@ class BackupCloudModule { } static func backupPassword(account: Account, name: String) -> UIViewController { - let service = BackupCloudPassphraseService(iCloudManager: App.shared.cloudAccountBackupManager, account: account, name: name) + let service = BackupCloudPassphraseService(iCloudManager: App.shared.cloudAccountBackupManager, walletManager: App.shared.walletManager, account: account, name: name) let viewModel = BackupCloudPassphraseViewModel(service: service) let controller = BackupCloudPassphraseViewController(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift index ba6e86e757..2c522f8eee 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift @@ -2,14 +2,16 @@ import Foundation class BackupCloudPassphraseService { private let iCloudManager: CloudAccountBackupManager + private let walletManager: WalletManager private let account: Account private let name: String var passphrase: String = "" var passphraseConfirmation: String = "" - init(iCloudManager: CloudAccountBackupManager, account: Account, name: String) { + init(iCloudManager: CloudAccountBackupManager, walletManager: WalletManager, account: Account, name: String) { self.iCloudManager = iCloudManager + self.walletManager = walletManager self.account = account self.name = name } @@ -41,7 +43,8 @@ extension BackupCloudPassphraseService { } do { - try iCloudManager.save(accountType: account.type, isManualBackedUp: account.backedUp, passphrase: passphrase, name: name) + let wallets = App.shared.walletManager.wallets(account: account) + try iCloudManager.save(accountType: account.type, wallets: wallets, isManualBackedUp: account.backedUp, passphrase: passphrase, name: name) } catch { if case .urlNotAvailable = error as? CloudAccountBackupManager.BackupError { throw CreateError.urlNotAvailable diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift index 806b88432b..19917aaeb7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift @@ -1,43 +1,45 @@ import Foundation +import MarketKit class WalletBackupConverter { - private static let version = 1 + private static let version = 2 - static func encode(accountType: AccountType, isManualBackedUp: Bool, passphrase: String) throws -> Data { + static func encode(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, passphrase: String) throws -> Data { let message = accountType.uniqueId(hashed: false) let iv = BackupCryptoHelper.generateInitialVector().hs.hex let cipherText = try BackupCryptoHelper.AES128( - operation: .encrypt, - ivHex: iv, - pass: passphrase, - message: message, - kdf: .defaultBackup + operation: .encrypt, + ivHex: iv, + pass: passphrase, + message: message, + kdf: .defaultBackup ) let encodedCipherText = cipherText.base64EncodedString() let mac = try BackupCryptoHelper.mac( - pass: passphrase, - message: encodedCipherText.hs.data, - kdf: .defaultBackup + pass: passphrase, + message: encodedCipherText.hs.data, + kdf: .defaultBackup ) let crypto = WalletBackupCrypto( - cipher: BackupCryptoHelper.defaultCypher, - cipherParams: CipherParams(iv: iv), - cipherText: encodedCipherText, - kdf: BackupCryptoHelper.defaultKdf, - kdfParams: .defaultBackup, - mac: mac.hs.hex) + cipher: BackupCryptoHelper.defaultCypher, + cipherParams: CipherParams(iv: iv), + cipherText: encodedCipherText, + kdf: BackupCryptoHelper.defaultKdf, + kdfParams: .defaultBackup, + mac: mac.hs.hex + ) let backup = WalletBackup( - crypto: crypto, - id: accountType.uniqueId().hs.hex, - type: AccountType.Abstract(accountType), - isManualBackedUp: isManualBackedUp, - version: Self.version, - timestamp: Date().timeIntervalSince1970 + crypto: crypto, + enabledWallets: wallets, + id: accountType.uniqueId().hs.hex, + type: AccountType.Abstract(accountType), + isManualBackedUp: isManualBackedUp, + version: Self.version, + timestamp: Date().timeIntervalSince1970 ) return try JSONEncoder().encode(backup) - } static func decode(data: Data, passphrase: String) throws -> AccountType { @@ -48,11 +50,12 @@ class WalletBackupConverter { } let decryptData = try BackupCryptoHelper.AES128( - operation: .decrypt, - ivHex: backup.crypto.cipherParams.iv, - pass: passphrase, - message: message, - kdf: .defaultBackup) + operation: .decrypt, + ivHex: backup.crypto.cipherParams.iv, + pass: passphrase, + message: message, + kdf: .defaultBackup + ) guard let accountType = AccountType.decode(uniqueId: decryptData, type: backup.type) else { throw CodingError.cantDecodeAccountType @@ -60,14 +63,11 @@ class WalletBackupConverter { return accountType } - } extension WalletBackupConverter { - enum CodingError: Error { case cantDecodeCipherText case cantDecodeAccountType } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift index 3555a45c19..b6fe4d0854 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift @@ -22,6 +22,7 @@ struct RestoreCloudModule { let name: String let accountType: AccountType let isManualBackedUp: Bool + let showSelectCoins: Bool } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift index d6c399489f..a215dd5fc3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift @@ -8,6 +8,7 @@ class RestoreCloudPassphraseModule { iCloudManager: App.shared.cloudAccountBackupManager, accountFactory: App.shared.accountFactory, accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager, item: item ) let viewModel = RestoreCloudPassphraseViewModel(service: service) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift index 238e4c3594..875feebdc0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift @@ -4,16 +4,34 @@ class RestoreCloudPassphraseService { private let iCloudManager: CloudAccountBackupManager private let accountFactory: AccountFactory private let accountManager: AccountManager + private let walletManager: WalletManager private let restoredBackup: RestoreCloudModule.RestoredBackup var passphrase: String = "" - init(iCloudManager: CloudAccountBackupManager, accountFactory: AccountFactory, accountManager: AccountManager, item: RestoreCloudModule.RestoredBackup) { + init(iCloudManager: CloudAccountBackupManager, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, item: RestoreCloudModule.RestoredBackup) { self.iCloudManager = iCloudManager self.accountFactory = accountFactory self.accountManager = accountManager - self.restoredBackup = item + self.walletManager = walletManager + restoredBackup = item + } + + private func createAccount(accountType: AccountType) { + let account = accountFactory.account(type: accountType, origin: .restored, backedUp: restoredBackup.walletBackup.isManualBackedUp, name: restoredBackup.name) + accountManager.save(account: account) + + let wallets = restoredBackup.walletBackup.enabledWallets.map { + EnabledWallet( + tokenQueryId: $0.tokenQueryId, + accountId: account.id, + coinName: $0.coinName, + coinCode: $0.coinCode, + tokenDecimals: $0.tokenDecimals + ) + } + walletManager.save(enabledWallets: wallets) } } @@ -77,10 +95,12 @@ extension RestoreCloudPassphraseService { accountManager.save(account: account) return .success default: + createAccount(accountType: accountType) return .restoredAccount(RestoreCloudModule.RestoredAccount( name: restoredBackup.name, accountType: accountType, - isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp + isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp, + showSelectCoins: restoredBackup.walletBackup.enabledWallets.isEmpty )) } } catch { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift index 5aa92a7d20..0f9d9da4e9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift @@ -67,8 +67,14 @@ extension RestoreCloudPassphraseViewModel { self?.processing = false switch result { - case .success: self?.successSubject.send() - case .restoredAccount(let account): self?.openSelectCoinsSubject.send(account) + case .success: + self?.successSubject.send() + case .restoredAccount(let account): + if account.showSelectCoins { + self?.openSelectCoinsSubject.send(account) + } else { + self?.successSubject.send() + } } } catch { switch (error as? RestoreCloudPassphraseService.RestoreError) { From b6b029bd42b9a9c14255de2651ec1f8c95a127cd Mon Sep 17 00:00:00 2001 From: _imadia Date: Thu, 14 Sep 2023 16:48:46 +0600 Subject: [PATCH 05/63] Add Trump Style for test --- .../en.lproj/Localizable.strings | 4 - .../ru.lproj/Localizable.strings | 2725 ++++++++--------- 2 files changed, 1356 insertions(+), 1373 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 60350ff05e..142033878d 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -243,7 +243,6 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.title" = "Backup to iCloud"; "backup.cloud.description" = "iCloud storage is a third-party cloud storage service provided by Apple. It's important to know that your data will be stored on Apple's servers, not on your personal devices. This means that you are entrusting your data and handing over the security of your information to a third-party service."; - "backup.cloud.terms.item.1" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; "backup.cloud.name.title" = "Backup Name"; @@ -270,10 +269,8 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.cant_create_file" = "Can't save File to iCloud"; "backup.cloud.cant_delete_file" = "Can't delete from iCloud"; "backup.cloud.no_access.title" = "Access iCloud"; -"backup.cloud.no_access.title" = "Access iCloud"; "backup.cloud.no_access.description" = "To create a backup, you need to provide access to iCloud storage."; - // Errors "error.send.self_transfer" = "Sending to yourself is not supported"; @@ -542,7 +539,6 @@ Go to Settings - > %@ and allow access to the camera."; "swap.confirmation.maximum_sent" = "Maximum Sent"; "swap.dex_info.description" = "This exchange service is powered by %@, a decentralized token exchange protocol built on the %@ blockchain. \n\n%@ is fully automated and managed by smart contracts that facilitate token exchanges in a reliable manner without any means to cheat."; - "swap.dex_info.header_dex_related" = "%@ Related"; "swap.dex_info.header_allowance" = "Allowance"; "swap.dex_info.content_allowance" = "The amount an exchange can spend on a user’s behalf when executing token swaps. A proceeding transaction setting sufficient allowance is required before an actual swap transaction can take place."; diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index d5ece57cf3..a418495d4d 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -1,1071 +1,1070 @@ -"button.ok" = "ОК"; -"button.apply" = "Применить"; -"button.cancel" = "Отменить"; -"button.close" = "Закрыть"; -"button.continue" = "Продолжить"; -"button.done" = "Готово"; -"button.reset" = "Сбросить"; -"button.import" = "Импорт"; -"button.next" = "Далее"; -"button.delete" = "Удалить"; -"button.share" = "Поделиться"; -"button.paste" = "Вставить"; -"button.resend" = "Отправить повторно"; -"button.backup" = "Резервная копия"; -"button.copy" = "Копировать"; -"button.retry" = "Повторить"; -"button.report" = "Пожаловаться"; -"button.add" = "Добавить"; -"button.approve" = "Разрешить"; -"button.revoke" = "Отменить"; -"button.reject" = "Отклонить"; -"button.connect" = "Подключиться"; -"button.save" = "Сохранить"; -"button.send" = "Отправить"; -"button.sign" = "Подписать"; -"button.more" = "Показать больше"; -"button.i_understand" = "Я понимаю"; -"button.learn_more" = "Подробнее"; - -"alert" = "Предупреждение"; -"alert.copied" = "Скопировано"; -"alert.saved" = "Сохранено"; -"alert.saved_to_icloud" = "Сохранено в iCloud"; -"alert.no_internet" = "Нет интернета"; -"alert.wrong_amount" = "Неправильная сумма"; -"alert.no_fee" = "Неправильная комиссия"; -"alert.warning" = "Внимание"; -"alert.error" = "Ошибка"; -"alert.unknown_error" = "Неизвестная ошибка"; -"alert.success_action" = "Готово"; -"alert.success" = "Успешно"; - -"alert.added_to_watchlist" = "Добавлено в избранное"; -"alert.removed_from_watchlist" = "Удалить из избранного"; -"alert.added_to_wallet" = "Добавлено в кошелек"; -"alert.removed_from_wallet" = "Удалено из кошелька"; -"alert.already_added_to_wallet" = "Уже добавлено в кошелек"; -"alert.not_supported_yet" = "Ещё не поддерживается"; -"alert.copied" = "Скопировано"; -"alert.created" = "Создан"; -"alert.imported" = "Импортировано"; -"alert.wallet_added" = "Кошелек добавлен"; -"alert.deleted" = "Удалено"; -"alert.waiting_for_session" = "Ожидание cессии"; -"alert.try_again" = "Попробовать ещё раз"; -"alert.disconnecting" = "Отсоединение"; -"alert.disconnected" = "Отсоединено"; -"alert.enabling" = "Идет активация"; -"alert.enabled_coins" = "Включено ещё %@ монет"; -"alert.sending" = "Отправка"; -"alert.sent" = "Отправлено"; -"alert.swapping" = "Обмен"; -"alert.swapped" = "Обмен выполнен"; -"alert.approving" = "Подтверждение"; -"alert.approved" = "Разрешено"; -"alert.revoking" = "Отмена"; -"alert.revoked" = "Отменен"; -"alert.cant_recognize" = "Не удалось распознать"; - -"no_results_found" = "Ничего не найдено"; -"action.loading" = "загрузка..."; -"placeholder.search" = "Поиск"; - -"status" = "Статус"; -"connecting" = "Подключение..."; -"online" = "Доступен"; -"offline" = "Не доступен"; -"confirm" = "Подтвердить"; -"connect" = "Подключиться"; -"price" = "Цена"; -"value" = "Значение"; -"note" = "Примечание"; - -"version" = "Версия %@"; - -"n/a" = "Нет данных"; - -"sync_error" = "Ошибка синхронизации. Повторите попытку."; - -"number.thousand" = "%@ тыс."; -"number.million" = "%@ млн"; -"number.billion" = "%@ млрд"; -"number.trillion" = "%@ трл"; -"number.quadrillion" = "%@ квадрлн"; - -"selector.any" = "Любое"; +"button.ok" = "OK"; +"button.apply" = "Apply"; +"button.cancel" = "Cancel"; +"button.close" = "Close"; +"button.continue" = "Continue"; +"button.done" = "Done"; +"button.reset" = "Reset"; +"button.import" = "Import"; +"button.next" = "Next"; +"button.delete" = "Delete"; +"button.share" = "Share"; +"button.paste" = "Paste"; +"button.resend" = "Resend"; +"button.backup" = "Backup"; +"button.copy" = "Copy"; +"button.retry" = "Retry"; +"button.report" = "Report"; +"button.add" = "Add"; +"button.approve" = "Approve"; +"button.revoke" = "Revoke"; +"button.reject" = "Reject"; +"button.connect" = "Connect"; +"button.save" = "Save"; +"button.send" = "Send"; +"button.sign" = "Sign"; +"button.more" = "Show More"; +"button.i_understand" = "I Understand"; +"button.learn_more" = "Learn More"; + +"alert" = "Alert"; +"alert.copied" = "Copied"; +"alert.saved" = "Saved"; +"alert.saved_to_icloud" = "Saved to iCloud"; +"alert.no_internet" = "No Internet"; +"alert.wrong_amount" = "Wrong Amount"; +"alert.no_fee" = "Wrong Fee"; +"alert.warning" = "Warning"; +"alert.error" = "Error"; +"alert.unknown_error" = "Unknown Error"; +"alert.success_action" = "Done"; +"alert.success" = "Success"; + +"alert.added_to_watchlist" = "Added to Watchlist"; +"alert.removed_from_watchlist" = "Removed from Watchlist"; +"alert.added_to_wallet" = "Added to Wallet"; +"alert.removed_from_wallet" = "Removed from Wallet"; +"alert.already_added_to_wallet" = "Already added to Wallet"; +"alert.not_supported_yet" = "Not Supported Yet"; +"alert.created" = "Created"; +"alert.imported" = "Imported"; +"alert.wallet_added" = "Wallet Added"; +"alert.deleted" = "Deleted"; +"alert.waiting_for_session" = "Waiting for Session"; +"alert.try_again" = "Try Again"; +"alert.disconnecting" = "Disconnecting"; +"alert.disconnected" = "Disconnected"; +"alert.enabling" = "Enabling"; +"alert.enabled_coins" = "Enabled %@ more coins"; +"alert.sending" = "Sending"; +"alert.sent" = "Sent"; +"alert.swapping" = "Swapping"; +"alert.swapped" = "Swapped"; +"alert.approving" = "Approving"; +"alert.approved" = "Approved"; +"alert.revoking" = "Revoking"; +"alert.revoked" = "Revoked"; +"alert.cant_recognize" = "Can't Recognize"; + +"no_results_found" = "No results found"; +"action.loading" = "loading..."; +"placeholder.search" = "Search"; + +"status" = "Status"; +"connecting" = "Connecting..."; +"online" = "Online"; +"offline" = "Offline"; +"confirm" = "Confirm"; +"connect" = "Connect"; +"price" = "Price"; +"value" = "Value"; +"note" = "Note"; + +"version" = "Version %@"; + +"n/a" = "N/A"; + +"sync_error" = "Sync error. Try again."; + +"number.thousand" = "%@K"; +"number.million" = "%@M"; +"number.billion" = "%@B"; +"number.trillion" = "%@T"; +"number.quadrillion" = "%@Q"; + +"selector.any" = "Any"; // Access Camera -"access_camera.message" = "%@ требуется доступ к вашей камере для сканирования QR-кода. +"access_camera.message" = "Listen folks, %@ needs to see through your camera, okay. +To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; -Перейдите в \"Настройки\" - > %@ и разрешите доступ к камере."; -"access_camera.settings" = "Настройки"; +"access_camera.settings" = "Settings"; // Restore -"restore.title" = "Импорт кошелька"; -"restore.advanced" = "Доп. настройки"; -"restore.import_by" = "Через"; -"restore.restore_type.mnemonic" = "Фраза восстановления"; -"restore.restore_type.private_key" = "Приватный ключ"; -"restore.mnemonic.placeholder" = "Введите фразу восстановления"; -"restore.private_key.placeholder" = "Введите приватный ключ EVM, BIP32 Root Key или Account Extended Private Key"; -"restore.private_key.invalid_key" = "Неверный ключ"; -"restore_error.mnemonic_word_count" = "Неправильное количество слов. Должно быть 12-24 слова. Вы ввели: %@"; -"restore.checksum_error" = "Неверная контрольная сумма"; -"restore.passphrase" = "Кодовая фраза"; -"restore.input.passphrase" = "Кодовая фраза"; -"restore.passphrase_description" = "Этот пароль используется для шифрования файлов резервной копии вашего кошелька. Он не связан с вашим паролем Apple iCloud. Если вы потеряете или забудете этот пароль, его нельзя будет восстановить или сбросить."; -"restore.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; -"restore.non_standard_import" = "Нестандартный импорт"; -"restore.non_standard_import.description" = "На этой странице представлен специальный механизм восстановления кошелька для пользователей %@ с нестандартным кошельком. Как правило, такие кошельки могли быть созданы в версиях %@ 0.27–0.28 с использованием неанглоязычного списка мнемонических слов и/или специального символа в парольной фразе кошелька (например, диакритического знака). -\n\nЕсли вы являетесь затронутым пользователем, то баланс вашего кошелька будет отображаться как 0 после восстановления такого кошелька в версии 0.29 или выше.Эта страница позволит вам восстановить доступ к вашему нестандартному кошельку. После восстановления рекомендуется создать новый кошелек (который будет соответствовать стандарту) и перевести туда средства."; -"restore.warning.non_recommended.description" = "Похоже, этот кошелек использует нестандартный символ в списке мнемонических слов и/или парольной фразе. Если вы не видите баланс или транзакции, пожалуйста, прочтите подробности ниже. -\n\nПОЖАЛУЙСТА НАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ."; -"restore.error.non_standard.description" = "Это нестандартный кошелек.\n\nНАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; +"restore.title" = "Import Wallet"; +"restore.advanced" = "Advanced"; +"restore.import_by" = "Import By"; +"restore.restore_type.mnemonic" = "Recovery Phrase"; +"restore.restore_type.private_key" = "Top secret! Private Key."; +"restore.mnemonic.placeholder" = "Give me those magic words! Recovery Phrase, come on!"; +"restore.private_key.placeholder" = "Enter EVM Private Key, BIP32 Root Key or Account Extended Private Key"; +"restore.private_key.invalid_key" = "This key? Not the best. It's wrong!"; +"restore_error.mnemonic_word_count" = "Listen, we need 12 to 24 words. Not more, not less. Perfect balance. You gave me: %@"; +"restore.checksum_error" = "Checksum? Fake news! It's wrong!"; +"restore.passphrase" = "Passphrase"; +"restore.input.passphrase" = "Passphrase"; +"restore.passphrase_description" = "You know, we have this tremendous password. It's for your wallet backup, not your Apple thing. It's so good, if you lose it, even I can't get it back for you!"; +"restore.error.empty_passphrase" = "Come on folks, you've got to give me something here! Can't have an empty passphrase."; +"restore.non_standard_import" = "Something a little different, a bit out of the box, folks."; +"restore.non_standard_import.description" = "This page, it's special, believe me. For all you with those non-standard wallets. Maybe you've used some fancy foreign words or characters. You know which version I'm talking about, the %@ one. Very specific."; +"restore.warning.non_recommended.description" = "Looks like this wallet's been to some exotic places with its characters! If your balance looks funny, it's not fake news. Dive in below for the real story."; +"restore.error.non_standard.description" = "This wallet? It's not your everyday kind of thing. Tap below, and I'll spill the beans."; // Restore Type -"restore_type.title" = "Импорт кошелька"; +"restore_type.title" = "Importing The Best Wallet Ever!"; -"restore_type.recovery.title" = "Фраза восстановления"; +"restore_type.recovery.title" = "The Tremendous Recovery Phrase!"; "restore_type.cloud.title" = "iCloud"; -"restore_type.cex.title" = "с кошелька биржи"; +"restore_type.cex.title" = "From the Exchange Wallet – The Best!"; -"restore_type.recovery.description" = "Импорт с помощью фразы восстановления или приватного ключа."; -"restore_type.cloud.description" = "Импорт из файла резервной копии в вашем keychain."; -"restore_type.cex.description" = "Подключиться к кошельку на централизованной бирже."; +"restore_type.recovery.description" = "Importing with the mightiest recovery phrase or a super private key!"; +"restore_type.cloud.description" = "Importing from the backup – straight outta your keychain!"; +"restore_type.cex.description" = "Connecting to the wallet on the grand centralized exchange!"; // Restore Cloud -"restore.cloud.title" = "Резерв. копия"; -"restore.cloud.description" = "Выберите резервную копию кошелька, который вы хотите восстановить."; -"restore.cloud.empty" = "Резервные копии не найдены."; -"restore.cloud.imported" = "Импортированные кошельки"; +"restore.cloud.title" = "Backup? The Best Backup!"; +"restore.cloud.description" = "Pick your wallet's backup, the one you really wanna bring back!"; +"restore.cloud.empty" = "Backups? None found. Sad!"; +"restore.cloud.imported" = "Wallets that we've made great again!"; -"restore.cloud.password.title" = "Введите пароль"; -"restore.cloud.password.placeholder" = "Пароль резервной копии"; -"restore.cloud.password.description" = "Введите пароль резервной копии для импорта вашего кошелька из iCloud."; +"restore.cloud.password.title" = "Type in the most secret password!"; +"restore.cloud.password.placeholder" = "Your fantastic backup password here!"; +"restore.cloud.password.description" = "Key in your majestic backup password to get that wallet out of iCloud!"; // Restore Cex -"restore.cex.title" = "Выберите CEX"; -"restore.cex.description" = "Выберите централизованный обмен, к которому вы хотите подключиться."; +"restore.cex.title" = "Pick Your CEX – The Best CEX!"; +"restore.cex.description" = "Choose your centralized exchange, the greatest of them all, believe me!"; // Restore Binance -"restore.binance.description" = "Пожалуйста, предоставьте ключ API и секрет API, чтобы связать вашу биржу."; -"restore.binance.api_key" = "API ключ"; -"restore.binance.secret_key" = "Секретный ключ"; -"restore.binance.connect" = "Подключиться"; -"restore.binance.connecting" = "Подключение..."; -"restore.binance.get_api_keys" = "Получить API ключ"; -"restore.binance.failed_to_connect" = "Не удалось подключиться к твоему API ключу"; -"restore.binance.invalid_qr_code" = "Недопустимый QR-код"; +"restore.binance.description" = "Hand over those API Keys and Secret! Only the best for our exchange!"; +"restore.binance.api_key" = "The Unbeatable API Key!"; +"restore.binance.secret_key" = "Secret Key"; +"restore.binance.connect" = "Connect"; +"restore.binance.connecting" = "Connecting..."; +"restore.binance.get_api_keys" = "Get API Keys"; +"restore.binance.failed_to_connect" = "Couldn't connect your API Key. Sad! But we'll try again!"; +"restore.binance.invalid_qr_code" = "That QR Code? Not the best. Try another!"; // Coin Settings -"coin_settings.title" = "Настройки блокчейна"; +"coin_settings.title" = "Blockchain Settings"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (рекомендованный)"; -"sync_mode.from_blockchain" = "Из блокчейна"; -"blockchain_settings.description" = "Выберите формат адреса для получения средств. При восстановлении существующего кошелька должен быть выбран правильный формат."; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; +"sync_mode.from_blockchain" = "From Blockchain"; +"blockchain_settings.description" = "Choose your address format. Let's make your payments great, just like when you pick the right tie! Always be spot on."; // Coin Platforms -"coin_platforms.native" = "Нативный"; +"coin_platforms.native" = "Native"; // Copy Warning -"copy_warning.title" = "Риск копирования"; -"copy_warning.description" = "В качестве меры безопасности мы рекомендуем не использовать действие копирования."; -"copy_warning.dont_copy" = "Не копировать"; -"copy_warning.i_will_risk_it" = "Я рискую"; +"copy_warning.title" = "Woah! Risky copying ahead."; +"copy_warning.description" = "For safety, folks, I'd say don't copy this! I have the best safety tips."; +"copy_warning.dont_copy" = "Don't Copy"; +"copy_warning.i_will_risk_it" = "I'm feeling brave today!"; // Recovery Phrase -"recovery_phrase.title" = "Фраза восстановления"; -"recovery_phrase.warning" = "Никогда ни с кем не делитесь вашей фразой восстановления. Команда %@ никогда не запросит вашу фразу восстановления."; -"recovery_phrase.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; -"recovery_phrase.passphrase" = "Кодовая фраза"; -"recovery_phrase.copy_warning.title" = "Риск копирования фразы восстановления"; -"recovery_phrase.copy_warning.description" = "В целях безопасности мы не рекомендуем использовать действие копирования фразы восстановления."; +"recovery_phrase.title" = "Recovery Phrase"; +"recovery_phrase.warning" = "Never ever share! Remember, the %@ team won't ask. It's like your personal hair secret."; +"recovery_phrase.tap_to_show" = "Give it a tap! Unveil the magic words."; +"recovery_phrase.passphrase" = "Passphrase"; +"recovery_phrase.copy_warning.title" = "Risk of Recovery Phrase copy"; +"recovery_phrase.copy_warning.description" = "Stay safe! I wouldn't copy that if I were you. Best recommendation."; // EVM Private Key -"evm_private_key.title" = "Приватный Ключ EVM"; -"evm_private_key.tap_to_show" = "Нажмите, чтобы показать приватный ключ"; +"evm_private_key.title" = "EVM Private Key"; +"evm_private_key.tap_to_show" = "Give it a little tap! The big reveal."; // Extended Key -"extended_key.bip32_root_key" = "BIP32 Root Key"; -"extended_key.account_extended_private_key" = "Account Extended Private Key"; -"extended_key.account_extended_public_key" = "Account Extended Public Key"; -"extended_key.purpose" = "Purpose"; -"extended_key.blockchain" = "Blockchain"; -"extended_key.account" = "Account"; -"extended_key.tap_to_show" = "Нажмите, чтобы показать extended private key"; + "extended_key.bip32_root_key" = "BIP32 Root Key"; + "extended_key.account_extended_private_key" = "Account Extended Private Key"; + "extended_key.account_extended_public_key" = "Account Extended Public Key"; + "extended_key.purpose" = "Purpose"; + "extended_key.blockchain" = "Blockchain"; + "extended_key.account" = "Account"; + "extended_key.tap_to_show" = "Drumroll, please... Tap to unveil!"; // Backup -"backup.title" = "Фраза восстановления"; -"backup.description" = "Напишите эти слова в правильном порядке и храните их в безопасном месте"; -"backup.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; -"backup.passphrase" = "Кодовая фраза"; -"backup.verify" = "Подтвердить"; -"backup.verified" = "Подтверждено"; +"backup.title" = "Yuuuge Recovery Phrase"; +"backup.description" = "Jot these words down, in the right order, just like my best speeches. Keep them safe, maybe next to your tax returns!"; +"backup.tap_to_show" = "Give it a tap to reveal the best recovery phrase!"; +"backup.passphrase" = "Passphrase"; +"backup.verify" = "Double-check"; +"backup.verified" = "Boom! Verified, and you know I love verifying things."; // Backup Verify Words -"backup_verify_words.title" = "Подтвердить"; -"backup_verify_words.description" = "Выберите два запрошенные слова из вашей фразы восстановления"; -"backup_verify_words.incorrect_word" = "Неверное слово"; +"backup_verify_words.title" = "Double-check Time"; +"backup_verify_words.description" = "Pick the two words I'm asking for, from your top-notch recovery phrase"; +"backup_verify_words.incorrect_word" = "Oops! Not the word we wanted."; // Backup Verify Passphrase -"backup_verify_passphrase.title" = "Подтвердить"; -"backup_verify_passphrase.description" = "Введите кодовую фразу"; -"backup_verify_passphrase.incorrect_passphrase" = "Неверная кодовая фраза"; +"backup_verify_passphrase.title" = "Verification Station"; +"backup_verify_passphrase.description" = "Type in the passphrase. Make sure it's terrific!"; +"backup_verify_passphrase.incorrect_passphrase" = "That's not the right passphrase, but we'll get there!"; // Backup Required -"backup_required.title" = "Требуется резервная копия"; - -// Backup Prompt - -"backup_prompt.title" = "Ручное резервное копирование"; -"backup_prompt.warning" = "Создайте резервную копию вашей фразы восстановления и пароля, которые позволят вам восстановить ваш кошелек, если телефон потерян, украден, сломал и т.д."; -"backup_prompt.backup" = "Резервная копия"; -"backup_prompt.backup_manual" = "Ручное резервное копирование"; -"backup_prompt.backup_cloud" = "Резерв. копирование в iCloud"; -"backup_prompt.later" = "Позже"; - -// Backup to iCloud - -"backup.cloud.title" = "Резерв. копирование в iCloud"; -"backup.cloud.description" = "Хранилище iCloud является облачным хранилищем, предоставляемым сторонней компанией и доступным через Apple. Важно знать, что ваши данные будут храниться на серверах Apple, а не на ваших личных устройствах. Это означает, что вы доверяете свои данные и передаете безопасность своей информации стороннему сервису."; - -"backup.cloud.terms.item.1" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; - -"backup.cloud.name.title" = "Имя резервной копии"; -"backup.cloud.name.description" = "Введите имя файла резервной копии."; -"backup.cloud.name.empty" = "Имя резервной копии не может быть пустым!"; -"backup.cloud.name.error.empty" = "Имя резервной копии не должно быть пустым"; -"backup.cloud.name.error.already_exist" = "Имя резервной копии уже существует!"; -"backup.cloud.name.placeholder" = "Название"; - -"backup.cloud.password.title" = "Установить пароль"; -"backup.cloud.password.description" = "Установите пароль разблокировки для своей резервной копии. Пароль должен содержать не менее 8 символов и включать как минимум одну строчную букву, заглавную букву, цифру и специальный символ."; -"backup.cloud.password.highlighted_description" = "Не забудьте этот пароль! Он отличается от вашего пароля для Apple iCloud и не может быть восстановлен или сброшен."; -"backup.cloud.password.placeholder" = "Пароль"; -"backup.cloud.password.confirm.placeholder" = "Подтвердить"; -"backup.cloud.password.save" = "Сохранить и создать резервную копию"; - -"backup.cloud.password.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; -"backup.cloud.password.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"backup.cloud.password.error.minimum_requirement" = "Не менее 8 символов, включая одну заглавную букву, одну строчную букву, одну цифру и один символ"; -"backup.cloud.password.error.invalid_password" = "Неверный пароль"; -"backup.cloud.password.error.invalid_backup" = "Резервная копия повреждена"; -"backup.cloud.password.confirm.error.doesnt_match" = "Пароли не совпадают"; -"backup.cloud.not_available" = "iCloud недоступен"; -"backup.cloud.cant_create_file" = "Не удается сохранить файл в iCloud"; -"backup.cloud.cant_delete_file" = "Не удается удалить из iCloud"; -"backup.cloud.no_access.title" = "Доступ к iCloud"; -"backup.cloud.no_access.title" = "Доступ к iCloud"; -"backup.cloud.no_access.description" = "Для создания резервной копии необходимо предоставить доступ к iCloud памяти."; - - -// Errors - -"error.send.self_transfer" = "Отправка самому себе невозможна"; -"error.send_binance.memo_required" = "При отправке транзакции получателю необходимо заполнить поле мемо"; -"error.send_binance.only_digits_allowed" = "Поле мемо должно содержать только цифры"; -"error.send_z_cash.transparent_address" = "%@ не поддерживает платежи на публичный адрес"; - -// Balance - -"balance.title" = "Баланс"; -"balance.tab_bar_item" = "Баланс"; -"balance.send" = "Отправить"; -"balance.withdraw" = "Вывод средств"; -"balance.swap" = "Обменять"; -"balance.receive" = "Получить"; -"balance.deposit" = "Получить"; -"balance.address" = "Адрес"; -"balance.rate_per_coin" = "%@ за %@"; -"balance.syncing" = "Синхронизация..."; -"balance.searching" = "Поиск транзакций..."; -"balance.downloading_sapling" = "Загрузка sapling... %d%%"; -"balance.downloading_blocks" = "Загрузка блоков"; -"balance.scanning_blocks" = "Сканирование блоков"; -"balance.enhancing_transactions" = "Улучшение транзакций"; - -"balance.searching.count" = "%@ tx"; -"balance.syncing_percent" = "Синхронизация... %@"; -"balance.synced_through" = "до %@"; -"balance.add_coin" = "Добавить токен"; -"balance.invalid_api_key" = "Недействительный ключ API"; -"balance.empty.add_coins" = "Добавить токены"; -"balance.empty.description" = "Вы еще не добавили токены в этот кошелек."; -"balance.watch_empty.description" = "У кошелька с этим адресом нет баланса"; -"balance.sort_by" = "Сортировать"; -"balance.sort.header" = "Сортировать"; -"balance.sort.valueHighToLow" = "Баланс"; -"balance.sort.az" = "Название"; -"balance.sort.price_change" = "По изменению цены (%)"; - -"balance_error.change_source" = "Изменить источник"; -"balance_error.sync_error" = "Ошибка синхронизации"; -"lost_accounts.warning_title" = "Ошибка связки ключей iOS"; -"lost_accounts.warning_message" = "Зашифрованные данные вашего кошелька были недавно аннулированы, поскольку ваш экран блокировки iOS был изменен"; +"backup_required.title" = "Backup is a Must, Folks"; + +// **Backup Prompt** + +"backup_prompt.title" = "Backup Bonanza"; +"backup_prompt.warning" = "You want to make a yuuuge backup of this recovery phrase, trust me. Don't lose out if your phone goes missing, or breaks!"; +"backup_prompt.backup" = "Backup Time!"; +"backup_prompt.backup_manual" = "Do It the Old Fashioned Way"; +"backup_prompt.backup_cloud" = "Send It Up to iCloud!"; +"backup_prompt.later" = "Not now, but don't wait too long!"; + +// **Backup to iCloud** + +"backup.cloud.title" = "Sky High iCloud Backup"; +"backup.cloud.description" = "So, iCloud's an Apple thing. They'll keep your stuff, not on your gadgets. You're trusting them, not us. Just so we're clear!"; +"backup.cloud.terms.item.1" = "Listen up! Lose access to iCloud, and you say goodbye to this backup."; + +"backup.cloud.name.title" = "What's in a Name?"; +"backup.cloud.name.description" = "Name your backup, maybe after one of my hotels?"; +"backup.cloud.name.empty" = "Give it a name, c'mon!"; +"backup.cloud.name.error.empty" = "A name's what we need!"; +"backup.cloud.name.error.already_exist" = "Been there, named that. Pick another!"; +"backup.cloud.name.placeholder" = "Fabulous Name"; + +"backup.cloud.password.title" = "The Best Password"; +"backup.cloud.password.description" = "Craft a password that's tremendous! Needs 8 characters and a mix of the alphabet, numbers, and those little symbols."; +"backup.cloud.password.highlighted_description" = "Remember this one! It's not your Apple password, and it's irreplaceable!"; +"backup.cloud.password.placeholder" = "Amazing Password"; +"backup.cloud.password.confirm.placeholder" = "Confirm the Brilliance"; +"backup.cloud.password.save" = "Seal the Deal & Backup"; + +"backup.cloud.password.error.empty_passphrase" = "Hey, we need that passphrase!"; +"backup.cloud.password.error.forbidden_symbols" = "Stick to the basics, folks: A-Z, a-z, 0-9, and those symbols!"; +"backup.cloud.password.error.minimum_requirement" = "Needs 8 characters, at least! Upper, lower, number, and symbol. The total package!"; +"backup.cloud.password.error.invalid_password" = "Not the right password, but keep trying!"; +"backup.cloud.password.error.invalid_backup" = "Whoops! Something's fishy with the backup."; +"backup.cloud.password.confirm.error.doesnt_match" = "Those passwords aren't buddies yet. Try again!"; +"backup.cloud.not_available" = "iCloud's taking a break!"; +"backup.cloud.cant_create_file" = "Can't ship that file to iCloud!"; +"backup.cloud.cant_delete_file" = "Can't kick it out of iCloud!"; +"backup.cloud.no_access.title" = "Knock on iCloud's Door"; +"backup.cloud.no_access.description" = "Need your green light for iCloud storage!"; + +// **Errors** + +"error.send.self_transfer" = "C'mon now, sending to yourself? We don't do that here!"; +"error.send_binance.memo_required" = "The receiver's like 'I need a memo!' Don't leave it empty!"; +"error.send_binance.only_digits_allowed" = "Memo's gotta have digits only! Numbers, always winning!"; +"error.send_z_cash.transparent_address" = "%@ says 'No way!' to payments to a transparent address."; + +// **Balance** + +"balance.title" = "Big League Balance"; +"balance.tab_bar_item" = "Yuuuge Balance"; +"balance.send" = "Ship It"; +"balance.withdraw" = "Pull Out"; +"balance.swap" = "Switcheroo"; +"balance.receive" = "Gimme"; +"balance.deposit" = "Throw In"; +"balance.address" = "Prime Location"; +"balance.rate_per_coin" = "%@ for each %@"; +"balance.syncing" = "Getting in sync..."; +"balance.searching" = "On the hunt for transactions..."; +"balance.downloading_sapling" = "Grabbing Sapling... %d%%"; +"balance.downloading_blocks" = "Fetching those Blocks"; +"balance.scanning_blocks" = "Eyeing those Blocks"; +"balance.enhancing_transactions" = "Sprucing up Transactions"; + +"balance.searching.count" = "%@ big deals"; +"balance.syncing_percent" = "Syncing... %@"; +"balance.synced_through" = "all the way to %@"; +"balance.add_coin" = "Add some Bling"; +"balance.invalid_api_key" = "API Key's a no-go!"; +"balance.empty.add_coins" = "More Bling!"; +"balance.empty.description" = "No coins in this wallet yet. Sad!"; +"balance.watch_empty.description" = "This wallet address is feeling a bit light!"; +"balance.sort_by" = "Rank 'em"; +"balance.sort.header" = "Rank them like"; +"balance.sort.valueHighToLow" = "By the Numbers"; +"balance.sort.az" = "By Name"; +"balance.sort.price_change" = "Who's Moving Up"; + +"balance_error.change_source" = "Switch it Up!"; +"balance_error.sync_error" = "Whoops, sync hiccup!"; +"lost_accounts.warning_title" = "Oh No, iOS Keychain Error!"; +"lost_accounts.warning_message" = "The encrypted treasure holding your wallet got messed up 'cause you changed your iOS lock screen."; // Token Balance Page -"balance.token.locked" = "Заблокировано"; -"balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; -"balance.token.staked" = "Staked"; -"balance.token.staked.info.title" = "Staked title"; -"balance.token.staked.info.description" = "Staked Description Text"; -"balance.token.frozen" = "Frozen"; -"balance.token.frozen.info.title" = "Frozen title"; -"balance.token.frozen.info.description" = "Frozen Description Text"; +"balance.token.locked" = "On Lockdown"; +"balance.token.locked.info.title" = "Hold On!"; +"balance.token.locked.info.description" = "The sender's put a timer on this. The Bitcoins are yours, but you gotta wait a tad to spend them on the Bitcoin network."; +"balance.token.staked" = "All In"; +"balance.token.staked.info.title" = "In The Game"; +"balance.token.staked.info.description" = "Let's talk staking!"; +"balance.token.frozen" = "Chilled"; +"balance.token.frozen.info.title" = "Cool Off"; +"balance.token.frozen.info.description" = "Let's chat about why it's so cool."; -// Account switcher +// **Account switcher** -"switch_account.title" = "Переключить кошелек"; -"switch_account.wallets" = "Кошельки"; -"switch_account.watch_wallets" = "Просмотр кошелька"; +"switch_account.title" = "Swap Wallets"; +"switch_account.wallets" = "Fat Wallets"; +"switch_account.watch_wallets" = "Wallet Watchlist"; -// Release notes +// **Release notes** -"release_notes.title" = "Что нового"; -"release_notes.follow_us" = "Мы в соцсетях"; +"release_notes.title" = "The Latest & Greatest"; +"release_notes.follow_us" = "Join the Trump Unstoppable Train"; // Deposit -"deposit.receive_coin" = "Получить %@"; -"deposit.address" = "Адрес"; -"deposit.your_address" = "Ваш адрес"; -"receive_alert.not_backed_up_description" = "Вам нужно сделать резервную копию %@ перед получением %@."; -"receive_alert.any_coins.not_backed_up_description" = "Вам нужно сделать резервную копию %@ перед тем, как вы сможете получить любые монеты."; -"deposit.no_adapter.error" = "Не удается предоставить адрес"; +"deposit.receive_coin" = "Look, folks, get ready to grab your %@!"; +"deposit.address" = "Big beautiful Address!"; +"deposit.your_address" = "Your very own Address!"; +"receive_alert.not_backed_up_description" = "Hold on! You've got to back up %@ before we're winning with %@."; +"receive_alert.any_coins.not_backed_up_description" = "You've gotta backup %@ before making any yuge coin deals!"; +"deposit.no_adapter.error" = "Whoops! Can't get that address right now."; -"deposit.address_format" = "Формат"; -"deposit.address_network" = "Сеть"; -"deposit.qr_code_description" = "Ваш адрес для депозита %@"; -"deposit.qr_code_description.watch" = "Отслеживаемый адрес %@"; -"deposit.account" = "Account"; +"deposit.address_format" = "Flashy Format"; +"deposit.address_network" = "The Best Network"; +"deposit.qr_code_description" = "Your VIP ticket to deposit %@"; +"deposit.qr_code_description.watch" = "Keep an eye on this %@ address!"; +"deposit.account" = "Your Gold Account"; -"deposit.not_active" = "не активен"; -"deposit.not_active.title" = "Неактивный адрес"; -"deposit.not_active.tron_description" = "Недавно созданные учетные записи в блокчейне TRON неактивны и не могут быть запрошены или изучены. Они должны быть активированы.\n\nАктивация новой учетной записи в цепочке Tron требует комиссию в размере 1 TRX. Для активации достаточно просто перевести токены TRX или TRC-10 на неактивный адрес аккаунта"; +"deposit.not_active" = "Taking a nap"; +"deposit.not_active.title" = "Sleepy Address"; +"deposit.not_active.tron_description" = "Just so you know, new accounts on TRON aren't awake yet. To get them going, send over some TRX or TRC-10 tokens. It'll cost you just 1 TRX."; -"deposit.zcash.restore.description" = "Вы уже владели какими-либо монетами ZEC?"; -"deposit.zcash.restore.already_own" = "Да, у меня уже есть"; -"deposit.zcash.restore.dont_have" = "Нет, у меня нет"; +"deposit.zcash.restore.description" = "Ever had a little ZEC action before?"; +"deposit.zcash.restore.already_own" = "Of course, I've got some!"; +"deposit.zcash.restore.dont_have" = "Nope, fresh start!"; -"deposit.warning" = "Отправка только %@ на этот адрес. Отправка других типов токенов на этот адрес приведет к их окончательной потере."; +"deposit.warning" = "Only send %@ here, or you'll be losing bigly!"; -"receive_network_select.title" = "Сеть"; -"receive_network_select.description" = "Выберите сеть и получите адрес для получения."; +"receive_network_select.title" = "Pick your Network"; +"receive_network_select.description" = "Choose the best network and grab an address!"; -"receive_address_format_select.title" = "Формат адреса"; -"receive_address_format_select.description" = "Выберите формат адреса для получения вашего адреса."; -"receive_address_format_select.bitcoin.bottom_description" = "Формат Native SegWit предпочтителен в Биткойне для повышения пропускной способности и безопасности. Все форматы адресов (Taproot, SegWit, Legacy) могут использоваться взаимозаменяемо для получения BTC независимо от формата адреса отправителя, что обеспечивает бесперебойные транзакции между различными типами монет."; -"receive_address_format_select.bitcoin_cash.bottom_description" = "Формат Cash Address предпочтителен для получения Bitcoin Cash (BCH) из-за улучшенного пользовательского опыта и совместимости. Однако, оба формата адреса могут использоваться взаимозаменяемо для получения BCH, независимо от формата адреса отправителя."; +"receive_address_format_select.title" = "Prestige Address Format"; +"receive_address_format_select.description" = "Pick a network, get the finest address!"; +"receive_address_format_select.bitcoin.bottom_description" = "For the best deals in Bitcoin, Native SegWit is the way. Any address will do – Taproot, SegWit, Legacy. It's all great!"; +"receive_address_format_select.bitcoin_cash.bottom_description" = "Want the smoothest experience for Bitcoin Cash? Go with the Cash Address."; -"blockchain_type.recommended" = " (рекомендовано)"; +"blockchain_type.recommended" = " (Total winner)"; // Send -"send.title" = "Отправить %@"; -"send.send" = "Отправить"; -"send.no_assets" = "У вас нет активов для отправки."; -"send.amount_placeholder" = "Сумма"; -"send.address_placeholder" = "Адрес"; -"send.address_or_domain_placeholder" = "Адрес или домен"; -"send.fee" = "Комиссия"; -"send.network_fee" = "Комиссия сети"; -"send.estimated_fee" = "Предполагаемая комиссия"; -"send.max_fee" = "Макс. комиссия"; -"send.duration.hours" = "%d ч."; -"send.duration.minutes" = "%d мин."; -"send.available_balance" = "Доступный баланс"; -"send.max_button" = "Макс."; -"send.next_button" = "Далее"; -"send.error.invalid" = "Неверно"; -"send.error.address" = "Адрес"; -"send.hodler_locktime" = "TimeLock"; -"send.hodler_locktime_hour" = "1 час"; -"send.hodler_locktime_month" = "1 месяц"; -"send.hodler_locktime_half_year" = "6 месяцев"; -"send.hodler_locktime_year" = "1 год"; -"send.hodler_locktime_off" = "Выкл."; -"send.hodler_error.unsupported_address" = "TimeLock работает только при отправке на платёжные адреса, начинающиеся с 1... (также известных как BIP44 адреса)"; -"send.fee_info.title" = "Комиссия"; -"send.fee_info.description" = "Блокчейн требует от пользователей оплаты сетевых сборов при отправке транзакций. Комиссия выше, когда в сети проходит много транзакций.\n\nКошелек %@ оценивает комиссию на основе текущей активности блокчейна и рекомендует оптимальное значение для того, чтобы транзакция была обработана в разумные сроки.\n\nРекомендованная ставка комиссии показана как количество сатоши, которое пользователь должен заплатить за один байт транзакции. Таким образом, общая сумма комиссии зависит от общего размера транзакции, который измеряется в байтах.\n\n\nПользователи могут использовать предусмотренные элементы управления, чтобы увеличить или уменьшить значение ставки комиссии. Изменение ставки комиссии изменяет общую плату за транзакцию, которую заплатит пользователь.\n\n\nУстановка комиссии ниже рекомендуемого значения может привести к тому, что транзакция будет находиться в ожидании в течение нескольких часов или будет отклонена. Чем ниже значение, тем больше времени потребуется для подтверждения транзакции. Для транзакций, где важен приоритет, мы рекомендуем установить более высокую ставку комиссии."; -"send.transaction_inputs_outputs_info.title" = "Вводы / выводы транзакций"; -"send.transaction_inputs_outputs_info.description" = "Большинство транзакций с биткоином, а также транзакций с подобными криптовалютами, включая Bitcoin Cash, Dash и Litecoin, генерируют два выхода. Один выход - это сумма, которая поступает получателю, а другой - это изменение, которое возвращается отправителю. То, как большинство кошельков строят транзакции, позволяет третьей стороне легко понять, какой из выходов достался получающей стороне, а какой - сумма сдачи, возвращенная отправителю. Поскольку сумма, возвращенная отправителю, впоследствии используется в будущих транзакциях, связь между этими двумя транзакциями становится очевидной.\n\nВ кошельке %@ реализованы меры, чтобы затруднить кому-либо выяснение того, какой вывод куда идет.\n\nДля пользователей %@ доступны два варианта:"; -"send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "Порядок выхода транзакций меняется случайным образом в каждой транзакции. Иногда изменение может быть первым выводом, иногда - вторым. Если пользователь доверяет разработчику приложения, то рекомендуем использовать этот вариант."; -"send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; -"send.transaction_inputs_outputs_info.deterministic.description" = "Существует общепринятый стандарт упорядочивания выходов транзакций (известный как BIP69). В кошельках с открытым исходным кодом этот стандарт гарантирует, что пользователям кошелька не нужно доверять тому, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт является новым, не многие кошельки его еще внедрили. В результате на блокчейне можно увидеть, была ли транзакция отправлена из кошелька, использующего этот стандарт, или нет."; - -"send.confirmation.you_send" = "Вы отправляете"; -"send.confirmation.to" = "Кому"; -"send.confirmation.contact_name" = "Имя контакта"; -"send.confirmation.domain" = "Домен"; -"send.confirmation.address" = "Адрес"; -"send.confirmation.account" = "Account"; -"send.confirmation.memo" = "Memo"; -"send.confirmation.memo_placeholder" = "Memo"; -"send.confirmation.total" = "Всего"; -"send.confirmation.fee" = "Комиссия"; -"send.confirmation.time_lock" = "TimeLock"; -"send.confirmation.slide_to_send" = "Проведите для отправки"; -"send.confirmation.sending" = "Отправка"; -"send.confirmation.resend_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее с более высокой комиссией. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; -"send.confirmation.resend" = "Отправить повторно"; -"send.confirmation.cancel_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее себе как транзакцию с нулевой суммой. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; -"send.confirmation.cancel" = "Отменить транзакцию"; -"send.confirmation.nonce" = "Memo"; -"send.confirmation.method" = "Метод"; -"send.amount_error.balance" = "Недостаточно средств"; -"send.address_error.own_address" = "Невозможно отправить TRX самому себе"; -"send.amount_error.maximum_amount" = "Макс. сумма %@"; -"send.amount_error.minimum_amount" = "Мин. сумма %@"; -"send.amount_error.min_required_balance" = "Мин. обязательный остаток %@"; -"send.amount_warning.coin_needed_for_fee" = "Вы можете оставить некоторую сумму в размере %@ на балансе, чтобы оплачивать будущие транзакции."; -"send.token.insufficient_fee_alert" = "Комиссии за транзакцию %@ (%@) взимаются в %@. Вам нужно %@."; - -"send.fee_settings.amount_error.balance.title" = "Недостаточный баланс"; -"send.fee_settings.amount_error.balance" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; - -"send.fee_settings.stuck_warning.title" = "Внимание! Транзакция может застрять в сети"; -"send.fee_settings.stuck_warning" = "Транзакция может застрять в сети или будет отклонена."; +"send.title" = "Dispatch some %@!"; +"send.send" = "Ship it!"; +"send.no_assets" = "Looks like your vault's empty, sad!"; +"send.amount_placeholder" = "How many chips?"; +"send.address_placeholder" = "Where to?"; +"send.address_or_domain_placeholder" = "Address or fancy Domain"; +"send.fee" = "Little Charge"; +"send.network_fee" = "Network’s Tiny Fee"; +"send.estimated_fee" = "Ballpark Fee"; +"send.max_fee" = "Maximum Fee"; +"send.duration.hours" = "%d tremendous hours"; +"send.duration.minutes" = "%d quick mins"; +"send.available_balance" = "What's in the kitty?"; +"send.max_button" = "Go Yuge!"; +"send.next_button" = "Onward!"; +"send.error.invalid" = "No good!"; +"send.error.address" = "Wonky Address!"; +"send.hodler_locktime" = "TimeVault"; +"send.hodler_locktime_hour" = "Just an hour"; +"send.hodler_locktime_month" = "1 big month"; +"send.hodler_locktime_half_year" = "Solid 6 months"; +"send.hodler_locktime_year" = "1 winning year"; +"send.hodler_locktime_off" = "Shut it!"; +"send.hodler_error.unsupported_address" = "Time vaulting? Only for top-tier addresses starting with 1...!"; +"send.fee_info.title" = "The Fee Game"; +"send.fee_info.description" = "Everybody pays a bit to keep things moving on the blockchain. Busy days mean higher fees. But hey, we'll suggest the best deal for you!"; +"send.transaction_inputs_outputs_info.title" = "In's and Out's of Transactions"; +"send.transaction_inputs_outputs_info.description" = "Most transactions have two parts: what you send and what you get back. We're smart, making it tricky for anyone trying to snoop."; +"send.transaction_inputs_outputs_info.shuffle.title" = "1. The Trump Card Shuffle"; +"send.transaction_inputs_outputs_info.shuffle.description" = "We mix things up, so it's all unpredictable. Trust me, it's the best!"; +"send.transaction_inputs_outputs_info.deterministic.title" = "2. Classic Play"; +"send.transaction_inputs_outputs_info.deterministic.description" = "We've got standards, like the BIP69. It's a new thing, so not everyone's on board yet."; + +"send.confirmation.you_send" = "You're parting with"; +"send.confirmation.to" = "To the lucky one"; +"send.confirmation.contact_name" = "Who's getting rich?"; +"send.confirmation.domain" = "Fancy Domain"; +"send.confirmation.address" = "Top-tier Address"; +"send.confirmation.account" = "Lucky Account"; +"send.confirmation.memo" = "Your two cents"; +"send.confirmation.memo_placeholder" = "Add a note"; +"send.confirmation.total" = "The whole enchilada"; +"send.confirmation.fee" = "A small fee"; +"send.confirmation.time_lock" = "Time Vault"; +"send.confirmation.slide_to_send" = "Swipe to make it rain!"; +"send.confirmation.sending" = "Making it rain!"; +"send.confirmation.resend_description" = "Let's try ousting that old transaction with a bigger, better fee. Only the best one stays!"; +"send.confirmation.resend" = "Do-over!"; +"send.confirmation.cancel_description" = "We'll try sneaking a new zero-amount transaction past the old one. Only the top dog will make it!"; +"send.confirmation.cancel" = "Bail on it!"; +"send.confirmation.nonce" = "Secret Sauce"; +"send.confirmation.method" = "The Plan"; +"send.amount_error.balance" = "You're short, buddy!"; +"send.address_error.own_address" = "Why send TRX to yourself? That's just showing off!"; +"send.amount_error.maximum_amount" = "That's a bit much, even for %@!"; +"send.amount_error.minimum_amount" = "Come on, %@ deserves more!"; +"send.amount_error.min_required_balance" = "Gotta leave some %@ behind for the party!"; +"send.amount_warning.coin_needed_for_fee" = "Keep some %@ around, it's always good for the bill!"; +"send.token.insufficient_fee_alert" = "Heads up! You'll need %@ to cover the %@ transfer on %@."; + +"send.fee_settings.amount_error.balance.title" = "Uh-oh, low balance!"; +"send.fee_settings.amount_error.balance" = "Your %@ is a bit low for this grand move."; +"send.fee_settings.stuck_warning.title" = "Could get sticky!"; +"send.fee_settings.stuck_warning" = "Might get tangled up or just flop."; + "send.fee_settings.fee_error.title" = "Ошибка комиссии"; "send.fee_settings.too_low" = "Ставка комиссии слишком низкая."; "send.fee_settings.fee_rate_unavailable" = "Ставка комиссии недоступна. Пожалуйста, проверьте ставки комиссии вручную"; "send.stuck_warning" = "Внимание! Транзакция может застрять в сети"; -"send.lock_time" = "TimeLock"; +"send.lock_time" = "Time's Locked! Trust me, nobody locks time better than this app."; -"approve.confirmation.you_approve" = "Вы разрешаете"; -"approve.confirmation.you_revoke" = "Вы отменяете"; -"approve.confirmation.spender" = "Покупатель"; +"approve.confirmation.you_approve" = "You just approved, bigly. Everyone's talking about it!"; +"approve.confirmation.you_revoke" = "You revoked it! I've always said, you're the best decision-maker!"; +"approve.confirmation.spender" = "This spender, very talented person, believe me!"; // Donate -"donate.list.title" = "Поддержи нас"; -"donate.list.get_address" = "Получить адрес"; -"donate.list.get_address.title" = "Адреса"; -"donate.title" = "Пожертвовать %@"; -"donate.no_assets" = "У вас нет активов для пожертвования."; -"donate.support.description" = "С вашей поддержкой мы вместе сможем сделать это приложение еще лучше!"; +"donate.list.title" = "Wanna give a little?"; +"donate.list.get_address" = "Gimme the Address!"; +"donate.list.get_address.title" = "Where's the money going?"; +"donate.title" = "Let's Make Donations Great Again, Donate %@!"; +"donate.no_assets" = "Looks like you're a bit short, buddy."; +"donate.support.description" = "Let's work together to make this app HUGE! The best app ever."; // CoinSelector -"choose_coin.title" = "Выберите токены"; +"choose_coin.title" = "Pick your winning coin!"; // Swap -"swap.title" = "Обменять"; -"swap.no_assets" = "У вас нет активов для обмена."; -"swap.you_pay" = "Платите"; -"swap.estimated" = "приблизительно"; -"swap.balance" = "Баланс"; -"swap.allowance" = "Разрешение"; -"swap.you_get" = "Получите"; -"swap.token" = "Выбрать"; -"swap.advanced_settings" = "Настройки обмена"; -"swap.proceed_button" = "Далее"; -"swap.approve.title" = "Разрешение обмена"; -"swap.approve.description" = "Вы должны предоставить разрешение на использование смарт-контракта для замены данного токена от вашего имени. Это разрешение устанавливает сумму, которая может быть использована смарт-контрактом. Это не повлияет на ваш баланс, но требует небольшой комиссии для выполнения утвержденной транзакции. \n\nХотя это может быть сделано по требованию перед каждой сделкой, предварительно одобрить более высокую сумму для будущих сделок."; -"swap.approve.amount_error.already_approved" = "У вас уже есть разрешение на эту сумму"; -"swap.approving_button" = "Разрешение..."; -"swap.revoke_warning" = "Вы можете обменять %@, или вы должны отменить и одобрить новую сумму"; -"swap.revoking_button" = "Отмена..."; -"swap.not_available_button" = "Баланс N/А"; -"swap.trade_error.not_found" = "Невозможно обменять эти токены"; -"swap.trade_error.wrap_unwrap_not_allowed" = "Эта служба не позволяет wrapping/unwrapping. Пожалуйста, попробуйте другой сервис обмена. Рекомендуется 1inch"; -"swap.button_error.insufficient_balance" = "Недостаточный баланс"; -"swap.switch_provider.title" = "Служба обмена"; -"swap.amount_type.coin" = "Монета"; - -"swap.price" = "Цена"; -"swap.buy_price" = "Цена покупки"; -"swap.sell_price" = "Цена продажи"; -"swap.price_impact" = "Отклонение от рын. цены"; -"swap.maximum_paid" = "Макс. сумма"; -"swap.minimum_got" = "Гарантированная сумма"; -"swap.estimate_short" = "(прим.)"; -"swap.minimum_short" = "(мин)"; -"swap.maximum_short" = "(макс)"; +"swap.title" = "Let's Swap!"; +"swap.no_assets" = "You're empty! Fill it up."; +"swap.you_pay" = "What you're giving"; +"swap.estimated" = "around-ish"; +"swap.balance" = "Your stash"; +"swap.allowance" = "Permission slip"; +"swap.you_get" = "The good stuff you get"; +"swap.token" = "Choose"; +"swap.advanced_settings" = "Big brain settings"; +"swap.proceed_button" = "Onward!"; +"swap.approve.title" = "Give the thumbs up!"; +"swap.approve.description" = "Time to grant permission! But don't worry, it doesn't touch your stash. Just a little fee, and you're golden!"; +"swap.approve.amount_error.already_approved" = "Been there, done that! You're set."; +"swap.approving_button" = "Doing the thing..."; +"swap.revoke_warning" = "Trade or revoke and try again!"; +"swap.revoking_button" = "Taking it back..."; +"swap.not_available_button" = "No cash here!"; +"swap.trade_error.not_found" = "This swap's a no-go!"; +"swap.trade_error.wrap_unwrap_not_allowed" = "No wrapping or unwrapping here! 1Inch is the way to go!"; +"swap.button_error.insufficient_balance" = "Need more green!"; +"swap.switch_provider.title" = "Who's the dealer?"; +"swap.amount_type.coin" = "Shiny Coin"; + +"swap.price" = "The tag"; +"swap.buy_price" = "What it'll cost ya"; +"swap.sell_price" = "Cashing in!"; +"swap.price_impact" = "Price rollercoaster"; +"swap.maximum_paid" = "Top Dollar"; +"swap.minimum_got" = "The least you'll get"; +"swap.estimate_short" = "(ballpark)"; +"swap.minimum_short" = "(low end)"; +"swap.maximum_short" = "(high end)"; // Swap Advanced Settings -"swap.advanced_settings.slippage" = "Допустимость отклонений"; -"swap.advanced_settings.slippage.footer" = "Ваша транзакция будет отменена, если цена изменится в неблагоприятную сторону более чем на этот процент"; -"swap.advanced_settings.deadline" = "Срок транзакции"; -"swap.advanced_settings.deadline.footer" = "Ваша транзакция будет отменена, если перевод займет больше указанного срока."; -"swap.advanced_settings.recipient.footer" = "После операции обмена сумма будет переведена на указанный адрес"; -"swap.advanced_settings.deadline_minute" = "%@ мин"; -"swap.advanced_settings.recipient_address" = "Адрес получателя"; -"swap.advanced_settings.warning.unusual_slippage" = "Ваша транзакция может быть подвержена фронтраннингу."; -"swap.advanced_settings.service_fee_description" = "Комиссия за услугу обмена на платформе обычно 0.3% или 0.6%"; -"swap.advanced_settings.error.lower_slippage" = "Возможно, ваша транзакция не удалась."; -"swap.advanced_settings.error.higher_slippage" = "Сопротивление скольжению не может превышать %@%%"; -"swap.advanced_settings.error.invalid_address" = "Неверный адрес"; -"swap.advanced_settings.error.invalid_slippage" = "Неверное отклонение"; -"swap.advanced_settings.error.invalid_deadline" = "Недопустимый срок"; - -"swap.one_inch.error.cannot_estimate" = "Ошибка оценки"; -"swap.one_inch.error.cannot_estimate.info" = "Проверьте баланс и убедитесь, что на нем достаточно %@ для покрытия комиссии. Или попробуйте увеличить предел скольжения цены и повторите попытку снова. Следующая попытка через 3 секунды..."; -"swap.one_inch.error.insufficient_liquidity" = "Недостаточно ликвидности"; -"swap.one_inch.error.insufficient_liquidity.info" = "Кажется, для этой сделки не хватает ликвидности. Попробуйте уменьшить сумму сделки."; - -"swap.service" = "Сервис"; -"swap.service.title" = "Сервис"; +"swap.advanced_settings.slippage" = "Slippage Tolerance"; +"swap.advanced_settings.slippage.footer" = "If prices dip more than this, no deal!"; +"swap.advanced_settings.deadline" = "Transaction Deadline"; +"swap.advanced_settings.deadline.footer" = "No dilly-dallying! Time's ticking."; +"swap.advanced_settings.recipient.footer" = "Where's the money going after the swap?"; +"swap.advanced_settings.deadline_minute" = "Hurry up, %@ min!"; +"swap.advanced_settings.recipient_address" = "Recipient Address"; +"swap.advanced_settings.warning.unusual_slippage" = "Watch out! Others might jump the line!"; +"swap.advanced_settings.service_fee_description" = "A small thank you fee for our service. Usually just a tiny 0.3% or 0.6%!"; +"swap.advanced_settings.error.lower_slippage" = "This might flop."; +"swap.advanced_settings.error.higher_slippage" = "Can't go over %@%%, buddy!"; +"swap.advanced_settings.error.invalid_address" = "Wonky address alert!"; +"swap.advanced_settings.error.invalid_slippage" = "Invalid Slippage"; +"swap.advanced_settings.error.invalid_deadline" = "Invalid Deadline"; + +"swap.one_inch.error.cannot_estimate" = "Math's a bit tricky."; +"swap.one_inch.error.cannot_estimate.info" = "Check your stash. Maybe up the wiggle room and try again. Give it another go in 3..."; +"swap.one_inch.error.insufficient_liquidity" = "Pool's a bit shallow!"; +"swap.one_inch.error.insufficient_liquidity.info" = "Not enough in the pot. Lower the ante."; + +"swap.service" = "Service"; +"swap.service.title" = "Service"; // Swap Approving -"swap.approve.subtitle" = "Обменять"; +"swap.approve.subtitle" = "Swap it out!"; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Проведите для обмена"; -"swap.confirmation.swapping" = "Обмен"; -"swap.confirmation.impact_too_high" = "%@ отключил действие \"swap\" для этой сделки, так как вы получаете чрезвычайно невыгодную цену. Это связано с крайне низкой ликвидностью. \nЕсли вы все еще хотите поменять валюту, используйте веб-сайт %@ вместо этого."; -"swap.confirmation.impact_warning" = "Важно! Вы получаете чрезвычайно неблагоприятную цену. Это связано с крайне низкой ликвидностью."; - -"swap.confirmation.minimum_received" = "Получено минимум"; -"swap.confirmation.maximum_sent" = "Максимальная трата"; - -"swap.dex_info.description" = "Этот обменный сервис работает при поддержке %@ - децентрализованного протокола обмена токенов, созданного в блокчейне %@.\n\n%@ полностью автоматизирован и управляется смарт-контрактами, которые надежным способом упрощают обмен токенами без осуществления каких-либо махинаций."; - -"swap.dex_info.header_dex_related" = "%@"; -"swap.dex_info.header_allowance" = "Разрешение"; -"swap.dex_info.content_allowance" = "Сумма, которую exchange может потратить от имени пользователя при выполнении обмена токенов. Действующая транзакция, устанавливающая достаточный уровень допустимости, необходима для того, чтобы транзакция обмена была осуществлена."; -"swap.dex_info.header_price_impact" = "Отклонение от рын. цены"; -"swap.dex_info.content_price_impact" = "Ожидаемое отклонение цены от указанной цены, обычно увеличивается при увеличении суммы обмена."; -"swap.dex_info.header_swap_fee" = "Комиссия за обмен"; -"swap.dex_info.content_swap_fee" = "Плата за услугу обмена на платформе, показана в валюте, в которой продает пользователь. Для большинства заказов составляет или 0,3% или 0,6%."; -"swap.dex_info.header_guaranteed_amount" = "Гарантированная сумма"; -"swap.dex_info.content_guaranteed_amount" = "Минимальная сумма, которую получит пользователь в результате обмена."; -"swap.dex_info.header_maximum_spend" = "Максимальная трата"; -"swap.dex_info.content_maximum_spend" = "Максимальная сумма, которую получит пользователь в результате обмена."; - -"swap.dex_info.header_other" = "Другое"; -"swap.dex_info.header_transaction_fee" = "Комиссия за транзакцию"; -"swap.dex_info.content_transaction_fee" = "Примерная стоимость услуги по обработке данной транзакции на блокчейне %@. Стоимость транзакций, связанных с %@, обычно выше, чем стоимость транзакций по передаче обычных токенов."; -"swap.dex_info.header_transaction_speed" = "Скорость транзакции"; -"swap.dex_info.content_transaction_speed" = "Обработка транзакций с более высокой комиссией будет ускорена. Также вено и обратное."; - -"swap.dex_info.link_button" = "%@ сайт"; +"swap.confirmation.slide_to_swap" = "Slide to the deal!"; +"swap.confirmation.swapping" = "Making magic!"; +"swap.confirmation.impact_too_high" = "Hold up! %@ says this deal's a dud. Check out %@ instead!"; +"swap.confirmation.impact_warning" = "Watch out! It's a wild ride!"; + +"swap.confirmation.minimum_received" = "You'll at least get"; +"swap.confirmation.maximum_sent" = "At most you'll send"; + +"swap.dex_info.description" = "This swanky service is by %@, the big shots in decentralized trading on the %@ chain. 100% automated, 100% reliable!"; +"swap.dex_info.header_dex_related" = "All about %@"; +"swap.dex_info.header_allowance" = "Spending Limit"; +"swap.dex_info.content_allowance" = "How much the exchange can use on your behalf. Gotta get this before the main event."; +"swap.dex_info.header_price_impact" = "Price Wave"; +"swap.dex_info.content_price_impact" = "How wild the price might get. Bigger swaps, bigger waves."; +"swap.dex_info.header_swap_fee" = "Thank you fee"; +"swap.dex_info.content_swap_fee" = "Our little thank you note. Usually 0.3% or 0.6%."; +"swap.dex_info.header_guaranteed_amount" = "Sure thing amount"; +"swap.dex_info.content_guaranteed_amount" = "The least you're gonna get after swapping."; +"swap.dex_info.header_maximum_spend" = "Max Spend"; +"swap.dex_info.content_maximum_spend" = "The most you're gonna use in the swap."; + +"swap.dex_info.header_other" = "Other stuff"; +"swap.dex_info.header_transaction_fee" = "The cost of doing business"; +"swap.dex_info.content_transaction_fee" = "What you're paying to use the %@ chain. %@ stuff might be pricier."; +"swap.dex_info.header_transaction_speed" = "How fast it goes"; +"swap.dex_info.content_transaction_speed" = "Pay more, go faster. It's that simple."; + +"swap.dex_info.link_button" = "Check out %@"; // Market -"market.tab_bar_item" = "Рынки"; -"market.title" = "Рынки"; -"market.category.overview" = "Обзор"; -"market.category.posts" = "Новости"; -"market.category.watchlist" = "Избранное"; -"market.total_market_cap" = "Общая капитализация"; -"market.24h_volume" = "Объем торгов (24ч)"; -"market.defi_cap" = "Капитализация DeFi"; -"market.defi_tvl" = "TVL в DeFi"; - -"market.project_has_no_coin" = "У этого проекта нет токена"; - -"market.top.section.header.see_all" = "Посмотреть всё"; -"market.top.section.header.top_gainers" = "Взлеты"; -"market.top.section.header.top_losers" = "Падения"; -"market.top.section.header.top_sectors" = "Топ секторы"; -"market.top.section.header.news" = "Новости"; -"market.top.volume.title" = "Объём"; -"market.top.market_cap.title" = "Рын. кап."; -"market.top.diluted_market_cap.title" = "Разводненная рын.кап."; - -"market.market_field.mcap" = "Рын. кап."; -"market.market_field.vol" = "Объём"; - -"market.tvl.market_field.value" = "USD"; -"market.tvl.market_field.diff" = "Процент"; - -"market.tvl.platform_field.all" = "Все"; - -"market.sort_by" = "Сортировать"; - -"market.top.title" = "Лучшие токены"; -"market.top.description" = "Топ токенов по рыночной капитализации"; - -"market.top.highest_cap" = "Наивысшая кап."; -"market.top.lowest_cap" = "Наименьшая кап."; -"market.top.highest_volume" = "Наивысший объем"; -"market.top.lowest_volume" = "Наименьший объем"; -"market.top.top_gainers" = "Взлеты"; -"market.top.top_losers" = "Падения"; -"market.top.top_collections" = "Топ NFT коллекции"; -"market.top.floor_price" = "Минимальная цена:"; -"market.top.top_platforms" = "Топ платформы"; -"market.top.protocols" = "Протоколы"; - -"top_platforms.title" = "Рейтинг платформ"; -"top_platforms.description" = "Лучшие ведущие блокчейн-платформы кумулятивного рынка проектов."; - -"top_platform.title" = "%@ Экосистема"; -"top_platform.description" = "Капитализация рынка всех протоколов на блокчейне %@"; - -"market_discovery.title" = "Токены"; -"market_discovery.filters" = "Фильтры"; -"market_discovery.browse_categories" = "Обзор категорий"; -"market_discovery.top_coins" = "Топ токены"; -"market_discovery.not_found" = "Ничего не найдено"; - -"market_watchlist.empty.caption" = "У вас нет токенов в избранном."; - -"market.search.title" = "Поиск"; -"market.search.empty_text" = "Ничего не найдено"; - -"market.advanced_search.title" = "Фильтры"; -"market.advanced_search.show_results" = "Показать результаты"; -"market.advanced_search.empty_results" = "Сбросить результаты"; -"market.advanced_search.dex_description" = "Эта настройка применяется к токенам, торгуемым на Ethereum (Uniswap DEX) и Binance Smart Chain (Pancake DEX)."; -"market.advanced_search.24h" = "24ч"; - -"market.advanced_search.market_parameters" = "Параметры рынка"; -"market.advanced_search.network_parameters" = "Параметры сети"; -"market.advanced_search.price_parameters" = "Параметры цены"; -"market.advanced_search.choose_set" = "Выбор набора"; -"market.advanced_search.market_cap" = "Рын. капитализация"; -"market.advanced_search.volume" = "Объем торговли"; -"market.advanced_search.liquidity" = "Ликвидность DEX"; -"market.advanced_search.blockchains" = "Блокчейны"; -"market.advanced_search.price_period" = "Ценовой период"; -"market.advanced_search.price_change" = "По изменению цены (%)"; - -"market.advanced_search.outperformed_btc" = "Обошел BTC"; -"market.advanced_search.outperformed_eth" = "Обошел ETH"; -"market.advanced_search.outperformed_bnb" = "Обошел BNB"; -"market.advanced_search.price_close_to_ath" = "Цена близка к ATH"; -"market.advanced_search.price_close_to_atl" = "Цена близка к ATL"; - -"market.advanced_search.top" = "Топ %d"; -"market.advanced_search.reset_all" = "Сбросить"; - -"market.advanced_search.less_5_m" = "< 5млн"; -"market.advanced_search.less_10_m" = "< 10млн"; -"market.advanced_search.less_50_m" = "< 50млн"; -"market.advanced_search.less_500_m" = "< 500млн"; -"market.advanced_search.m_5_m_20" = "5млн - 20млн"; -"market.advanced_search.m_10_m_40" = "10млн - 40млн"; -"market.advanced_search.m_20_m_100" = "20млн - 100млн"; -"market.advanced_search.m_40_m_200" = "40млн - 200млн"; -"market.advanced_search.m_50_m_200" = "50млн - 200млн"; -"market.advanced_search.m_100_b_1" = "100млн - 1млрд"; -"market.advanced_search.m_200_b_1" = "200млн - 1млрд"; -"market.advanced_search.m_200_b_2" = "200млн - 2млрд"; -"market.advanced_search.m_500_b_2" = "500млн - 2млрд"; -"market.advanced_search.b_1_b_5" = "1млрд - 5млрд"; -"market.advanced_search.b_1_b_10" = "1млрд - 10млрд"; -"market.advanced_search.b_2_b_10" = "2млрд - 10млрд"; -"market.advanced_search.b_10_b_50" = "10млрд - 50млрд"; -"market.advanced_search.b_10_b_100" = "10млрд - 100млрд"; -"market.advanced_search.b_100_b_500" = "100млрд - 500млрд"; -"market.advanced_search.more_5_b" = "> 5млрд"; -"market.advanced_search.more_10_b" = "> 10млрд"; -"market.advanced_search.more_50_b" = "> 50млрд"; -"market.advanced_search.more_500_b" = "> 500млрд"; - -"market.advanced_search.day" = "1 день"; -"market.advanced_search.week" = "1 неделя"; -"market.advanced_search.week2" = "2 недели"; -"market.advanced_search.month" = "1 месяц"; -"market.advanced_search.month6" = "6 месяцев"; -"market.advanced_search.year" = "1 Год"; - -"market.advanced_search_results.title" = "Результаты"; - -"market.global.total_market_cap.title" = "Полная рын. кап."; -"market.global.total_market_cap.description" = "Общая рыночная стоимость всех криптовалют"; - -"market.global.volume_24h.title" = "Объем торгов (24ч)"; -"market.global.volume_24h.description" = "24-часовой объем крипторынка"; - -"market.global.defi_cap.title" = "Капитализация DeFi"; -"market.global.defi_cap.description" = "Общая рыночная стоимость проектов DeFi"; - -"market.global.tvl_in_defi.title" = "TVL в DeFi"; -"market.global.tvl_in_defi.description" = "Всего заблокировано (TVL) в DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Мультичейн"; -"market.global.tvl_in_defi.filter_by_chain" = "Сортировать по блокчейну"; +"market.tab_bar_item" = "YUGE Markets!"; +"market.title" = "Best Markets Ever!"; +"market.category.overview" = "The Best View"; +"market.category.posts" = "Real News, Not Fake!"; +"market.category.watchlist" = "My Favorite List"; +"market.total_market_cap" = "The Big Money Cap"; +"market.24h_volume" = "24 Hours of Winning"; +"market.defi_cap" = "Big DeFi Bucks"; +"market.defi_tvl" = "The Biggest Money in DeFi"; + +"market.project_has_no_coin" = "No coin? Sad!"; + +"market.top.section.header.see_all" = "See Everything!"; +"market.top.section.header.top_gainers" = "Total Winners"; +"market.top.section.header.top_losers" = "Not Winning… Yet!"; +"market.top.section.header.top_sectors" = "Top of the Tops"; +"market.top.section.header.news" = "The News You Need"; +"market.top.volume.title" = "Volume? Huge!"; +"market.top.market_cap.title" = "Massive MCap"; +"market.top.diluted_market_cap.title" = "Even Bigger MCap"; + +"market.market_field.mcap" = "Huge Cap"; +"market.market_field.vol" = "Big Volumes"; + +"market.tvl.market_field.value" = "Big Bucks"; +"market.tvl.market_field.diff" = "Up or Down?"; + +"market.tvl.platform_field.all" = "All of 'em"; + +"market.sort_by" = "Pick Your Winners"; + +"market.top.title" = "Best Coins Ever"; +"market.top.description" = "Top Coins, because we only deal with the best!"; + +"market.top.highest_cap" = "The Richest Cap"; +"market.top.lowest_cap" = "Room for Growth Cap"; +"market.top.highest_volume" = "Lots of Noise"; +"market.top.lowest_volume" = "Quiet Winners"; +"market.top.top_gainers" = "On Fire Gainers"; +"market.top.top_losers" = "They'll Bounce Back"; +"market.top.top_collections" = "Top Art Stash"; +"market.top.floor_price" = "Bottom Price? Maybe!"; +"market.top.top_platforms" = "Best Stages"; +"market.top.protocols" = "The Rules"; + +"top_platforms.title" = "Platform Kings"; +"top_platforms.description" = "The best places to build greatness."; + +"top_platform.title" = "%@ Ecosystem"; +"top_platform.description" = "Where the money's at on the %@ stage"; + +"market_discovery.title" = "Discovery"; +"market_discovery.filters" = "Filters"; +"market_discovery.browse_categories" = "Browse Categories"; +"market_discovery.top_coins" = "TOP Coins"; +"market_discovery.not_found" = "No results found"; + +"market_watchlist.empty.caption" = "It's lonely here."; + +"market.search.title" = "Search"; +"market.search.empty_text" = "No results found"; + +"market.advanced_search.title" = "Nitty Gritty Filters"; +"market.advanced_search.show_results" = "Show Me The Gold"; +"market.advanced_search.empty_results" = "Where'd they go?"; +"market.advanced_search.dex_description" = "This is for the Ethereum (Uniswap) and Binance (Pancake) hotshots."; +"market.advanced_search.24h" = "A Day in the Life"; + +"market.advanced_search.market_parameters" = "Market Magic"; +"market.advanced_search.network_parameters" = "Network Know-How"; +"market.advanced_search.price_parameters" = "Price Party!"; +"market.advanced_search.choose_set" = "Pick and Choose"; +"market.advanced_search.market_cap" = "How Big's the Hat?"; +"market.advanced_search.volume" = "Making Waves"; +"market.advanced_search.liquidity" = "Liquid Gold"; +"market.advanced_search.blockchains" = "The Chains That Bind"; +"market.advanced_search.price_period" = "Pricey Times"; +"market.advanced_search.price_change" = "Up or Down?"; + +"market.advanced_search.outperformed_btc" = "Beat the Bitcoin!"; +"market.advanced_search.outperformed_eth" = "Crushed Ethereum!"; +"market.advanced_search.outperformed_bnb" = "Bounced Binance!"; +"market.advanced_search.price_close_to_ath" = "Almost At The Top!"; +"market.advanced_search.price_close_to_atl" = "Low, But Not For Long!"; + +"market.advanced_search.top" = "Top %d"; +"market.advanced_search.reset_all" = "Reset"; + +"market.advanced_search.less_5_m" = "< 5M"; +"market.advanced_search.less_10_m" = "< 10M"; +"market.advanced_search.less_50_m" = "< 50M"; +"market.advanced_search.less_500_m" = "< 500M"; +"market.advanced_search.m_5_m_20" = "5M - 20M"; +"market.advanced_search.m_10_m_40" = "10M - 40M"; +"market.advanced_search.m_20_m_100" = "20M - 100M"; +"market.advanced_search.m_40_m_200" = "40M - 200M"; +"market.advanced_search.m_50_m_200" = "50M - 200M"; +"market.advanced_search.m_100_b_1" = "100M - 1B"; +"market.advanced_search.m_200_b_1" = "200M - 1B"; +"market.advanced_search.m_200_b_2" = "200M - 2B"; +"market.advanced_search.m_500_b_2" = "500M - 2B"; +"market.advanced_search.b_1_b_5" = "1B - 5B"; +"market.advanced_search.b_1_b_10" = "1B - 10B"; +"market.advanced_search.b_2_b_10" = "2B - 10B"; +"market.advanced_search.b_10_b_50" = "10B - 50B"; +"market.advanced_search.b_10_b_100" = "10B - 100B"; +"market.advanced_search.b_100_b_500" = "100B - 500B"; +"market.advanced_search.more_5_b" = "> 5B"; +"market.advanced_search.more_10_b" = "> 10B"; +"market.advanced_search.more_50_b" = "> 50B"; +"market.advanced_search.more_500_b" = "> 500B"; + +"market.advanced_search.day" = "1 Day"; +"market.advanced_search.week" = "1 Week"; +"market.advanced_search.week2" = "2 Weeks"; +"market.advanced_search.month" = "1 Month"; +"market.advanced_search.month6" = "6 Months"; +"market.advanced_search.year" = "1 Year"; + +"market.advanced_search_results.title" = "Results"; +"market.global.total_market_cap.title" = "All the Money"; +"market.global.total_market_cap.description" = "Every penny, because we're that good."; + +"market.global.volume_24h.title" = "A Day's Worth"; +"market.global.volume_24h.description" = "All the moves in 24 hours"; + +"market.global.defi_cap.title" = "DeFi Dynasty"; +"market.global.defi_cap.description" = "Where DeFi makes its mark."; + +"market.global.tvl_in_defi.title" = "Locked Up in DeFi"; +"market.global.tvl_in_defi.description" = "Where the real money sleeps."; +"market.global.tvl_in_defi.multi_chain" = "All the Chains, One Place"; +"market.global.tvl_in_defi.filter_by_chain" = "Pick Your Chain"; + // Coin Page -"coin_page.overview" = "Цена"; -"coin_page.analytics" = "Аналитика"; -"coin_page.markets" = "Рынки"; -"coin_page.tweets" = "Твиты"; +"coin_page.overview" = "Price"; +"coin_page.analytics" = "Analytics"; +"coin_page.markets" = "Markets"; +"coin_page.tweets" = "Tweets"; // Coin Page -> Overview -"coin_overview.indicators" = "Индикаторы"; -"coin_overview.indicators.show" = "Показать"; -"coin_overview.indicators.hide" = "Скрыть"; -"coin_overview.market_cap" = "Рын. капитализация"; -"coin_overview.circulating_supply" = "В обороте"; -"coin_overview.total_supply" = "Макс.выпуск"; -"coin_overview.diluted_market_cap" = "Разводненная рын.кап."; -"coin_overview.genesis_date" = "Дата старта"; -"coin_overview.trading_volume" = "Объем торговли"; - -"coin_overview.roi.hour24" = "1 день"; -"coin_overview.roi.day7" = "1 неделя"; -"coin_overview.roi.day14" = "2 недели"; -"coin_overview.roi.day30" = "1 месяц"; -"coin_overview.roi.day200" = "6 месяцев"; -"coin_overview.roi.year1" = "1 Год"; - -"coin_overview.category" = "Категория"; - -"coin_overview.blockchains" = "Блокчейны"; -"coin_overview.bips" = "BIPы"; -"coin_overview.coin_types" = "Типы токенов"; -"coin_overview.show_more" = "Показать больше"; -"coin_overview.show_less" = "Показать меньше"; -"coin_overview.links" = "Ссылки"; - -"coin_overview.guide" = "Руководство"; -"coin_overview.website" = "Веб-сайт"; -"coin_overview.whitepaper" = "Тех. описание (whitepaper)"; +"coin_overview.indicators" = "Indicators"; +"coin_overview.indicators.show" = "Show"; +"coin_overview.indicators.hide" = "Hide"; +"coin_overview.market_cap" = "Market Cap"; +"coin_overview.circulating_supply" = "In Circulation"; +"coin_overview.total_supply" = "Total Supply"; +"coin_overview.diluted_market_cap" = "Diluted MCap"; +"coin_overview.genesis_date" = "Inception Date"; +"coin_overview.trading_volume" = "Trading Volume"; + +"coin_overview.roi.hour24" = "1 Day"; +"coin_overview.roi.day7" = "1 Week"; +"coin_overview.roi.day14" = "2 Weeks"; +"coin_overview.roi.day30" = "1 Month"; +"coin_overview.roi.day200" = "6 Month"; +"coin_overview.roi.year1" = "1 Year"; + +"coin_overview.category" = "Category"; + +"coin_overview.blockchains" = "Blockchains"; +"coin_overview.bips" = "BIPs"; +"coin_overview.coin_types" = "Coin Types"; +"coin_overview.show_more" = "Show More"; +"coin_overview.show_less" = "Show Less"; +"coin_overview.links" = "Links"; + +"coin_overview.guide" = "Guide"; +"coin_overview.website" = "Website"; +"coin_overview.whitepaper" = "Whitepaper"; // Coin Page -> Analytics -"coin_analytics.indicators.summary" = "Общая оценка"; -"coin_analytics.indicators.title" = "Технические индикаторы"; -"coin_analytics.indicators.no_data" = "Нет данных"; -"coin_analytics.indicators.strong_buy" = "Активно покупать"; -"coin_analytics.indicators.buy" = "Покупать"; -"coin_analytics.indicators.neutral" = "Нейтральный"; -"coin_analytics.indicators.sell" = "Продавать"; -"coin_analytics.indicators.strong_sell" = "Активно продавать"; -"coin_analytics.period" = "Период"; -"coin_analytics.period.select_title" = "Выберите период"; -"coin_analytics.period.1h" = "1 час"; -"coin_analytics.period.4h" = "4 часа"; -"coin_analytics.period.1d" = "1 день"; -"coin_analytics.period.1w" = "1 неделя"; - -"coin_analytics.details" = "Подробности"; - -"coin_analytics.not_available" = "В этом проекте нет аналитических данных"; - -"coin_analytics.technical_indicators" = "Технические индикаторы"; -"coin_analytics.technical_indicators.info1" = "Общая оценка: Это общий обзор технических средств актива с учетом различных технических показателей и временных рамок. Он обеспечивает консенсусную точку зрения (купить, продать или нейтраль) на основе этих показателей."; -"coin_analytics.technical_indicators.info2" = "Скользящие средние (MA): Это обычно используемые технические индикаторы, позволяющие сгладить данные о ценах для создания индикатора следующего тренда. Они показывают среднюю цену за определенный период времени. Существует несколько типов MAs:\n\nSimple Moving среднее значение (SMA): Это вычисляет среднее значение выбранного диапазона цен, обычно закрывают цены, по количеству периодов в этом диапазоне.\n\nЭкспоненциальное скользящее среднее (EMA): Это даёт больше веса для последних цен, тем самым реагируя быстрее на последние изменения цен."; -"coin_analytics.technical_indicators.info3" = "Осцилляторы: Это технические индикаторы, которые колеблются со временем в диапазоне (выше и ниже центральной линии или между заданными уровнями). Они предназначены для идентификации перекупленных и перепродаваемых условий на рынке. Вот несколько распространенных осцилляторов:\n\nИндекс относительной силы (RSI): Это измеряет скорость и изменение движений цен. Обычно он используется для идентификации перекупленных или перепроданных условий.\n\nДвижение среднего сближения (MACD): Используется для выявления потенциальных сигналов на покупку и продажу. Она запускает технические сигналы, когда она пересекает линии сигнала выше (покупать) или ниже (продавать)."; - -"coin_analytics.cex_volume" = "Объем CEX"; -"coin_analytics.cex_volume_rank" = "Рейтинг объема CEX"; -"coin_analytics.cex_volume_rank.description" = "Торговый объем токена на централизованных биржах."; -"coin_analytics.cex_volume.info1" = "Общий объем торгов по токену на ведущих централизованных биржах за 30-дневный период."; -"coin_analytics.cex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих централизованных биржах за 1 год."; -"coin_analytics.cex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих централизованных биржах за 30-дневный период."; -"coin_analytics.cex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на децентрализованных биржах за 24ч/7дн/1мес."; - -"coin_analytics.dex_volume" = "Объем DEX"; -"coin_analytics.dex_volume_rank" = "Рейтинг объема DEX"; -"coin_analytics.dex_volume_rank.description" = "Торговый объем токена на децентрализованных биржах."; -"coin_analytics.dex_volume.info1" = "Общий объем торгов по токену на ведущих децентрализованных биржах за 30-дневный период."; -"coin_analytics.dex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих децентрализованных биржах за 1 год."; -"coin_analytics.dex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих децентрализованных биржах за 30-дневный период."; -"coin_analytics.dex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на децентрализованных биржах за 24ч/7дн/1мес."; -"coin_analytics.dex_volume.tracked_dexes" = "Отслеживаемые DEX биржи:"; +"coin_analytics.indicators.summary" = "Summary"; +"coin_analytics.indicators.title" = "Technical Indicators"; +"coin_analytics.indicators.no_data" = "No Data"; +"coin_analytics.indicators.strong_buy" = "Strong Buy"; +"coin_analytics.indicators.buy" = "Buy"; +"coin_analytics.indicators.neutral" = "Neutral"; +"coin_analytics.indicators.sell" = "Sell"; +"coin_analytics.indicators.strong_sell" = "Strong Sell"; +"coin_analytics.period" = "Period"; +"coin_analytics.period.select_title" = "Select Period"; +"coin_analytics.period.1h" = "1 hour"; +"coin_analytics.period.4h" = "4 hours"; +"coin_analytics.period.1d" = "1 day"; +"coin_analytics.period.1w" = "1 week"; + +"coin_analytics.details" = "Details"; + +"coin_analytics.not_available" = "Analytical data in this project? I've been told they're missing. But we're making everything better!"; + +"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators.info1" = "The Overview: We've got the best overview on asset's technicals. Everybody's talking about it! It tells you straight – Buy, Sell, or just stay Neutral. The best advice, believe me!"; +"coin_analytics.technical_indicators.info2" = "Moving Averages? Everybody loves them. Best trend indicators out there. Shows you average prices, and we've got the best averages.\n\nSimple Moving Average (SMA): Calculates averages like nobody else. Usually based on the top closing prices.\n\nExponential Moving Average (EMA): Super smart! Focuses on the latest prices, reacting faster than anything else. Everyone's talking about it!"; +"coin_analytics.technical_indicators.info3" = "Oscillators: They fluctuate, and they're tremendous. Helps you spot the best deals in the market. The very best!\n\nRelative Strength Index (RSI): Measures everything super fast! Used by the best to spot those overbought and oversold markets.\n\nMoving Average Convergence Divergence (MACD): This is the future! Points out the best times to buy or sell. Super smart, trust me."; + +"coin_analytics.cex_volume" = "CEX Volume"; +"coin_analytics.cex_volume_rank" = "CEX Volume Rank - we're always number one!"; +"coin_analytics.cex_volume_rank.description" = "We rank tokens better than anyone else, especially based on trading volume. Everyone wants to be on top!"; +"coin_analytics.cex_volume.info1" = "Just look at our trading volume on the top centralized exchanges. Leading the game for 30 days straight!"; +"coin_analytics.cex_volume.info2" = "This chart? It's a masterpiece. Shows the trading volume changes like you've never seen before. Top-notch stuff!"; +"coin_analytics.cex_volume.info3" = "Our token's rank? Always leading based on volume. Best of the best!"; +"coin_analytics.cex_volume.info4" = "List of tokens? Only the winners, ranked by their top trading volumes. We're always on top, 24/7, every month."; + +"coin_analytics.dex_volume" = "DEX Volume"; +"coin_analytics.dex_volume_rank.description" = "Tokens ranked? We've got the best rankings, especially for decentralized exchanges. Nobody does it better!"; +"coin_analytics.dex_volume.info1" = "The numbers are huge! Unbelievable trading volume for the token on the greatest decentralized exchanges. We're leading for 30 days, and everyone's talking about it!"; +"coin_analytics.dex_volume.info2" = "Ever seen a chart like this? Didn't think so. It shows the incredible variation in daily trading volume. The best chart for the best decentralized exchanges over a whole year!"; +"coin_analytics.dex_volume.info3" = "Where's our token's rank? At the top, of course! All thanks to the amazing trading volume on the finest decentralized exchanges for a whole month!"; +"coin_analytics.dex_volume.info4" = "Our list? Only the best tokens! All ranked by the highest trading volumes. Always updated - every 24 hours, every week, every month!"; +"coin_analytics.dex_volume.tracked_dexes" = "Which DEXes are we tracking? Only the best, most incredible ones out there. Everyone's asking about it!"; "coin_analytics.dex_volume.tracked_dexes.info1" = "Ethereum : Uniswap V2/3, Sushiswap"; "coin_analytics.dex_volume.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap"; -"coin_analytics.dex_liquidity" = "Ликвидность DEX"; -"coin_analytics.dex_liquidity_rank" = "Рейтинг ликвидности DEX"; -"coin_analytics.dex_liquidity_rank.description" = "Рейтинг токенов основан по доступной ликвидности на децентрализованных биржах."; -"coin_analytics.dex_liquidity.info1" = "Общая ликвидность, доступная в настоящий момент для токена на ведущих децентрализованных биржах."; -"coin_analytics.dex_liquidity.info2" = "График, отражающий колебания доступной ликвидности для токена на ведущих децентрализованных биржах за 1 год."; -"coin_analytics.dex_liquidity.info3" = "Список всех токенов, ранжированных по доступной ликвидности на ведущих децентрализованных биржах."; -"coin_analytics.dex_liquidity.tracked_dexes" = "Отслеживаемые DEX биржи:"; +"coin_analytics.dex_liquidity" = "DEX Liquidity"; +"coin_analytics.dex_liquidity_rank" = "DEX Liquidity Rank"; +"coin_analytics.dex_liquidity_rank.description" = "Tokens ranked by liquidity? We've got the best numbers for decentralized exchanges. Simply unbeatable!"; +"coin_analytics.dex_liquidity.info1" = "Look at this. Unbelievable amounts of liquidity for our token. Nobody has this kind of presence on decentralized exchanges!"; +"coin_analytics.dex_liquidity.info2" = "Have you seen this chart? Shows our dominance in liquidity. Leading the pack over a whole year!"; +"coin_analytics.dex_liquidity.info3" = "You want a list of top tokens by liquidity? Here it is. And guess who's always on top?"; +"coin_analytics.dex_liquidity.tracked_dexes" = "You're curious about which DEXes we're tracking? Only the absolute best in the business!"; "coin_analytics.dex_liquidity.tracked_dexes.info1" = "Ethereum : Uniswap V2/3, Balancer V1/2, Bancor V2, Curve, Sushiswap"; "coin_analytics.dex_liquidity.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap, DODO V1/2"; -"coin_analytics.active_addresses" = "Ежедневные активные адреса"; -"coin_analytics.active_addresses.30_day_unique_addresses" = "Уникальные адреса за 30 дней"; -"coin_analytics.active_addresses_rank" = "Рейтинг активных адресов"; -"coin_analytics.active_addresses_rank.description" = "Список токенов, ранжированных по количеству уникальных адресов транзакций с токеном."; -"coin_analytics.active_addresses.info1" = "Общее количество уникальных ежедневных активных адресов за 24 часа."; -"coin_analytics.active_addresses.info2" = "График, показывающий вариацию количества ежедневных активных адресов в течение 1 года."; -"coin_analytics.active_addresses.info3" = "Общее количество уникальных адресов блокчейна, с которых проводились транзакции с токеном за 30 дней."; -"coin_analytics.active_addresses.info4" = "Рейтинг токена основан на количестве активных кошельков, используемых для транзакций с токеном за 30-дневный период."; -"coin_analytics.active_addresses.info5" = "Список всех токенов, ранжированных по ежедневному количеству активных адресов транзакций с токеном с интервалом 24ч. / 7 дн. / 1 мес."; - -"coin_analytics.transaction_count" = "Количество транзакций"; -"coin_analytics.transaction_count_rank" = "Рейтинг кол-ва транзакций"; -"coin_analytics.transaction_count_rank.description" = "Токены ранжируются по количеству транзакций в блокчейне."; -"coin_analytics.transaction_count.info1" = "Общее количество уникальных транзакций блокчейна с токеном более 30 дней."; -"coin_analytics.transaction_count.info2" = "График, отражающий колебания количества транзакций за 1 год."; -"coin_analytics.transaction_count.info3" = "Рейтинг токена основан на количестве транзакций с токеном за 30-дневный период."; -"coin_analytics.transaction_count.info4" = "Список всех токенов, ранжированных на основе количества транзакций с интервалом 24ч / 7D / 1М."; -"coin_analytics.transaction_count.info5" = "Общее количество токенов, отправленных через блокчейн за 30-дневный период."; - -"coin_analytics.holders" = "Держатели"; -"coin_analytics.holders_rank" = "Рейтинг держателей"; -"coin_analytics.holders_rank.description" = "Рейтинг токенов по уникальным адресам, содержащим их в нескольких блокчейнах."; -"coin_analytics.holders.info1" = "Общее количество уникальных адресов с токенами в различных блокчейнах."; -"coin_analytics.holders.info2" = "Топ-10 кошельков с токенами в каждом блокчейне."; -"coin_analytics.holders.tracked_blockchains" = "Отслеживаемые блокчейны: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; -"coin_analytics.holders.in_top_10_addresses" = "в топ-10 держателей"; -"coin_analytics.holders.count" = "Всего держателей: %@"; -"coin_analytics.holders.see_all" = "Посмотреть всё"; - -"coin_analytics.project_tvl" = "Проект TVL"; -"coin_analytics.tvl_ratio" = "Рын.кап / Соотношение TVL "; -"coin_analytics.project_tvl.info_title" = "Проект TVL (совокупная сумма средств заблокирована)"; -"coin_analytics.project_tvl.info1" = "TVL (или Активы под управлением) в проектных смарт-контрактах."; -"coin_analytics.project_tvl.info2" = "График, отражающий колебания TVL в проектных смарт-контрактах за период, превышающий 1 год."; -"coin_analytics.project_tvl.info3" = "Рейтинг токена по текущему TVL."; -"coin_analytics.project_tvl.info4" = "Список всех токенов, ранжированных по текущему TVL."; -"coin_analytics.project_tvl.info5" = "Соотношение рыночной капитализации/TVL в рамках проекта."; - -"coin_analytics.project_fee" = "Комиссия проекта"; -"coin_analytics.project_fee_rank" = "Рейтинг оплаты за проект"; -"coin_analytics.project_fee_rank.description" = "Токены ранжируются в соответствии с платами, полученными в рамках соответствующих проектов."; - -"coin_analytics.project_revenue" = "Доход от проекта"; -"coin_analytics.project_revenue_rank" = "Рейтинг доходов"; -"coin_analytics.project_revenue_rank.description" = "Токены ранжируются по пассивному доходу владельцев, получаемому через стекинг или сжигание токенов."; - -"coin_analytics.other_data" = "Другие данные"; - -"coin_analytics.reports" = "Отчёты"; - -"coin_analytics.funding" = "Финансирование"; -"coin_analytics.funding.lead" = "Лид"; +//Active Addresses & Info + +"coin_analytics.active_addresses" = "Daily Active Addresses"; +"coin_analytics.active_addresses.30_day_unique_addresses" = "30-Day Unique Addresses"; +"coin_analytics.active_addresses_rank" = "Active Addresses Rank"; +"coin_analytics.active_addresses_rank.description" = "Rankings? Tokens ranked by unique addresses? We're always on top. No one else even comes close!"; +"coin_analytics.active_addresses.info1" = "The numbers are HUGE. Unbeatable daily unique addresses. 24 hours of dominance!"; +"coin_analytics.active_addresses.info2" = "This chart? Shows our incredible journey over a year. People love using our token. Tremendous growth in daily active addresses. You've got to see it to believe it!"; +"coin_analytics.active_addresses.info3" = "Look at these numbers! Over the past month alone, countless unique blockchain addresses have been transacting with our token. We're hotter than ever!"; +"coin_analytics.active_addresses.info4" = "Rankings? Of course, we're up there. We're ranked based on the sheer number of active wallets transacting with us in just 30 days. Everyone's talking about us!"; +"coin_analytics.active_addresses.info5" = "Who wants a list? We've got the top tokens based on daily activity. Updated regularly - 24 hours, weekly, monthly!"; + +//Transaction Count & Info + +"coin_analytics.transaction_count" = "Transaction Count"; +"coin_analytics.transaction_count_rank" = "Tx Count Rank"; + +"coin_analytics.transaction_count_rank.description" = "Tokens ranked by transactions? We're the gold standard!"; +"coin_analytics.transaction_count.info1" = "Count 'em! Massive number of transactions with our token in 30 days!"; +"coin_analytics.transaction_count.info5" = "Look at this! A sheer number of tokens moved across the blockchain in just 30 days. Big league!"; +"coin_analytics.transaction_count.info4" = "Everyone's talking about it! Here's the definitive list of tokens - but remember, the more transactions, the better the token. We're making transactions happen!"; +"coin_analytics.transaction_count.info2" = "Want to see a chart that shows real winning? Look at our transaction count over the past year. It's tremendous growth, folks!"; +"coin_analytics.transaction_count.info3" = "Where do we rank in terms of transactions in the last 30 days? It's at the top. It's always been at the top. No token transacts like us!"; + +//Holders & Info + +"coin_analytics.holders" = "Holders"; +"coin_analytics.holders_rank" = "Holders Rank"; + +"coin_analytics.holders_rank.description" = "Rankings by holders? Our token is held by the best people on multiple blockchains!"; +"coin_analytics.holders.info1" = "The numbers? Only growing! So many unique addresses holding our amazing token across various blockchains."; +"coin_analytics.holders.info2" = "Top 10 wallets? Everyone's asking! Holding our token on each and every blockchain!"; +"coin_analytics.holders.tracked_blockchains" = "Where are we tracking? Everywhere! Ethereum, Binance Smart Chain, you name it. Leading everywhere!"; +"coin_analytics.holders.in_top_10_addresses" = "in top 10 holders"; +"coin_analytics.holders.count" = "Total Holders: %@"; +"coin_analytics.holders.see_all" = "See All"; + +"coin_analytics.project_tvl" = "Project TVL"; +"coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; +"coin_analytics.project_tvl.info_title" = "Project TVL (Total Value Locked)"; +"coin_analytics.project_tvl.info1" = "Our TVL? Unbelievable! People are locking in their assets with us like never before. Everyone wants to be part of this project!"; +"coin_analytics.project_tvl.info2" = "This chart shows our meteoric rise. Look at the variation in Total-Value-Locked in just one year. We've truly made smart contracts great again!"; +"coin_analytics.project_tvl.info3" = "When it comes to TVL, we're the top pick! Look at our ranking. Everyone's taking notice."; +"coin_analytics.project_tvl.info4" = "Want to see how we stack up? Here's a list of tokens, and trust me, we're always at the top. Everyone loves our TVL!"; +"coin_analytics.project_tvl.info5" = "The numbers don't lie. Check out our Market Cap to TVL ratio. We're in a league of our own!"; + +"coin_analytics.project_fee" = "Project Fee"; +"coin_analytics.project_fee_rank" = "Project Fee Rank"; +"coin_analytics.project_fee_rank.description" = "We're ranking tokens on who's really making money here, folks. Look at these fees! And every project has its own way, its own secret sauce. Some tokens are just better at it than others, believe me!"; +"coin_analytics.project_revenue" = "Project Revenue"; +"coin_analytics.project_revenue_rank" = "Project Revenue Rank"; +"coin_analytics.project_revenue_rank.description" = "Let's rank tokens on who's actually putting money in the pockets of their people! Some of these tokens have brilliant strategies, like staking or even burning their tokens to create value. Not all tokens do it, but the smart ones do, and they're winning bigly for their holders!"; +"coin_analytics.other_data" = "Other Data"; + +"coin_analytics.reports" = "Reports"; + +"coin_analytics.funding" = "Funding"; +"coin_analytics.funding.lead" = "Lead"; "coin_analytics.treasuries" = "Treasuries"; -"coin_analytics.treasuries.filters" = "Фильтры"; -"coin_analytics.treasuries.filter.all" = "Все"; -"coin_analytics.treasuries.filter.public" = "Публичный"; -"coin_analytics.treasuries.filter.private" = "Приватный"; +"coin_analytics.treasuries.filters" = "Filters"; +"coin_analytics.treasuries.filter.all" = "All"; +"coin_analytics.treasuries.filter.public" = "Public"; +"coin_analytics.treasuries.filter.private" = "Private"; "coin_analytics.treasuries.filter.etf" = "ETF"; -"coin_analytics.audits" = "Аудиты"; -"coin_analytics.audits.issues" = "Запросы"; -"coin_analytics.audits.no_reports" = "Нет аудиторских отчетов"; - -"coin_analytics.last_30d" = "Последние 30 дн."; -"coin_analytics.current" = "текущее"; - -"coin_analytics.overall_score" = "Общий балл"; -"coin_analytics.overall_score.excellent" = "Отлично"; -"coin_analytics.overall_score.good" = "Хорошо"; -"coin_analytics.overall_score.fair" = "Приемлемо"; -"coin_analytics.overall_score.poor" = "Плохо"; -"coin_analytics.overall_score.cex_volume" = "Общий балл основан на средневзвешенном объеме торговли за последние 7 дней на централизованных биржах."; -"coin_analytics.overall_score.dex_volume" = "Общий балл основан на среднем ежедневном объеме торговли на децентрализованных биржах за последние 7 дней."; -"coin_analytics.overall_score.dex_liquidity" = "Общий балл основан на общей доступной ликвидности на децентрализованных биржах."; -"coin_analytics.overall_score.active_addresses" = "Общий балл основан на среднедневных активных адресах за последние 7 дней."; -"coin_analytics.overall_score.project_tvl" = "Общая балл основан на общей закрытой стоимости (активы, находящиеся в ведении управления) на проекте, представленном данным токеном."; -"coin_analytics.overall_score.transaction_count" = "Общий счет основан на среднем подсчете ежедневных транзакций за последние 7 дней."; -"coin_analytics.overall_score.holders" = "Общий балл основан на общем количестве адресов, содержащих соответствующий токен."; - -"coin_analytics.rank" = "Рейтинг"; -"coin_analytics.30_day_rank" = "Рейтинг на 30д"; -"coin_analytics.30_day_volume" = "Объем за 30д"; +"coin_analytics.audits" = "Audits"; +"coin_analytics.audits.issues" = "Issues"; +"coin_analytics.audits.no_reports" = "No audit reports"; + +"coin_analytics.last_30d" = "last 30d"; +"coin_analytics.current" = "current"; + +"coin_analytics.overall_score" = "Overall Score"; +"coin_analytics.overall_score.excellent" = "Excellent"; +"coin_analytics.overall_score.good" = "Good"; +"coin_analytics.overall_score.fair" = "Fair"; +"coin_analytics.overall_score.poor" = "Poor"; +"coin_analytics.overall_score.cex_volume" = "When it comes to centralized exchanges, the real measure of a token's greatness is the average daily trading volume over the past week. Believe me, that's where the action is!"; +"coin_analytics.overall_score.dex_volume" = "For the decentralized exchanges, which are absolutely taking off, by the way, the top tokens are the ones with the best average daily trading volume over the last 7 days. Winners keep winning!"; +"coin_analytics.overall_score.dex_liquidity" = "Now, liquidity? It's huge! The best tokens have massive amounts of available liquidity on decentralized exchanges. That's where the power is, folks."; +"coin_analytics.overall_score.active_addresses" = "Let's talk active addresses. In just the last week, the really special tokens are the ones buzzing with daily activity. That's the mark of true success!"; +"coin_analytics.overall_score.project_tvl" = "TVL, or Total Value Locked, is where the smart money is. The best projects, and I mean the very best, have the most assets under management. That's a sign of trust!"; +"coin_analytics.overall_score.transaction_count" = "Transactions count - it's like the pulse of a token. Over the last week, the champions are the ones with the highest daily transaction. They're doing something right!"; +"coin_analytics.overall_score.holders" = "And holders? They're the foundation! The top-tier tokens have armies of holders, dedicated believers backing their play. That's the real strength!"; + +"coin_analytics.rank" = "Rank"; +"coin_analytics.30_day_rank" = "30-Day Rank"; +"coin_analytics.30_day_volume" = "30-Day Volume"; // Coin Page -> Markets -"coin_markets.empty" = "Нет доступных данных"; +"coin_markets.empty" = "No data available"; // Coin Page -> Tweets -"coin_tweets.reference_type.retweeted" = "Ретвитнуто @%@"; -"coin_tweets.reference_type.quoted" = "Цитировано @%@"; -"coin_tweets.reference_type.replied" = "Ответил @%@"; -"coin_tweets.no_tweets_yet" = "Пока нет твитов"; -"coin_tweets.not_available" = "Twitter не доступен"; -"coin_tweets.see_on_twitter" = "Посмотреть в Twitter"; +"coin_tweets.reference_type.retweeted" = "Retweeted @%@"; +"coin_tweets.reference_type.quoted" = "Quoted @%@"; +"coin_tweets.reference_type.replied" = "Replied to @%@"; +"coin_tweets.no_tweets_yet" = "No tweets yet"; +"coin_tweets.not_available" = "No twitter available"; +"coin_tweets.see_on_twitter" = "See on Twitter"; // Coin Page -> Indicators -"chart_indicators.title" = "Индикаторы"; -"chart_indicators.moving_averages" = "Скользящие средние"; -"chart_indicators.oscillators" = "Осцилляторы"; +"chart_indicators.title" = "Indicators"; +"chart_indicators.moving_averages" = "Moving Averages"; +"chart_indicators.oscillators" = "Oscillators"; -"chart_indicators.settings.period.error" = "Период не может быть больше чем %d"; +"chart_indicators.settings.period.error" = "Period can’t be more than %d"; -"chart_indicators.settings.ma.description" = "В EMA, SMA и WMA используются средние значения для технического анализа:\n\nEMA подчеркивает последние цены для более быстрых реакций.\nСредние цены SMA для общего представления тренда.\nЧувствительность и шумовое снижение ВМА путем линейного взвешивания последних данных"; -"chart_indicators.settings.ma.type_title" = "Тип"; -"chart_indicators.settings.ma.period_title" = "Период"; +"chart_indicators.settings.ma.description" = "The EMA, SMA, and WMA are moving averages used in technical analysis:\n\nEMA emphasizes recent prices for quicker reactions.\nSMA averages price data for a general trend view.\nWMA balances sensitivity and noise reduction by linearly weighting recent data"; +"chart_indicators.settings.ma.type_title" = "Type"; +"chart_indicators.settings.ma.period_title" = "Period"; "chart_indicators.settings.rsi.title" = "RSI"; -"chart_indicators.settings.rsi.description" = "Индекс относительной силы (RSI) - это колебатель импульса, который измеряет скорость и изменения цен, определяющий перекупленные (более 70) или перепродаваемые (менее 30) рыночные условия. Она также может обнаружить изменения цен через расхождения."; -"chart_indicators.settings.rsi.period_title" = "Длина RSI"; + +"chart_indicators.settings.rsi.description" = "The Relative Strength Index (RSI)? Tremendous tool! It's like the pulse-checker of the market. When you see it above 70, that's a hot market, maybe too hot. Below 30? It might be a sale! And the best part? It can even show you when prices might take a U-turn. Genius!"; +"chart_indicators.settings.rsi.period_title" = "RSI Length"; "chart_indicators.settings.macd.title" = "MACD"; -"chart_indicators.settings.macd.description" = "Дивергенция Moving Average Convergence (MACD) — индикатор динамики, который отслеживает отношения между двумя ЭРА ценой безопасности. Это сигнализирует о возможности покупки или продажи, когда линия MACD (12-ти периодная EMA минус 26-ти периодов EMA) пересекает девять периодов EMA, известную как сигнальная линия."; +"chart_indicators.settings.macd.description" = "The Moving Average Convergence Divergence (MACD) is absolutely one of the big league players in the indicator world. It watches two EMAs like a hawk and tells you when things are heating up or cooling down. When the MACD line does a little dance with the 9-period EMA, the signal line, it might be showtime!"; "chart_indicators.settings.macd.fast_period_title" = "Fast Length"; + "chart_indicators.settings.macd.slow_period_title" = "Slow Length"; "chart_indicators.settings.macd.signal_period_title" = "Signal Smoothing"; -"chart_indicators.settings.macd.slow_fast.error" = "Fast Length должна быть меньше Slow Length"; +"chart_indicators.settings.macd.slow_fast.error" = "Fast Length must be less than Slow Length"; // Transactions -"transactions.title" = "Транзакции"; -"transactions.tab_bar_item" = "Транзакции"; +"transactions.title" = "Transactions"; +"transactions.tab_bar_item" = "Transactions"; "transactions.blockchain" = "Blockchain"; -"transactions.all_blockchains" = "Все блокчейны"; -"transactions.all_coins" = "Все токены"; -"transactions.choose_coin" = "Выберите токен"; -"transactions.filter_all" = "Все"; -"transactions.empty_text" = "У вас ещё нет незавершенных или прошлых транзакций"; -"transactions.pending" = "В обработке"; -"transactions.processing" = "В процессе"; -"transactions.completed" = "Завершено"; -"transactions.failed" = "Не удалось"; - -"transactions.receive" = "Получить"; -"transactions.send" = "Отправить"; -"transactions.burn" = "Сжечь"; -"transactions.mint" = "Минт"; -"transactions.approve" = "Разрешить"; -"transactions.swap" = "Обменять"; -"transactions.contract_call" = "Вызов контракта"; -"transactions.contract_creation" = "Создание контракта"; -"transactions.external_call" = "Внешний вызов"; - -"transactions.to" = "Кому %@"; -"transactions.from" = "От %@"; - -"transactions.multiple" = "Множество"; - -"transactions.value.unlimited" = "безлимитный"; - -"transactions.today" = "Сегодня"; -"transactions.yesterday" = "Вчера"; - -"transactions.types.all" = "Все"; -"transactions.types.incoming" = "Получено"; -"transactions.types.outgoing" = "Отправлено"; -"transactions.types.swap" = "Обмены"; -"transactions.types.approve" = "Разрешения"; - -"transactions.unknown_transaction.title" = "Неизвестная транзакция"; -"transactions.unknown_transaction.description" = "Транзакция не может быть обработана"; +"transactions.all_blockchains" = "All Blockchains"; +"transactions.all_coins" = "All Coins"; +"transactions.choose_coin" = "Choose Coin"; +"transactions.filter_all" = "All"; +"transactions.empty_text" = "You don't have any pending or past transactions yet"; +"transactions.pending" = "Pending"; +"transactions.processing" = "Processing"; +"transactions.completed" = "Completed"; +"transactions.failed" = "Failed"; + +"transactions.receive" = "Receive"; +"transactions.send" = "Send"; +"transactions.burn" = "Burn"; +"transactions.mint" = "Mint"; +"transactions.approve" = "Approve"; +"transactions.swap" = "Swap"; +"transactions.contract_call" = "Contract Call"; +"transactions.contract_creation" = "Contract Creation"; +"transactions.external_call" = "External Call"; + +"transactions.to" = "To %@"; +"transactions.from" = "From %@"; + +"transactions.multiple" = "Multiple"; + +"transactions.value.unlimited" = "unlimited"; + +"transactions.today" = "Today"; +"transactions.yesterday" = "Yesterday"; + +"transactions.types.all" = "All"; +"transactions.types.incoming" = "Received"; +"transactions.types.outgoing" = "Sent"; +"transactions.types.swap" = "Swaps"; +"transactions.types.approve" = "Approvals"; + +"transactions.unknown_transaction.title" = "Unknown Transaction"; +"transactions.unknown_transaction.description" = "Transaction can not be parsed"; // Transaction Info -"tx_info.title" = "Детали транзакции"; -"tx_info.date" = "Дата"; -"tx_info.title_approval" = "Разрешение обмена"; -"tx_info.status.pending" = "В обработке"; -"tx_info.status.completed" = "Завершено"; -"tx_info.status.failed" = "Не удалось"; -"tx_info.from_hash" = "От"; +"tx_info.title" = "Transaction Info"; +"tx_info.date" = "Date"; +"tx_info.title_approval" = "Swap Approval"; +"tx_info.status.pending" = "Pending"; +"tx_info.status.completed" = "Completed"; +"tx_info.status.failed" = "Failed"; +"tx_info.from_hash" = "From"; "tx_info.transaction_id" = "ID"; -"tx_info.to_hash" = "Кому"; -"tx_info.spender" = "Покупатель"; -"tx_info.contact_name" = "Имя контакта"; -"tx_info.button_explorer" = "Посмотреть на %@"; -"tx_info.rate" = "Исторический курс"; -"tx_info.options.speed_up" = "Ускорить"; -"tx_info.options.cancel" = "Отменить транзакцию"; -"tx_info.transaction.already_in_block" = "Транзакция уже в блоке"; -"tx_info.fee" = "Комиссия"; -"tx_info.fee.estimated" = "Комиссия (прим.)"; -"tx_info.to_self_note" = "Данная транзакция отправлена на собственный адрес"; -"tx_info.double_spent_note" = "Риск двойной траты!"; -"tx_info.locked_until" = "Заблокировано до %@"; -"tx_info.unlocked_at" = "Разблокировано %@"; -"tx_info.recipient_hash" = "Получатель"; -"tx_info.raw_transaction" = "Неподтвержденная транзакция"; +"tx_info.to_hash" = "To"; +"tx_info.spender" = "Spender"; +"tx_info.contact_name" = "Contact Name"; +"tx_info.button_explorer" = "View on %@"; +"tx_info.rate" = "Historical Rate"; +"tx_info.options.speed_up" = "Speed Up"; +"tx_info.options.cancel" = "Cancel Transaction"; +"tx_info.transaction.already_in_block" = "Transaction already in block"; +"tx_info.fee" = "Fee"; +"tx_info.fee.estimated" = "Fee (est.)"; +"tx_info.to_self_note" = "This transaction is sent to own address"; +"tx_info.double_spent_note" = "Double Spend Risk!"; +"tx_info.locked_until" = "Locked until %@"; +"tx_info.unlocked_at" = "Unlocked at %@"; +"tx_info.recipient_hash" = "Recipient"; +"tx_info.raw_transaction" = "Raw Transaction"; "tx_info.memo" = "Memo"; -"tx_info.service" = "Сервис"; -"tx_info.view_on" = "Посмотреть на %@"; -"tx_info.you_pay" = "Платите"; -"tx_info.you_get" = "Получите"; -"tx_info.you_paid" = "Вы заплатили"; -"tx_info.you_got" = "Вы получили"; -"tx_info.price" = "Цена"; +"tx_info.service" = "Service"; +"tx_info.view_on" = "View on %@"; +"tx_info.you_pay" = "You Pay"; +"tx_info.you_get" = "You Get"; +"tx_info.you_paid" = "You Paid"; +"tx_info.you_got" = "You Got"; +"tx_info.price" = "Price"; // Settings -"settings.title" = "Настройки"; -"settings.tab_bar_item" = "Настройки"; -"settings.manage_accounts" = "Кошельки"; -"settings.blockchain_settings" = "Настройки блокчейна"; -"settings.security" = "Безопасность"; -"settings.experimental_features" = "Экспериментальные функции"; -"settings.personal_support" = "Персональная поддержка"; -"settings.base_currency" = "Базовая валюта"; -"settings.language" = "Язык"; +"settings.title" = "Settings"; +"settings.tab_bar_item" = "Settings"; +"settings.manage_accounts" = "Manage Wallets"; +"settings.blockchain_settings" = "Blockchain Settings"; +"settings.security" = "Security"; +"settings.experimental_features" = "Experimental"; +"settings.personal_support" = "Personal Support"; +"settings.base_currency" = "Base Currency"; +"settings.language" = "Language"; "settings.faq" = "FAQ"; -"settings.theme" = "Тема"; -"settings.info_subtitle" = "децентрализованное приложение"; -"settings.donate.description" = "С вашей поддержкой мы вместе сможем сделать это приложение еще лучше!"; -"settings.donate.title" = "Поддержи нас"; +"settings.theme" = "Theme"; +"settings.info_subtitle" = "decentralized app"; +"settings.donate.description" = "Together, with your support, we can make this app even better!"; +"settings.donate.title" = "Donate"; // Settings -> Base Currency -"settings.base_currency.title" = "Базовая валюта"; -"settings.base_currency.other" = "Другое"; -"settings.base_currency.disclaimer" = "Отказ от ответственности"; -"settings.base_currency.disclaimer.description" = "Данные об обменном курсе предоставлены третьим лицом Coingecko.сom\n\n Приложение %@ Wallet не гарантирует, что эти данные всегда верны и соответствуют рыночным. Шанс на несоответствие повышается при выборе базовой валюты, отличающейся от %@."; -"settings.base_currency.disclaimer.set" = "Установить"; +"settings.base_currency.title" = "Base Currency"; +"settings.base_currency.other" = "Other"; +"settings.base_currency.disclaimer" = "Disclaimer"; +"settings.base_currency.disclaimer.description" = "Hey there! So, we get our exchange rates from Coingecko.com, alright? While we aim for accuracy, we can't promise it's 100% spot-on, especially if you pick a base currency other than %@. Just so you know!"; +"settings.base_currency.disclaimer.set" = "Got it!"; // Settings -> Manage Wallet -"manage_wallets.title" = "Токены"; -"manage_wallets.not_found" = "Ничего не найдено. Попробуйте добавить токен вручную."; -"manage_wallets.search_placeholder" = "Имя, код или адрес контракта"; -"manage_wallets.contract_address" = "Адрес контракта"; -"manage_wallets.derivation_description" = "Существует 4 распространенных формата адресов %@ кошельков для получения входящих платежей:\n\n- BIP44 (более старый)\n- BIP49\n- BIP84 (рекомендованный)\n- BIP86 (самый новый)\n\nХотя кошелек %@ поддерживает все четыре формата, рекомендуется использовать %@ кошелек, работающий в формате BIP84."; -"manage_wallets.bitcoin_cash_coin_type_description" = "Существует 2 формата адресов, которые кошельки Bitcoin Cash могут использовать для приема входящих платежей:\n\n- TYPE 0 (более старый)\n- TYPE 145 (более новый)\n\nХотя кошелек %@ поддерживает оба формата, рекомендуется использовать кошелек Bitcoin Cash, работающий в формате TYPE 145."; +"manage_wallets.title" = "Coin Manager"; +"manage_wallets.not_found" = "Hmm, no luck. Maybe try adding the token by hand?"; +"manage_wallets.search_placeholder" = "Whatcha looking for? Name, code, or maybe contract address?"; +"manage_wallets.contract_address" = "The Contract's Address"; +"manage_wallets.derivation_description" = "Fun fact: there are 4 cool address formats %@ wallets can use. They are BIP44, BIP49, BIP84 (our fav!), and BIP86. But no stress, we got you covered with all of them!"; +"manage_wallets.bitcoin_cash_coin_type_description" = "For Bitcoin Cash, there are 2 formats - TYPE 0 and TYPE 145. We say TYPE 145 is the way to go!"; // Settings -> Personal Support -"settings.personal_support.telegram_username.title" = "Account"; -"settings.personal_support.telegram_username.placeholder" = "@username"; -"settings.personal_support.description" = "Введите имя аккаунта Telegram, чтобы открыть личный чат поддержки, и мы отправим вам сообщение."; -"settings.personal_support.request" = "Запрос"; -"settings.personal_support.requested" = "Запрошено"; -"settings.personal_support.failed" = "Ошибка запроса"; -"settings.personal_support.need_subscription" = "Эта функция доступна только для премиум-пользователей %@ Wallet . Дополнительная информация на нашем официальном сайте."; -"settings.personal_support.requested.description" = "Вы уже запросили приватный чат, найдите его в Telegram"; -"settings.personal_support.requested.open_telegram" = "Открыть Telegram"; -"settings.personal_support.requested.new_request" = "Новый запрос"; +"settings.personal_support.telegram_username.title" = "Your TG Handle"; +"settings.personal_support.telegram_username.placeholder" = "It's the @username one!"; +"settings.personal_support.description" = "Pop in your Telegram username, and we'll slide into your DMs for some one-on-one chat."; +"settings.personal_support.request" = "Hit me up!"; +"settings.personal_support.requested" = "Message sent!"; +"settings.personal_support.failed" = "Oops! That didn't go as planned."; +"settings.personal_support.need_subscription" = "Heads up! This cool feature? It's for the %@ Wallet VIPs. Wanna know more? Check our website!"; +"settings.personal_support.requested.description" = "You've already pinged us! Check your Telegram for our message."; +"settings.personal_support.requested.open_telegram" = "Jump to Telegram"; +"settings.personal_support.requested.new_request" = "Ping again?"; // Settings -> Experimental Features -"settings.experimental_features.title" = "Экспериментальные функции"; -"settings.experimental_features.description" = "Функции ниже являются экспериментальными. Несмотря на то, что мы детально их протестировали, используя наши собственные криптосредства, мы не можем гарантировать, что они всегда будут работать так, как предусмотрено."; +"settings.experimental_features.title" = "Lab Stuff"; +"settings.experimental_features.description" = "Below are some fun (but experimental) features. We've played with them, and while we think they're cool, use them at your own risk!"; "settings.experimental_features.bitcoin_hodling" = "TimeLock"; // Settings -> Experimental Features -> Bitcoin HODLing -"settings.bitcoin_hodling.title" = "TimeLock"; -"settings.bitcoin_hodling.lock_time" = "Активировать"; -"settings.bitcoin_hodling.description" = "Позволяет отправлять биткойны, которые могут быть потрачены только после указанной даты.\n\nПолучатель такой транзакции должен использовать версию 0.10 приложения %@ Wallet или же более новую версию с форматом адреса BIP44 для Bitcoin.\n\nТолько кошелек %@ может правильно распознать и обработать такие транзакции в сети Bitcoin, и позволить получателю тратить эти биткойны по истечении периода блокировки.\n\nЕсли вы HODLer (долгосрочный инвестор), то вы можете использовать эту функцию для удержания своих биткойнов, отправив их себе. \n\nТак как это экспериментальная функция, максимальная сумма транзакции ограничена 0,5 BTC."; +"settings.bitcoin_hodling.title" = "The TimeLock Trick"; +"settings.bitcoin_hodling.lock_time" = "Switch it on!"; +"settings.bitcoin_hodling.description" = "So this? It's a cool trick to lock up your Bitcoins till a certain date. But make sure the receiver's using %@ wallet app version 0.10 or newer. It's a fun way to HODL, especially if you're sending to yourself."; // Settings -> Terms -"terms.title" = "Условия использования"; -"terms.i_agree" = "Я принимаю"; - -"terms.item.1" = "Безопасное резервное копирование фраз для каждого кошелька. Это единственный способ восстановить доступ к средствам, если приложение не работает."; -"terms.item.2" = "Фразы восстановления кошелька генерируются случайным образом на устройстве во время установки и не хранятся в другом месте."; -"terms.item.3" = "Отключение PIN-кода разблокировки на смартфоне удаляет все кошельки из приложения. Для восстановления доступа к средствам потребуется восстановление фраз."; -"terms.item.4" = "Jailbreaking (rooting), использование устаревших ОС и установка приложений из неизвестных источников могут поставить под угрозу безопасность средств."; -"terms.item.5" = "В процессе работы кода могут возникнуть проблемы с программным обеспечением, что может привести к сбоям приложения."; +"terms.title" = "The Fine Print"; +"terms.i_agree" = "Alright, I'm in!"; +"terms.item.1" = "Always backup your wallet recovery phrases. It's your key to the kingdom if things go sideways."; +"terms.item.2" = "Your wallet recovery phrases are made just for you, right here, and we don't keep them anywhere else."; +"terms.item.3" = "Heads up! If you switch off the unlock PIN on your phone, all wallets in the app will vanish. You'll need those recovery phrases to get back in."; +"terms.item.4" = "Messing with your phone's settings or downloading sketchy apps? That might put your coins at risk."; +"terms.item.5" = "Just so you know, there's always a teeny chance that a bug sneaks into our code, which might make the app act a little funky."; // Settings -> Tell Friends @@ -1073,346 +1072,341 @@ // Settings -> Blockchain Settings -"blockchain_settings.title" = "Настройки блокчейна"; +"blockchain_settings.title" = "Blockchain Settings"; // Settings -> Security -"settings_security.title" = "Безопасность"; -"settings_security.passcode" = "Код доступа"; -"settings_security.change_pin" = "Изменить код"; +"settings_security.title" = "Security"; +"settings_security.passcode" = "Passcode"; +"settings_security.change_pin" = "Edit Passcode"; "settings_security.touch_id" = "Touch ID"; "settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Настройки блокчейна"; -"security_settings.delete_alert_button" = "Удалить с телефона"; +"settings_security.blockchain_settings" = "Blockchain Settings"; +"security_settings.delete_alert_button" = "Delete from Phone"; -"btc_blockchain_settings.restore_source" = "Источник восстановления"; -"btc_blockchain_settings.restore_source.description" = "Выберите источник данных для восстановления кошелька с транзакциями."; -"btc_blockchain_settings.restore_source.alert" = "После изменения источника восстановления, кошелек должен будет повторно синхронизироваться с блокчейном %@."; +"btc_blockchain_settings.restore_source.description" = "Where do you wanna pull your wallet's past deeds from?"; +"btc_blockchain_settings.restore_source.alert" = "Just so you know, if you tweak the Restore Source, the wallet's gotta get in sync with the %@ blockchain again."; -"btc_restore_mode.recommended" = "Рекомендовано"; -"btc_restore_mode.more_private" = "Более Приватно"; +"btc_restore_mode.recommended" = "What We Suggest"; +"btc_restore_mode.more_private" = "On the Down Low"; "btc_transaction_sort_mode.shuffle" = "Shuffle"; -"btc_transaction_sort_mode.shuffle.description" = "Случайное индексирование"; +"btc_transaction_sort_mode.shuffle.description" = "Random Indexing"; "btc_transaction_sort_mode.bip69" = "Deterministic Bip69"; -"btc_transaction_sort_mode.bip69.description" = "Лексикографическая индексация"; +"btc_transaction_sort_mode.bip69.description" = "Lexicographical Indexing"; -"blockchain_settings.info.restore_source" = "Источник восстановления"; -"blockchain_settings.info.restore_source.content" = "Этот параметр актуален только при восстановлении существующего кошелька. Это процесс получения истории транзакций для данной монеты, чтобы приложение кошелька могло отображать предыдущие транзакции и определять последний баланс. Это должно произойти только один раз при восстановлении пользователем ранее созданного кошелька. \n\nНа данный момент у такого мобильного кошелька, как %@, есть два возможных способа для реализации этого вопроса: \n\n1. С сервера API: существует предопределенный сторонний сервер, на котором размещается вся цепочка блоков, происходит обработка всех данных и их подготовка к быстрому предоставлению. Это быстрый метод, однако он предполагает потенциальное отслеживание IP-адресов и запрашиваемых данных. Этот вариант рекомендуется использовать из-за скорости получения данных (5-10 минут). Он сопряжен с минимальными рисками. Если пользователь восстанавливает данные со своего собственного сервера, то нет никакой угрозы конфиденциальности.\n\n2. Из блокчейна: в этом случае приложение пытается восстановить данные напрямую из p2p-сети узлов блокчейна. Приложение пингует многие из этих узлов и запрашивает у них данные. Потенциально эти узлы могут отслеживать IP-адрес пользователя и сделанные запросы, однако во время восстановления данных приложение может переключаться между узлами. В любое время в сети находится около 10 000 биткойн-узлов. Это медленный вариант обработки данных, она может занять 2-3 часа и приложение должно быть открыто во время их восстановления."; -"blockchain_settings.info.rpc_source" = "Источник RPC"; -"blockchain_settings.info.rpc_source.content" = "Этот параметр определяет способ взаимодействия приложения с блокчейнами при отправке или получении транзакций.\n\nКогда речь идет о Bitcoin, Bitcoin Cash, Litecoin, и Dash, связь с сетевыми узлами блокчейна полностью равноправна. %@ пингует многие узлы и связывается с одним из них. Каждый раз приложение подключается к другому узлу.\n\nЧто касается Ethereum и Binance Smart Chain, то пока нет подходящих альтернативных вариантов мобильных кошельков для взаимодействия с соответствующими блокчейнами кроме сторонних поставщиков услуг RPC (т.е. Infura.io) или личными узлами Ethereum. Это значит, что ваше взаимодействие с блокчейном не децентрализовано. \n\nЭто влияет не на ваши средства, а на возможность подключения к сети блокчейн."; +"blockchain_settings.info.restore_source" = "Source of the Comeback"; +"blockchain_settings.info.restore_source.content" = "Alright, here's the deal. If you're bringing back an old wallet, you've got to know where it's been. %@ can tap into two ways: 1. A super-fast API server (think 5-10 minutes) that's kinda like asking a friend who knows everything. But it’s not as private and leans on that friend always being around. 2. Straight from the blockchain network, which is more private and independent but also a slow poke (could take 2-3 hours)."; +"blockchain_settings.info.rpc_source" = "Chatting with Blockchains"; +"blockchain_settings.info.rpc_source.content" = "How do we chat with the blockchains when money's moving? With some (like Bitcoin), it's like chatting in a big group. With others (like Ethereum), we gotta use a middleman like Infura.io. No worries about your coins though, just how we connect. We're brainstorming more freestyle ways to link up. Stay tuned!"; // Manage Accounts -"manage_accounts.migration_required" = "Требуется миграция"; -"manage_accounts.migration_recommended" = "Рекомендуется миграция"; -"manage_accounts.backup_required" = "Необходима резерв. копия"; +"manage_accounts.migration_required" = "Time to Move!"; +"manage_accounts.migration_recommended" = "Might Wanna Consider Moving"; +"manage_accounts.backup_required" = "Need a Safety Net"; // Manage Keys -"settings_manage_keys.title" = "Кошельки"; -"settings_manage_keys.delete" = "Удалить"; -"settings_manage_keys.backup" = "Резервная копия"; -"settings_manage_keys.delete.title" = "Удалить кошелек"; -"settings_manage_keys.delete.confirmation_remove" = "Действие удалит этот кошелек с устройства."; -"settings_manage_keys.delete.confirmation_loose" = "Если вы не сделали резервную копию приватного ключа для этого кошелька, вы потеряете доступ к вашим средствам."; -"settings_manage_keys.delete.confirmation_watch" = "Вы хотите приостановить просмотр этого адреса кошелька?"; -"settings_manage_keys.delete.confirmation_watch.button" = "Остановить просмотр"; +"settings_manage_keys.title" = "Wallet Wizardry"; +"settings_manage_keys.delete" = "Toss It"; +"settings_manage_keys.backup" = "Safekeep"; +"settings_manage_keys.delete.title" = "Ditch the Wallet"; +"settings_manage_keys.delete.confirmation_remove" = "Heads up! This will kick the wallet off the device."; +"settings_manage_keys.delete.confirmation_loose" = "Hey, if you haven't stashed the key for this wallet, you could be saying bye to your bucks."; +"settings_manage_keys.delete.confirmation_watch" = "Done watching this wallet's every move?"; +"settings_manage_keys.delete.confirmation_watch.button" = "Eyes Off"; // Settings -> About App -"settings.about_app.title" = "О приложении"; -"settings.about_app.app_name" = "%@ кошелек"; -"settings.about_app.description" = "Кошелек %@ создан для тех, кто хочет инвестировать и хранить криптовалюты частным и независимым образом.\n\nЭто одноранговый кошелек, где только пользователь имеет контроль над средствами. Он не собирает никаких данных и обеспечивает независимость пользователя, не привязывая средства пользователя к определенному приложению.\n\nКошелек %@ имеет полностью открытый исходный код, и любой может подтвердить, что приложение работает именно так, как заявлено."; -"settings.about_app.whats_new" = "Что нового"; -"settings.about_app.website" = "Веб-сайт"; -"settings.about_app.contact" = "Связаться с нами"; -"settings.about_app.rate_us" = "Оценить нас"; -"settings.about_app.tell_friends" = "Рассказать друзьям"; +"settings.about_app.title" = "About App"; +"settings.about_app.app_name" = "%@ Wallet"; +"settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; +"settings.about_app.whats_new" = "What's New"; +"settings.about_app.website" = "Website"; +"settings.about_app.contact" = "Contact Us"; +"settings.about_app.rate_us" = "Rate Us"; +"settings.about_app.tell_friends" = "Tell Friends"; // Settings -> About App -> Contact -"settings.contact.title" = "Связаться с нами"; -"settings.contact.via_email" = "по электронной почте"; -"settings.contact.via_telegram" = "через Telegram"; +"settings.contact.title" = "Contact Us"; +"settings.contact.via_email" = "via E-mail"; +"settings.contact.via_telegram" = "via Telegram"; // Settings -> Privacy -"settings.privacy" = "Приватность"; -"settings.privacy.description" = "%@ не собирает данные и не использует инструменты аналитики, которые могут раскрывать любые данные о своих пользователях. Кошелек разработан для обеспечения высокого уровня конфиденциальности для своих пользователей."; -"settings.privacy.statement.user_data_storage" = "Данные пользователя всегда остаются на устройстве пользователя."; -"settings.privacy.statement.data_usage" = "Кошелек не собирает никаких данных о пользователях."; -"settings.privacy.statement.data_privacy" = "Кошелек не передает никаких данных о пользователях."; -"settings.privacy.statement.user_account" = "Нет учетных записей или баз данных, хранящих данные пользователя в другом месте."; + +"settings.privacy" = "Privacy Vibes"; +"settings.privacy.description" = "Just so you know, %@ is like that friend who never spills your secrets. No data collection, no sneaky analytics, nada! We've built this wallet with your privacy front and center."; +"settings.privacy.statement.user_data_storage" = "Your stuff? It stays right on your device, and that's it."; +"settings.privacy.statement.data_usage" = "We're not in the biz of snooping or collecting any deets about you."; +"settings.privacy.statement.data_privacy" = "Sharing your data? Nah, that's not our style."; +"settings.privacy.statement.user_account" = "And guess what? No user accounts or mysterious databases storing your data out in the wild."; // Settings -> Appearance -"appearance.title" = "Оформление"; +"appearance.title" = "Appearance"; -"appearance.theme" = "Тема"; -"appearance.theme.system" = "Системная"; -"appearance.theme.dark" = "Тёмная"; -"appearance.theme.light" = "Светлая"; +"appearance.theme" = "Theme"; +"appearance.theme.system" = "System"; +"appearance.theme.dark" = "Dark"; +"appearance.theme.light" = "Light"; -"appearance.tab_settings" = "Настройки вкладки"; -"appearance.markets_tab" = "Вкладка рынки"; -"appearance.launch_screen" = "Запуск экрана"; -"appearance.launch_screen.auto" = "По умолчанию"; -"appearance.launch_screen.balance" = "Баланс"; -"appearance.launch_screen.market_overview" = "Обзор рынка"; -"appearance.launch_screen.watchlist" = "Избранное"; +"appearance.tab_settings" = "Tab Settings"; +"appearance.markets_tab" = "Markets Tab"; +"appearance.launch_screen" = "Launch Screen"; +"appearance.launch_screen.auto" = "Auto"; +"appearance.launch_screen.balance" = "Balance"; +"appearance.launch_screen.market_overview" = "Market Overview"; +"appearance.launch_screen.watchlist" = "Watchlist"; -"appearance.app_icon" = "Иконка приложения"; +"appearance.app_icon" = "App Icon"; -"appearance.balance_conversion" = "Конвертация баланса"; +"appearance.balance_conversion" = "Balance Conversion"; -"appearance.balance_value" = "Значение баланса"; -"appearance.balance_value.coin_value" = "Токен значение"; -"appearance.balance_value.fiat_value" = "Фиатное значение"; +"appearance.balance_value" = "Balance Value"; +"appearance.balance_value.coin_value" = "Coin Value"; +"appearance.balance_value.fiat_value" = "Fiat Value"; -"appearance.balance_auto_hide" = "Автоскрытие баланса"; +"appearance.balance_auto_hide" = "Balance Auto Hide"; // Settings -> Contacts -"contacts.title" = "Контакты"; -"contacts.list.search_placeholder" = "Поиск по имени"; -"contacts.list.not_found" = "У вас нет добавленных контактов"; -"contacts.list.not_found_search" = "Ничего не найдено"; -"contacts.add_new_contact" = "Добавить новый контакт"; -"contacts.update_contact.already_has_address" = "Выбранный контакт уже имеет адрес на %@. Это действие заменит адрес %@ на %@."; -"contacts.update_contact.replace" = "Заменить"; -"contacts.list.addresses_count" = "Адреса: %d"; -"contacts.contact.new.title" = "Новый контакт"; -"contacts.contact.name.placeholder" = "Название"; -"contacts.contact.update.error.name_already_exist" = "Имя уже существует"; -"contacts.contact.add_address" = "Добавить адрес"; -"contacts.contact.delete" = "Удалить контакт"; -"contacts.contact.address.blockchains" = "Блокчейны"; +"contacts.title" = "Contacts"; +"contacts.list.search_placeholder" = "Search by name"; +"contacts.list.not_found" = "You do not have an added contact"; +"contacts.list.not_found_search" = "No results found"; +"contacts.add_new_contact" = "Add New Contact"; +"contacts.update_contact.already_has_address" = "Selected contact already has an address on %@. This action will replace the address %@ with %@."; +"contacts.update_contact.replace" = "Replace"; +"contacts.list.addresses_count" = "Addresses: %d"; +"contacts.contact.new.title" = "New Contact"; +"contacts.contact.name.placeholder" = "Name"; +"contacts.contact.update.error.name_already_exist" = "Name Already Exist"; +"contacts.contact.add_address" = "Add Address"; +"contacts.contact.delete" = "Delete Contact"; +"contacts.contact.address.blockchains" = "Blockchains"; "contacts.contact.address.blockchain" = "Blockchain"; -"contacts.contact.address.delete_address" = "Удалить адрес"; +"contacts.contact.address.delete_address" = "Delete Address"; -"contacts.restore.restored" = "Восстановлено"; -"contacts.restore.parsing_error" = "Файл имеет неправильные данные!"; -"contacts.restore.restore_error" = "Не удалось восстановить контакты"; -"contacts.restore.overwrite_alert.description" = "Это действие перезапишет ваши локальные платежные контакты, а также их копии в iCloud (при наличии таковых)."; -"contacts.restore.overwrite_alert.replace" = "Заменить"; +"contacts.restore.restored" = "Restored"; +"contacts.restore.parsing_error" = "File has wrong data!"; +"contacts.restore.restore_error" = "Failed to restore contacts"; +"contacts.restore.overwrite_alert.description" = "This action will overwrite your local payment contacts as well as its iCloud copy (if there is one)."; +"contacts.restore.overwrite_alert.replace" = "Replace"; -"contacts.add_address.title" = "Добавить адрес"; -"contacts.add_address.create_new" = "Создать новый контакт"; -"contacts.add_address.add_to_contact" = "Добавить в существующий контакт"; -"contacts.add_address.exist_address" = "Этот адрес уже используется для %@"; +"contacts.add_address.title" = "Add Address"; +"contacts.add_address.create_new" = "Create New Contact"; +"contacts.add_address.add_to_contact" = "Add to Existing Contact"; +"contacts.add_address.exist_address" = "This address is already used for %@"; -"contacts.contact.delete_alert.title" = "Удалить контакт"; -"contacts.contact.delete_alert.description" = "Вы действительно хотите удалить этот контакт?"; -"contacts.contact.delete_alert.delete" = "Удалить"; +"contacts.contact.delete_alert.title" = "Delete Contact"; +"contacts.contact.delete_alert.description" = "Are you sure you want to delete this contact?"; +"contacts.contact.delete_alert.delete" = "Delete"; -"contacts.contact.dismiss_changes.description" = "Вы уверены, что хотите отменить эти изменения?"; -"contacts.contact.dismiss_changes.discard_changes" = "Отменить изменения"; -"contacts.contact.dismiss_changes.keep_editing" = "Продолжить редактирование"; +"contacts.contact.dismiss_changes.description" = "Are you sure you want to discard these new changes?"; +"contacts.contact.dismiss_changes.discard_changes" = "Discard Changes"; +"contacts.contact.dismiss_changes.keep_editing" = "Keep Editing"; -"contacts.add_address.delete_alert.title" = "Удалить адрес"; -"contacts.add_address.delete_alert.description" = "Вы уверены, что хотите удалить этот адрес?"; -"contacts.add_address.delete_alert.delete" = "Удалить"; +"contacts.add_address.delete_alert.title" = "Delete Address"; +"contacts.add_address.delete_alert.description" = "Are you sure you want to delete this address?"; +"contacts.add_address.delete_alert.delete" = "Delete"; // Contacts -> Settings -"contacts.settings.title" = "Настройки"; +"contacts.settings.title" = "Settings"; -"contacts.settings.restore_contacts" = "Восстановить контакты"; -"contacts.settings.backup_contacts" = "Резерв. копирование контактов"; +"contacts.settings.restore_contacts" = "Restore Contacts"; +"contacts.settings.backup_contacts" = "Backup Contacts"; -"contacts.settings.icloud_sync" = "iCloud синхр."; -"contacts.settings.description" = "Синхронизируйте платежные контакты с iCloud для легкого резервного копирования и доступа к ним на нескольких устройствах."; -"contacts.settings.lost_synchronization.description" = "Синхронизация iCloud утеряна. Пожалуйста, проверьте, что iCloud Storage включен на вашем устройстве."; -"contacts.settings.merge_disclaimer" = "Ваши локальные платежные контакты будут объединены с данными, хранящимися в iCloud."; +"contacts.settings.icloud_sync" = "iCloud Sync"; +"contacts.settings.description" = "Sync payment contacts to iCloud for easy backup and access across multiple devices."; +"contacts.settings.lost_synchronization.description" = "iCloud synchronization is lost. Please check that iCloud Storage is enabled on your device."; +"contacts.settings.merge_disclaimer" = "Your local payment contacts will be merged with ones stored on iCloud."; -"contacts.settings.alert.title" = "iCloud синхр."; -"contacts.settings.alert.description" = "Пожалуйста, убедитесь, что iCloud хранилище включено на вашем устройстве."; +"contacts.settings.alert.title" = "iCloud Sync"; +"contacts.settings.alert.description" = "Please check that iCloud Storage is enabled on your device."; -"contacts.settings.alert_error.title" = "Ошибка iCloud"; +"contacts.settings.alert_error.title" = "iCloud Error"; // Set PIN -"set_pin.title" = "Код доступа"; -"set_pin.info" = "Код доступа будет использоваться для разблокировки вашего кошелька"; -"set_pin.wrong_confirmation" = "Код доступа не совпадает. Попробуйте ещё раз"; +"set_pin.title" = "Passcode"; +"set_pin.info" = "Your passcode will be used to unlock your wallet"; +"set_pin.wrong_confirmation" = "Passcode did not match. Try again"; // Edit PIN -"edit_pin.title" = "Изменить код"; -"edit_pin.unlock_info" = "Текущий код"; -"edit_pin.new_pin_info" = "Новый код"; +"edit_pin.title" = "Edit Passcode"; +"edit_pin.unlock_info" = "Current Passcode"; +"edit_pin.new_pin_info" = "New Passcode"; // Unlock PIN -"unlock_pin.info" = "Код доступа"; -"unlock_pin.cant_save_pin" = "Ой! Мы не можем сохранить ваш код доступа. Пожалуйста, свяжитесь с нами как можно скорее!"; -"unlock_pin.blocked_until" = "Отключено до: %@"; +"unlock_pin.info" = "Passcode"; +"unlock_pin.cant_save_pin" = "Ouch! We cannot save your passcode, please contact us asap!"; +"unlock_pin.blocked_until" = "Disabled until: %@"; // Key Types -"chart.time_duration.day" = "24Ч"; -"chart.time_duration.week" = "7Д"; -"chart.time_duration.week2" = "2Н"; -"chart.time_duration.month" = "1М"; -"chart.time_duration.month3" = "3М"; -"chart.time_duration.halfyear" = "6М"; -"chart.time_duration.year" = "1Г"; -"chart.time_duration.year2" = "2Г"; -"chart.time_duration.all" = "ВСЕ"; - -"chart.market_cap" = "Рын. капитализация"; -"chart.volume" = "Объём (за 24 ч)"; -"chart.circulation" = "В обороте"; -"chart.selected.volume" = "Объём"; - -"chart.market.header" = "Рынок"; -"chart.market.market_cap" = "Рын. капитализация"; -"chart.market.volume" = "Объём (за 24 ч)"; -"chart.market.circulation" = "В обороте"; -"chart.market.total_supply" = "Макс.выпуск"; - -"chart.performance.week_changes" = "Изменения (за 1Н)"; -"chart.performance.month_changes" = "Изменения (за 1М)"; - -"chart.about.header" = "О Проекте"; -"chart.about.read_more" = "Подробнее"; -"chart.about.read_less" = "Свернуть"; +"chart.time_duration.day" = "24H"; +"chart.time_duration.week" = "7D"; +"chart.time_duration.week2" = "2W"; +"chart.time_duration.month" = "1M"; +"chart.time_duration.month3" = "3M"; +"chart.time_duration.halfyear" = "6M"; +"chart.time_duration.year" = "1Y"; +"chart.time_duration.year2" = "2Y"; +"chart.time_duration.all" = "ALL"; + +"chart.market_cap" = "Market Cap"; +"chart.volume" = "Volume (24h)"; +"chart.circulation" = "In Circulation"; +"chart.selected.volume" = "Vol."; + +"chart.market.header" = "Market"; +"chart.market.market_cap" = "Market Cap"; +"chart.market.volume" = "Volume (24h)"; +"chart.market.circulation" = "In Circulation"; +"chart.market.total_supply" = "Total Supply"; + +"chart.performance.week_changes" = "Changes (1W)"; +"chart.performance.month_changes" = "Changes (1M)"; + +"chart.about.header" = "About"; +"chart.about.read_more" = "Read More"; +"chart.about.read_less" = "Read Less"; "coin_page.return_of_investments" = "ROI"; // Create Wallet -"create_wallet.title" = "Новый кошелек"; -"create_wallet.name" = "Название"; -"create_wallet.advanced_setup" = "Доп. настройки"; -"create_wallet.create" = "Создать"; -"create_wallet.advanced" = "Доп. настройки"; -"create_wallet.phrase_count" = "Фраза восстановления"; -"create_wallet.12_words" = "12 слов (рекомендуется)"; -"create_wallet.n_words" = "%@ слов"; -"create_wallet.word_list" = "Список слов"; -"create_wallet.passphrase" = "Кодовая фраза"; -"create_wallet.input.passphrase" = "Кодовая фраза"; -"create_wallet.input.confirm" = "Подтвердить"; -"create_wallet.passphrase_description" = "Кодовая фраза добавляет дополнительный слой безопасности для кошельков. Чтобы восстановить такой кошелек требуется как мнемоник, так и кодовая фраза.\n\\Кодовая фраза также облегчает пользователям возможность иметь множество мульти-монетных кошельков, используя одну фразу восстановления, но другой пароль."; -"create_wallet.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; -"create_wallet.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"create_wallet.error.invalid_confirmation" = "Подтвержденная кодовая фраза не совпадает"; - +"create_wallet.title" = "New Wallet"; +"create_wallet.name" = "Name"; +"create_wallet.advanced_setup" = "Advanced"; +"create_wallet.create" = "Create"; +"create_wallet.advanced" = "Advanced"; +"create_wallet.phrase_count" = "Recovery Phrase"; +"create_wallet.12_words" = "12 words (recommended)"; +"create_wallet.n_words" = "%@ words"; +"create_wallet.word_list" = "Word List"; +"create_wallet.passphrase" = "Passphrase"; +"create_wallet.input.passphrase" = "Passphrase"; +"create_wallet.input.confirm" = "Confirm"; +"create_wallet.passphrase_description" = "Think of passphrases as your wallet's secret handshake. If you ever need to bring back a lost wallet, you'll need the recovery phrase and this passphrase. Plus, with just one mnemonic, you can unlock a bunch of multi-coin wallets with different passphrases. Neat, right?"; +"create_wallet.error.empty_passphrase" = "Oops! Looks like you missed the passphrase. Please fill it in."; +"create_wallet.error.forbidden_symbols" = "Hold on there, cowboy! Let's stick to the symbols we know and love::A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"create_wallet.error.invalid_confirmation" = "Hmm, your passphrase confirmations aren't matching. Let's try that again."; // Restore Select -"restore_select.title" = "Выберите блокчейны"; +"restore_select.title" = "Choose Blockchains"; // Lock Info "lock_info.title" = "TimeLock"; -"lock_info.text" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; +"lock_info.text" = "Heads up! The sender locked these funds for a bit. The lock will lift on the indicated date. \n\nRest assured, those Bitcoins are already in your pocket. You'll just need to wait until the lock"; // Double Spend Info -"double_spend_info.title" = "Двойная трата"; -"double_spend_info.header" = "Риск двойной траты! Существует другая транзакция в блокчейне, которая пытается использовать входные данные, используемые в этой транзакции. Только одна транзакция будет принята сетью"; -"double_spend_info.this_hash" = "Эта транзакция"; -"double_spend_info.conflicting_hash" = "Конфликтующая транзакция"; +"double_spend_info.title" = "Double Spend"; +"double_spend_info.header" = "Double Spend Risk! There is another transaction on the blockchain that is trying to spend inputs used in this transaction. Only one transaction will be accepted by the network"; +"double_spend_info.this_hash" = "This Tx"; +"double_spend_info.conflicting_hash" = "Conflicting Tx"; // Relative Date -"timestamp.days_ago" = "%luд назад"; -"timestamp.hours_ago" = "%luч назад"; -"timestamp.min_ago" = "%luмин назад"; +"timestamp.days_ago" = "%lud ago"; +"timestamp.hours_ago" = "%luh ago"; +"timestamp.min_ago" = "%lum ago"; // Intro -"intro.unchain_assets.title" = "Непривязанные активы"; -"intro.unchain_assets.description" = "Распоряжайтесь активами свободно и без ограничений"; -"intro.go_borderless.title" = "Избавьтесь от границ"; -"intro.go_borderless.description" = "Преодолейте барьеры и получите доступ к рынкам"; -"intro.stay_private.title" = "Сохраняйте конфиденциальность"; -"intro.stay_private.description" = "Не допускайте утечку личных и финансовых данных"; +"intro.unchain_assets.title" = "Break Those Chains!"; +"intro.unchain_assets.description" = "Why cage yourself in? And don’t let any losers do that to you. Be free!"; +"intro.go_borderless.title" = "No Walls Here!"; +"intro.go_borderless.description" = "Skip those silly barriers. Access markets everywhere, like a boss!"; +"intro.stay_private.title" = "Keep it Secret, Keep it Safe!"; +"intro.stay_private.description" = "Why broadcast your business? Keep your secrets, just like I do!"; // Guides -"guides.tab_bar_item" = "Академия"; -"guides.title" = "Академия"; +"guides.tab_bar_item" = "Academy"; +"guides.title" = "Academy"; // Add Token -"add_token.title" = "Добавить токен"; +"add_token.title" = "Add Token"; "add_token.blockchain" = "Blockchain"; -"add_token.already_added" = "Этот токен уже есть в списке"; -"add_token.invalid_contract_address" = "Неверный адрес контракта"; -"add_token.invalid_bep2_symbol" = "Неверный символ BEP2"; -"add_token.contract_address_not_found" = "Адрес контракта не найден в %@ блокчейне "; -"add_token.bep2_symbol_not_found" = "Символ BEP2 не найден"; -"add_token.input_placeholder.contract_address" = "Адрес контракта"; -"add_token.input_placeholder.bep2_symbol" = "Символ BEP2"; -"add_token.coin_name" = "Название токена"; -"add_token.symbol" = "Символ"; -"add_token.decimals" = "Десятичные знаки"; +"add_token.already_added" = "This coin? It's already on the A-list, like me in the world of real estate. No need to add it again, folks!"; +"add_token.invalid_contract_address" = "Invalid contract address"; +"add_token.invalid_bep2_symbol" = "Invalid BEP2 symbol"; +"add_token.contract_address_not_found" = "Contract address not found in %@ blockchain"; +"add_token.bep2_symbol_not_found" = "BEP2 symbol not found"; +"add_token.input_placeholder.contract_address" = "Contract Address"; +"add_token.input_placeholder.bep2_symbol" = "BEP2 Symbol"; +"add_token.coin_name" = "Coin Name"; +"add_token.symbol" = "Symbol"; +"add_token.decimals" = "Decimals"; // Wallet Connect "wallet_connect.title" = "WalletConnect"; -"wallet_connect.error.invalid_url" = "Неверный URL-адрес"; +"wallet_connect.error.invalid_url" = "Invalid URL Address"; "wallet_connect.url" = "URL"; -"wallet_connect.active_account" = "Активный Кошелек"; -"wallet_connect.address" = "Адрес"; -"wallet_connect.network" = "Сеть"; -"wallet_connect.address" = "Адрес"; -"wallet_connect.network" = "Сеть"; -"wallet_connect.list.pending_requests" = "Ожидающие запросы"; -"wallet_connect.main.no_any_supported_chains" = "Нет поддерживаемых блокчейнов!"; -"wallet_connect.main.unsupported_chains" = "Некоторые блокчейны не поддерживаются!"; -"wallet_connect.connect_description" = "Нажимая \"Подтвердить\", вы разрешаете этому приложению просматривать ваш общедоступный адрес. Это важный шаг для защиты ваших данных от потенциальных рисков фишинга."; -"wallet_connect.usage_description" = "Вы можете перейти в браузер. Не закрывайте эту страницу во время использования браузера."; -"wallet_connect.no_connection" = "Не удалось установить соединение. Попробуйте переподключиться снова."; -"wallet_connect.button_reconnect" = "Повторное подключение"; -"wallet_connect.button_disconnect" = "Отсоединить"; -"ethereum_transaction.error.title" = "Ошибка"; -"ethereum_transaction.error.insufficient_balance" = "Для отправки транзакции требуется %@."; -"ethereum_transaction.error.insufficient_balance_with_fee" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; -"ethereum_transaction.error.lower_than_base_gas_limit" = "Выбранное значение комиссии слишком низкое и будет отклонено!"; -"ethereum_transaction.error.nonce_already_in_block" = "Транзакция уже в блоке!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Недостаточно комиссии для замены транзакции"; -"ethereum_transaction.error.transaction_underpriced" = "Комиссия недостаточна для отправки транзакции"; -"ethereum_transaction.error.tips_higher_than_max_fee" = "Максимальное вознаграждение не может быть меньше чаевых, так как максимальное вознаграждение включает чаевые."; -"ethereum_transaction.error.reverted" = "Транзакция не может быть выполнена: %@"; -"wallet_connect.request_title" = "Вызов контракта"; -"wallet_connect.button.confirm" = "Подтвердить"; -"wallet_connect.sign.request_title" = "Запрос на подпись"; -"wallet_connect.sign.domain" = "Домен"; +"wallet_connect.active_account" = "Active Wallet"; +"wallet_connect.address" = "Address"; +"wallet_connect.network" = "Network"; +"wallet_connect.list.pending_requests" = "Pending Requests"; +"wallet_connect.main.unsupported_chains" = "Some of these chains? Very low energy. Not supported!"; +"wallet_connect.connect_description" = "Hit that approve, and you're letting the app see your VIP address. We're doing this, folks, to make security great again and keep the phishers out!"; +"wallet_connect.usage_description" = "You can head over to the browser, but remember - keep this page open. It's the best page!"; +"wallet_connect.no_connection" = "Couldn't make the connection. Probably the system's fault. Try again and let's win this!"; +"wallet_connect.button_reconnect" = "Reconnect"; +"wallet_connect.button_disconnect" = "Disconnect"; +"ethereum_transaction.error.title" = "Error"; +"ethereum_transaction.error.insufficient_balance" = "Hey, we need more %@ to send this. Not enough funds, folks!"; +"ethereum_transaction.error.insufficient_balance_with_fee" = "Your %@ balance? Too low! Can't cover the transaction and the tiny fee!"; +"ethereum_transaction.error.lower_than_base_gas_limit" = "You're offering peanuts for the fee! Too low, it'll be shown the door!"; +"ethereum_transaction.error.nonce_already_in_block" = "This transaction? Already in the block! Done and dusted!"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Trying to replace? But your fee? Way too cheap! Up the ante!"; +"ethereum_transaction.error.transaction_underpriced" = "Your fee's too low! It's like trying to buy Mar-a-Lago with pocket change!"; +"ethereum_transaction.error.tips_higher_than_max_fee" = "Tips are rolling in higher than the max fee! Remember, the max fee counts those tips too!"; +"ethereum_transaction.error.reverted" = "Can't do the transaction! Here's why: %@"; +"wallet_connect.request_title" = "Contract Call"; +"wallet_connect.button.confirm" = "Confirm"; +"wallet_connect.sign.request_title" = "Sign Request"; +"wallet_connect.sign.domain" = "Domain"; "wallet_connect.sign.dapp_name" = "dApp"; -"wallet_connect.sign.message" = "Сообщение для подписи"; +"wallet_connect.sign.message" = "Message to sign"; "wallet_connect_list.title" = "WalletConnect"; -"wallet_connect.list.empty_view_text" = "Нет активных сессий"; +"wallet_connect.list.empty_view_text" = "No active sessions"; -"wallet_connect.list.pairings" = "Соединения"; -"wallet_connect.list.version_text" = "Версия %@"; -"wallet_connect.list.v1_bottom_text" = "В первой версии WalletConnect вы должны войти в сессии, чтобы увидеть и подтвердить запрос."; -"wallet_connect_list.new_connection" = "Новое соединение"; -"wallet_connect_list.disconnecting" = "Отсоединение"; +"wallet_connect.list.pairings" = "Pairings"; +"wallet_connect.list.version_text" = "Version %@"; +"wallet_connect.list.v1_bottom_text" = "In the first version of WalletConnect, you must go into sessions to see and confirm the request"; +"wallet_connect_list.new_connection" = "New Connection"; +"wallet_connect_list.disconnecting" = "Disconnecting"; -"wallet_connect.no_account.description" = "Вам нужно создать или импортировать кошелек перед использованием WalletConnect."; -"wallet_connect.unbackuped_account.description" = "Вам нужно сделать резервную копию %@ перед использованием WalletConnect"; +"wallet_connect.no_account.description" = "Before you jump into WalletConnect, you've gotta either build a wallet or bring one in, just like building or buying a beautiful hotel!"; +"wallet_connect.unbackuped_account.description" = "Hey, you've gotta backup %@ before using WalletConnect. Protect it, just like I protect my Twitter account!"; +"wallet_connect.non_supported_account.description" = "That wallet of yours, the %@ type? Not a fan of WalletConnect, sorry folks!"; +"wallet_connect.non_supported_account.switch" = "Switch"; -"wallet_connect.non_supported_account.description" = "Ваш текущий тип кошелька %@ не поддерживает WalletConnect"; -"wallet_connect.non_supported_account.switch" = "Перекл."; - -"wallet_connect.pending_requests_title" = "Ожидающие запросы"; -"wallet_connect.paired_dapps.title" = "Соединеные dApp"; -"wallet_connect.paired_dapps.cant_disconnect" = "Не удалось отключить"; -"wallet_connect.paired_dapps.disconnect_all" = "Удалить всё"; -"wallet_connect.pending_requests.nonactive_footer" = "Чтобы открыть запрос, необходимо активировать желаемый кошелек"; +"wallet_connect.pending_requests_title" = "Pending Requests"; +"wallet_connect.paired_dapps.title" = "Paired dApps"; +"wallet_connect.paired_dapps.cant_disconnect" = "Can't Disconnect"; +"wallet_connect.paired_dapps.disconnect_all" = "Delete All"; +"wallet_connect.pending_requests.nonactive_footer" = "To kickstart a request, you gotta turn on the wallet you want. Activate it like you mean it, folks!"; // App Status -"app_status.title" = "Статус приложения"; -"app_status.application_status" = "Статус приложения"; -"app_status.linked_wallets" = "Связанные кошельки"; -"app_status.version_history" = "История версий"; -"app_status.blockchain_status" = "Статус блокчейна"; +"app_status.title" = "App Status"; +"app_status.application_status" = "Application status"; +"app_status.linked_wallets" = "Linked Wallets"; +"app_status.version_history" = "Version History"; +"app_status.blockchain_status" = "Blockchain status"; // FAQ @@ -1420,324 +1414,317 @@ // Status Info -"status_info.title" = "Статус"; -"status_info.pending.title" = "В обработке"; -"status_info.pending.content" = "Транзакция еще не подтверждена в блокчейне. Транзакции, отправленные с рекомендованной или более высокой комиссией, обычно обрабатываются в течение минут. Транзакции, отправленные с низкой комиссией, могут обрабатываться в течение нескольких часов или дней и даже отклоняться. Учтите, что обновление статуса каждой транзакции, отображаемого в интерфейсе кошелька %@, обычно происходит с небольшой задержкой. "; -"status_info.processing.title" = "В процессе"; -"status_info.processing.content" = "Транзакция уже включена в блокчейн, но пока не обработана. На данном этапе транзакции считаются завершенными для небольших платежей. Что касается завершения обработки более крупных транзакций, стоит дождаться смены статуса."; -"status_info.completed.title" = "Завершено"; -"status_info.confirmed.content" = "Транзакция завершена и считается постоянной и необратимой."; -"status_info.failed.title" = "Не удалось"; -"status_info.failed.content" = "Транзакция не была обработана и передача средств не произошла. В зависимости от причины неудачи, некоторые неудачные транзакции могут использовать комиссию за транзакцию. Транзакции, которые были заменены или отменены другой транзакцией, не потребляют транзакционные сборы и также будут показаны как \"не удалось\". Приложение %@ не может показать причину неудачных транзакций, но пользователи могут посмотреть это в etherscan.io."; +"status_info.title" = "Status"; +"status_info.pending.title" = "Pending"; +"status_info.pending.content" = "Your transaction? It's like waiting for an applause at one of my rallies - coming but not here yet. If you paid a good fee, it'll be there in a jiffy. But if you went low-ball, might take longer - days even! And it could be shown the exit door. Remember, updates on the %@ wallet might lag a tad."; +"status_info.processing.title" = "Getting Things Done"; +"status_info.processing.content" = "Your transaction? It's in the blockchain but not set in stone. For small deals, you can consider it done. But for the big league stuff, you might wanna hold your horses till it's totally completed."; +"status_info.completed.title" = "Victory!"; +"status_info.confirmed.content" = "It's a done deal! That transaction? Set in stone and it's not going back."; +"status_info.failed.title" = "Oops!"; +"status_info.failed.content" = "Didn't work out this time - like trying to get a Democrat to agree with me! Some slip-ups might still cost you fees. If another transaction nudged yours out or if it was canceled, no fees, but still an 'Oops'. The %@ app can't tell you why it failed, but you can play detective on block explorers like etherscan.io."; // Onboarding -"onboarding.balance.create" = "Новый кошелек"; -"onboarding.balance.import" = "Импорт кошелька"; -"onboarding.balance.watch" = "Просмотр кошелька"; +"onboarding.balance.create" = "New Wallet"; +"onboarding.balance.import" = "Import Wallet"; +"onboarding.balance.watch" = "Watch Wallet"; // Manage Accounts -"manage_accounts.n_words" = "%@ слов"; -"manage_accounts.n_words_with_passphrase" = "%@ слов с кодовой фразой"; +"manage_accounts.n_words" = "%@ words"; +"manage_accounts.n_words_with_passphrase" = "%@ words with passphrase"; // Manage Account -"manage_account.name" = "Название"; -"manage_account.recovery_phrase" = "Фраза восстановления"; -"manage_account.public_keys" = "Публичные ключи"; -"manage_account.private_keys" = "Приватные ключи"; -"manage_account.backup_recovery_phrase" = "Ручное резервное копирование"; -"manage_account.cloud_backup_recovery_phrase" = "Резерв. копирование в iCloud"; -"manage_account.cloud_delete_backup_recovery_phrase" = "Удалить резерв. копию из iCloud"; -"manage_account.manual_backup_required" = "Требуется резервное копирование вручную"; -"manage_account.manual_backup_required.description" = "Чтобы безопасно удалить резервную копию в iCloud, вам необходимо сначала сделать резервную копию фразы восстановления вручную."; -"manage_account.manual_backup_required.button" = "Сделать резервную копию"; -"manage_account.unlink" = "Отключить кошелек"; -"manage_account.backup.no_backup_yet_description" = "Завершите одну из опций резервного копирования кошелька, чтобы начать использовать кошелек."; -"manage_account.backup.has_backup_description" = "Рекомендуется создать ручную резервную копию для каждого кошелька."; - -"manage_account.cloud_delete_backup_recovery_phrase.description" = "Вы уверены, что хотите удалить резервную копию кошелька из iCloud?"; +"manage_account.name" = "Name"; +"manage_account.recovery_phrase" = "Recovery Phrase"; +"manage_account.public_keys" = "Public Keys"; +"manage_account.private_keys" = "Private Keys"; +"manage_account.backup_recovery_phrase" = "Manual Backup"; +"manage_account.cloud_backup_recovery_phrase" = "Backup to iCloud"; +"manage_account.cloud_delete_backup_recovery_phrase" = "Delete Backup from iCloud"; +"manage_account.manual_backup_required" = "You Need a Personal Backup!"; +"manage_account.manual_backup_required.description" = "Before you throw out the iCloud backup, be smart and write down your recovery phrase. It's like insurance for your wallet!"; +"manage_account.manual_backup_required.button" = "Do the Backup!"; +"manage_account.unlink" = "Cut Ties with Wallet"; +"manage_account.backup.no_backup_yet_description" = "Want to use the wallet? First, pick a way to back it up, just like you'd have a backup plan for a business deal."; +"manage_account.backup.has_backup_description" = "Always have a personal backup for each wallet. It's just good sense!"; +"manage_account.cloud_delete_backup_recovery_phrase.description" = "Are you sure you want to delete your wallet backup from iCloud?"; + // Manage Account -> Public Keys -"public_keys.title" = "Публичные ключи"; -"public_keys.evm_address" = "EVM адрес"; -"public_keys.evm_address.description" = "Позволяет отслеживать кошельки с активами на Ethereum, Binance Smart Chain и других блокчейнах на базе EVM, только для чтения."; -"public_keys.account_extended_public_key" = "Account Extended Public Key"; -"public_keys.account_extended_public_key.description" = "Позволяет осуществлять мониторинг кошельков, содержащих Bitcoin и другие криптовалюты на основе UTXO (например, Litecoin, Bitcoin Cash, Dash и т.д.), только для чтения."; +"public_keys.title" = "Public Keys"; +"public_keys.evm_address" = "EVM Address"; +"public_keys.evm_address.description" = "With this, you can peek (just peek!) at wallets on Ethereum, Binance Smart Chain, and those other EVM blockchains. Only the best blockchains!"; +"public_keys.account_extended_public_key" = "The Ultra Public Key for Accounts"; +"public_keys.account_extended_public_key.description" = "This lets you have a little sneak peek (read-only, of course!) at wallets holding the big ones like Bitcoin, and the others like Litecoin, Bitcoin Cash, Dash, and more. It's huge!"; // Manage Account -> Private Keys -"private_keys.title" = "Приватные ключи"; -"private_keys.evm_private_key" = "Приватный Ключ EVM"; -"private_keys.evm_private_key.description" = "Предоставляет полный контроль над криптовалютами на базе EVM, т.е. Ethereum, Binance Smart Chain и т.д. в пределах соответствующего кошелька."; -"private_keys.bip32_root_key" = "BIP32 Root Key"; -"private_keys.bip32_root_key.description" = "Предоставляет полный контроль над активами на соответствующем кошельке."; -"private_keys.account_extended_private_key" = "Account Extended Private Key"; -"private_keys.account_extended_private_key.description" = "Предоставляет полный контроль над Bitcoin и другими криптовалютами на базе UTXO, такими как Litecoin, Bitcoin Cash, Dash и т.д. в пределах соответствующего кошелька."; +"private_keys.title" = "Private Keys"; +"private_keys.evm_private_key" = "EVM Private Key"; +"private_keys.evm_private_key.description" = "You've got the golden key! Total power over EVM cryptos like Ethereum and Binance Smart Chain in the wallet. It's like owning the Trump Tower of crypto!"; +"private_keys.bip32_root_key" = "The Ultimate BIP32 Key"; +"private_keys.bip32_root_key.description" = "This key? It's yuge! Complete command over everything in the wallet. It's the Art of the Deal of keys!"; +"private_keys.account_extended_private_key" = "The Super Private Key for Accounts"; +"private_keys.account_extended_private_key.description" = "With this, you're the boss of Bitcoin and those other coins like Litecoin, Bitcoin Cash, Dash, and more in the wallet. It's like being the president of cryptos!"; // Manage Account -> EVM Address -"evm_address.title" = "EVM адрес"; +"evm_address.title" = "EVM Address"; // Birthday Height -"birthday_height.title" = "Блок создания"; +"birthday_height.title" = "Birthday Height"; // Birthday Input -"birthday_input.title" = "Блок создания"; -"birthday_input.description" = "Введите блок создания кошелька для ускорения синхронизации."; -"birthday_input.new_wallet" = "Новый кошелек"; -"birthday_input.new_wallet.description" = "Нет транзакций"; -"birthday_input.old_wallet" = "Существующий кошелек"; -"birthday_input.old_wallet.description" = "Есть транзакции"; -"birthday_input.input_placeholder" = "%@ (опционально)"; +"birthday_input.title" = "Birthday Height"; +"birthday_input.description" = "Enter wallet's birthday height for faster synchronization."; +"birthday_input.new_wallet" = "New Wallet"; +"birthday_input.new_wallet.description" = "Doesn't have any transactions"; +"birthday_input.old_wallet" = "Existing Wallet"; +"birthday_input.old_wallet.description" = "Has transactions"; +"birthday_input.input_placeholder" = "%@ (optional)"; -"restore_setting.birthday_height" = "%@ Блок создания"; -"restore_setting.download.disclaimer" = "Первоначальная синхронизация с блокчейном может потреблять много интернет-трафика."; +"restore_setting.birthday_height" = "%@ Birthday Height"; +"restore_setting.download.disclaimer" = "The initial synchronization with the blockchain can consume a lot of internet traffic."; // EVM Network -"evm_network.rpc_source" = "Источник RPC"; -"evm_network.added" = "Добавлен"; -"evm_network.add_new" = "Добавить новый"; +"evm_network.rpc_source" = "RPC Source"; +"evm_network.added" = "Added"; +"evm_network.add_new" = "Add New"; // Add RPC Source -"add_evm_sync_source.title" = "Добавить источник RPC"; -"add_evm_sync_source.name" = "Название"; +"add_evm_sync_source.title" = "Add RPC Source"; +"add_evm_sync_source.name" = "Name"; "add_evm_sync_source.rpc_url" = "RPC URL"; -"add_evm_sync_source.basic_auth" = "Базовая авторизация (опц.)"; -"add_evm_sync_source.warning.url_exists" = "RPC Источник с таким url уже существует"; -"add_evm_sync_source.error.invalid_url" = "Введенный url неверен. Допустимый url должен иметь одну из следующих схем: http, https, ws, wss"; - +"add_evm_sync_source.basic_auth" = "Basic Auth (optional)"; +"add_evm_sync_source.warning.url_exists" = "This RPC Source URL? It's already in the club. No duplicates, folks!"; +"add_evm_sync_source.error.invalid_url" = "The URL you punched in? Not good! Make sure it starts with https or wss, otherwise it's a no-go. It's basic URL etiquette!"; // Send Settings -"evm_send_settings.nonce" = "Nonce транзакции"; -"evm_send_settings.nonce.info" = "Nonce - это уникальное целочисленное значение для транзакции в кошельке пользователя. Обычно оно увеличивается с каждой транзакцией и не нуждается в изменении. Продвинутые пользователи могут установить его равным nonce отложенной транзакции, чтобы отменить и заменить эту транзакцию, при условии, что новая транзакция имеет достаточно большую плату, чтобы старая не была подтверждена вместо нее (например, они могут захотеть ускорить ее подтверждение или полностью изменить параметры транзакции). Когда несколько ожидающих транзакций имеют одинаковый nonce, подтверждается только одна, обычно с наибольшей комиссией. -"; -"evm_send_settings.nonce.errors.already_in_use" = "Использованый Nonce"; -"evm_send_settings.nonce.errors.already_in_use.info" = "Выполненная транзакция с таким nonce уже существует."; - -"fee_settings" = "Доп. настройки"; -"fee_settings.fee" = "Комиссия"; -"fee_settings.fee.info" = "Блокчейн требует от пользователей оплаты сетевых сборов при отправке транзакций. Эти сборы выше, когда в сети происходит много транзакций."; -"fee_settings.fee_rate" = "Комиссия"; -"fee_settings.fee_rate.description" = "Это рекомендуемое значение для попадания в следующие 2 блока"; -"fee_settings.inputs_outputs" = "Вводы/Выводы"; -"fee_settings.transaction_settings" = "Настройки транзакций"; -"fee_settings.transaction_settings.description" = "Затрудните отслеживание Биткойн-транзакций, изменив их структурирование."; -"fee_settings.time_lock" = "TimeLock"; -"fee_settings.time_lock.description" = "TimeLock работает только для отправки на адреса BIP44 (начинаются с 1) -"; - -"fee_settings.network_fee" = "Комиссия сети"; -"fee_settings.network_fee.info" = "Ориентировочная стоимость отправки данной транзакции по сети."; +"evm_send_settings.nonce" = "Transaction Nonce"; +"evm_send_settings.nonce.info" = "The nonce? Think of it as a unique badge number for your transactions. Usually, it goes up one by one, like my ratings! But hey, if you're a smart cookie, you can use it to swap out a slow transaction with a faster one. Just make sure you're willing to pay top dollar in fees. Because, remember, if two transactions have the same badge number, only the one that's willing to tip the most gets the VIP treatment!";"evm_send_settings.nonce.errors.already_in_use" = "Used Nonce"; +"evm_send_settings.nonce.errors.already_in_use.info" = "An executed transaction with this nonce already exists."; + +"fee_settings" = "Advanced"; +"fee_settings.fee" = "Fee"; +"fee_settings.fee.info" = "Think of blockchains like a fancy club in Manhattan. When it's crowded, you gotta pay a premium to get in. So, when there's a lot of action on the network, fees go up. Big league!"; +"fee_settings.fee_rate" = "Fee Rate"; +"fee_settings.fee_rate.description" = "Here is the recommended value for hitting the next 2 blocks"; +"fee_settings.inputs_outputs" = "Inputs/Outputs"; +"fee_settings.transaction_settings" = "Transaction Settings"; +"fee_settings.transaction_settings.description" = "Want to move your Bitcoin with some mystery, like a billionaire in disguise? Tweak how the transactions are done, and you'll be harder to track!"; +"fee_settings.time_lock" = "Time Lock"; +"fee_settings.time_lock.description" = "TimeLock works only for sending to BIP44 addresses (starting with 1)"; + +"fee_settings.network_fee" = "Network Fee"; +"fee_settings.network_fee.info" = "The estimated cost of sending given transaction on the network."; "fee_settings.gas_limit" = "Gas Limit"; -"fee_settings.gas_limit.info" = "Единица сложности транзакций - газ, она варьирует в зависимости от выполняемого смарт-контракта. Лимит газа - это предполагаемый максимальный размер газа, необходимый для выполнения смарт-контракта. Обычно используется меньшее количество газа."; - +"fee_settings.gas_limit.info" = "You know, transactions have their own kind of 'energy', and they call it 'gas'. Think of the Gas Limit as the biggest energy drink you'd need for a transaction. But usually, you'll sip less than what you think."; "fee_settings.gas_price" = "Gas Price"; -"fee_settings.gas_price.info" = "Комиссия за сетевую транзакцию измеряется в единицах газа. Цена газа – это сумма, которую пользователь готов заплатить за единицу газа. Когда сеть занята, цены на газ повышаются, а во время простоя они наоборот снижаются. Слишком низкая цена газа зачастую является основанием для длительного сохранения отложенного статуса транзакции."; - -"fee_settings.base_fee" = "Базовая комиссия"; -"fee_settings.base_fee.info" = "Сетевой протокол определяет базовую стоимость газа для каждого блока, которая называется базовая ставка комиссии. Он варьирует в зависимости от уровня загрузки сети от блока к блоку. В следующем блоке он может увеличиться или уменьшиться не более чем на 12.5%, что делает взимаемый тариф более предсказуемым. Здесь отражена базовая ставка комиссии текущего блока."; -"fee_settings.max_fee_rate" = "Макс. ставка комиссии"; -"fee_settings.max_fee_rate.info" = "Это максимальная общая цена газа, которую пользователь готов заплатить. Она должна покрывать базовую тарифную ставку сети и максимальный приоритетный тариф. Указанное здесь значение предлагается на основе оценки суммы базовой тарифной ставки следующего блока и максимального приоритетного тарифа, выбранного пользователем. Фактический размер платёжного тарифа обычно меньше. Снижение настройки текущего базового тарифа ограничит оплачиваемую сумму тарифа, но приведет к удлинению периода ожидания подтверждения транзакции или даже к ее подвисанию."; -"fee_settings.tips" = "Макс. приоритет. комиссия"; -"fee_settings.tips.info" = "Пользователи платят приоритетную комиссию, в целях ускорения процесса подтверждения транзакции. Иногда эти тарифы называются чаевыми. Максимальный размер приоритетного сбора - это максимальная дополнительная цена за газ, которую пользователь готов оплатить поверх базовой ставки комиссии. Приведенная здесь стоимость определяется исходя из прогнозируемых сетевых условий. Фактический размер приоритетного тарифа будет меньше. Обнуление этого тарифа может привести к удлинению периода ожидания подтверждения транзакции ввиду её размещения в конце очереди незавершенных транзакций ото всех пользователей."; - -"fee_settings.errors.insufficient_balance" = "Недостаточный баланс"; -"fee_settings.errors.unexpected_error" = "Непредвиденная ошибка"; -"fee_settings.errors.insufficient_balance.info" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; -"fee_settings.errors.low_max_fee" = "Низкая комиссия"; -"fee_settings.errors.low_max_fee.info" = "Установленная сумма комиссии недостаточно для обработки этой транзакции."; -"fee_settings.errors.nonce_already_in_block" = "Не удается заменить транзакцию"; -"fee_settings.errors.replacement_transaction_underpriced" = "Низкая комиссия на замену транзакции"; -"fee_settings.errors.transaction_underpriced" = "Низкая комиссия за транзакцию"; -"fee_settings.errors.tips_higher_than_max_fee" = "Слишком низкая макс. комиссия"; -"fee_settings.errors.zero_amount.info" = "Невозможно отправить 0 TRX"; -"fee_settings.warning.risk_of_getting_stuck" = "Риски"; -"fee_settings.warning.risk_of_getting_stuck.info" = "Обработка транзакции может отложиться на некоторое время, или транзакцию могут полностью отклонить."; -"fee_settings.warning.overpricing" = "Слишком высокая комиссия"; -"fee_settings.warning.overpricing.info" = "Установленная комиссия за транзакцию больше чем требуется для обработки этой транзакции."; +"fee_settings.gas_price.info" = "So here's the deal: doing business on the network costs 'gas'. The Gas Price? It's like your bid at an auction - how much you're willing to drop for each gas unit. If the network's buzzing like one of my rallies, prices soar. When it's calm, they dip. And hey, if you're stingy on the gas price, don't be surprised if your transaction hangs around longer than expected!"; +"fee_settings.base_fee" = "Base Fee"; +"fee_settings.base_fee.info" = "So, every block has a ticket price set by the big network – they call it the base fee rate. Think of it as a fluctuating stock; sometimes up, sometimes down, but never more than 12.5% change. This ticket price changes based on how packed the network is. The number you're seeing? That's the current rate. Very transparent!"; +"fee_settings.max_fee_rate" = "Top Dollar Rate"; +"fee_settings.max_fee_rate.info" = "This is your ceiling - the max you're willing to spend per gas unit. It's gotta cover the basic fee and the special priority fee. The number here? It's a clever estimate, but usually, you'll end up spending less. Cut corners on this and you might be waiting a long time, or even get your transaction stuck in limbo!"; + +"fee_settings.tips" = "The Big Tipper's Fee"; +"fee_settings.errors.insufficient_balance" = "Running on empty!"; +"fee_settings.errors.unexpected_error" = "Whoops, something went Trump-sized wrong!"; +"fee_settings.errors.insufficient_balance.info" = "Listen, your %@ stash is looking a bit thin for this transaction, especially when we throw in the fee."; +"fee_settings.errors.low_max_fee" = "Your fee's too puny!"; +"fee_settings.errors.low_max_fee.info" = "That fee you set? Way too low for the big league transaction you're trying to do!"; +"fee_settings.errors.nonce_already_in_block" = "Too late, already made the move!"; +"fee_settings.errors.replacement_transaction_underpriced" = "Trying to switch things up? Your fee's still too skimpy!"; +"fee_settings.errors.transaction_underpriced" = "That's a bargain-bin fee!"; +"fee_settings.errors.tips_higher_than_max_fee" = "Your max fee? It's not gonna cut it!"; +"fee_settings.errors.zero_amount.info" = "You can't just send thin air!"; +"fee_settings.warning.risk_of_getting_stuck" = "Living on the edge!"; +"fee_settings.warning.risk_of_getting_stuck.info" = "This might take a while... or, who knows, might not even happen at all!"; +"fee_settings.warning.overpricing" = "Throwing your money away!"; +"fee_settings.warning.overpricing.info" = "Your fee is looking more lavish than Mar-a-Lago! Way more than what you need for this."; // Watch Address -"watch_address.title" = "Просмотр кошелька"; -"watch_address.watch" = "Далее"; -"watch_address.address" = "Адрес"; -"watch_address.by" = "Из"; -"watch_address.watch_by" = "Смотреть из"; -"watch_address.evm_address" = "EVM адрес"; -"watch_address.tron_address" = "TRON адрес"; +"watch_address.title" = "Watch Wallet"; +"watch_address.watch" = "Next"; +"watch_address.address" = "Address"; +"watch_address.by" = "By"; +"watch_address.watch_by" = "Watch By"; +"watch_address.evm_address" = "EVM Address"; +"watch_address.tron_address" = "TRON Address"; "watch_address.public_key" = "Account xPubKey"; -"watch_address.public_key.placeholder" = "Введите Account Extended Public Key"; -"watch_address.public_key.invalid_key" = "Неверный ключ"; -"watch_address.choose_blockchain" = "Выбрать блокчейн"; -"watch_address.choose_coin" = "Выберите токен"; +"watch_address.public_key.placeholder" = "Enter Account Extended Public Key "; +"watch_address.public_key.invalid_key" = "Invalid Key"; +"watch_address.choose_blockchain" = "Choose Blockchain"; +"watch_address.choose_coin" = "Choose Coin"; // Nft Collections -"nft_collections.title" = "NFT"; -"nft_collections.price_mode" = "Режим цены"; -"nft_collections.last_sale" = "Последняя продажа"; -"nft_collections.average_7d" = "Средн. 7Д"; -"nft_collections.average_30d" = "Средн. 30Д"; -"nft_collections.on_sale" = "В продаже"; -"nft_collections.empty" = "В вашем кошельке нет NFT"; +"nft_collections.title" = "NFTs"; +"nft_collections.price_mode" = "Price Mode"; +"nft_collections.last_sale" = "Last Sale"; +"nft_collections.average_7d" = "Average 7D"; +"nft_collections.average_30d" = "Average 30D"; +"nft_collections.on_sale" = "On Sale"; +"nft_collections.empty" = "You don’t have any NFTs in your wallet"; -"top_nft_collections.title" = "Топ NFT коллекции"; -"top_nft_collections.description" = "Ведущие коллекции NFT по объему торгов."; +"top_nft_collections.title" = "Top NFT Collections"; +"top_nft_collections.description" = "Leading NFT collections by trading volume."; // Nft Asset -"nft_asset.tab.overview" = "Обзор"; -"nft_asset.tab.activity" = "Активность"; - -"nft_asset.last_sale" = "Последняя продажа"; -"nft_asset.average_7d" = "Среднее за 7 дней"; -"nft_asset.average_30d" = "Среднее за 30 дней"; -"nft_asset.floor_price" = "Минимальная цена"; -"nft_asset.on_sale" = "В продаже"; -"nft_asset.on_auction" = "На аукционе"; -"nft_asset.until_date" = "до %@"; -"nft_asset.buy_now" = "Купить сейчас"; -"nft_asset.minimum_bid" = "Мин. cтавка"; -"nft_asset.best_offer" = "Лучшее предложение"; -"nft_asset.properties" = "Параметры"; -"nft_asset.description" = "Описание"; -"nft_asset.details" = "Подробности"; -"nft_asset.details.contract_address" = "Адрес контракта"; -"nft_asset.details.token_id" = "ID токена"; -"nft_asset.details.token_standard" = "Стандартный токен"; +"nft_asset.tab.overview" = "Overview"; +"nft_asset.tab.activity" = "Activity"; + +"nft_asset.last_sale" = "Last Sale"; +"nft_asset.average_7d" = "7 Day Average"; +"nft_asset.average_30d" = "30 Day Average"; +"nft_asset.floor_price" = "Floor Price"; +"nft_asset.on_sale" = "On Sale"; +"nft_asset.on_auction" = "On Auction"; +"nft_asset.until_date" = "until %@"; +"nft_asset.buy_now" = "Buy Now"; +"nft_asset.minimum_bid" = "Minimum Bid"; +"nft_asset.best_offer" = "Best Offer"; +"nft_asset.properties" = "Properties"; +"nft_asset.description" = "Description"; +"nft_asset.details" = "Details"; +"nft_asset.details.contract_address" = "Contract Address"; +"nft_asset.details.token_id" = "Token ID"; +"nft_asset.details.token_standard" = "Token Standard"; "nft_asset.details.blockchain" = "Blockchain"; -"nft_asset.links" = "Ссылки"; -"nft_asset.links.website" = "Веб-сайт"; -"nft_asset.options.save_to_photos" = "Сохранить в фото"; -"nft_asset.options.set_as_watch_face" = "Установить как Watch Face"; -"nft_asset.save_to_photos.failed" = "Не удалось сохранить NFT-изображение"; +"nft_asset.links" = "Links"; +"nft_asset.links.website" = "Website"; +"nft_asset.options.save_to_photos" = "Save to Photos"; +"nft_asset.options.set_as_watch_face" = "Set as Watch Face"; +"nft_asset.save_to_photos.failed" = "Failed to save NFT image"; // Nft Collection -"nft_collection.tab.overview" = "Обзор"; -"nft_collection.tab.assets" = "Элементы"; -"nft_collection.tab.activity" = "Активность"; - -"nft_collection.overview.description" = "Описание"; -"nft_collection.overview.contracts" = "Контракты"; -"nft_collection.overview.links" = "Ссылки"; -"nft_collection.overview.links.website" = "Веб-сайт"; - - -"nft_collection.overview.owners" = "Владельцы"; -"nft_collection.overview.items" = "Элементы"; -"nft_collection.overview.24h_volume" = "Объем торгов (24ч)"; -"nft_collection.overview.today_sellers" = "Продавцы сегодня"; -"nft_collection.overview.floor_price" = "Минимальная цена"; -"nft_collection.overview.all_time_average" = "Среднее за все время"; -"nft_collection.overview.royalty" = "Вознаграждение"; -"nft_collection.overview.inception_date" = "Дата старта"; - -"nft.activity.contracts" = "Контракты"; -"nft.activity.empty_list" = "Нет активности"; -"nft.activity.event_types" = "Типы событий"; -"nft.activity.event_type.all" = "Все события"; -"nft.activity.event_type.sale" = "Продажа"; -"nft.activity.event_type.transfer" = "Перевод"; -"nft.activity.event_type.mint" = "Минт"; -"nft.activity.event_type.list" = "Залистить"; -"nft.activity.event_type.listCancel" = "Убрать из листинга"; -"nft.activity.event_type.offer" = "Предложение"; -"nft.activity.event_type.offerCancel" = "Предложение отменено"; +"nft_collection.tab.overview" = "Overview"; +"nft_collection.tab.assets" = "Items"; +"nft_collection.tab.activity" = "Activity"; + +"nft_collection.overview.description" = "Description"; +"nft_collection.overview.contracts" = "Contracts"; +"nft_collection.overview.links" = "Links"; +"nft_collection.overview.links.website" = "Website"; + + +"nft_collection.overview.owners" = "Owners"; +"nft_collection.overview.items" = "Items"; +"nft_collection.overview.24h_volume" = "24h Volume"; +"nft_collection.overview.today_sellers" = "Today's Sellers"; +"nft_collection.overview.floor_price" = "Floor Price"; +"nft_collection.overview.all_time_average" = "All time average"; +"nft_collection.overview.royalty" = "Royalty"; +"nft_collection.overview.inception_date" = "Inception Date"; + +"nft.activity.contracts" = "Contracts"; +"nft.activity.empty_list" = "No item activity yet"; +"nft.activity.event_types" = "Event Types"; +"nft.activity.event_type.all" = "All Events"; +"nft.activity.event_type.sale" = "Sale"; +"nft.activity.event_type.transfer" = "Transfer"; +"nft.activity.event_type.mint" = "Mint"; +"nft.activity.event_type.list" = "List"; +"nft.activity.event_type.listCancel" = "List Cancel"; +"nft.activity.event_type.offer" = "Offer"; +"nft.activity.event_type.offerCancel" = "Offer Cancel"; // Subscription Info -"subscription_info.title" = "Премиум-функции"; -"subscription_info.info1.title" = "Анализ криптовалют"; -"subscription_info.info1.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; -"subscription_info.info2.title" = "Индикаторы"; -"subscription_info.info2.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; -"subscription_info.info3.title" = "Персональная поддержка"; -"subscription_info.info3.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; -"subscription_info.get_premium" = "Получите Премиум"; -"subscription_info.already_have" = "У меня уже есть Премиум"; +"subscription_info.title" = "Premium Features"; +"subscription_info.info1.title" = "Cryptocurrency Analytics"; +"subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; +"subscription_info.info2.title" = "Chart Indicators"; +"subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; +"subscription_info.info3.title" = "Personal Support"; +"subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; +"subscription_info.get_premium" = "Get Premium"; +"subscription_info.already_have" = "I already have Premium"; // Activate Subscription -"activate_subscription.title" = "Активировать"; -"activate_subscription.wallet" = "Кошелек"; -"activate_subscription.address" = "Адрес"; -"activate_subscription.message" = "Сообщение для подписи"; -"activate_subscription.sign" = "Подписать"; -"activate_subscription.activating" = "Активация..."; -"activate_subscription.failed_to_activate" = "Не удалось активировать подписку"; -"activate_subscription.activated" = "Активировано"; -"activate_subscription.no_subscriptions" = "Адрес вашего кошелька не имеет подписки на премиум-функции. Вам нужно его приобрести, чтобы активировать подписку."; +"activate_subscription.title" = "Activate"; +"activate_subscription.wallet" = "Wallet"; +"activate_subscription.address" = "Address"; +"activate_subscription.message" = "Message to Sign"; +"activate_subscription.sign" = "Sign"; +"activate_subscription.activating" = "Activating..."; +"activate_subscription.failed_to_activate" = "Failed to activate subscription"; +"activate_subscription.activated" = "Activated"; +"activate_subscription.no_subscriptions" = "Your wallet address does not have a subscription to premium features, you need to purchase it to activate the subscription."; // Launch -"launch.failed_to_launch" = "Не удалось запустить приложение из-за внутренней ошибки. Пожалуйста, попробуйте перезапустить приложение или сообщите об ошибке нашей службе поддержки."; -"launch.failed_to_launch.report" = "Пожаловаться"; +"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting app or report the error to our support team."; +"launch.failed_to_launch.report" = "Report"; // Tron -"tron.send.activation_fee" = "Комиссия за активацию"; -"tron.send.resources_consumed" = "Расходуемые ресурсы"; +"tron.send.activation_fee" = "Activation Fee"; +"tron.send.resources_consumed" = "Resources Consumed"; "tron.send.bandwidth" = "Bandwidth"; "tron.send.energy" = "Energy"; -"tron.send.fee.info" = "Ориентировочные затраты на отправку данной транзакции по сети (без учёта energy, Bandwidth и комиссии за активацию) предполагаются"; -"tron.send.resources_consumed.info" = "Bandwidth - это единица, которая измеряет размер байт транзакции, хранящихся в базе данных блокчейна. Чем больше транзакция, тем больше bandwidth будет потребляться.\n\nEnergy — это единица, измеряющая количество вычислений, требуемых виртуальной машиной TRON для выполнения конкретных операций в сети TRON.\n\nПоскольку транзакции смарт-контракта требуют выполнения вычислительных ресурсов, каждая сделка по контракту требует оплаты energy комиссии."; -"tron.send.activation_fee.info" = "Передача токенов TRX или TRC-10 на адрес неактивного аккаунта активирует его."; -"tron.send.inactive_address" = "Этот адрес не активен"; +"tron.send.fee.info" = "Think of this as the ticket price for your transaction's grand premiere on the network. And remember, this includes the full experience – the Energy, Bandwidth, and that special Activation fee!"; +"tron.send.resources_consumed.info" = "Picture Bandwidth as the amount of space your transaction takes up in the blockchain’s exclusive club.\n\nNow, Energy? That's the juice that powers the behind-the-scenes magic, especially when the TRON virtual machine starts performing its signature moves.\n\nWhen you call on a smart contract to dazzle with its performance, it needs a bit of Energy to shine. Hence, the energy fee!"; +"tron.send.activation_fee.info" = "Sending some TRX or TRC-10 tokens to a dormant address? Consider it your magic touch to awaken and activate that account!"; +"tron.send.inactive_address" = "Psst... this address is still in slumber mode."; // Cex Coin Select -"cex_coin_select.title" = "Выберите токен"; -"cex_coin_select.withdraw" = "Вывод средств"; -"cex_coin_select.withdraw.empty" = "У вас нет активов для вывода."; -"cex_coin_select.search_placeholder" = "Код или имя"; -"cex_coin_select.suspended" = "Приостановлено"; +"cex_coin_select.title" = "Choose Coin"; +"cex_coin_select.withdraw" = "Withdraw"; +"cex_coin_select.withdraw.empty" = "You have no assets to withdraw."; +"cex_coin_select.search_placeholder" = "Coin Code or Name"; +"cex_coin_select.suspended" = "Suspended"; // Cex Deposit Network Select -"cex_deposit_network_select.title" = "Выберите сеть"; -"cex_deposit_network_select.description" = "Выберите сеть и получите адрес для пополнения."; +"cex_deposit_network_select.title" = "Choose Network"; +"cex_deposit_network_select.description" = "Choose a network and get an address to deposit."; // Cex Deposit -"cex_deposit.title" = "Депозит %@"; -"cex_deposit.address" = "Адрес"; +"cex_deposit.title" = "Deposit %@"; +"cex_deposit.address" = "Address"; -"cex_deposit.network" = "Сеть"; +"cex_deposit.network" = "Network"; "cex_deposit.memo" = "Memo (Tag)"; -"cex_deposit.min_amount" = "Мин. сумма"; -"cex_deposit.warning_memo" = "Необходимо memo (tag), или вы потеряете ваши монеты."; -"cex_deposit.copy_address" = "Копировать"; -"cex_deposit.share_address" = "Поделиться"; -"cex_deposit.failed" = "Не удалось загрузить адрес депозита. Повторите попытку."; -"cex_deposit.memo_warning.title" = "Важно"; -"cex_deposit.memo_warning.description" = "Для получения активов необходимы memo(tag) и адрес. В противном случае Ваши средства будут утеряны."; +"cex_deposit.min_amount" = "Min. Amount"; +"cex_deposit.warning_memo" = "Memo (tag) is required, or your will lose your coins."; +"cex_deposit.copy_address" = "Copy"; +"cex_deposit.share_address" = "Share"; +"cex_deposit.failed" = "Failed to load deposit address. Please retry."; +"cex_deposit.memo_warning.title" = "Important"; +"cex_deposit.memo_warning.description" = "Both a memo (tag) and the address are needed to ensure that assets are received. Otherwise your funds will be lost."; // Cex Widthdraw -"cex_withdraw.network" = "Сеть"; -"cex_withdraw.network_warning" = "Убедитесь, что сеть совпадает с адресом снятия, а платформа депозита поддерживает его, или активы могут быть потеряны."; -"cex_withdraw.fee" = "Комиссия"; -"cex_withdraw.fee_from_amount" = "Комиссия от суммы"; -"cex_withdraw.error.insufficient_funds" = "Недостаточно доступных средств"; -"cex_withdraw.error.max_amount_violated" = "Макс.сумма вывода: %@"; -"cex_withdraw.error.min_amount_violated" = "Мин. сумма, которую вы можете вывести - %@"; -"cex_withdraw.address_required" = "Адрес обязателен"; +"cex_withdraw.network" = "Network"; +"cex_withdraw.network_warning" = "Ensure the network matches the withdrawal address and the deposit platform supports it, or assets may be lost."; +"cex_withdraw.fee" = "Fee"; +"cex_withdraw.fee_from_amount" = "Fee from amount"; +"cex_withdraw.error.insufficient_funds" = "Not enough available balance"; +"cex_withdraw.error.max_amount_violated" = "The maximum amount you can withdraw is %@"; +"cex_withdraw.error.min_amount_violated" = "The minimum amount you can withdraw is %@"; +"cex_withdraw.address_required" = "Address is required"; // Cex Withdraw Network Select -"cex_withdraw_network_select.title" = "Сеть"; -"cex_withdraw_network_select.description" = "Убедитесь, что сеть совпадает с адресом снятия и платформа депозита поддерживает его."; +"cex_withdraw_network_select.title" = "Network"; +"cex_withdraw_network_select.description" = "Ensure the network matches the withdrawal address and the deposit platform supports it."; // Cex Withdraw Confirm -"cex_withdraw_confirm.you_withdraw" = "Вывод средств"; -"cex_withdraw_confirm.network" = "Сеть"; -"cex_withdraw_confirm.withdraw" = "Вывод средств"; -"cex_withdraw_confirm.withdraw_failed" = "Вывод средств не выполнен"; +"cex_withdraw_confirm.you_withdraw" = "You Withdraw"; +"cex_withdraw_confirm.network" = "Network"; +"cex_withdraw_confirm.withdraw" = "Withdraw"; +"cex_withdraw_confirm.withdraw_failed" = "Withdraw failed"; From c4180eaf86912bfbc0869f69680db3bda7b82dc6 Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 14 Sep 2023 18:18:51 +0600 Subject: [PATCH 06/63] Add restore settings to backup files. - Make models for full backup --- .../project.pbxproj | 12 +++++ .../UnstoppableWallet/Core/App.swift | 3 +- .../Core/Crypto/AppearanceBackup.swift | 29 +++++++++++ .../Core/Crypto/FullBackup.swift | 8 +++ .../Core/Crypto/WalletBackup.swift | 19 +++++-- .../Core/Factories/AdapterFactory.swift | 2 +- .../Managers/CloudAccountBackupManager.swift | 51 ++++++++++--------- .../Managers/RestoreSettingsManager.swift | 12 ++--- .../Models/AccountType.swift | 10 +++- .../BackupCloudPassphraseService.swift | 2 +- .../ManageWallets/ManageWalletsService.swift | 2 +- .../RestoreCloudPassphraseModule.swift | 1 + .../RestoreCloudPassphraseService.swift | 18 ++++++- .../RestoreSettingsService.swift | 6 +-- 14 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 3f1d9daa63..1054730417 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1955,6 +1955,7 @@ ABC9A0F42A6687705CAD1340 /* NftAssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */; }; ABC9A1117A41AB8CE00FDEDB /* WalletConnectAppShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */; }; ABC9A133A6BF0FC9A87FA14A /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; + ABC9A13D78DD5F176A170B65 /* FullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */; }; ABC9A13DB3ADB580D59F66E4 /* SendEip1155ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A23CB332521C0607CC6B /* SendEip1155ViewModel.swift */; }; ABC9A13F4C814FFB31FF13CA /* SendEip721ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7315E119F0B1581B70C /* SendEip721ViewController.swift */; }; ABC9A140CD70E91A1F4A3A5B /* DonateAddressModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0B7E7360DC0357B2D0F /* DonateAddressModule.swift */; }; @@ -1992,6 +1993,7 @@ ABC9A2AA80535822D8731DA4 /* ContactBookViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2D87362E00FD9FB5688 /* ContactBookViewController.swift */; }; ABC9A2B6F1CF39D5CE9EA489 /* BackupCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8E4CDD143171A1F9C46 /* BackupCloudPassphraseViewController.swift */; }; ABC9A2BE94B97921C3017C3F /* ContactBookContactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5E6F7C6887DD5DFF6E4 /* ContactBookContactService.swift */; }; + ABC9A2C4301447E0EEA1D16F /* FullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */; }; ABC9A2C671DE8C67F192D22E /* ContactBookAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */; }; ABC9A2CA505DB49DE0FB28DD /* WalletTokenBalanceCustomAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD448DC071D8800C6B12 /* WalletTokenBalanceCustomAmountCell.swift */; }; ABC9A2D0ACEDCFA5FDB04D89 /* IndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A12E4155640075755699 /* IndicatorDataSource.swift */; }; @@ -2159,6 +2161,7 @@ ABC9A96132AD85DD613EC773 /* ProFeaturesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB785128005F6C2C9F9A /* ProFeaturesStorage.swift */; }; ABC9A994D6AC5771ED49EFD1 /* DonateAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A72B62F6152709348A6D /* DonateAddressViewModel.swift */; }; ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */; }; + ABC9A99861B1F83A19EA370D /* AppearanceBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */; }; ABC9A998ECDE5438D94FBAE7 /* MarketDiscoveryCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADFD9DA59BD2FB21C51B /* MarketDiscoveryCategoryService.swift */; }; ABC9A9A9FE5A83A6F0C3BFE9 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; }; ABC9A9AC7890BE4AAE7DDC84 /* WalletConnectSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */; }; @@ -2171,6 +2174,7 @@ ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9AA27A709AC5F85176A53 /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; ABC9AA309248821942E78740 /* MarketCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2B7FBA735A76083990C /* MarketCardCell.swift */; }; + ABC9AA39ED35D6EF41A5353D /* AppearanceBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */; }; ABC9AA462C94586CD8233295 /* WalletConnectAppShowModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9E2C039C005650491D2 /* WalletConnectAppShowModule.swift */; }; ABC9AA4B0A6C33CAD5F3B050 /* ChartIndicatorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB3EC7A1FB0D6C9F7F89 /* ChartIndicatorsModule.swift */; }; ABC9AA78419B8BFEC23E8E02 /* TokenSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */; }; @@ -3704,6 +3708,7 @@ ABC9A3F41BDCD5F4146E6E06 /* SendBinanceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBinanceService.swift; sourceTree = ""; }; ABC9A3FB680357E569B6DB5F /* WalletConnectAppShowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowViewModel.swift; sourceTree = ""; }; ABC9A3FBE68E228E3BE66F7B /* WalletTokenListDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListDataSource.swift; sourceTree = ""; }; + ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullBackup.swift; sourceTree = ""; }; ABC9A4544AB5CA22ADE16417 /* WalletConnectSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSession.swift; sourceTree = ""; }; ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudModule.swift; sourceTree = ""; }; ABC9A4674CCDED7C12EB5C09 /* ContactBookAddressModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressModule.swift; sourceTree = ""; }; @@ -3780,6 +3785,7 @@ ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudViewController.swift; sourceTree = ""; }; ABC9AA77C414AC06C41F9319 /* SessionRequestFilterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRequestFilterManager.swift; sourceTree = ""; }; ABC9AA7F2ECF212EF8B70470 /* SendConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationViewController.swift; sourceTree = ""; }; + ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearanceBackup.swift; sourceTree = ""; }; ABC9AA8F31619609907AD67E /* MacdIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacdIndicatorDataSource.swift; sourceTree = ""; }; ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewModel.swift; sourceTree = ""; }; ABC9AAB6BA03FFE92F247FF6 /* ProChartFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProChartFetcher.swift; sourceTree = ""; }; @@ -6995,6 +7001,8 @@ ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.swift */, ABC9A89726499CDB4F697EDD /* CipherParams.swift */, ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */, + ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */, + ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */, ); path = Crypto; sourceTree = ""; @@ -9104,6 +9112,8 @@ 11B359425D03F504ECA51B1A /* BlockchainSettingsView.swift in Sources */, 11B35FFD159D864F6D914F08 /* AppearanceView.swift in Sources */, 11B350CA618DD7BBA452FC33 /* AppearanceViewModel.swift in Sources */, + ABC9A13D78DD5F176A170B65 /* FullBackup.swift in Sources */, + ABC9AA39ED35D6EF41A5353D /* AppearanceBackup.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10380,6 +10390,8 @@ 11B35B6E11AE440A79D53E0F /* BlockchainSettingsView.swift in Sources */, 11B35245CD0D5B0E44E413F4 /* AppearanceView.swift in Sources */, 11B35A18AA61F8C06AB1C15B /* AppearanceViewModel.swift in Sources */, + ABC9A2C4301447E0EEA1D16F /* FullBackup.swift in Sources */, + ABC9A99861B1F83A19EA370D /* AppearanceBackup.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 09f513070d..bb6e55aaae 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -158,7 +158,6 @@ class App { accountRestoreWarningManager = AccountRestoreWarningManager(accountManager: accountManager, localStorage: StorageKit.LocalStorage.default) accountFactory = AccountFactory(accountManager: accountManager) - cloudAccountBackupManager = CloudAccountBackupManager(ubiquityContainerIdentifier: AppConfig.sharedCloudContainer, logger: logger) backupManager = BackupManager(accountManager: accountManager) kitCleaner = KitCleaner(accountManager: accountManager) @@ -192,6 +191,8 @@ class App { restoreSettingsManager = RestoreSettingsManager(storage: restoreSettingsStorage) predefinedBlockchainService = PredefinedBlockchainService(restoreSettingsManager: restoreSettingsManager) + cloudAccountBackupManager = CloudAccountBackupManager(ubiquityContainerIdentifier: AppConfig.sharedCloudContainer, restoreSettingsManager: restoreSettingsManager, logger: logger) + let hsLabelProvider = HsLabelProvider(networkManager: networkManager) let evmLabelStorage = EvmLabelStorage(dbPool: dbPool) let syncerStateStorage = SyncerStateStorage(dbPool: dbPool) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift new file mode 100644 index 0000000000..271721518a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift @@ -0,0 +1,29 @@ +import Foundation +import Chart +import CurrencyKit +import ThemeKit + +struct AppearanceBackup { + let lockTimeEnabled: Bool + let remoteContactsSync: Bool + let defaultProviders: [DefaultProvider] + let chartIndicators: [ChartIndicator] + let indicatorsShown: Bool + let currentLanguage: String + let baseCurrency: Currency + + let mode: ThemeMode + let showMarketTab: Bool + let launchScreen: LaunchScreen + let conversionTokenQueryId: String? + let balancePrimaryValue: BalancePrimaryValue + let balanceAutoHide: Bool + let appIcon: AppIcon +} + +extension AppearanceBackup { + struct DefaultProvider { + let blockchainTypeId: String + let provider: String + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift new file mode 100644 index 0000000000..abc65b27c8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -0,0 +1,8 @@ +import Foundation + +struct FullBackup { + let wallets: [WalletBackup] + let watchlistIds: [String] + let contacts: ContactBook? + let appearance: AppearanceBackup? +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift index 63807ee6bf..e2691ab3f7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift @@ -54,31 +54,40 @@ class WalletBackup: Codable { } extension WalletBackup { + struct Settings: Codable { + let type: String + let value: String + } + struct EnabledWallet: Codable { let tokenQueryId: String let coinName: String? let coinCode: String? let tokenDecimals: Int? + let settings: [String: String] enum CodingKeys: String, CodingKey { case tokenQueryId = "token_query_id" case coinName = "coin_name" case coinCode = "coin_code" case tokenDecimals = "decimals" + case settings } - init(tokenQueryId: String, coinName: String?, coinCode: String?, tokenDecimals: Int?) { + init(tokenQueryId: String, coinName: String?, coinCode: String?, tokenDecimals: Int?, settings: [String: String]) { self.tokenQueryId = tokenQueryId self.coinName = coinName self.coinCode = coinCode self.tokenDecimals = tokenDecimals + self.settings = settings } - init(_ wallet: Wallet) { + init(_ wallet: Wallet, settings: [String: String]) { tokenQueryId = wallet.token.tokenQuery.id coinName = wallet.coin.name coinCode = wallet.coin.code tokenDecimals = wallet.decimals + self.settings = settings } init(from decoder: Decoder) throws { @@ -87,8 +96,9 @@ extension WalletBackup { let coinName = try? container.decode(String.self, forKey: .coinName) let coinCode = try container.decode(String.self, forKey: .coinCode) let tokenDecimals = try container.decode(Int.self, forKey: .tokenDecimals) + let settings = try? container.decode([String: String].self, forKey: .settings) - self.init(tokenQueryId: tokenQueryId, coinName: coinName, coinCode: coinCode, tokenDecimals: tokenDecimals) + self.init(tokenQueryId: tokenQueryId, coinName: coinName, coinCode: coinCode, tokenDecimals: tokenDecimals, settings: settings ?? [:]) } public func encode(to encoder: Encoder) throws { @@ -97,6 +107,9 @@ extension WalletBackup { try container.encode(coinName, forKey: .coinName) try container.encode(coinCode, forKey: .coinCode) try container.encode(tokenDecimals, forKey: .tokenDecimals) + if !settings.isEmpty { + try container.encode(settings, forKey: .settings) + } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift index c8a5881b8c..c98200b98f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift @@ -117,7 +117,7 @@ extension AdapterFactory { return try? DashAdapter(wallet: wallet, syncMode: syncMode) case (.native, .zcash): - let restoreSettings = restoreSettingsManager.settings(account: wallet.account, blockchainType: .zcash) + let restoreSettings = restoreSettingsManager.settings(accountId: wallet.account.id, blockchainType: .zcash) return try? ZcashAdapter(wallet: wallet, restoreSettings: restoreSettings) case (.native, .binanceChain), (.bep2, .binanceChain): diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift index 748dc386d6..e8000f493b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift @@ -1,14 +1,15 @@ -import Foundation import Combine -import HsToolKit +import Foundation import HsExtensions +import HsToolKit class CloudAccountBackupManager { - static private let batchingInterval: TimeInterval = 1 - static private let fileExtension = ".json" + private static let batchingInterval: TimeInterval = 1 + private static let fileExtension = ".json" private let ubiquityContainerIdentifier: String? private let fileStorage: FileStorage + private let restoreSettingsManager: RestoreSettingsManager private let logger: Logger? private var metadataMonitor: MetadataMonitor? @@ -16,16 +17,17 @@ class CloudAccountBackupManager { var iCloudUrl: URL? { FileManager - .default - .url(forUbiquityContainerIdentifier: ubiquityContainerIdentifier)? - .appendingPathComponent("Documents") + .default + .url(forUbiquityContainerIdentifier: ubiquityContainerIdentifier)? + .appendingPathComponent("Documents") } @PostPublished private(set) var items = [String: WalletBackup]() @PostPublished private(set) var state = State.loading - init(ubiquityContainerIdentifier: String?, logger: Logger?) { + init(ubiquityContainerIdentifier: String?, restoreSettingsManager: RestoreSettingsManager, logger: Logger?) { self.ubiquityContainerIdentifier = ubiquityContainerIdentifier + self.restoreSettingsManager = restoreSettingsManager fileStorage = FileStorage(logger: logger) self.logger = logger @@ -49,10 +51,10 @@ class CloudAccountBackupManager { logger?.debug("=C-MANAGER> Turn ON monitor") metadataMonitor.needUpdatePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.reload() - }.store(in: &publishers) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.reload() + }.store(in: &publishers) } private func reload() { @@ -113,11 +115,9 @@ class CloudAccountBackupManager { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, read \(items.count) files") return items } - } extension CloudAccountBackupManager { - func backedUp(uniqueId: Data) -> Bool { items.contains { _, backup in backup.id == uniqueId.hs.hex } } @@ -125,23 +125,32 @@ extension CloudAccountBackupManager { var existFilenames: [String] { items.map { ($0.key as NSString).deletingPathExtension } } - } extension CloudAccountBackupManager { - var isAvailable: Bool { iCloudUrl != nil } - func save(accountType: AccountType, wallets: [Wallet], isManualBackedUp: Bool, passphrase: String, name: String) throws { + func save(account: Account, wallets: [Wallet], isManualBackedUp: Bool, passphrase: String, name: String) throws { guard let iCloudUrl else { throw BackupError.urlNotAvailable } do { let name = name + Self.fileExtension - let encoded = try WalletBackupConverter.encode(accountType: accountType, wallets: wallets.map { WalletBackup.EnabledWallet($0) }, isManualBackedUp: isManualBackedUp, passphrase: passphrase) + let encoded = try WalletBackupConverter.encode( + accountType: account.type, + wallets: wallets.map { + let settings = restoreSettingsManager + .settings(accountId: account.id, blockchainType: $0.token.blockchainType) + .reduce(into: [:], { $0[$1.0.rawValue] = $1.1 }) + + return WalletBackup.EnabledWallet($0, settings: settings) + }, + isManualBackedUp: isManualBackedUp, + passphrase: passphrase + ) try fileStorage.write(directoryUrl: iCloudUrl, filename: name, data: encoded) logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, save \(name)") @@ -149,7 +158,6 @@ extension CloudAccountBackupManager { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") throw error } - } func delete(uniqueId: Data) throws { @@ -162,7 +170,7 @@ extension CloudAccountBackupManager { throw BackupError.urlNotAvailable } - guard let item = items.first(where: { name, backup in backup.id == uniqueId }) else { + guard let item = items.first(where: { _, backup in backup.id == uniqueId }) else { throw BackupError.itemNotFound } @@ -179,11 +187,9 @@ extension CloudAccountBackupManager { throw error } } - } extension CloudAccountBackupManager { - enum BackupError: Error { case urlNotAvailable case itemNotFound @@ -194,5 +200,4 @@ extension CloudAccountBackupManager { case success case error(Error) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RestoreSettingsManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RestoreSettingsManager.swift index d55876092b..6b07828990 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RestoreSettingsManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RestoreSettingsManager.swift @@ -7,13 +7,11 @@ class RestoreSettingsManager { init(storage: RestoreSettingsStorage) { self.storage = storage } - } extension RestoreSettingsManager { - - func settings(account: Account, blockchainType: BlockchainType) -> RestoreSettings { - let records = storage.restoreSettings(accountId: account.id, blockchainUid: blockchainType.uid) + func settings(accountId: String, blockchainType: BlockchainType) -> RestoreSettings { + let records = storage.restoreSettings(accountId: accountId, blockchainUid: blockchainType.uid) var settings = RestoreSettings() @@ -46,11 +44,11 @@ extension RestoreSettingsManager { storage.save(restoreSettingRecords: records) } - } enum RestoreSettingType: String { - case birthdayHeight + case birthdayHeight = "birthday_height" + func createdAccountValue(blockchainType: BlockchainType) -> String? { switch self { case .birthdayHeight: @@ -71,9 +69,7 @@ enum RestoreSettingType: String { typealias RestoreSettings = [RestoreSettingType: String] extension RestoreSettings { - var birthdayHeight: Int? { self[.birthdayHeight].flatMap { Int($0) } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index c2504fd7ab..8ea57d439f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -260,8 +260,14 @@ extension AccountType { } catch { return nil } - case .evmAddress, .tronAddress: - return nil + case .evmAddress: + return AccountType.evmAddress(address: EvmKit.Address(raw: uniqueId)) + case .tronAddress: + do { + return AccountType.tronAddress(address: try TronKit.Address(raw: uniqueId)) + } catch { + return nil + } case .cex: guard let cexAccount = CexAccount.decode(uniqueId: string) else { return nil diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift index 2c522f8eee..d59eeb3917 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift @@ -44,7 +44,7 @@ extension BackupCloudPassphraseService { do { let wallets = App.shared.walletManager.wallets(account: account) - try iCloudManager.save(accountType: account.type, wallets: wallets, isManualBackedUp: account.backedUp, passphrase: passphrase, name: name) + try iCloudManager.save(account: account, wallets: wallets, isManualBackedUp: account.backedUp, passphrase: passphrase, name: name) } catch { if case .urlNotAvailable = error as? CloudAccountBackupManager.BackupError { throw CreateError.urlNotAvailable diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsService.swift index 9ac4632721..5c67da2cae 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsService.swift @@ -243,7 +243,7 @@ extension ManageWalletsService { for restoreSettingType in blockchainType.restoreSettingTypes { switch restoreSettingType { case .birthdayHeight: - let settings = restoreSettingsService.settings(account: account, blockchainType: blockchainType) + let settings = restoreSettingsService.settings(accountId: account.id, blockchainType: blockchainType) if let birthdayHeight = settings.birthdayHeight { return InfoItem(token: token, type: .birthdayHeight(height: birthdayHeight)) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift index a215dd5fc3..840a0d1417 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift @@ -9,6 +9,7 @@ class RestoreCloudPassphraseModule { accountFactory: App.shared.accountFactory, accountManager: App.shared.accountManager, walletManager: App.shared.walletManager, + restoreSettingsManager: App.shared.restoreSettingsManager, item: item ) let viewModel = RestoreCloudPassphraseViewModel(service: service) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift index 875feebdc0..665c3b3283 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift @@ -1,20 +1,23 @@ import Foundation +import MarketKit class RestoreCloudPassphraseService { private let iCloudManager: CloudAccountBackupManager private let accountFactory: AccountFactory private let accountManager: AccountManager private let walletManager: WalletManager + private let restoreSettingsManager: RestoreSettingsManager private let restoredBackup: RestoreCloudModule.RestoredBackup var passphrase: String = "" - init(iCloudManager: CloudAccountBackupManager, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, item: RestoreCloudModule.RestoredBackup) { + init(iCloudManager: CloudAccountBackupManager, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, restoreSettingsManager: RestoreSettingsManager, item: RestoreCloudModule.RestoredBackup) { self.iCloudManager = iCloudManager self.accountFactory = accountFactory self.accountManager = accountManager self.walletManager = walletManager + self.restoreSettingsManager = restoreSettingsManager restoredBackup = item } @@ -23,7 +26,18 @@ class RestoreCloudPassphraseService { accountManager.save(account: account) let wallets = restoredBackup.walletBackup.enabledWallets.map { - EnabledWallet( + 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: account, blockchainType: tokenQuery.blockchainType) + } + } + return EnabledWallet( tokenQueryId: $0.tokenQueryId, accountId: account.id, coinName: $0.coinName, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreSettings/RestoreSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreSettings/RestoreSettingsService.swift index acf1ec5497..73d291aea7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreSettings/RestoreSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreSettings/RestoreSettingsService.swift @@ -44,7 +44,7 @@ extension RestoreSettingsService { return } - let existingSettings = account.map { manager.settings(account: $0, blockchainType: blockchainType) } ?? [:] + let existingSettings = account.map { manager.settings(accountId: $0.id, blockchainType: blockchainType) } ?? [:] if blockchainType.restoreSettingTypes.contains(.birthdayHeight) && existingSettings[.birthdayHeight] == nil { let request = Request( @@ -79,8 +79,8 @@ extension RestoreSettingsService { rejectApproveSettingsRelay.accept(token) } - func settings(account: Account, blockchainType: BlockchainType) -> RestoreSettings { - manager.settings(account: account, blockchainType: blockchainType) + func settings(accountId: String, blockchainType: BlockchainType) -> RestoreSettings { + manager.settings(accountId: accountId, blockchainType: blockchainType) } } From c56f7b7a4eec5f845fdfe1fe58add78ba9684456 Mon Sep 17 00:00:00 2001 From: ant013 Date: Fri, 15 Sep 2023 10:53:29 +0600 Subject: [PATCH 07/63] Revert "Disable swap function" This reverts commit 6eaae15abac5480ded1405e8336b7810c5104a30. --- .../WalletTokenBalanceViewItemFactory.swift | 6 +++++- .../Modules/Wallet/WalletViewItemFactory.swift | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index 399158f2d1..7776682656 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -14,7 +14,7 @@ class WalletTokenBalanceViewItemFactory { var buttons = [WalletModule.Button: ButtonState]() switch item.element { - case .wallet: + case .wallet(let wallet): if item.watchAccount { buttons[.address] = .enabled } else { @@ -22,6 +22,10 @@ class WalletTokenBalanceViewItemFactory { buttons[.send] = sendButtonState buttons[.receive] = .enabled + + if wallet.token.swappable { + buttons[.swap] = sendButtonState + } } case .cexAsset(let cexAsset): buttons[.withdraw] = cexAsset.withdrawEnabled ? .enabled : .disabled diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index cd654bdb2c..da6e27e4a6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -143,7 +143,8 @@ class WalletViewItemFactory { case .evmPrivateKey, .hdExtendedKey, .mnemonic: return [ .send: .enabled, - .receive: .enabled + .receive: .enabled, + .swap: .enabled ] case .evmAddress, .tronAddress: return [:] } From 4f277b691b363cbec13c6b7a3954919282dca824 Mon Sep 17 00:00:00 2001 From: Esenbek Date: Fri, 15 Sep 2023 12:52:31 +0600 Subject: [PATCH 08/63] Pass TronGrid API key from ENV to XCConfig --- .github/workflows/deploy_appstore.yml | 1 + .github/workflows/deploy_dev.yml | 1 + fastlane/Fastfile | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/deploy_appstore.yml b/.github/workflows/deploy_appstore.yml index c5d3c4fb0c..c4d55d882e 100644 --- a/.github/workflows/deploy_appstore.yml +++ b/.github/workflows/deploy_appstore.yml @@ -71,3 +71,4 @@ jobs: XCCONFIG_PROD_HS_PROVIDER_API_KEY: ${{ secrets.XCCONFIG_PROD_HS_PROVIDER_API_KEY }} XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY: ${{ secrets.XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY }} XCCONFIG_PROD_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_PROD_OPEN_SEA_API_KEY }} + XCCONFIG_PROD_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_PROD_TRONGRID_API_KEY }} diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml index e1e47f339d..e23c0debf4 100644 --- a/.github/workflows/deploy_dev.yml +++ b/.github/workflows/deploy_dev.yml @@ -72,3 +72,4 @@ jobs: XCCONFIG_DEV_HS_PROVIDER_API_KEY: ${{ secrets.XCCONFIG_DEV_HS_PROVIDER_API_KEY }} XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY: ${{ secrets.XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY }} XCCONFIG_DEV_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_DEV_OPEN_SEA_API_KEY }} + XCCONFIG_DEV_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_DEV_TRONGRID_API_KEY }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bb8195b382..3a6be9b4b3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -28,6 +28,7 @@ XCCONFIG_DEV_TWITTER_BEARER_TOKEN = ENV["XCCONFIG_DEV_TWITTER_BEARER_TOKEN"] XCCONFIG_DEV_HS_PROVIDER_API_KEY = ENV["XCCONFIG_DEV_HS_PROVIDER_API_KEY"] XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY = ENV["XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY"] XCCONFIG_DEV_OPEN_SEA_API_KEY = ENV["XCCONFIG_DEV_OPEN_SEA_API_KEY"] +XCCONFIG_DEV_TRONGRID_API_KEY = ENV["XCCONFIG_DEV_TRONGRID_API_KEY"] XCCONFIG_PROD_INFURA_PROJECT_ID = ENV["XCCONFIG_PROD_INFURA_PROJECT_ID"] XCCONFIG_PROD_INFURA_PROJECT_SECRET = ENV["XCCONFIG_PROD_INFURA_PROJECT_SECRET"] @@ -44,6 +45,7 @@ XCCONFIG_PROD_TWITTER_BEARER_TOKEN = ENV["XCCONFIG_PROD_TWITTER_BEARER_TOKEN"] XCCONFIG_PROD_HS_PROVIDER_API_KEY = ENV["XCCONFIG_PROD_HS_PROVIDER_API_KEY"] XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY = ENV["XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY"] XCCONFIG_PROD_OPEN_SEA_API_KEY = ENV["XCCONFIG_PROD_OPEN_SEA_API_KEY"] +XCCONFIG_PROD_TRONGRID_API_KEY = ENV["XCCONFIG_PROD_TRONGRID_API_KEY"] def delete_temp_keychain(name) delete_keychain( @@ -117,6 +119,7 @@ def apply_dev_xcconfig update_dev_xcconfig('hs_provider_api_key', XCCONFIG_DEV_HS_PROVIDER_API_KEY) update_dev_xcconfig('wallet_connect_v2_project_key', XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY) update_dev_xcconfig('open_sea_api_key', XCCONFIG_DEV_OPEN_SEA_API_KEY) + update_dev_xcconfig('trongrid_api_key', XCCONFIG_DEV_TRONGRID_API_KEY) end def apply_prod_xcconfig @@ -135,6 +138,7 @@ def apply_prod_xcconfig update_prod_xcconfig('hs_provider_api_key', XCCONFIG_PROD_HS_PROVIDER_API_KEY) update_prod_xcconfig('wallet_connect_v2_project_key', XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY) update_prod_xcconfig('open_sea_api_key', XCCONFIG_PROD_OPEN_SEA_API_KEY) + update_prod_xcconfig('trongrid_api_key', XCCONFIG_PROD_TRONGRID_API_KEY) end def force_update_devices(type, username) From 2cda645fdca90184ba479aaef081104fd3b3ff96 Mon Sep 17 00:00:00 2001 From: ant013 Date: Tue, 19 Sep 2023 17:16:15 +0600 Subject: [PATCH 09/63] Implement classes for full backup --- .../project.pbxproj | 54 ++-- .../UnstoppableWallet/Core/App.swift | 30 +- .../Core/Crypto/AppearanceBackup.swift | 29 -- .../Core/Crypto/BackupCrypto.swift | 141 +++++++++ .../Core/Crypto/FullBackup.swift | 42 ++- .../Core/Crypto/SettingsBackup.swift | 55 ++++ .../Core/Crypto/WalletBackup.swift | 8 +- .../Core/Crypto/WalletBackupCrypto.swift | 51 --- .../Managers/BalanceConversionManager.swift | 7 + ...Manager.swift => CloudBackupManager.swift} | 96 +++--- .../Core/Managers/EvmSyncSourceManager.swift | 265 ++++++++++------ .../Core/Storage/ContactBookManager.swift | 10 +- .../Core/Storage/EvmSyncSourceStorage.swift | 6 + .../Storage/FavoriteCoinRecordStorage.swift | 7 + .../Core/Storage/LocalStorage.swift | 63 +--- .../Extensions/ThemeMode.swift | 3 + .../Models/BackupContact.swift | 16 +- .../Models/BalancePrimaryValue.swift | 2 +- .../UnstoppableWallet/Models/Contact.swift | 21 +- .../Models/LaunchScreen.swift | 9 + .../Modules/Backup/BackupModule.swift | 2 +- .../Backup/ICloud/AppBackupProvider.swift | 291 ++++++++++++++++++ .../Backup/ICloud/BackupCloudModule.swift | 6 +- .../ICloud/Name/ICloudBackupNameService.swift | 4 +- .../BackupCloudPassphraseService.swift | 26 +- .../BackupCloudPassphraseViewModel.swift | 14 +- .../Terms/ICloudBackupTermsService.swift | 4 +- .../Backup/ICloud/WalletBackupConverter.swift | 73 ----- .../Modules/Favorites/FavoritesManager.swift | 4 + .../ManageAccount/ManageAccountModule.swift | 2 +- .../ManageAccount/ManageAccountService.swift | 6 +- .../ManageAccounts/ManageAccountsModule.swift | 2 +- .../ManageAccountsService.swift | 6 +- .../RestoreCloud/RestoreCloudModule.swift | 18 +- .../RestoreCloudPassphraseModule.swift | 3 +- .../RestoreCloudPassphraseService.swift | 102 ++---- .../RestoreCloudPassphraseViewModel.swift | 2 +- .../RestoreCloud/RestoreCloudService.swift | 8 +- .../RestoreType/RestoreTypeModule.swift | 2 +- .../RestoreType/RestoreTypeViewModel.swift | 4 +- .../Settings/Main/MainSettingsModule.swift | 2 +- .../Settings/Main/MainSettingsService.swift | 4 +- .../Receive/SelectCoin/CoinProvider.swift | 2 - .../WalletTokenBalanceModule.swift | 2 +- .../WalletTokenBalanceService.swift | 4 +- .../Modules/Wallet/WalletModule.swift | 2 +- .../Modules/Wallet/WalletService.swift | 4 +- .../WalletConnectAppShowModule.swift | 2 +- .../WalletConnectAppShowService.swift | 4 +- 49 files changed, 985 insertions(+), 535 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackupCrypto.swift rename UnstoppableWallet/UnstoppableWallet/Core/Managers/{CloudAccountBackupManager.swift => CloudBackupManager.swift} (67%) create mode 100644 UnstoppableWallet/UnstoppableWallet/Extensions/ThemeMode.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index db5d929dfe..6782e6ca86 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1912,8 +1912,8 @@ 6BCD53172A161F4800993F20 /* BackupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD53112A161F4800993F20 /* BackupViewController.swift */; }; 6BCD53192A161F9200993F20 /* BackupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD53182A161F9100993F20 /* BackupService.swift */; }; 6BCD531A2A161F9200993F20 /* BackupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD53182A161F9100993F20 /* BackupService.swift */; }; - 6BCD531C2A16203F00993F20 /* CloudAccountBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD531B2A16203F00993F20 /* CloudAccountBackupManager.swift */; }; - 6BCD531D2A16203F00993F20 /* CloudAccountBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD531B2A16203F00993F20 /* CloudAccountBackupManager.swift */; }; + 6BCD531C2A16203F00993F20 /* CloudBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD531B2A16203F00993F20 /* CloudBackupManager.swift */; }; + 6BCD531D2A16203F00993F20 /* CloudBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD531B2A16203F00993F20 /* CloudBackupManager.swift */; }; 6BDA29AB29D6F37C003847ED /* ECashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AA29D6F37C003847ED /* ECashKit */; }; 6BDA29AD29D6F384003847ED /* ECashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AC29D6F384003847ED /* ECashKit */; }; 6BDA29B029D6F934003847ED /* HsToolKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AF29D6F934003847ED /* HsToolKit */; }; @@ -2066,6 +2066,7 @@ ABC9A57EB423CAD56190F36B /* ChartIndicatorSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */; }; ABC9A59B465A9C59F93DFB96 /* ChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9F6635146BEBFB432D1 /* ChartCell.swift */; }; ABC9A5A0C65184DF54C48C5A /* TechnicalIndicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */; }; + ABC9A5A4C6213D58CDA2EB73 /* ThemeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */; }; ABC9A5BBFC1960B1DD8F62B7 /* SendBinanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3F41BDCD5F4146E6E06 /* SendBinanceService.swift */; }; ABC9A5C2E2976341520D2F6D /* WalletConnectListModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC09A586D88BAB3B9C67 /* WalletConnectListModule.swift */; }; ABC9A5CB5C5D56F50FE5F64C /* SendTimeLockErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADF114FCFABEA148AF04 /* SendTimeLockErrorService.swift */; }; @@ -2083,7 +2084,7 @@ ABC9A63EC83A82A76E67778B /* SendNftModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */; }; ABC9A66E5775762856F8927D /* NftAssetOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A90781302D793E0773CB /* NftAssetOverviewModule.swift */; }; ABC9A67A87DFB11102AB607A /* SendBitcoinFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3DC5DA5B7BFDBF72B5D /* SendBitcoinFactory.swift */; }; - ABC9A6887B716464A5813EE9 /* WalletBackupCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.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 */; }; ABC9A69BADD39C6E9239A2A1 /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */; }; @@ -2091,6 +2092,7 @@ ABC9A69FA41A9BC474DD1915 /* DiffLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A916C64B5EA9D96B8FDA /* DiffLabel.swift */; }; ABC9A6A484F9B3F7F1054379 /* WalletConnectMainPendingRequestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFF8093DEB7AFD7DBBCC /* WalletConnectMainPendingRequestService.swift */; }; ABC9A6A792282ACC8DAB62BC /* IntegerFormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */; }; + ABC9A6A9C28C95352232B062 /* ThemeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */; }; ABC9A6BC79804B3D3AAFA8F1 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; ABC9A6C0A45A33C83B632D58 /* SendFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */; }; ABC9A6C1B2F55F1FFA8910CA /* ContactBookSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */; }; @@ -2122,7 +2124,7 @@ ABC9A7CBFDC0DF741E29EA44 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF26FDCB363793BF66E1 /* Integer.swift */; }; ABC9A7E1F93B0A85976C826D /* UniswapV3Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A253877D9FB972EFB8D7 /* UniswapV3Provider.swift */; }; ABC9A7E28714A9A19A2160D4 /* SendModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD1F2311CC6425CF9D90 /* SendModule.swift */; }; - ABC9A7EACB2FA65355C2BA4E /* WalletBackupConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* WalletBackupConverter.swift */; }; + ABC9A7EACB2FA65355C2BA4E /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; ABC9A7F5ECEC3311216A407F /* SendMemoInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A80143F95E28346C81FE /* SendMemoInputService.swift */; }; ABC9A802418438F6BD1FC1E3 /* WalletTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A64A66778C137FA9642C /* WalletTokenViewController.swift */; }; ABC9A806CB34CB9A5E27A0A3 /* RestoreCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */; }; @@ -2144,7 +2146,7 @@ ABC9A8916A5DFA7A33F4FF79 /* SendBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF15BD67548E6D755CA0 /* SendBinanceViewController.swift */; }; ABC9A89499016C8AC8341238 /* NftCollectionCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CF40E995105E7F38AC /* NftCollectionCellFactory.swift */; }; ABC9A8A74C527C4E01EBB8A5 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; - ABC9A8AE39B8925B28B97F77 /* WalletBackupConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* WalletBackupConverter.swift */; }; + ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9A8D215CC5D6A70736E84 /* SendBaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A48552CF0C90E22686A9 /* SendBaseService.swift */; }; ABC9A8D8709EC2B40D74A97A /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; @@ -2162,7 +2164,7 @@ ABC9A96132AD85DD613EC773 /* ProFeaturesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB785128005F6C2C9F9A /* ProFeaturesStorage.swift */; }; ABC9A994D6AC5771ED49EFD1 /* DonateAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A72B62F6152709348A6D /* DonateAddressViewModel.swift */; }; ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */; }; - ABC9A99861B1F83A19EA370D /* AppearanceBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */; }; + ABC9A99861B1F83A19EA370D /* SettingsBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */; }; ABC9A998ECDE5438D94FBAE7 /* MarketDiscoveryCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADFD9DA59BD2FB21C51B /* MarketDiscoveryCategoryService.swift */; }; ABC9A9A9FE5A83A6F0C3BFE9 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; }; ABC9A9AC7890BE4AAE7DDC84 /* WalletConnectSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */; }; @@ -2175,7 +2177,7 @@ ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9AA27A709AC5F85176A53 /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; ABC9AA309248821942E78740 /* MarketCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2B7FBA735A76083990C /* MarketCardCell.swift */; }; - ABC9AA39ED35D6EF41A5353D /* AppearanceBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */; }; + ABC9AA39ED35D6EF41A5353D /* SettingsBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */; }; ABC9AA462C94586CD8233295 /* WalletConnectAppShowModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9E2C039C005650491D2 /* WalletConnectAppShowModule.swift */; }; ABC9AA4B0A6C33CAD5F3B050 /* ChartIndicatorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB3EC7A1FB0D6C9F7F89 /* ChartIndicatorsModule.swift */; }; ABC9AA78419B8BFEC23E8E02 /* TokenSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */; }; @@ -2297,7 +2299,7 @@ ABC9AE9A8BECB2CE0EEF8271 /* DonateAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A525C1E9A53F37EC3918 /* DonateAddressViewController.swift */; }; ABC9AEA4EF88D31D00781014 /* ContactLabelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB89F64056FFB98928E7 /* ContactLabelService.swift */; }; ABC9AEA5C042362B5B5BE81C /* WalletConnectMainModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE8A193F58021C411311 /* WalletConnectMainModule.swift */; }; - ABC9AEA715281555878BF2A9 /* WalletBackupCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.swift */; }; + ABC9AEA715281555878BF2A9 /* BackupCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */; }; ABC9AEAA851D9BB91E8338D1 /* SwapInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A56611CF5E7B3F25CD5C /* SwapInputAccessoryView.swift */; }; ABC9AEB71EB75575A97408BC /* DonateDescriptionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABFE62D22F9FB0B3409A /* DonateDescriptionDataSource.swift */; }; ABC9AEC9C350F3CD059C9716 /* SendMemoInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A80143F95E28346C81FE /* SendMemoInputService.swift */; }; @@ -3654,7 +3656,7 @@ 6BCD53102A161F4800993F20 /* BackupViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupViewModel.swift; sourceTree = ""; }; 6BCD53112A161F4800993F20 /* BackupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupViewController.swift; sourceTree = ""; }; 6BCD53182A161F9100993F20 /* BackupService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupService.swift; sourceTree = ""; }; - 6BCD531B2A16203F00993F20 /* CloudAccountBackupManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudAccountBackupManager.swift; sourceTree = ""; }; + 6BCD531B2A16203F00993F20 /* CloudBackupManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudBackupManager.swift; sourceTree = ""; }; ABC9A021D71EDD24DFB6BA62 /* CoinProChartModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinProChartModule.swift; sourceTree = ""; }; ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineFormTextView.swift; sourceTree = ""; }; ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceView.swift; sourceTree = ""; }; @@ -3754,6 +3756,7 @@ ABC9A806FD17A129212E3F7C /* NftAssetOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewViewController.swift; sourceTree = ""; }; ABC9A8080797194017F736AB /* ContactBookContactViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactViewModel.swift; sourceTree = ""; }; ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendNftModule.swift; sourceTree = ""; }; + ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeMode.swift; sourceTree = ""; }; ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowView.swift; sourceTree = ""; }; ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceViewItemFactory.swift; sourceTree = ""; }; ABC9A88E126AB21F856522A7 /* IntegerAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerAmountInputView.swift; sourceTree = ""; }; @@ -3788,7 +3791,7 @@ ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudViewController.swift; sourceTree = ""; }; ABC9AA77C414AC06C41F9319 /* SessionRequestFilterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRequestFilterManager.swift; sourceTree = ""; }; ABC9AA7F2ECF212EF8B70470 /* SendConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationViewController.swift; sourceTree = ""; }; - ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearanceBackup.swift; sourceTree = ""; }; + ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsBackup.swift; sourceTree = ""; }; ABC9AA8F31619609907AD67E /* MacdIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacdIndicatorDataSource.swift; sourceTree = ""; }; ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewModel.swift; sourceTree = ""; }; ABC9AAB6BA03FFE92F247FF6 /* ProChartFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProChartFetcher.swift; sourceTree = ""; }; @@ -3796,14 +3799,14 @@ ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceCell.swift; sourceTree = ""; }; ABC9AAD55B8932EE75E3C037 /* SwapInputModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapInputModule.swift; sourceTree = ""; }; ABC9AAD79FD756DA69A52578 /* WalletConnectPendingRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsViewController.swift; sourceTree = ""; }; - ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackupCrypto.swift; sourceTree = ""; }; + ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCrypto.swift; sourceTree = ""; }; ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModel.swift; sourceTree = ""; }; ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; ABC9AB2DC4C4412EFE6BEFF7 /* WalletTokenBalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceCell.swift; sourceTree = ""; }; 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 = ""; }; - ABC9AB61EA3B39D8BDB1EEDE /* WalletBackupConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackupConverter.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 = ""; }; ABC9AB8907B0E779CA4DF8F1 /* NftAssetOverviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewViewModel.swift; sourceTree = ""; }; @@ -4423,6 +4426,7 @@ ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */, 11B3593FBD158050C9FEF6B9 /* Misc.swift */, ABC9A9B35C58F6525F3B2D5C /* FullCoin.swift */, + ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */, ); path = Extensions; sourceTree = ""; @@ -4582,7 +4586,7 @@ 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */, 11B35FA360A91FDE3EB0B85C /* RateAppManager.swift */, 1A5646B6231F2C52F27526F7 /* BtcBlockchainManager.swift */, - 6BCD531B2A16203F00993F20 /* CloudAccountBackupManager.swift */, + 6BCD531B2A16203F00993F20 /* CloudBackupManager.swift */, 11B35D9767615D8FBF7A314F /* GuidesManager.swift */, 11B35EB9BA551F2F1AF7739D /* TermsManager.swift */, 2FA5D690E78A4568F9FD9554 /* LogRecordManager.swift */, @@ -6653,7 +6657,7 @@ 6BCD52F82A161F4100993F20 /* Terms */, 6BCD52FC2A161F4100993F20 /* Name */, ABC9A3CED3BD03C1DBF797E2 /* Passphrase */, - ABC9AB61EA3B39D8BDB1EEDE /* WalletBackupConverter.swift */, + ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */, ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */, ); path = ICloud; @@ -7002,11 +7006,11 @@ children = ( ABC9AD5CB1911A698718213F /* BackupCryptoHelper.swift */, ABC9A6663522498A53CF4174 /* KdfParams.swift */, - ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.swift */, + ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */, ABC9A89726499CDB4F697EDD /* CipherParams.swift */, ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */, ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */, - ABC9AA7FC181E0E0FB74BEF5 /* AppearanceBackup.swift */, + ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */, ); path = Crypto; sourceTree = ""; @@ -8069,7 +8073,7 @@ 11B356F4F8D8486B00A2AA47 /* MainBadgeService.swift in Sources */, 11B3576791792D356B0BE916 /* MainViewModel.swift in Sources */, 11B35EFAFFA1E30F7765FEB2 /* MainService.swift in Sources */, - 6BCD531D2A16203F00993F20 /* CloudAccountBackupManager.swift in Sources */, + 6BCD531D2A16203F00993F20 /* CloudBackupManager.swift in Sources */, 58AAAF4236075971CC88F7ED /* SwapApproveService.swift in Sources */, 58AAAE666BAD91283206BA1C /* SwapApproveViewModel.swift in Sources */, 58AAA0D14CD9EDAE2DBF7540 /* SwapApproveViewController.swift in Sources */, @@ -8946,10 +8950,10 @@ ABC9ABE3189E497EC732B331 /* BackupCloudPassphraseViewModel.swift in Sources */, ABC9A2A249A94B271F56EBD0 /* BackupCryptoHelper.swift in Sources */, ABC9A543EB59D153FAD103F6 /* KdfParams.swift in Sources */, - ABC9AEA715281555878BF2A9 /* WalletBackupCrypto.swift in Sources */, + ABC9AEA715281555878BF2A9 /* BackupCrypto.swift in Sources */, ABC9ACDD29B7F82884A5AE39 /* CipherParams.swift in Sources */, ABC9AA80C5197F9CC6221FC8 /* WalletBackup.swift in Sources */, - ABC9A8AE39B8925B28B97F77 /* WalletBackupConverter.swift in Sources */, + ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */, ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */, ABC9A3BC9A18F74818EF5C17 /* MetadataMonitor.swift in Sources */, ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */, @@ -9117,8 +9121,9 @@ 11B35FFD159D864F6D914F08 /* AppearanceView.swift in Sources */, 11B350CA618DD7BBA452FC33 /* AppearanceViewModel.swift in Sources */, ABC9A13D78DD5F176A170B65 /* FullBackup.swift in Sources */, - ABC9AA39ED35D6EF41A5353D /* AppearanceBackup.swift in Sources */, + ABC9AA39ED35D6EF41A5353D /* SettingsBackup.swift in Sources */, ABC9AE1E60CABA0101D62738 /* FullCoin.swift in Sources */, + ABC9A6A9C28C95352232B062 /* ThemeMode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9349,7 +9354,7 @@ 11B35A108457DC44DD870138 /* MainBadgeService.swift in Sources */, 11B35EF9D9E8C1A814005CFD /* MainViewModel.swift in Sources */, 11B35CE67B7F5C5A5244C951 /* MainService.swift in Sources */, - 6BCD531C2A16203F00993F20 /* CloudAccountBackupManager.swift in Sources */, + 6BCD531C2A16203F00993F20 /* CloudBackupManager.swift in Sources */, 58AAA996A8547DBE1BF378CE /* SwapApproveService.swift in Sources */, 58AAA4915E1B70248A8DC620 /* SwapApproveViewModel.swift in Sources */, 58AAA7B99324DDA9C53692AD /* SwapApproveViewController.swift in Sources */, @@ -10224,10 +10229,10 @@ ABC9A191F1E62A20D2D38262 /* BackupCloudPassphraseViewModel.swift in Sources */, ABC9AB83EE3F909BD80E0539 /* BackupCryptoHelper.swift in Sources */, ABC9A60BA5DF119C7FC8A859 /* KdfParams.swift in Sources */, - ABC9A6887B716464A5813EE9 /* WalletBackupCrypto.swift in Sources */, + ABC9A6887B716464A5813EE9 /* BackupCrypto.swift in Sources */, ABC9AB6EB596E2F8B15D00E4 /* CipherParams.swift in Sources */, ABC9AF9C828BEBB740468204 /* WalletBackup.swift in Sources */, - ABC9A7EACB2FA65355C2BA4E /* WalletBackupConverter.swift in Sources */, + ABC9A7EACB2FA65355C2BA4E /* AppBackupProvider.swift in Sources */, ABC9AF5B0B1D5FE002288AE1 /* FileStorage.swift in Sources */, ABC9AF371FBB4BEA654A78B6 /* MetadataMonitor.swift in Sources */, ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */, @@ -10396,8 +10401,9 @@ 11B35245CD0D5B0E44E413F4 /* AppearanceView.swift in Sources */, 11B35A18AA61F8C06AB1C15B /* AppearanceViewModel.swift in Sources */, ABC9A2C4301447E0EEA1D16F /* FullBackup.swift in Sources */, - ABC9A99861B1F83A19EA370D /* AppearanceBackup.swift in Sources */, + ABC9A99861B1F83A19EA370D /* SettingsBackup.swift in Sources */, ABC9A3EA19771B14B0502A0A /* FullCoin.swift in Sources */, + ABC9A5A4C6213D58CDA2EB73 /* ThemeMode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index bb6e55aaae..b519b4335a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -6,6 +6,7 @@ import MarketKit import PinKit import StorageKit import ThemeKit +import LanguageKit class App { static var instance: App? @@ -105,7 +106,8 @@ class App { let appManager: AppManager let contactManager: ContactBookManager - let cloudAccountBackupManager: CloudAccountBackupManager + let appBackupProvider: AppBackupProvider + let cloudBackupManager: CloudBackupManager let appEventHandler = EventHandler() @@ -191,8 +193,6 @@ class App { restoreSettingsManager = RestoreSettingsManager(storage: restoreSettingsStorage) predefinedBlockchainService = PredefinedBlockchainService(restoreSettingsManager: restoreSettingsManager) - cloudAccountBackupManager = CloudAccountBackupManager(ubiquityContainerIdentifier: AppConfig.sharedCloudContainer, restoreSettingsManager: restoreSettingsManager, logger: logger) - let hsLabelProvider = HsLabelProvider(networkManager: networkManager) let evmLabelStorage = EvmLabelStorage(dbPool: dbPool) let syncerStateStorage = SyncerStateStorage(dbPool: dbPool) @@ -306,6 +306,30 @@ class App { let cexAssetRecordStorage = CexAssetRecordStorage(dbPool: dbPool) cexAssetManager = CexAssetManager(accountManager: accountManager, marketKit: marketKit, storage: cexAssetRecordStorage) + appBackupProvider = AppBackupProvider( + accountManager: accountManager, + accountFactory: accountFactory, + walletManager: walletManager, + favoritesManager: favoritesManager, + evmSyncSourceManager: evmSyncSourceManager, + restoreSettingsManager: restoreSettingsManager, + localStorage: localStorage, + languageManager: LanguageManager.shared, + currencyKit: currencyKit, + themeManager: themeManager, + launchScreenManager: launchScreenManager, + appIconManager: appIconManager, + balancePrimaryValueManager: balancePrimaryValueManager, + balanceConversionManager: balanceConversionManager, + balanceHiddenManager: balanceHiddenManager, + contactManager: contactManager + ) + cloudBackupManager = CloudBackupManager( + ubiquityContainerIdentifier: AppConfig.sharedCloudContainer, + appBackupProvider: appBackupProvider, + logger: logger + ) + appManager = AppManager( accountManager: accountManager, walletManager: walletManager, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift deleted file mode 100644 index 271721518a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/AppearanceBackup.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import Chart -import CurrencyKit -import ThemeKit - -struct AppearanceBackup { - let lockTimeEnabled: Bool - let remoteContactsSync: Bool - let defaultProviders: [DefaultProvider] - let chartIndicators: [ChartIndicator] - let indicatorsShown: Bool - let currentLanguage: String - let baseCurrency: Currency - - let mode: ThemeMode - let showMarketTab: Bool - let launchScreen: LaunchScreen - let conversionTokenQueryId: String? - let balancePrimaryValue: BalancePrimaryValue - let balanceAutoHide: Bool - let appIcon: AppIcon -} - -extension AppearanceBackup { - struct DefaultProvider { - let blockchainTypeId: String - let provider: String - } -} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift new file mode 100644 index 0000000000..e3fb6d3577 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift @@ -0,0 +1,141 @@ +import Foundation + +class BackupCrypto: Codable { + static var defaultBackup = KdfParams(dklen: 32, n: 16384, p: 4, r: 8, salt: AppConfig.backupSalt) + + let cipher: String + let cipherParams: CipherParams + let cipherText: String + let kdf: String + let kdfParams: KdfParams + let mac: String + + enum CodingKeys: String, CodingKey { + case cipher + case cipherParams = "cipherparams" + case cipherText = "ciphertext" + case kdf + case kdfParams = "kdfparams" + case mac + } + + init(cipher: String, cipherParams: CipherParams, cipherText: String, kdf: String, kdfParams: KdfParams, mac: String) { + self.cipher = cipher + self.cipherParams = cipherParams + self.cipherText = cipherText + self.kdf = kdf + self.kdfParams = kdfParams + self.mac = mac + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + cipher = try container.decode(String.self, forKey: .cipher) + cipherParams = try container.decode(CipherParams.self, forKey: .cipherParams) + cipherText = try container.decode(String.self, forKey: .cipherText) + kdf = try container.decode(String.self, forKey: .kdf) + kdfParams = try container.decode(KdfParams.self, forKey: .kdfParams) + mac = try container.decode(String.self, forKey: .mac) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(cipher, forKey: .cipher) + try container.encode(cipherParams, forKey: .cipherParams) + try container.encode(cipherText, forKey: .cipherText) + try container.encode(kdf, forKey: .kdf) + try container.encode(kdfParams, forKey: .kdfParams) + try container.encode(mac, forKey: .mac) + } +} + +extension BackupCrypto { + func data(passphrase: String) throws -> Data { + try Self.validate(passphrase: passphrase) + // Validation data + guard let data = Data(base64Encoded: cipherText) else { + throw RestoreCloudModule.RestoreError.invalidBackup + } + + // validation passphrase + let isValid = (try? BackupCryptoHelper.isValid( + macHex: mac, + pass: passphrase, + message: cipherText.hs.data, + kdf: kdfParams + )) ?? false + guard isValid else { + throw RestoreCloudModule.RestoreError.invalidPassword + } + + return try BackupCryptoHelper.AES128( + operation: .decrypt, + ivHex: cipherParams.iv, + pass: passphrase, + message: data, + kdf: kdfParams + ) + } + + func accountType(type: AccountType.Abstract, passphrase: String) throws -> AccountType { + let data = try data(passphrase: passphrase) + + guard let accountType = AccountType.decode(uniqueId: data, type: type) else { + throw RestoreCloudModule.RestoreError.invalidBackup + } + + return accountType + } +} + +extension BackupCrypto { + static func validate(passphrase: String) throws { + // Validation passphrase + guard !passphrase.isEmpty else { + throw ValidationError.emptyPassphrase + } + guard passphrase.count >= BackupCloudModule.minimumPassphraseLength else { + throw ValidationError.simplePassword + } + + let allSatisfy = BackupCloudModule.PassphraseCharacterSet.allCases.allSatisfy { set in set.contains(passphrase) } + if !allSatisfy { + throw ValidationError.simplePassword + } + } + + static func instance(data: Data, passphrase: String, kdf: KdfParams = .defaultBackup) throws -> BackupCrypto { + let iv = BackupCryptoHelper.generateInitialVector().hs.hex + + let cipherText = try BackupCryptoHelper.AES128( + operation: .encrypt, + ivHex: iv, + pass: passphrase, + message: data, + kdf: kdf + ) + + let encodedCipherText = cipherText.base64EncodedString() + let mac = try BackupCryptoHelper.mac( + pass: passphrase, + message: encodedCipherText.hs.data, + kdf: kdf + ) + + return BackupCrypto( + cipher: BackupCryptoHelper.defaultCypher, + cipherParams: CipherParams(iv: iv), + cipherText: encodedCipherText, + kdf: BackupCryptoHelper.defaultKdf, + kdfParams: kdf, + mac: mac.hs.hex + ) + } +} + +extension BackupCrypto { + enum ValidationError: Error { + case emptyPassphrase + case simplePassword + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift index abc65b27c8..6a2fc7d82c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -1,8 +1,44 @@ import Foundation struct FullBackup { - let wallets: [WalletBackup] + let id: String + let wallets: [RestoreCloudModule.RestoredBackup] let watchlistIds: [String] - let contacts: ContactBook? - let appearance: AppearanceBackup? + let contacts: BackupCrypto? + let settings: SettingsBackup? + let version: Int + let timestamp: TimeInterval? +} + +extension FullBackup: Codable { + enum CodingKeys: String, CodingKey { + case id + case wallets + case watchlistIds = "watchlist" + case contacts + case settings + case version + case timestamp + } + +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// wallets = (try? container.decode([RestoreCloudModule.RestoredBackup].self, forKey: .wallets)) ?? [] +// watchlistIds = (try? container.decode([String].self, forKey: .watchlistIds)) ?? [] +// contacts = try? container.decode([BackupContact].self, forKey: .contacts) +// evmSyncSources = try? container.decode(SyncSourceBackup.self, forKey: .evmSyncSources) +// settings = try? container.decode(SettingsBackup.self, forKey: .settings) +// } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + if !wallets.isEmpty { try container.encode(wallets, forKey: .wallets) } + if !watchlistIds.isEmpty { try container.encode(watchlistIds, forKey: .watchlistIds) } + if let contacts { try container.encode(contacts, forKey: .contacts) } + if let settings { try container.encode(settings, forKey: .settings) } + try container.encode(version, forKey: .version) + try container.encode(version, forKey: .version) + try? container.encode(timestamp, forKey: .timestamp) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift new file mode 100644 index 0000000000..b4e3643a4e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift @@ -0,0 +1,55 @@ +import Foundation +import Chart +import CurrencyKit +import ThemeKit + +struct SettingsBackup: Codable { + let evmSyncSources: EvmSyncSourceManager.SyncSourceBackup + + let lockTimeEnabled: Bool + let remoteContactsSync: Bool + let defaultProviders: [DefaultProvider] + let chartIndicators: [ChartIndicator] + let indicatorsShown: Bool + let currentLanguage: String + let baseCurrency: String + + let mode: ThemeMode + let showMarketTab: Bool + let launchScreen: LaunchScreen + let conversionTokenQueryId: String? + let balancePrimaryValue: BalancePrimaryValue + let balanceAutoHide: Bool + let appIcon: String + + enum CodingKeys: String, CodingKey { + case evmSyncSources = "evm_sync_sources" + case lockTimeEnabled = "lock_time" + case remoteContactsSync = "contacts_sync" + case defaultProviders = "default_providers" + case chartIndicators = "indicators" + case indicatorsShown = "indicators_shown" + case currentLanguage = "language" + case baseCurrency = "currency" + case mode = "theme_mode" + case showMarketTab = "show_market" + case launchScreen = "launch_screen" + case conversionTokenQueryId = "conversion_token_query_id" + case balancePrimaryValue = "balance_primary_value" + case balanceAutoHide = "balance_auto_hide" + case appIcon = "app_icon" + } + +} + +extension SettingsBackup { + struct DefaultProvider: Codable { + enum CodingKeys: String, CodingKey { + case blockchainTypeId = "blockchain_type_id" + case provider + } + + let blockchainTypeId: String + let provider: String + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift index e2691ab3f7..df55844c58 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift @@ -1,7 +1,7 @@ import Foundation class WalletBackup: Codable { - let crypto: WalletBackupCrypto + let crypto: BackupCrypto let id: String let type: AccountType.Abstract let isManualBackedUp: Bool @@ -19,19 +19,19 @@ class WalletBackup: Codable { case timestamp } - init(crypto: WalletBackupCrypto, enabledWallets: [EnabledWallet], id: String, type: AccountType.Abstract, isManualBackedUp: Bool, version: Int, timestamp: TimeInterval) { + init(crypto: BackupCrypto, enabledWallets: [EnabledWallet], id: String, type: AccountType.Abstract, isManualBackedUp: Bool, version: Int, timestamp: TimeInterval) { self.crypto = crypto self.enabledWallets = enabledWallets self.id = id self.type = type self.isManualBackedUp = isManualBackedUp self.version = version - self.timestamp = timestamp.rounded() + self.timestamp = timestamp } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - crypto = try container.decode(WalletBackupCrypto.self, forKey: .crypto) + crypto = try container.decode(BackupCrypto.self, forKey: .crypto) enabledWallets = (try? container.decode([EnabledWallet].self, forKey: .enabledWallets)) ?? [] id = try container.decode(String.self, forKey: .id) type = try container.decode(AccountType.Abstract.self, forKey: .type) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackupCrypto.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackupCrypto.swift deleted file mode 100644 index e992aade68..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackupCrypto.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation - -class WalletBackupCrypto: Codable { - static var defaultBackup = KdfParams(dklen: 32, n: 16384, p: 4, r: 8, salt: AppConfig.backupSalt) - - let cipher: String - let cipherParams: CipherParams - let cipherText: String - let kdf: String - let kdfParams: KdfParams - let mac: String - - enum CodingKeys: String, CodingKey { - case cipher - case cipherParams = "cipherparams" - case cipherText = "ciphertext" - case kdf - case kdfParams = "kdfparams" - case mac - } - - init(cipher: String, cipherParams: CipherParams, cipherText: String, kdf: String, kdfParams: KdfParams, mac: String) { - self.cipher = cipher - self.cipherParams = cipherParams - self.cipherText = cipherText - self.kdf = kdf - self.kdfParams = kdfParams - self.mac = mac - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - cipher = try container.decode(String.self, forKey: .cipher) - cipherParams = try container.decode(CipherParams.self, forKey: .cipherParams) - cipherText = try container.decode(String.self, forKey: .cipherText) - kdf = try container.decode(String.self, forKey: .kdf) - kdfParams = try container.decode(KdfParams.self, forKey: .kdfParams) - mac = try container.decode(String.self, forKey: .mac) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(cipher, forKey: .cipher) - try container.encode(cipherParams, forKey: .cipherParams) - try container.encode(cipherText, forKey: .cipherText) - try container.encode(kdf, forKey: .kdf) - try container.encode(kdfParams, forKey: .kdfParams) - try container.encode(mac, forKey: .mac) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift index 48d366230f..c8054b88d7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift @@ -66,4 +66,11 @@ extension BalanceConversionManager { func set(conversionToken: Token?) { self.conversionToken = conversionToken } + + func set(tokenQueryId: String?) { + conversionToken = tokenQueryId + .flatMap { TokenQuery(id: $0) } + .flatMap { try? marketKit.token(query: $0) } ?? + conversionTokens.first + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift similarity index 67% rename from UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift rename to UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift index e8000f493b..7f26b1c417 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudAccountBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift @@ -3,13 +3,13 @@ import Foundation import HsExtensions import HsToolKit -class CloudAccountBackupManager { +class CloudBackupManager { private static let batchingInterval: TimeInterval = 1 private static let fileExtension = ".json" private let ubiquityContainerIdentifier: String? private let fileStorage: FileStorage - private let restoreSettingsManager: RestoreSettingsManager + private let appBackupProvider: AppBackupProvider private let logger: Logger? private var metadataMonitor: MetadataMonitor? @@ -22,12 +22,13 @@ class CloudAccountBackupManager { .appendingPathComponent("Documents") } - @PostPublished private(set) var items = [String: WalletBackup]() + @PostPublished private(set) var oneWalletItems = [String: WalletBackup]() + @PostPublished private(set) var fullBackupItems = [String: FullBackup]() @PostPublished private(set) var state = State.loading - init(ubiquityContainerIdentifier: String?, restoreSettingsManager: RestoreSettingsManager, logger: Logger?) { + init(ubiquityContainerIdentifier: String?, appBackupProvider: AppBackupProvider, logger: Logger?) { self.ubiquityContainerIdentifier = ubiquityContainerIdentifier - self.restoreSettingsManager = restoreSettingsManager + self.appBackupProvider = appBackupProvider fileStorage = FileStorage(logger: logger) self.logger = logger @@ -70,11 +71,14 @@ class CloudAccountBackupManager { do { forceDownloadContainerFiles(url: url) - let items = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) + let oneWalletItems: [String: WalletBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) + let fullBackupItems: [String: FullBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) state = .success logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)") - self.items = items + + self.oneWalletItems = oneWalletItems + self.fullBackupItems = fullBackupItems } catch { state = .error(error) logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)") @@ -98,14 +102,14 @@ class CloudAccountBackupManager { } } - private static func downloadItems(url: URL, fileStorage: FileStorage, logger: Logger? = nil) throws -> [String: WalletBackup] { + private static func downloadItems(url: URL, fileStorage: FileStorage, logger: Logger? = nil) throws -> [String: T] { let files = try fileStorage.fileList(url: url).filter { s in s.contains(Self.fileExtension) } - var items = [String: WalletBackup]() + var items = [String: T]() for file in files { do { let data = try fileStorage.read(directoryUrl: url, filename: file) - let backup = try JSONDecoder().decode(WalletBackup.self, from: data) + let backup = try JSONDecoder().decode(T.self, from: data) items[file] = backup } catch { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't read \(file). Because: \(error)") @@ -115,45 +119,66 @@ class CloudAccountBackupManager { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, read \(items.count) files") return items } + + private func save(encoded: Data, name: String) throws { + guard let iCloudUrl else { + throw BackupError.urlNotAvailable + } + + do { + let name = name + Self.fileExtension + + try fileStorage.write(directoryUrl: iCloudUrl, filename: name, data: encoded) + logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, save \(name)") + } catch { + logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") + throw error + } + } } -extension CloudAccountBackupManager { +extension CloudBackupManager { func backedUp(uniqueId: Data) -> Bool { - items.contains { _, backup in backup.id == uniqueId.hs.hex } + oneWalletItems.contains { _, backup in backup.id == uniqueId.hs.hex } } var existFilenames: [String] { - items.map { ($0.key as NSString).deletingPathExtension } + oneWalletItems.map { ($0.key as NSString).deletingPathExtension } + + fullBackupItems.map { ($0.key as NSString).deletingPathExtension } } } -extension CloudAccountBackupManager { +extension CloudBackupManager { var isAvailable: Bool { iCloudUrl != nil } - func save(account: Account, wallets: [Wallet], isManualBackedUp: Bool, passphrase: String, name: String) throws { - guard let iCloudUrl else { - throw BackupError.urlNotAvailable - } + func save(account: Account, passphrase: String, name: String) throws { + let backup = try appBackupProvider.walletBackup( + account: account, + passphrase: passphrase + ) do { - let name = name + Self.fileExtension - let encoded = try WalletBackupConverter.encode( - accountType: account.type, - wallets: wallets.map { - let settings = restoreSettingsManager - .settings(accountId: account.id, blockchainType: $0.token.blockchainType) - .reduce(into: [:], { $0[$1.0.rawValue] = $1.1 }) - - return WalletBackup.EnabledWallet($0, settings: settings) - }, - isManualBackedUp: isManualBackedUp, + let encoded = try JSONEncoder().encode(backup) + try save(encoded: encoded, name: name) + } catch { + logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") + throw error + } + } + + func save(fields: [AppBackupProvider.Field], passphrase: String, name: String) throws { + let backup = try appBackupProvider.fullBackup( + fields: fields, passphrase: passphrase - ) + ) - try fileStorage.write(directoryUrl: iCloudUrl, filename: name, data: encoded) - logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, save \(name)") + do { + let encoder = JSONEncoder() + let data = try encoder.encode(backup) + let encoded = try JSONEncoder().encode(backup) + try save(encoded: encoded, name: name) } catch { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") throw error @@ -170,7 +195,7 @@ extension CloudAccountBackupManager { throw BackupError.urlNotAvailable } - guard let item = items.first(where: { _, backup in backup.id == uniqueId }) else { + guard let item = oneWalletItems.first(where: { _, backup in backup.id == uniqueId }) else { throw BackupError.itemNotFound } @@ -179,8 +204,7 @@ extension CloudAccountBackupManager { try fileStorage.deleteFile(url: fileUrl) // system will automatically updates items but after 1-2 seconds. So we need force update - items[item.key] = nil - + oneWalletItems[item.key] = nil logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) successful") } catch { logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) unsuccessful because: \(error)") @@ -189,7 +213,7 @@ extension CloudAccountBackupManager { } } -extension CloudAccountBackupManager { +extension CloudBackupManager { enum BackupError: Error { case urlNotAvailable case itemNotFound diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift index b5194abbfe..e4561c32a1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift @@ -1,8 +1,8 @@ -import Foundation -import RxSwift -import RxRelay import EvmKit +import Foundation import MarketKit +import RxRelay +import RxSwift class EvmSyncSourceManager { private let testNetManager: TestNetManager @@ -31,11 +31,9 @@ class EvmSyncSourceManager { default: fatalError("Non-supported EVM blockchain") } } - } extension EvmSyncSourceManager { - var syncSourceObservable: Observable { syncSourceRelay.asObservable() } @@ -50,141 +48,141 @@ extension EvmSyncSourceManager { if testNetManager.testNetEnabled { return [ EvmSyncSource( - name: "Infura Sepolia", - rpcSource: .http(urls: [URL(string: "https://sepolia.infura.io/v3/\(AppConfig.infuraCredentials.id)")!], auth: AppConfig.infuraCredentials.secret), - transactionSource: EvmKit.TransactionSource( - name: "sepolia.etherscan.io", - type: .etherscan(apiBaseUrl: "https://api-sepolia.etherscan.io", txBaseUrl: "https://sepiloa.etherscan.io", apiKey: AppConfig.etherscanKey) - ) - ) + name: "Infura Sepolia", + rpcSource: .http(urls: [URL(string: "https://sepolia.infura.io/v3/\(AppConfig.infuraCredentials.id)")!], auth: AppConfig.infuraCredentials.secret), + transactionSource: EvmKit.TransactionSource( + name: "sepolia.etherscan.io", + type: .etherscan(apiBaseUrl: "https://api-sepolia.etherscan.io", txBaseUrl: "https://sepiloa.etherscan.io", apiKey: AppConfig.etherscanKey) + ) + ), ] } else { return [ EvmSyncSource( - name: "Infura", - rpcSource: .ethereumInfuraWebsocket(projectId: AppConfig.infuraCredentials.id, projectSecret: AppConfig.infuraCredentials.secret), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Infura", + rpcSource: .ethereumInfuraWebsocket(projectId: AppConfig.infuraCredentials.id, projectSecret: AppConfig.infuraCredentials.secret), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Infura", - rpcSource: .ethereumInfuraHttp(projectId: AppConfig.infuraCredentials.id, projectSecret: AppConfig.infuraCredentials.secret), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Infura", + rpcSource: .ethereumInfuraHttp(projectId: AppConfig.infuraCredentials.id, projectSecret: AppConfig.infuraCredentials.secret), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "LlamaNodes", - rpcSource: .http(urls: [URL(string: "https://eth.llamarpc.com")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "LlamaNodes", + rpcSource: .http(urls: [URL(string: "https://eth.llamarpc.com")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] } case .binanceSmartChain: if testNetManager.testNetEnabled { return [ EvmSyncSource( - name: "Binance TestNet", - rpcSource: .http(urls: [URL(string: "https://data-seed-prebsc-1-s1.binance.org:8545")!], auth: nil), - transactionSource: EvmKit.TransactionSource( - name: "testnet.bscscan.com", - type: .etherscan(apiBaseUrl: "https://api-testnet.bscscan.com", txBaseUrl: "https://testnet.bscscan.com", apiKey: AppConfig.bscscanKey) - ) - ) + name: "Binance TestNet", + rpcSource: .http(urls: [URL(string: "https://data-seed-prebsc-1-s1.binance.org:8545")!], auth: nil), + transactionSource: EvmKit.TransactionSource( + name: "testnet.bscscan.com", + type: .etherscan(apiBaseUrl: "https://api-testnet.bscscan.com", txBaseUrl: "https://testnet.bscscan.com", apiKey: AppConfig.bscscanKey) + ) + ), ] } else { return [ EvmSyncSource( - name: "Binance", - rpcSource: .binanceSmartChainHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Binance", + rpcSource: .binanceSmartChainHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "BSC RPC", - rpcSource: .bscRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "BSC RPC", + rpcSource: .bscRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Omnia", - rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/bsc/mainnet/public")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "Omnia", + rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/bsc/mainnet/public")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] } case .polygon: return [ EvmSyncSource( - name: "Polygon RPC", - rpcSource: .polygonRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Polygon RPC", + rpcSource: .polygonRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "LlamaNodes", - rpcSource: .http(urls: [URL(string: "https://polygon.llamarpc.com")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "LlamaNodes", + rpcSource: .http(urls: [URL(string: "https://polygon.llamarpc.com")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] case .avalanche: return [ EvmSyncSource( - name: "Avax Network", - rpcSource: .avaxNetworkHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Avax Network", + rpcSource: .avaxNetworkHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "PublicNode", - rpcSource: .http(urls: [URL(string: "https://avalanche-evm.publicnode.com")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "PublicNode", + rpcSource: .http(urls: [URL(string: "https://avalanche-evm.publicnode.com")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] case .optimism: return [ EvmSyncSource( - name: "Optimism", - rpcSource: .optimismRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Optimism", + rpcSource: .optimismRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Omnia", - rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/op/mainnet/public")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "Omnia", + rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/op/mainnet/public")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] case .arbitrumOne: return [ EvmSyncSource( - name: "Arbitrum", - rpcSource: .arbitrumOneRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Arbitrum", + rpcSource: .arbitrumOneRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Omnia", - rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/arbitrum/one/public")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "Omnia", + rpcSource: .http(urls: [URL(string: "https://endpoints.omniatech.io/v1/arbitrum/one/public")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] case .gnosis: return [ EvmSyncSource( - name: "Gnosis Chain", - rpcSource: .gnosisRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Gnosis Chain", + rpcSource: .gnosisRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Ankr", - rpcSource: .http(urls: [URL(string: "https://rpc.ankr.com/gnosis")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "Ankr", + rpcSource: .http(urls: [URL(string: "https://rpc.ankr.com/gnosis")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] case .fantom: return [ EvmSyncSource( - name: "Fantom Chain", - rpcSource: .fantomRpcHttp(), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: "Fantom Chain", + rpcSource: .fantomRpcHttp(), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ), EvmSyncSource( - name: "Ankr", - rpcSource: .http(urls: [URL(string: "https://rpc.ankr.com/fantom")!], auth: nil), - transactionSource: defaultTransactionSource(blockchainType: blockchainType) - ) + name: "Ankr", + rpcSource: .http(urls: [URL(string: "https://rpc.ankr.com/fantom")!], auth: nil), + transactionSource: defaultTransactionSource(blockchainType: blockchainType) + ), ] default: return [] @@ -209,9 +207,9 @@ extension EvmSyncSourceManager { } return EvmSyncSource( - name: url.host ?? "", - rpcSource: rpcSource, - transactionSource: defaultTransactionSource(blockchainType: blockchainType) + name: url.host ?? "", + rpcSource: rpcSource, + transactionSource: defaultTransactionSource(blockchainType: blockchainType) ) } } catch { @@ -227,7 +225,8 @@ extension EvmSyncSourceManager { let syncSources = allSyncSources(blockchainType: blockchainType) if let urlString = blockchainSettingsStorage.evmSyncSourceUrl(blockchainType: blockchainType), - let syncSource = syncSources.first(where: { $0.rpcSource.url.absoluteString == urlString }) { + let syncSource = syncSources.first(where: { $0.rpcSource.url.absoluteString == urlString }) + { return syncSource } @@ -238,7 +237,8 @@ extension EvmSyncSourceManager { let syncSources = allSyncSources(blockchainType: blockchainType) if let urlString = blockchainSettingsStorage.evmSyncSourceUrl(blockchainType: blockchainType), - let syncSource = syncSources.first(where: { $0.rpcSource.url.absoluteString == urlString }), syncSource.isHttp { + let syncSource = syncSources.first(where: { $0.rpcSource.url.absoluteString == urlString }), syncSource.isHttp + { return syncSource } @@ -252,9 +252,9 @@ extension EvmSyncSourceManager { func saveSyncSource(blockchainType: BlockchainType, url: URL, auth: String?) { let record = EvmSyncSourceRecord( - blockchainTypeUid: blockchainType.uid, - url: url.absoluteString, - auth: auth + blockchainTypeUid: blockchainType.uid, + url: url.absoluteString, + auth: auth ) try? evmSyncSourceStorage.save(record: record) @@ -277,5 +277,92 @@ extension EvmSyncSourceManager { syncSourcesUpdatedRelay.accept(blockchainType) } +} + +extension EvmSyncSourceManager { + func backup(passphrase: String) -> SyncSourceBackup { + let customSources = ((try? evmSyncSourceStorage.getAll()) ?? []) + .map { record in + let crypto = record.auth + .flatMap { $0.isEmpty ? nil : $0 } + .flatMap { $0.data(using: .utf8) } + .flatMap { try? BackupCrypto.instance(data: $0, passphrase: passphrase) } + + return CustomSyncSource( + blockchainTypeUid: record.blockchainTypeUid, + url: record.url, + auth: crypto + ) + } + let selected = BlockchainType + .supported + .filter { !$0.allowedProviders.isEmpty } + .map { type in + SelectedSource( + blockchainTypeUid: type.uid, + url: syncSource(blockchainType: type).rpcSource.url.absoluteString + ) + } + return .init(selected: selected, custom: customSources) + } + + func restore(backup: SyncSourceBackup, passphrase: String? = nil) { + var blockchainTypes = Set() + backup.custom.forEach { source in + let auth: String? = source.auth.flatMap { + guard let passphrase else { return nil } + return try? $0.data(passphrase: passphrase) + } + .flatMap { String(data: $0, encoding: .utf8) } + let record = EvmSyncSourceRecord( + blockchainTypeUid: source.blockchainTypeUid, + url: source.url, + auth: auth + ) + + blockchainTypes.insert(BlockchainType(uid: source.blockchainTypeUid)) + try? evmSyncSourceStorage.save(record: record) + } + + backup.selected.forEach { source in + let blockchainType = BlockchainType(uid: source.blockchainTypeUid) + if let syncSource = allSyncSources(blockchainType: blockchainType) + .first(where: { $0.rpcSource.url.absoluteString == source.url }) { + saveCurrent(syncSource: syncSource, blockchainType: blockchainType) + } + } + + blockchainTypes.forEach { blockchainType in + syncSourcesUpdatedRelay.accept(blockchainType) + } + } +} + +extension EvmSyncSourceManager { + struct SelectedSource: Codable { + let blockchainTypeUid: String + let url: String + + enum CodingKeys: String, CodingKey { + case blockchainTypeUid = "blockchain_type_id" + case url + } + } + struct CustomSyncSource: Codable { + let blockchainTypeUid: String + let url: String + let auth: BackupCrypto? + + enum CodingKeys: String, CodingKey { + case blockchainTypeUid = "blockchain_type_id" + case url + case auth + } + } + + struct SyncSourceBackup: Codable { + let selected: [SelectedSource] + let custom: [CustomSyncSource] + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift index 1a8e2182f3..8002df04ba 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift @@ -415,7 +415,15 @@ extension ContactBookManager { return contacts } - func restore(contacts:[BackupContact]) throws { + func restore(crypto: BackupCrypto, passphrase: String) throws { + let data = try crypto.data(passphrase: passphrase) + let decoder = JSONDecoder() + let contacts = try decoder.decode([BackupContact].self, from: data) + + try restore(contacts: contacts) + } + + func restore(contacts: [BackupContact]) throws { guard let localUrl else { state = .failed(ContactBookManager.StorageError.localUrlNotAvailable) return diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/EvmSyncSourceStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/EvmSyncSourceStorage.swift index d408b1d25a..b6222c4f10 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/EvmSyncSourceStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/EvmSyncSourceStorage.swift @@ -11,6 +11,12 @@ class EvmSyncSourceStorage { extension EvmSyncSourceStorage { + func getAll() throws -> [EvmSyncSourceRecord] { + try dbPool.read { db in + try EvmSyncSourceRecord.fetchAll(db) + } + } + func records(blockchainTypeUid: String) throws -> [EvmSyncSourceRecord] { try dbPool.read { db in try EvmSyncSourceRecord.filter(EvmSyncSourceRecord.Columns.blockchainTypeUid == blockchainTypeUid).fetchAll(db) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift index 89eb3dd144..400355c607 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift @@ -31,6 +31,13 @@ extension FavoriteCoinRecordStorage { } } + func deleteAll() { + _ = try! dbPool.write { db in + try FavoriteCoinRecord + .deleteAll(db) + } + } + func deleteFavoriteCoinRecord(coinUid: String) { _ = try! dbPool.write { db in try FavoriteCoinRecord diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift index e7549ad6cc..cbe9fee884 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift @@ -101,60 +101,15 @@ extension LocalStorage { } extension LocalStorage { - var backup: [String: Any] { - var fields: [String: Any] = [ - "lock_time_enabled": lockTimeEnabled.description, - "contacts_sync": remoteContactsSync.description, - "show_indicators": indicatorsShown.description, - ] - - if let chartIndicators { - fields["chart_indicators"] = chartIndicators.hs.hexString - } - - let providers: [String: String] = BlockchainType - .supported - .filter { !$0.allowedProviders.isEmpty } - .map { ($0.uid, defaultProvider(blockchainType: $0).rawValue) } - .reduce(into: [:]) { $0[$1.0] = $1.1 } - - fields["swap_providers"] = providers - - return fields - } - - private func bool(value: Any?) -> Bool? { - if let string = value as? String, - let enabled = Bool(string) { - return enabled - } - return nil - } - - func restore(backup _: [String: Any]) { - if let lockTime = bool(value: backup["lock_time_enabled"]) { - lockTimeEnabled = lockTime - } - if let contactsSync = bool(value: backup["contacts_sync"]) { - remoteContactsSync = contactsSync - } - if let showIndicators = bool(value: backup["show_indicators"]) { - indicatorsShown = showIndicators - } - - if let chartIndicators = backup["chart_indicators"] as? String, - let data = chartIndicators.hs.hexData { - self.chartIndicators = data - } - - if let providers = backup["swap_providers"] as? [String: String] { - providers.forEach { key, value in - let type = BlockchainType(uid: key) - if let provider = SwapModule.Dex.Provider(rawValue: value), - !type.allowedProviders.isEmpty { - - setDefaultProvider(blockchainType: type, provider: provider) - } + func restore(backup: SettingsBackup) { + lockTimeEnabled = backup.lockTimeEnabled + remoteContactsSync = backup.remoteContactsSync + indicatorsShown = backup.indicatorsShown + chartIndicators = backup.chartIndicators.encoded //todo: + backup.defaultProviders.forEach { provider in + let blockchainType = BlockchainType(uid: provider.blockchainTypeId) + if let dexProvider = SwapModule.Dex.Provider(rawValue: provider.provider) { + return setDefaultProvider(blockchainType: blockchainType, provider: dexProvider) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/ThemeMode.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/ThemeMode.swift new file mode 100644 index 0000000000..0b4e5b9a96 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/ThemeMode.swift @@ -0,0 +1,3 @@ +import ThemeKit + +extension ThemeMode: Codable {} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BackupContact.swift b/UnstoppableWallet/UnstoppableWallet/Models/BackupContact.swift index 344f5328bf..ae0b6c698e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BackupContact.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BackupContact.swift @@ -1,7 +1,7 @@ import Foundation import ObjectMapper -class BackupContact: ImmutableMappable, Hashable, Equatable { +class BackupContact: Codable, ImmutableMappable, Hashable, Equatable { let uid: String let name: String let addresses: [ContactAddress] @@ -19,12 +19,12 @@ class BackupContact: ImmutableMappable, Hashable, Equatable { } func mapping(map: Map) { - uid >>> map["uid"] - name >>> map["name"] - addresses >>> map["addresses"] + uid >>> map["uid"] + name >>> map["name"] + addresses >>> map["addresses"] } - static func ==(lhs: BackupContact, rhs: BackupContact) -> Bool { + static func == (lhs: BackupContact, rhs: BackupContact) -> Bool { lhs.uid == rhs.uid } @@ -33,17 +33,15 @@ class BackupContact: ImmutableMappable, Hashable, Equatable { } func address(blockchainUid: String) -> ContactAddress? { - addresses.first { $0.blockchainUid == blockchainUid } + addresses.first { $0.blockchainUid == blockchainUid } } - } -class BackupContactBook { +class BackupContactBook: Codable { static let empty = BackupContactBook(contacts: []) let contacts: [BackupContact] init(contacts: [BackupContact]) { self.contacts = contacts } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift b/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift index 409abf7688..c4dc1d4c76 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift @@ -1,4 +1,4 @@ -enum BalancePrimaryValue: String, CaseIterable { +enum BalancePrimaryValue: String, CaseIterable, Codable { case coin case currency diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift b/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift index e1e046855d..19710a987e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift @@ -1,23 +1,28 @@ import Foundation import ObjectMapper -class ContactAddress: ImmutableMappable, Hashable, Equatable { +class ContactAddress: Codable, ImmutableMappable, Hashable, Equatable { let blockchainUid: String let address: String + enum CodingKeys: String, CodingKey { + case blockchainUid = "blockchain_uid" + case address = "address" + } + init(blockchainUid: String, address: String) { self.blockchainUid = blockchainUid self.address = address } required init(map: Map) throws { - blockchainUid = try map.value("blockchain_uid") - address = try map.value("address") + blockchainUid = try map.value(CodingKeys.blockchainUid.rawValue) + address = try map.value(CodingKeys.address.rawValue) } func mapping(map: Map) { - blockchainUid >>> map["blockchain_uid"] - address >>> map["address"] + blockchainUid >>> map[CodingKeys.blockchainUid.rawValue] + address >>> map[CodingKeys.address.rawValue] } func hash(into hasher: inout Hasher) { @@ -40,7 +45,7 @@ extension Array where Element == ContactAddress { } -class Contact: ImmutableMappable, Hashable, Equatable { +class Contact: Codable, ImmutableMappable, Hashable, Equatable { let uid: String let modifiedAt: TimeInterval let name: String @@ -81,7 +86,7 @@ class Contact: ImmutableMappable, Hashable, Equatable { } -class DeletedContact: ImmutableMappable, Hashable, Equatable { +class DeletedContact: Codable, ImmutableMappable, Hashable, Equatable { let uid: String let deletedAt: TimeInterval @@ -110,7 +115,7 @@ class DeletedContact: ImmutableMappable, Hashable, Equatable { } -class ContactBook: ImmutableMappable { +class ContactBook: Codable, ImmutableMappable { static let empty = ContactBook(contacts: [], deletedContacts: []) let version: Int let contacts: [Contact] diff --git a/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift b/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift index 9240948c28..6b4e4ec30c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift @@ -23,3 +23,12 @@ enum LaunchScreen: String, CaseIterable { } } + +extension LaunchScreen: Codable { + enum CodingKeys: String, CodingKey { + case auto + case balance + case marketOverview = "market_overview" + case watchlist + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift index f78e5a9a2d..aa65f89a97 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift @@ -15,7 +15,7 @@ struct BackupModule { } static func cloudViewController(account: Account) -> UIViewController { - let service = ICloudBackupTermsService(cloudAccountBackupManager: App.shared.cloudAccountBackupManager, account: account) + let service = ICloudBackupTermsService(cloudAccountBackupManager: App.shared.cloudBackupManager, account: account) let viewModel = ICloudBackupTermsViewModel(service: service) let viewController = ICloudBackupTermsViewController(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift new file mode 100644 index 0000000000..9e6276eb2f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -0,0 +1,291 @@ +import CurrencyKit +import Foundation +import LanguageKit +import MarketKit +import ThemeKit + +class AppBackupProvider { + private static let version = 2 + + private let accountManager: AccountManager + private let accountFactory: AccountFactory + private let walletManager: WalletManager + private let favoritesManager: FavoritesManager + private let evmSyncSourceManager: EvmSyncSourceManager + private let restoreSettingsManager: RestoreSettingsManager + private let localStorage: LocalStorage + private let languageManager: LanguageManager + private let currencyKit: CurrencyKit.Kit + private let themeManager: ThemeManager + private let launchScreenManager: LaunchScreenManager + private let appIconManager: AppIconManager + private let balancePrimaryValueManager: BalancePrimaryValueManager + private let balanceConversionManager: BalanceConversionManager + private let balanceHiddenManager: BalanceHiddenManager + private let contactManager: ContactBookManager + + init(accountManager: AccountManager, + accountFactory: AccountFactory, + walletManager: WalletManager, + favoritesManager: FavoritesManager, + evmSyncSourceManager: EvmSyncSourceManager, + restoreSettingsManager: RestoreSettingsManager, + localStorage: LocalStorage, + languageManager: LanguageManager, + currencyKit: CurrencyKit.Kit, + themeManager: ThemeManager, + launchScreenManager: LaunchScreenManager, + appIconManager: AppIconManager, + balancePrimaryValueManager: BalancePrimaryValueManager, + balanceConversionManager: BalanceConversionManager, + balanceHiddenManager: BalanceHiddenManager, + contactManager: ContactBookManager) + { + self.accountManager = accountManager + self.accountFactory = accountFactory + self.walletManager = walletManager + self.favoritesManager = favoritesManager + self.evmSyncSourceManager = evmSyncSourceManager + self.restoreSettingsManager = restoreSettingsManager + self.localStorage = localStorage + self.languageManager = languageManager + self.currencyKit = currencyKit + self.themeManager = themeManager + self.launchScreenManager = launchScreenManager + self.appIconManager = appIconManager + self.balancePrimaryValueManager = balancePrimaryValueManager + self.balanceConversionManager = balanceConversionManager + self.balanceHiddenManager = balanceHiddenManager + self.contactManager = contactManager + } + + private func walletBackups(ids: [String], passphrase: String) -> [RestoreCloudModule.RestoredBackup] { + ids.compactMap { + accountManager.account(id: $0) + }.compactMap { + try? walletBackup(account: $0, passphrase: passphrase) + } + } +} + +extension AppBackupProvider { + func walletBackup(account: Account, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { + let wallets = App.shared + .walletManager + .wallets(account: account).map { + let settings = restoreSettingsManager + .settings(accountId: account.id, blockchainType: $0.token.blockchainType) + .reduce(into: [:]) { $0[$1.0.rawValue] = $1.1 } + + return WalletBackup.EnabledWallet($0, settings: settings) + } + return try AppBackupProvider.walletBackup( + accountType: account.type, + wallets: wallets, + isManualBackedUp: account.backedUp, + name: account.name, + passphrase: passphrase + ) + } + + func fullBackup(fields: [Field], passphrase: String) throws -> FullBackup { + var wallets = [RestoreCloudModule.RestoredBackup]() + var watchlistIds = [String]() + var contacts = [BackupContact]() + var settings: SettingsBackup? + for field in fields { + switch field { + case let .accounts(ids): + wallets.append(contentsOf: walletBackups(ids: ids, passphrase: passphrase)) + case .watchlist: + watchlistIds = favoritesManager.allCoinUids + case .contacts: + contacts = contactManager.backupContactBook?.contacts ?? [] + case .settings: + let providers: [SettingsBackup.DefaultProvider] = BlockchainType + .supported + .filter { !$0.allowedProviders.isEmpty } + .map { SettingsBackup.DefaultProvider( + blockchainTypeId: $0.uid, + provider: localStorage.defaultProvider(blockchainType: $0).rawValue + ) + } + + settings = SettingsBackup( + evmSyncSources: evmSyncSourceManager.backup(passphrase: passphrase), + lockTimeEnabled: localStorage.lockTimeEnabled, + remoteContactsSync: localStorage.remoteContactsSync, + defaultProviders: providers, + chartIndicators: [], + indicatorsShown: localStorage.indicatorsShown, + currentLanguage: languageManager.currentLanguage, + baseCurrency: currencyKit.baseCurrency.code, + mode: themeManager.themeMode, + showMarketTab: launchScreenManager.showMarket, + launchScreen: launchScreenManager.launchScreen, + conversionTokenQueryId: balanceConversionManager.conversionToken?.tokenQuery.id, + balancePrimaryValue: balancePrimaryValueManager.balancePrimaryValue, + balanceAutoHide: balanceHiddenManager.balanceAutoHide, + appIcon: appIconManager.appIcon.title + ) + } + } + + guard !wallets.isEmpty || + !watchlistIds.isEmpty || + !contacts.isEmpty || + settings != nil + else { + throw CodingError.emptyParameters + } + + + var contactCrypto: BackupCrypto? + if !contacts.isEmpty { + let encoder = JSONEncoder() + let data = try encoder.encode(contacts) + contactCrypto = try BackupCrypto.instance(data: data, passphrase: passphrase) + } + + return FullBackup( + id: UUID().uuidString, + wallets: wallets, + watchlistIds: watchlistIds, + contacts: contactCrypto, + settings: settings, + version: AppBackupProvider.version, + timestamp: Date().timeIntervalSince1970.rounded() + ) + } + + func walletRestore(backup: RestoreCloudModule.RestoredBackup, accountType: AccountType) { + switch accountType { + case .cex: + let account = accountFactory.account( + type: accountType, + origin: .restored, + backedUp: backup.walletBackup.isManualBackedUp, + name: backup.name + ) + accountManager.save(account: account) + default: + let account = accountFactory.account(type: accountType, origin: .restored, backedUp: backup.walletBackup.isManualBackedUp, name: backup.name) + accountManager.save(account: account) + + let wallets = backup.walletBackup.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: account, blockchainType: tokenQuery.blockchainType) + } + } + return EnabledWallet( + tokenQueryId: $0.tokenQueryId, + accountId: account.id, + coinName: $0.coinName, + coinCode: $0.coinCode, + tokenDecimals: $0.tokenDecimals + ) + } + walletManager.save(enabledWallets: wallets) + } + } + + func fullRestore(backup: FullBackup, passphrase: String) throws { + var encryptionError: Error? + var encodedWallets = [(RestoreCloudModule.RestoredBackup, AccountType)]() + backup.wallets.forEach { wallet in + do { + let accountType = try wallet + .walletBackup + .crypto + .accountType(type: wallet.walletBackup.type, passphrase: passphrase) + encodedWallets.append((wallet, accountType)) + } catch { + encryptionError = error + } + } + + if encodedWallets.count != backup.wallets.count { + encryptionError = CodingError.invalidPassword + } + + if let encryptionError { + throw encryptionError + } + // restore only if all wallet was encrypted with password + encodedWallets.forEach { wallet in + walletRestore( + backup: wallet.0, + accountType: wallet.1 + ) + } + + favoritesManager.add(coinUids: backup.watchlistIds) + + if let contacts = backup.contacts { + try contactManager.restore(crypto: contacts, passphrase: passphrase) + } + + if let settings = backup.settings { + evmSyncSourceManager.restore(backup: settings.evmSyncSources) + localStorage.restore(backup: settings) + languageManager.currentLanguage = settings.currentLanguage + if let currency = currencyKit.currencies.first(where: { $0.code == settings.baseCurrency }) { + currencyKit.baseCurrency = currency + } + themeManager.themeMode = settings.mode + launchScreenManager.showMarket = settings.showMarketTab + launchScreenManager.launchScreen = settings.launchScreen + + balanceConversionManager.set(tokenQueryId: settings.conversionTokenQueryId) + balanceHiddenManager.set(balanceAutoHide: settings.balanceAutoHide) + let appIcon = AppIconManager.allAppIcons.first { $0.title == settings.appIcon } ?? .main + if appIconManager.appIcon != appIcon { + appIconManager.appIcon = appIcon + } + } + } +} + +extension AppBackupProvider { + static func walletBackup(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, name: String, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { + let message = accountType.uniqueId(hashed: false) + let crypto = try BackupCrypto.instance(data: message, passphrase: passphrase) + + let walletBackup = WalletBackup( + crypto: crypto, + enabledWallets: wallets, + id: accountType.uniqueId().hs.hex, + type: AccountType.Abstract(accountType), + isManualBackedUp: isManualBackedUp, + version: Self.version, + timestamp: Date().timeIntervalSince1970.rounded() + ) + + return .init(name: name, walletBackup: walletBackup) + } +} + +extension AppBackupProvider { + enum CodingError: Error { + case invalidPassword + case emptyParameters + } + + enum Field { + static func all(ids: [String]) -> [Self] { + [.accounts(ids: ids), .watchlist, .contacts, .settings] + } + + case accounts(ids: [String]) + case watchlist + case contacts + case settings + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift index dabc1a6829..ac491d3782 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/BackupCloudModule.swift @@ -6,7 +6,7 @@ class BackupCloudModule { static let minimumPassphraseLength = 8 static func backupTerms(account: Account) -> UIViewController { - let service = ICloudBackupTermsService(cloudAccountBackupManager: App.shared.cloudAccountBackupManager, account: account) + let service = ICloudBackupTermsService(cloudAccountBackupManager: App.shared.cloudBackupManager, account: account) let viewModel = ICloudBackupTermsViewModel(service: service) let controller = ICloudBackupTermsViewController(viewModel: viewModel) @@ -14,7 +14,7 @@ class BackupCloudModule { } static func backupName(account: Account) -> UIViewController { - let service = ICloudBackupNameService(iCloudManager: App.shared.cloudAccountBackupManager, account: account) + let service = ICloudBackupNameService(iCloudManager: App.shared.cloudBackupManager, account: account) let viewModel = ICloudBackupNameViewModel(service: service) let controller = ICloudBackupNameViewController(viewModel: viewModel) @@ -22,7 +22,7 @@ class BackupCloudModule { } static func backupPassword(account: Account, name: String) -> UIViewController { - let service = BackupCloudPassphraseService(iCloudManager: App.shared.cloudAccountBackupManager, walletManager: App.shared.walletManager, account: account, name: name) + let service = BackupCloudPassphraseService(iCloudManager: App.shared.cloudBackupManager, account: account, name: name) let viewModel = BackupCloudPassphraseViewModel(service: service) let controller = BackupCloudPassphraseViewController(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Name/ICloudBackupNameService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Name/ICloudBackupNameService.swift index 57573b2e57..bb83f3cc2e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Name/ICloudBackupNameService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Name/ICloudBackupNameService.swift @@ -3,12 +3,12 @@ import Combine import HsExtensions class ICloudBackupNameService { - private let iCloudManager: CloudAccountBackupManager + private let iCloudManager: CloudBackupManager let account: Account @PostPublished private(set) var state: State = .failure(error: NameError.empty) - init(iCloudManager: CloudAccountBackupManager, account: Account) { + init(iCloudManager: CloudBackupManager, account: Account) { self.iCloudManager = iCloudManager self.account = account diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift index d59eeb3917..abf936caa0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseService.swift @@ -1,17 +1,15 @@ import Foundation class BackupCloudPassphraseService { - private let iCloudManager: CloudAccountBackupManager - private let walletManager: WalletManager + private let iCloudManager: CloudBackupManager private let account: Account private let name: String var passphrase: String = "" var passphraseConfirmation: String = "" - init(iCloudManager: CloudAccountBackupManager, walletManager: WalletManager, account: Account, name: String) { + init(iCloudManager: CloudBackupManager, account: Account, name: String) { self.iCloudManager = iCloudManager - self.walletManager = walletManager self.account = account self.name = name } @@ -25,28 +23,16 @@ extension BackupCloudPassphraseService { } func createBackup() throws { - guard !passphrase.isEmpty else { - throw CreateError.emptyPassphrase - } - - guard passphrase.count >= BackupCloudModule.minimumPassphraseLength else { - throw CreateError.simplePassword - } - - let allSatisfy = BackupCloudModule.PassphraseCharacterSet.allCases.allSatisfy { set in set.contains(passphrase) } - if !allSatisfy { - throw CreateError.simplePassword - } + try BackupCrypto.validate(passphrase: passphrase) guard passphrase == passphraseConfirmation else { throw CreateError.invalidConfirmation } do { - let wallets = App.shared.walletManager.wallets(account: account) - try iCloudManager.save(account: account, wallets: wallets, isManualBackedUp: account.backedUp, passphrase: passphrase, name: name) + try iCloudManager.save(account: account, passphrase: passphrase, name: name) } catch { - if case .urlNotAvailable = error as? CloudAccountBackupManager.BackupError { + if case .urlNotAvailable = error as? CloudBackupManager.BackupError { throw CreateError.urlNotAvailable } throw CreateError.cantSaveFile(error) @@ -58,8 +44,6 @@ extension BackupCloudPassphraseService { extension BackupCloudPassphraseService { enum CreateError: Error { - case emptyPassphrase - case simplePassword case invalidConfirmation case urlNotAvailable case cantSaveFile(Error) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift index 2ceae1565f..53331cd609 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift @@ -81,18 +81,18 @@ extension BackupCloudPassphraseViewModel { processing = false finishSubject.send(()) } catch { - switch (error as? BackupCloudPassphraseService.CreateError) { - case .emptyPassphrase: + switch error { + case BackupCrypto.ValidationError.emptyPassphrase: passphraseCaution = Caution(text: "backup.cloud.password.error.empty_passphrase".localized, type: .error) - case .simplePassword: + case BackupCrypto.ValidationError.simplePassword: passphraseCaution = Caution(text: "backup.cloud.password.error.minimum_requirement".localized, type: .error) - case .invalidConfirmation: + case BackupCloudPassphraseService.CreateError.invalidConfirmation: passphraseConfirmationCaution = Caution(text: "backup.cloud.password.confirm.error.doesnt_match".localized, type: .error) - case .urlNotAvailable: + case BackupCloudPassphraseService.CreateError.urlNotAvailable: showErrorSubject.send("backup.cloud.not_available".localized) - case .cantSaveFile: + case BackupCloudPassphraseService.CreateError.cantSaveFile: showErrorSubject.send("backup.cloud.cant_create_file".localized) - case .none: + default: showErrorSubject.send(error.smartDescription) } processing = false diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Terms/ICloudBackupTermsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Terms/ICloudBackupTermsService.swift index 5734c0cc0b..5f4cd25e40 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Terms/ICloudBackupTermsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Terms/ICloudBackupTermsService.swift @@ -6,11 +6,11 @@ class ICloudBackupTermsService { let account: Account let termCount = 1 - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager @PostPublished private(set) var state: State = .selectedTerms(Set()) - init(cloudAccountBackupManager: CloudAccountBackupManager, account: Account) { + init(cloudAccountBackupManager: CloudBackupManager, account: Account) { self.account = account self.cloudAccountBackupManager = cloudAccountBackupManager } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift deleted file mode 100644 index 19917aaeb7..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/WalletBackupConverter.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import MarketKit - -class WalletBackupConverter { - private static let version = 2 - - static func encode(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, passphrase: String) throws -> Data { - let message = accountType.uniqueId(hashed: false) - let iv = BackupCryptoHelper.generateInitialVector().hs.hex - - let cipherText = try BackupCryptoHelper.AES128( - operation: .encrypt, - ivHex: iv, - pass: passphrase, - message: message, - kdf: .defaultBackup - ) - let encodedCipherText = cipherText.base64EncodedString() - let mac = try BackupCryptoHelper.mac( - pass: passphrase, - message: encodedCipherText.hs.data, - kdf: .defaultBackup - ) - - let crypto = WalletBackupCrypto( - cipher: BackupCryptoHelper.defaultCypher, - cipherParams: CipherParams(iv: iv), - cipherText: encodedCipherText, - kdf: BackupCryptoHelper.defaultKdf, - kdfParams: .defaultBackup, - mac: mac.hs.hex - ) - let backup = WalletBackup( - crypto: crypto, - enabledWallets: wallets, - id: accountType.uniqueId().hs.hex, - type: AccountType.Abstract(accountType), - isManualBackedUp: isManualBackedUp, - version: Self.version, - timestamp: Date().timeIntervalSince1970 - ) - return try JSONEncoder().encode(backup) - } - - static func decode(data: Data, passphrase: String) throws -> AccountType { - let backup = try JSONDecoder().decode(WalletBackup.self, from: data) - - guard let message = Data(base64Encoded: backup.crypto.cipherText) else { - throw CodingError.cantDecodeCipherText - } - - let decryptData = try BackupCryptoHelper.AES128( - operation: .decrypt, - ivHex: backup.crypto.cipherParams.iv, - pass: passphrase, - message: message, - kdf: .defaultBackup - ) - - guard let accountType = AccountType.decode(uniqueId: decryptData, type: backup.type) else { - throw CodingError.cantDecodeAccountType - } - - return accountType - } -} - -extension WalletBackupConverter { - enum CodingError: Error { - case cantDecodeCipherText - case cantDecodeAccountType - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift index 3d0a6771db..12d09d3e4d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift @@ -32,6 +32,10 @@ extension FavoritesManager { coinUidsUpdatedRelay.accept(()) } + func removeAll() { + storage.deleteAll() + } + func remove(coinUid: String) { storage.deleteFavoriteCoinRecord(coinUid: coinUid) coinUidsUpdatedRelay.accept(()) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift index 203d07d749..e110217c4a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift @@ -9,7 +9,7 @@ struct ManageAccountModule { guard let service = ManageAccountService( accountId: accountId, accountManager: App.shared.accountManager, - cloudBackupManager: App.shared.cloudAccountBackupManager, + cloudBackupManager: App.shared.cloudBackupManager, pinKit: App.shared.pinKit ) else { return nil diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift index e98a1eaa27..2fd125acc1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift @@ -13,7 +13,7 @@ class ManageAccountService { } private let accountManager: AccountManager - private let cloudBackupManager: CloudAccountBackupManager + private let cloudBackupManager: CloudBackupManager private let pinKit: PinKit.Kit private let disposeBag = DisposeBag() private var cancellables = Set() @@ -30,7 +30,7 @@ class ManageAccountService { private var newName: String - init?(accountId: String, accountManager: AccountManager, cloudBackupManager: CloudAccountBackupManager, pinKit: PinKit.Kit) { + init?(accountId: String, accountManager: AccountManager, cloudBackupManager: CloudBackupManager, pinKit: PinKit.Kit) { guard let account = accountManager.account(id: accountId) else { return nil } @@ -46,7 +46,7 @@ class ManageAccountService { subscribe(disposeBag, accountManager.accountUpdatedObservable) { [weak self] in self?.handleUpdated(account: $0) } subscribe(disposeBag, accountManager.accountDeletedObservable) { [weak self] in self?.handleDeleted(account: $0) } - cloudBackupManager.$items + cloudBackupManager.$oneWalletItems .sink { [weak self] _ in self?.cloudBackedUpRelay.accept(()) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsModule.swift index 054d153095..20d474027c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsModule.swift @@ -3,7 +3,7 @@ import UIKit struct ManageAccountsModule { static func viewController(mode: Mode, createAccountListener: ICreateAccountListener? = nil) -> UIViewController { - let service = ManageAccountsService(accountManager: App.shared.accountManager, cloudBackupManager: App.shared.cloudAccountBackupManager) + let service = ManageAccountsService(accountManager: App.shared.accountManager, cloudBackupManager: App.shared.cloudBackupManager) let viewModel = ManageAccountsViewModel(service: service, mode: mode) return ManageAccountsViewController(viewModel: viewModel, createAccountListener: createAccountListener) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsService.swift index e437dbccf6..6053ac1ea2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsService.swift @@ -4,7 +4,7 @@ import Combine class ManageAccountsService { private let accountManager: AccountManager - private let cloudBackupManager: CloudAccountBackupManager + private let cloudBackupManager: CloudBackupManager private let disposeBag = DisposeBag() private var cancellables = Set() @@ -15,14 +15,14 @@ class ManageAccountsService { } } - init(accountManager: AccountManager, cloudBackupManager: CloudAccountBackupManager) { + init(accountManager: AccountManager, cloudBackupManager: CloudBackupManager) { self.accountManager = accountManager self.cloudBackupManager = cloudBackupManager subscribe(disposeBag, accountManager.accountsObservable) { [weak self] _ in self?.syncItems() } subscribe(disposeBag, accountManager.activeAccountObservable) { [weak self] _ in self?.syncItems() } - cloudBackupManager.$items + cloudBackupManager.$oneWalletItems .sink { [weak self] _ in self?.syncItems() } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift index b6fe4d0854..c3801524d5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift @@ -6,16 +6,21 @@ struct RestoreCloudModule { static func viewController(returnViewController: UIViewController?) -> UIViewController { let service = RestoreCloudService( - cloudAccountBackupManager: App.shared.cloudAccountBackupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, accountManager: App.shared.accountManager ) let viewModel = RestoreCloudViewModel(service: service) return RestoreCloudViewController(viewModel: viewModel, returnViewController: returnViewController) } - struct RestoredBackup { + struct RestoredBackup: Codable { let name: String let walletBackup: WalletBackup + + enum CodingKeys: String, CodingKey { + case name + case walletBackup = "backup" + } } struct RestoredAccount { @@ -26,3 +31,12 @@ struct RestoreCloudModule { } } + +extension RestoreCloudModule { + enum RestoreError: Error { + case emptyPassphrase + case simplePassword + case invalidPassword + case invalidBackup + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift index 840a0d1417..9b89c77638 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift @@ -5,7 +5,8 @@ class RestoreCloudPassphraseModule { static func restorePassword(item: RestoreCloudModule.RestoredBackup, returnViewController: UIViewController?) -> UIViewController { let service = RestoreCloudPassphraseService( - iCloudManager: App.shared.cloudAccountBackupManager, + iCloudManager: App.shared.cloudBackupManager, + appBackupProvider: App.shared.appBackupProvider, accountFactory: App.shared.accountFactory, accountManager: App.shared.accountManager, walletManager: App.shared.walletManager, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift index 665c3b3283..b29f427712 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift @@ -2,7 +2,8 @@ import Foundation import MarketKit class RestoreCloudPassphraseService { - private let iCloudManager: CloudAccountBackupManager + private let iCloudManager: CloudBackupManager + private let appBackupProvider: AppBackupProvider private let accountFactory: AccountFactory private let accountManager: AccountManager private let walletManager: WalletManager @@ -12,8 +13,9 @@ class RestoreCloudPassphraseService { var passphrase: String = "" - init(iCloudManager: CloudAccountBackupManager, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, restoreSettingsManager: RestoreSettingsManager, item: RestoreCloudModule.RestoredBackup) { + init(iCloudManager: CloudBackupManager, appBackupProvider: AppBackupProvider, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, restoreSettingsManager: RestoreSettingsManager, item: RestoreCloudModule.RestoredBackup) { self.iCloudManager = iCloudManager + self.appBackupProvider = appBackupProvider self.accountFactory = accountFactory self.accountManager = accountManager self.walletManager = walletManager @@ -38,104 +40,42 @@ class RestoreCloudPassphraseService { } } return EnabledWallet( - tokenQueryId: $0.tokenQueryId, - accountId: account.id, - coinName: $0.coinName, - coinCode: $0.coinCode, - tokenDecimals: $0.tokenDecimals + tokenQueryId: $0.tokenQueryId, + accountId: account.id, + coinName: $0.coinName, + coinCode: $0.coinCode, + tokenDecimals: $0.tokenDecimals ) } walletManager.save(enabledWallets: wallets) } - } extension RestoreCloudPassphraseService { - func validate(text: String?) -> Bool { PassphraseValidator.validate(text: text) } func importWallet() async throws -> RestoreResult { - let crypto = restoredBackup.walletBackup.crypto - - guard !passphrase.isEmpty else { - throw RestoreError.emptyPassphrase - } - guard passphrase.count >= BackupCloudModule.minimumPassphraseLength else { - throw RestoreError.simplePassword - } - - let allSatisfy = BackupCloudModule.PassphraseCharacterSet.allCases.allSatisfy { set in set.contains(passphrase) } - if !allSatisfy { - throw RestoreError.simplePassword - } - - guard let walletData = Data(base64Encoded: crypto.cipherText) else { - throw RestoreError.invalidBackup - } - - let isValid = (try? BackupCryptoHelper.isValid( - macHex: crypto.mac, - pass: passphrase, - message: crypto.cipherText.hs.data, - kdf: crypto.kdfParams - )) ?? false - - guard isValid else { - throw RestoreError.invalidPassword - } - - do { - let data = try BackupCryptoHelper.AES128( - operation: .decrypt, - ivHex: crypto.cipherParams.iv, - pass: passphrase, - message: walletData, - kdf: crypto.kdfParams) - - guard let accountType = AccountType.decode(uniqueId: data, type: restoredBackup.walletBackup.type) else { - throw RestoreError.invalidBackup - } - - switch accountType { - case .cex: - let account = accountFactory.account( - type: accountType, - origin: .restored, - backedUp: restoredBackup.walletBackup.isManualBackedUp, - name: restoredBackup.name - ) - accountManager.save(account: account) - return .success - default: - createAccount(accountType: accountType) - return .restoredAccount(RestoreCloudModule.RestoredAccount( - name: restoredBackup.name, - accountType: accountType, - isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp, - showSelectCoins: restoredBackup.walletBackup.enabledWallets.isEmpty - )) - } - } catch { - throw RestoreError.invalidBackup + let accountType = try restoredBackup.walletBackup.crypto.accountType(type: restoredBackup.walletBackup.type, passphrase: passphrase) + appBackupProvider.walletRestore(backup: restoredBackup, accountType: accountType) + switch accountType { + case .cex: + return .success + default: + return .restoredAccount(RestoreCloudModule.RestoredAccount( + name: restoredBackup.name, + accountType: accountType, + isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp, + showSelectCoins: restoredBackup.walletBackup.enabledWallets.isEmpty + )) } } - } extension RestoreCloudPassphraseService { - - enum RestoreError: Error { - case emptyPassphrase - case simplePassword - case invalidPassword - case invalidBackup - } - enum RestoreResult { case restoredAccount(RestoreCloudModule.RestoredAccount) case success } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift index 0f9d9da4e9..bd7ef35223 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift @@ -77,7 +77,7 @@ extension RestoreCloudPassphraseViewModel { } } } catch { - switch (error as? RestoreCloudPassphraseService.RestoreError) { + switch (error as? RestoreCloudModule.RestoreError) { case .emptyPassphrase: self?.passphraseCaution = Caution(text: "backup.cloud.password.error.empty_passphrase".localized, type: .error) case .simplePassword: diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift index 98f773800c..1c85262138 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift @@ -2,7 +2,7 @@ import Foundation import Combine class RestoreCloudService { - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let accountManager: AccountManager private var cancellables = Set() @@ -10,17 +10,17 @@ class RestoreCloudService { private let deleteItemCompletedSubject = PassthroughSubject() @Published var items = [Item]() - init(cloudAccountBackupManager: CloudAccountBackupManager, accountManager: AccountManager) { + init(cloudAccountBackupManager: CloudBackupManager, accountManager: AccountManager) { self.cloudAccountBackupManager = cloudAccountBackupManager self.accountManager = accountManager - cloudAccountBackupManager.$items + cloudAccountBackupManager.$oneWalletItems .sink { [weak self] in self?.sync(backups: $0) } .store(in: &cancellables) - sync(backups: cloudAccountBackupManager.items) + sync(backups: cloudAccountBackupManager.oneWalletItems) } private func sync(backups: [String: WalletBackup]) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift index cc24f64694..1ee4f286d4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift @@ -4,7 +4,7 @@ import ThemeKit struct RestoreTypeModule { static func viewController(sourceViewController: UIViewController? = nil, returnViewController: UIViewController? = nil) -> UIViewController { - let viewModel = RestoreTypeViewModel(cloudAccountBackupManager: App.shared.cloudAccountBackupManager) + let viewModel = RestoreTypeViewModel(cloudAccountBackupManager: App.shared.cloudBackupManager) let viewController = RestoreTypeViewController(viewModel: viewModel, returnViewController: returnViewController) let module = ThemeNavigationController(rootViewController: viewController) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift index cffc62d0ae..12a7dceb28 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift @@ -2,12 +2,12 @@ import UIKit import Combine class RestoreTypeViewModel { - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let showCloudNotAvailableSubject = PassthroughSubject() private let showModuleSubject = PassthroughSubject() - init(cloudAccountBackupManager: CloudAccountBackupManager) { + init(cloudAccountBackupManager: CloudBackupManager) { self.cloudAccountBackupManager = cloudAccountBackupManager } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift index 942996a0d3..26a69bc867 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift @@ -5,7 +5,7 @@ struct MainSettingsModule { static func viewController() -> UIViewController { let service = MainSettingsService( backupManager: App.shared.backupManager, - cloudAccountBackupManager: App.shared.cloudAccountBackupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, accountRestoreWarningManager: App.shared.accountRestoreWarningManager, accountManager: App.shared.accountManager, contactBookManager: App.shared.contactManager, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift index e75eae927f..7314d41c78 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift @@ -12,7 +12,7 @@ class MainSettingsService { private let disposeBag = DisposeBag() private let backupManager: BackupManager - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let accountRestoreWarningManager: AccountRestoreWarningManager private let accountManager: AccountManager private let contactBookManager: ContactBookManager @@ -26,7 +26,7 @@ class MainSettingsService { private let iCloudAvailableErrorRelay = BehaviorRelay(value: false) private let noWalletRequiredActionsRelay = BehaviorRelay(value: false) - init(backupManager: BackupManager, cloudAccountBackupManager: CloudAccountBackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, accountManager: AccountManager, contactBookManager: ContactBookManager, pinKit: PinKit.Kit, termsManager: TermsManager, + init(backupManager: BackupManager, cloudAccountBackupManager: CloudBackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, accountManager: AccountManager, contactBookManager: ContactBookManager, pinKit: PinKit.Kit, termsManager: TermsManager, systemInfoManager: SystemInfoManager, currencyKit: CurrencyKit.Kit, walletConnectSessionManager: WalletConnectSessionManager, subscriptionManager: SubscriptionManager) { self.cloudAccountBackupManager = cloudAccountBackupManager self.backupManager = backupManager diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/CoinProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/CoinProvider.swift index d385315d28..0b67bde67d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/CoinProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/CoinProvider.swift @@ -60,8 +60,6 @@ extension CoinProvider { } catch { return [] } - - return predefined } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift index ad1a23ca77..ab4a64792c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift @@ -25,7 +25,7 @@ struct WalletTokenBalanceModule { coinPriceService: coinPriceService, elementService: elementService, appManager: App.shared.appManager, - cloudAccountBackupManager: App.shared.cloudAccountBackupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, balanceHiddenManager: App.shared.balanceHiddenManager, reachabilityManager: App.shared.reachabilityManager, account: account, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift index 9f286d71ff..fe816e2892 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift @@ -9,7 +9,7 @@ class WalletTokenBalanceService { private let coinPriceService: WalletCoinPriceService private let elementService: IWalletElementService - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let balanceHiddenManager: BalanceHiddenManager private let reachabilityManager: IReachabilityManager @@ -23,7 +23,7 @@ class WalletTokenBalanceService { private let queue = DispatchQueue(label: "\(AppConfig.label).wallet-token-balance-service", qos: .userInitiated) init(coinPriceService: WalletCoinPriceService, elementService: IWalletElementService, - appManager: IAppManager, cloudAccountBackupManager: CloudAccountBackupManager, + appManager: IAppManager, cloudAccountBackupManager: CloudBackupManager, balanceHiddenManager: BalanceHiddenManager, reachabilityManager: IReachabilityManager, account: Account, element: WalletModule.Element) { self.coinPriceService = coinPriceService diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift index 177c67cda9..321f6c3e4d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift @@ -30,7 +30,7 @@ struct WalletModule { balancePrimaryValueManager: App.shared.balancePrimaryValueManager, balanceHiddenManager: App.shared.balanceHiddenManager, balanceConversionManager: App.shared.balanceConversionManager, - cloudAccountBackupManager: App.shared.cloudAccountBackupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, rateAppManager: App.shared.rateAppManager, appManager: App.shared.appManager, feeCoinProvider: App.shared.feeCoinProvider, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift index c12823142f..bd4a20827a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift @@ -37,7 +37,7 @@ class WalletService { private let balancePrimaryValueManager: BalancePrimaryValueManager private let balanceHiddenManager: BalanceHiddenManager private let balanceConversionManager: BalanceConversionManager - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let rateAppManager: RateAppManager private let feeCoinProvider: FeeCoinProvider private let localStorage: StorageKit.ILocalStorage @@ -85,7 +85,7 @@ class WalletService { init(elementServiceFactory: WalletElementServiceFactory, coinPriceService: WalletCoinPriceService, accountManager: AccountManager, cacheManager: EnabledWalletCacheManager, accountRestoreWarningManager: AccountRestoreWarningManager, reachabilityManager: IReachabilityManager, balancePrimaryValueManager: BalancePrimaryValueManager, balanceHiddenManager: BalanceHiddenManager, balanceConversionManager: BalanceConversionManager, - cloudAccountBackupManager: CloudAccountBackupManager, rateAppManager: RateAppManager, appManager: IAppManager, feeCoinProvider: FeeCoinProvider, + cloudAccountBackupManager: CloudBackupManager, rateAppManager: RateAppManager, appManager: IAppManager, feeCoinProvider: FeeCoinProvider, localStorage: StorageKit.ILocalStorage ) { self.elementServiceFactory = elementServiceFactory diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift index f88fc97ca4..6a8f45983d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift @@ -4,7 +4,7 @@ class WalletConnectAppShowModule { static func handler(parentViewController: UIViewController? = nil) -> IEventHandler { let walletConnectWorkerService = WalletConnectAppShowService( walletConnectManager: App.shared.walletConnectSessionManager, - cloudAccountBackupManager: App.shared.cloudAccountBackupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, accountManager: App.shared.accountManager, pinKit: App.shared.pinKit ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift index 1e30b3f5df..c789afa373 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift @@ -8,14 +8,14 @@ class WalletConnectAppShowService { private var disposeBag = DisposeBag() private var cancellables = Set() private let walletConnectManager: WalletConnectSessionManager - private let cloudAccountBackupManager: CloudAccountBackupManager + private let cloudAccountBackupManager: CloudBackupManager private let accountManager: AccountManager private let pinKit: PinKit.Kit private let showSessionProposalSubject = PassthroughSubject() private let showSessionRequestSubject = PassthroughSubject() - init(walletConnectManager: WalletConnectSessionManager, cloudAccountBackupManager: CloudAccountBackupManager, accountManager: AccountManager, pinKit: PinKit.Kit) { + init(walletConnectManager: WalletConnectSessionManager, cloudAccountBackupManager: CloudBackupManager, accountManager: AccountManager, pinKit: PinKit.Kit) { self.walletConnectManager = walletConnectManager self.cloudAccountBackupManager = cloudAccountBackupManager self.accountManager = accountManager From 7a2acbcb881695f5bc56ca83aad93b793dcf7a2f Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 21 Sep 2023 16:26:41 +0600 Subject: [PATCH 10/63] Update full backup files --- .../UnstoppableWallet/Core/App.swift | 3 + .../Core/Crypto/FullBackup.swift | 1 - .../Core/Crypto/SettingsBackup.swift | 16 ++- .../Core/Managers/BtcBlockchainManager.swift | 49 ++++++- .../Storage/FavoriteCoinRecordStorage.swift | 2 +- .../Core/Storage/LocalStorage.swift | 3 +- .../Models/BtcRestoreMode.swift | 2 +- .../Backup/ICloud/AppBackupProvider.swift | 28 ++-- .../Main/ChartIndicatorsRepository.swift | 130 ++++++++++++++++-- .../Modules/Swap/SwapModule.swift | 11 ++ 10 files changed, 205 insertions(+), 40 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index b519b4335a..e19a4e10a9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -306,13 +306,16 @@ class App { let cexAssetRecordStorage = CexAssetRecordStorage(dbPool: dbPool) cexAssetManager = CexAssetManager(accountManager: accountManager, marketKit: marketKit, storage: cexAssetRecordStorage) + let chartRepository = ChartIndicatorsRepository(localStorage: localStorage, subscriptionManager: subscriptionManager) appBackupProvider = AppBackupProvider( accountManager: accountManager, accountFactory: accountFactory, walletManager: walletManager, favoritesManager: favoritesManager, evmSyncSourceManager: evmSyncSourceManager, + btcBlockchainManager: btcBlockchainManager, restoreSettingsManager: restoreSettingsManager, + chartRepository: chartRepository, localStorage: localStorage, languageManager: LanguageManager.shared, currencyKit: currencyKit, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift index 6a2fc7d82c..d811cd8c2d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -38,7 +38,6 @@ extension FullBackup: Codable { if let contacts { try container.encode(contacts, forKey: .contacts) } if let settings { try container.encode(settings, forKey: .settings) } try container.encode(version, forKey: .version) - try container.encode(version, forKey: .version) try? container.encode(timestamp, forKey: .timestamp) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift index b4e3643a4e..2bea154ba2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift @@ -5,11 +5,12 @@ import ThemeKit struct SettingsBackup: Codable { let evmSyncSources: EvmSyncSourceManager.SyncSourceBackup + let btcModes: [BtcBlockchainManager.BtcRestoreModeBackup] let lockTimeEnabled: Bool let remoteContactsSync: Bool - let defaultProviders: [DefaultProvider] - let chartIndicators: [ChartIndicator] + let swapProviders: [DefaultProvider] + let chartIndicators: ChartIndicatorsRepository.BackupIndicators let indicatorsShown: Bool let currentLanguage: String let baseCurrency: String @@ -24,9 +25,10 @@ struct SettingsBackup: Codable { enum CodingKeys: String, CodingKey { case evmSyncSources = "evm_sync_sources" + case btcModes = "btc_modes" case lockTimeEnabled = "lock_time" case remoteContactsSync = "contacts_sync" - case defaultProviders = "default_providers" + case swapProviders = "swap_providers" case chartIndicators = "indicators" case indicatorsShown = "indicators_shown" case currentLanguage = "language" @@ -44,12 +46,12 @@ struct SettingsBackup: Codable { extension SettingsBackup { struct DefaultProvider: Codable { + let blockchainTypeId: String + let provider: String + enum CodingKeys: String, CodingKey { case blockchainTypeId = "blockchain_type_id" case provider } - - let blockchainTypeId: String - let provider: String } -} \ No newline at end of file +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BtcBlockchainManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BtcBlockchainManager.swift index 53b3445f57..196c4d179e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BtcBlockchainManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BtcBlockchainManager.swift @@ -1,7 +1,7 @@ -import RxSwift -import RxRelay -import MarketKit import BitcoinCore +import MarketKit +import RxRelay +import RxSwift class BtcBlockchainManager { private let blockchainTypes: [BlockchainType] = [ @@ -9,7 +9,7 @@ class BtcBlockchainManager { .bitcoinCash, .ecash, .litecoin, - .dash + .dash, ] private let marketKit: MarketKit.Kit @@ -30,11 +30,9 @@ class BtcBlockchainManager { allBlockchains = [] } } - } extension BtcBlockchainManager { - func blockchain(token: Token) -> Blockchain? { allBlockchains.first(where: { token.blockchain == $0 }) } @@ -75,5 +73,44 @@ extension BtcBlockchainManager { storage.save(btcTransactionSortMode: transactionSortMode, blockchainType: blockchainType) transactionSortModeUpdatedRelay.accept(blockchainType) } +} +extension BtcBlockchainManager { + var backup: [BtcRestoreModeBackup] { + blockchainTypes.map { + BtcRestoreModeBackup( + blockchainTypeUid: $0.uid, + restoreMode: restoreMode(blockchainType: $0).rawValue, + sortMode: transactionSortMode(blockchainType: $0).rawValue + ) + } + } + + func restore(backup: [BtcRestoreModeBackup]) { + backup + .forEach { backup in + let type = BlockchainType(uid: backup.blockchainTypeUid) + + if let mode = BtcRestoreMode(rawValue: backup.restoreMode) { + save(restoreMode: mode, blockchainType: type) + } + if let mode = TransactionDataSortMode(rawValue: backup.sortMode) { + save(transactionSortMode: mode, blockchainType: type) + } + } + } +} + +extension BtcBlockchainManager { + struct BtcRestoreModeBackup: Codable { + let blockchainTypeUid: String + let restoreMode: String + let sortMode: String + + enum CodingKeys: String, CodingKey { + case blockchainTypeUid = "blockchain_type_id" + case restoreMode = "restore_mode" + case sortMode = "sort_mode" + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift index 400355c607..41ca31db8b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift @@ -24,7 +24,7 @@ extension FavoriteCoinRecordStorage { } func save(favoriteCoinRecords: [FavoriteCoinRecord]) { - _ = try! dbPool.write { db in + _ = try? dbPool.write { db in for record in favoriteCoinRecords { try record.insert(db) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift index cbe9fee884..3f4aa55bbe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift @@ -105,8 +105,7 @@ extension LocalStorage { lockTimeEnabled = backup.lockTimeEnabled remoteContactsSync = backup.remoteContactsSync indicatorsShown = backup.indicatorsShown - chartIndicators = backup.chartIndicators.encoded //todo: - backup.defaultProviders.forEach { provider in + backup.swapProviders.forEach { provider in let blockchainType = BlockchainType(uid: provider.blockchainTypeId) if let dexProvider = SwapModule.Dex.Provider(rawValue: provider.provider) { return setDefaultProvider(blockchainType: blockchainType, provider: dexProvider) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BtcRestoreMode.swift b/UnstoppableWallet/UnstoppableWallet/Models/BtcRestoreMode.swift index 89aae0dae6..5fab830cd8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BtcRestoreMode.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BtcRestoreMode.swift @@ -1,4 +1,4 @@ -enum BtcRestoreMode: String, CaseIterable, Identifiable { +enum BtcRestoreMode: String, CaseIterable, Identifiable, Codable { case api case blockchain diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index 9e6276eb2f..aecc2ef9fb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -12,7 +12,9 @@ class AppBackupProvider { private let walletManager: WalletManager private let favoritesManager: FavoritesManager private let evmSyncSourceManager: EvmSyncSourceManager + private let btcBlockchainManager: BtcBlockchainManager private let restoreSettingsManager: RestoreSettingsManager + private let chartRepository: ChartIndicatorsRepository private let localStorage: LocalStorage private let languageManager: LanguageManager private let currencyKit: CurrencyKit.Kit @@ -29,7 +31,9 @@ class AppBackupProvider { walletManager: WalletManager, favoritesManager: FavoritesManager, evmSyncSourceManager: EvmSyncSourceManager, + btcBlockchainManager: BtcBlockchainManager, restoreSettingsManager: RestoreSettingsManager, + chartRepository: ChartIndicatorsRepository, localStorage: LocalStorage, languageManager: LanguageManager, currencyKit: CurrencyKit.Kit, @@ -46,7 +50,9 @@ class AppBackupProvider { self.walletManager = walletManager self.favoritesManager = favoritesManager self.evmSyncSourceManager = evmSyncSourceManager + self.btcBlockchainManager = btcBlockchainManager self.restoreSettingsManager = restoreSettingsManager + self.chartRepository = chartRepository self.localStorage = localStorage self.languageManager = languageManager self.currencyKit = currencyKit @@ -105,18 +111,19 @@ extension AppBackupProvider { let providers: [SettingsBackup.DefaultProvider] = BlockchainType .supported .filter { !$0.allowedProviders.isEmpty } - .map { SettingsBackup.DefaultProvider( - blockchainTypeId: $0.uid, - provider: localStorage.defaultProvider(blockchainType: $0).rawValue - ) + .map { + SettingsBackup.DefaultProvider( + blockchainTypeId: $0.uid, + provider: localStorage.defaultProvider(blockchainType: $0).id + ) } - settings = SettingsBackup( evmSyncSources: evmSyncSourceManager.backup(passphrase: passphrase), + btcModes: btcBlockchainManager.backup, lockTimeEnabled: localStorage.lockTimeEnabled, remoteContactsSync: localStorage.remoteContactsSync, - defaultProviders: providers, - chartIndicators: [], + swapProviders: providers, + chartIndicators: chartRepository.backup, indicatorsShown: localStorage.indicatorsShown, currentLanguage: languageManager.currentLanguage, baseCurrency: currencyKit.baseCurrency.code, @@ -139,7 +146,6 @@ extension AppBackupProvider { throw CodingError.emptyParameters } - var contactCrypto: BackupCrypto? if !contacts.isEmpty { let encoder = JSONEncoder() @@ -221,8 +227,8 @@ extension AppBackupProvider { // restore only if all wallet was encrypted with password encodedWallets.forEach { wallet in walletRestore( - backup: wallet.0, - accountType: wallet.1 + backup: wallet.0, + accountType: wallet.1 ) } @@ -234,6 +240,8 @@ extension AppBackupProvider { if let settings = backup.settings { evmSyncSourceManager.restore(backup: settings.evmSyncSources) + btcBlockchainManager.restore(backup: settings.btcModes) + chartRepository.restore(backup: settings.chartIndicators) localStorage.restore(backup: settings) languageManager.currentLanguage = settings.currentLanguage if let currency = currencyKit.currencies.first(where: { $0.code == settings.baseCurrency }) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsRepository.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsRepository.swift index e6ab2423a4..2b0238152e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsRepository.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsRepository.swift @@ -1,6 +1,6 @@ -import Foundation -import Combine import Chart +import Combine +import Foundation protocol IChartIndicatorsRepository { var indicators: [ChartIndicator] { get } @@ -23,10 +23,10 @@ class ChartIndicatorsRepository { self.subscriptionManager = subscriptionManager subscriptionManager.$isAuthenticated - .sink { [weak self] _ in - self?.updatedSubject.send() - } - .store(in: &cancellables) + .sink { [weak self] _ in + self?.updatedSubject.send() + } + .store(in: &cancellables) } private var userIndicators: [ChartIndicator] { @@ -43,7 +43,6 @@ class ChartIndicatorsRepository { } extension ChartIndicatorsRepository: IChartIndicatorsRepository { - var indicators: [ChartIndicator] { if subscriptionManager.isAuthenticated { return userIndicators @@ -57,11 +56,9 @@ extension ChartIndicatorsRepository: IChartIndicatorsRepository { return } - let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys - - let oldIndicators = userIndicators - if indicators != oldIndicators { + if indicators != userIndicators { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys localStorage.chartIndicators = try? encoder.encode(ChartIndicators(with: indicators)) updatedSubject.send() } @@ -76,5 +73,114 @@ extension ChartIndicatorsRepository: IChartIndicatorsRepository { greatest = indicator.enabled ? max(greatest, indicator.greatestPeriod) : greatest } } +} + +extension ChartIndicatorsRepository { + var backup: BackupIndicators { + var ma = [BackupMaIndicator]() + var rsi = [BackupRsiIndicator]() + var macd = [BackupMacdIndicator]() + + userIndicators.forEach { indicator in + switch indicator { + case let indicator as MaIndicator: + ma.append(BackupMaIndicator( + period: indicator.period, + type: indicator.type.rawValue, + enabled: indicator.enabled + )) + case let indicator as RsiIndicator: + rsi.append(BackupRsiIndicator( + period: indicator.period, + enabled: indicator.enabled + )) + case let indicator as MacdIndicator: + macd.append(BackupMacdIndicator( + slow: indicator.slow, + fast: indicator.fast, + signal: indicator.signal, + enabled: indicator.enabled + )) + default: () + } + } + return BackupIndicators( + ma: ma, + rsi: rsi, + macd: macd + ) + } + func restore(backup: BackupIndicators) { + var indicators = [ChartIndicator]() + backup.ma.enumerated().forEach { index, element in + indicators.append( + MaIndicator( + id: "MA", + index: index, + enabled: element.enabled, + period: element.period, + type: MaIndicator.MaType(rawValue: element.type) ?? .sma, + onChart: true, + single: false, + configuration: ChartIndicatorFactory.maConfiguration(index) + ) + ) + } + backup.rsi.enumerated().forEach { index, element in + indicators.append( + RsiIndicator( + id: "RSI", + index: index, + enabled: element.enabled, + period: element.period, + onChart: false, + single: true, + configuration: ChartIndicatorFactory.rsiConfiguration + ) + ) + } + backup.macd.enumerated().forEach { index, element in + indicators.append( + MacdIndicator( + id: "MACD", + index: index, + enabled: element.enabled, + fast: element.fast, + slow: element.slow, + signal: element.signal, + onChart: false, + single: true, + configuration: ChartIndicatorFactory.macdConfiguration + ) + ) + } + set(indicators: indicators) + } +} + +extension ChartIndicatorsRepository { + struct BackupIndicators: Codable { + let ma: [BackupMaIndicator] + let rsi: [BackupRsiIndicator] + let macd: [BackupMacdIndicator] + } + + struct BackupMaIndicator: Codable { + let period: Int + let type: String + let enabled: Bool + } + + struct BackupRsiIndicator: Codable { + let period: Int + let enabled: Bool + } + + struct BackupMacdIndicator: Codable { + let slow: Int + let fast: Int + let signal: Int + let enabled: Bool + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapModule.swift index ce4099cbba..70c420e058 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapModule.swift @@ -181,6 +181,17 @@ extension SwapModule.Dex { case pancakeV3 = "PancakeSwap V3" case quickSwap = "QuickSwap" + var id: String { + switch self { + case .uniswap: return "uniswap" + case .uniswapV3: return "uniswap_v3" + case .oneInch: return "oneinch" + case .pancake: return "pancake" + case .pancakeV3: return "pancake_v3" + case .quickSwap: return "quickswap" + } + } + var allowedBlockchainTypes: [BlockchainType] { switch self { case .uniswap: return [.ethereum] From b43ad9cb1cac6d41dcf1c13a98700a68d101988d Mon Sep 17 00:00:00 2001 From: _imadia Date: Thu, 21 Sep 2023 17:31:52 +0600 Subject: [PATCH 11/63] Add new speech style --- .../de.lproj/Localizable.strings | 1837 ++++++++--------- .../en.lproj/Localizable.strings | 3 - .../ru.lproj/Localizable.strings | 587 +++--- 3 files changed, 1213 insertions(+), 1214 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index 80c2c6b015..2307a3e8b8 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -1,91 +1,90 @@ "button.ok" = "OK"; -"button.apply" = "Übernehmen"; -"button.cancel" = "Abbrechen"; -"button.close" = "Schließen"; -"button.continue" = "Weiter"; -"button.done" = "Fertig"; -"button.reset" = "Zurücksetzen"; -"button.import" = "Wiederherstellen"; -"button.next" = "Weiter"; -"button.delete" = "Löschen"; -"button.share" = "Teilen"; -"button.paste" = "Einsetzen"; -"button.resend" = "Nochmal senden"; -"button.backup" = "Sicherung"; -"button.copy" = "Kopieren"; -"button.retry" = "Wiederholen"; -"button.report" = "Melden"; -"button.add" = "Hinzufügen"; -"button.approve" = "Genehmigen"; -"button.revoke" = "Widerrufen"; -"button.reject" = "Verwerfen"; -"button.connect" = "Verbinden"; -"button.save" = "Speichern"; -"button.send" = "Senden"; -"button.sign" = "Signieren"; -"button.more" = "Mehr anzeigen"; -"button.i_understand" = "Ich stimme zu"; +"button.apply" = "Apply"; +"button.cancel" = "Cancel"; +"button.close" = "Close"; +"button.continue" = "Continue"; +"button.done" = "Done"; +"button.reset" = "Reset"; +"button.import" = "Import"; +"button.next" = "Next"; +"button.delete" = "Delete"; +"button.share" = "Share"; +"button.paste" = "Paste"; +"button.resend" = "Resend"; +"button.backup" = "Backup"; +"button.copy" = "Copy"; +"button.retry" = "Retry"; +"button.report" = "Report"; +"button.add" = "Add"; +"button.approve" = "Approve"; +"button.revoke" = "Revoke"; +"button.reject" = "Reject"; +"button.connect" = "Connect"; +"button.save" = "Save"; +"button.send" = "Send"; +"button.sign" = "Sign"; +"button.more" = "Show More"; +"button.i_understand" = "I Understand"; "button.learn_more" = "Learn More"; -"alert" = "Alarm"; -"alert.copied" = "Kopiert"; -"alert.saved" = "Gespeichert"; -"alert.saved_to_icloud" = "In iCloud gespeichert"; -"alert.no_internet" = "Kein Internet"; -"alert.wrong_amount" = "Falscher Betrag"; -"alert.no_fee" = "Falsche Gebühr"; -"alert.warning" = "Warnung"; -"alert.error" = "Fehler"; -"alert.unknown_error" = "Unbekannter Fehler"; -"alert.success_action" = "Fertig"; -"alert.success" = "Erfolgreich"; - -"alert.added_to_watchlist" = "Zur Merkliste hinzugefügt"; -"alert.removed_from_watchlist" = "Aus Merkliste entfernt"; -"alert.added_to_wallet" = "Zum Wallet hinzugefügt"; -"alert.removed_from_wallet" = "Aus Wallet entfernt"; -"alert.already_added_to_wallet" = "Bereits im Wallet enthalten"; -"alert.not_supported_yet" = "Wird noch nicht unterstützt"; -"alert.copied" = "Kopiert"; -"alert.created" = "Erstellt"; -"alert.imported" = "Wiederherstellen"; -"alert.wallet_added" = "Wallet hinzugefügt"; -"alert.deleted" = "Gelöscht"; -"alert.waiting_for_session" = "Warte auf Sitzung"; -"alert.try_again" = "Nochmals versuchen"; -"alert.disconnecting" = "Verbindung wird getrennt"; -"alert.disconnected" = "Verbindung getrennt"; -"alert.enabling" = "Aktivieren"; -"alert.enabled_coins" = "%@ weitere Coins aktiviert"; -"alert.sending" = "Wird gesendet"; -"alert.sent" = "Gesendet"; +"alert" = "Alert"; +"alert.copied" = "Copied"; +"alert.saved" = "Saved"; +"alert.saved_to_icloud" = "Saved to iCloud"; +"alert.no_internet" = "No Internet"; +"alert.wrong_amount" = "Wrong Amount"; +"alert.no_fee" = "Wrong Fee"; +"alert.warning" = "Warning"; +"alert.error" = "Error"; +"alert.unknown_error" = "Unknown Error"; +"alert.success_action" = "Done"; +"alert.success" = "Success"; + +"alert.added_to_watchlist" = "Added to Watchlist"; +"alert.removed_from_watchlist" = "Removed from Watchlist"; +"alert.added_to_wallet" = "Added to Wallet"; +"alert.removed_from_wallet" = "Removed from Wallet"; +"alert.already_added_to_wallet" = "Already added to Wallet"; +"alert.not_supported_yet" = "Not Supported Yet"; +"alert.created" = "Created"; +"alert.imported" = "Imported"; +"alert.wallet_added" = "Wallet Added"; +"alert.deleted" = "Deleted"; +"alert.waiting_for_session" = "Waiting for Session"; +"alert.try_again" = "Try Again"; +"alert.disconnecting" = "Disconnecting"; +"alert.disconnected" = "Disconnected"; +"alert.enabling" = "Enabling"; +"alert.enabled_coins" = "Enabled %@ more coins"; +"alert.sending" = "Sending"; +"alert.sent" = "Sent"; "alert.swapping" = "Swapping"; "alert.swapped" = "Swapped"; -"alert.approving" = "Wird bestätigt"; -"alert.approved" = "Bestätigt"; -"alert.revoking" = "Widerrufen"; -"alert.revoked" = "Widerrufen"; -"alert.cant_recognize" = "Kann nicht erkennen"; +"alert.approving" = "Approving"; +"alert.approved" = "Approved"; +"alert.revoking" = "Revoking"; +"alert.revoked" = "Revoked"; +"alert.cant_recognize" = "Can't Recognize"; "no_results_found" = "No results found"; -"action.loading" = "wird geladen ..."; -"placeholder.search" = "Suche"; +"action.loading" = "loading..."; +"placeholder.search" = "Search"; "status" = "Status"; -"connecting" = "Verbinde..."; +"connecting" = "Connecting..."; "online" = "Online"; "offline" = "Offline"; -"confirm" = "Bestätigen"; -"connect" = "Verbinden"; -"price" = "Preis"; -"value" = "Wert"; -"note" = "Hinweis"; +"confirm" = "Confirm"; +"connect" = "Connect"; +"price" = "Price"; +"value" = "Value"; +"note" = "Note"; "version" = "Version %@"; "n/a" = "N/A"; -"sync_error" = "Sync-Fehler. Versuchen Sie es erneut."; +"sync_error" = "Sync error. Try again."; "number.thousand" = "%@K"; "number.million" = "%@M"; @@ -93,439 +92,436 @@ "number.trillion" = "%@T"; "number.quadrillion" = "%@Q"; -"selector.any" = "Beliebig"; +"selector.any" = "Any"; // Access Camera -"access_camera.message" = "%@ benötigt Zugriff auf Ihre Kamera, um den QR-Code zu scannen. -Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; -"access_camera.settings" = "Einstellungen"; +"access_camera.message" = "Yo, %@ wants a lil' peek with that camera to catch that flashy QR code. 📸 -// Restore +Swag your way to Settings -> %@ and let it shine!"; +"access_camera.settings" = "Settings"; -"restore.title" = "Wallet importieren"; -"restore.advanced" = "Erweitert"; -"restore.import_by" = "Importieren von"; -"restore.restore_type.mnemonic" = "Wiederherstellungsphrase"; -"restore.restore_type.private_key" = "Privater Schlüssel"; -"restore.mnemonic.placeholder" = "12, 15, 18, 21 oder 24 Wörter eingeben"; -"restore.private_key.placeholder" = "Geben Sie EVM privaten Schlüssel, BIP32 Root-Schlüssel oder Account Extended Private Key ein"; -"restore.private_key.invalid_key" = "Ungültiger Schlüssel"; -"restore_error.mnemonic_word_count" = "Falsche Wortzahl. Sie muss zwischen 12 und 24 Wörtern liegen. Sie haben eingegeben: %@"; -"restore.checksum_error" = "Ungültige Prüfsumme"; -"restore.passphrase" = "Passwort"; -"restore.input.passphrase" = "Passwort"; -"restore.passphrase_description" = "Dieses Passwort wird verwendet, um die Sicherungsdatei Ihrer Wallet zu verschlüsseln. Es hat nichts mit Ihrem Apple iCloud-Passwort zu tun. Wenn Sie es vergessen oder verlieren, kann es nicht wiederhergestellt oder zurückgesetzt werden."; -"restore.error.empty_passphrase" = "Die Passphrase darf nicht leer sein"; -"restore.non_standard_import" = "Nicht standardmäßige Wiederherstellung"; -"restore.non_standard_import.description" = "Diese Seite bietet einen speziellen Wallet-Importmechanismus für Benutzer von %@ die eine nicht standardmäßige Wallet haben. Solche Brieftaschen könnten in %@ Version 0 erstellt worden sein. 7 - 0.28 unter Verwendung einer nicht-englischen Wiederherstellungsphrase und/oder eines Sonderzeichens in der Brieftasche Passphrase (d.h. diakritisches Symbol).\n\nWenn Sie ein betroffener Benutzer sind, wird Ihr Wallet-Guthaben nach dem Import in Version 0.29 oder höher 0 angezeigt. Auf dieser Seite können Sie den Zugriff auf Ihre nicht standardmäßige Brieftasche wiederherstellen. Sobald Sie importiert haben, empfehlen wir Ihnen eine neue Wallet zu erstellen (die standardkonform ist) und Geld dorthin zu verschieben."; -"restore.warning.non_recommended.description" = "Diese Brieftasche scheint ein nicht standardmäßiges Zeichen in ihrer geheimen Wortliste und/oder einer Passphrase zu verwenden. Wenn Sie kein Guthaben oder Transaktionen sehen, lesen Sie bitte unten für Details.\n\nKLICKEN, UM WEITERE INFORMATIONEN ZU ERHALTEN"; -"restore.error.non_standard.description" = "Dies ist eine nicht standardmäßige Wallet. \n\nKLICKEN SIE MEHR INFO"; +// Restore +"restore.title" = "Import Wallet"; +"restore.advanced" = "Advanced"; +"restore.import_by" = "Import By"; +"restore.restore_type.mnemonic" = "Recovery Phrase"; +"restore.restore_type.private_key" = "Private Key"; +"restore.mnemonic.placeholder" = "Enter Recovery Phrase"; +"restore.private_key.placeholder" = "Enter EVM Private Key, BIP32 Root Key or Account Extended Private Key"; +"restore.private_key.invalid_key" = "Invalid Key"; +"restore_error.mnemonic_word_count" = "Hold up, fam! We need it between 12 and 24 words. You droppin' %@ on me? Let's keep it wavy"; +"restore.checksum_error" = "Invalid checksum"; +"restore.passphrase" = "Passphrase"; +"restore.input.passphrase" = "Passphrase"; +"restore.passphrase_description" = "This is the password that locks up your wallet backup, like a secret track. Ain't no iCloud vibes here. Lose it and it's gone, like old Kanye mixtapes." +"restore.error.empty_passphrase" = "The passphrase cannot be empty"; +"restore.non_standard_import" = "Non-Standard Import"; +"restore.non_standard_import.description" = "Yo, %@ fam, this page is for y'all with that unique Yeezy-style wallet. Maybe you were vibing in the %@ 0.27 - 0.28 era with some international lyrics or fancy symbols in your wallet. Seeing a big zero on that balance after the 0.29 drop? We got you. Dive in here to get back to the beat. And after? Consider rocking a brand-new, standard-compliant wallet and make those money moves." +"restore.warning.non_recommended.description" = "Hold up! Your wallet's got that avant-garde character style. Not seeing the cash or the flow? Dive deep for the deets below. 🎤\n\nTAP IN FOR THE SCOOP" +"restore.error.non_standard.description" = "Yo, this wallet's got its own wave. It's non-standard. 🕶️\n\nTAP TO UNRAVEL THE MYSTERY" // Restore Type -"restore_type.title" = "Wallet importieren"; +"restore_type.title" = "Import Wallet"; -"restore_type.recovery.title" = "von Wiederherstellungsphrase"; -"restore_type.cloud.title" = "von iCloud"; -"restore_type.cex.title" = "von Exchange Wallet"; +"restore_type.recovery.title" = "from Recovery Phrase"; +"restore_type.cloud.title" = "from iCloud"; +"restore_type.cex.title" = "from Exchange Wallet"; -"restore_type.recovery.description" = "Importieren Sie mithilfe der Wiederherstellungsphrase oder des privaten Schlüssels."; -"restore_type.cloud.description" = "Importieren Sie aus einer Sicherungsdatei in Ihrem Schlüsselbund."; -"restore_type.cex.description" = "Verbinden Sie sich mit einer Brieftasche bei zentralisiertem Austausch."; +"restore_type.recovery.description" = "Bring it back with that secret lyric or the key to the studio. "; +"restore_type.cloud.description" = "Pull that backup track from your keychain playlist."; +"restore_type.cex.description" = "Link up with the main stage – the centralized exchange. Let's make those money moves!"; // Restore Cloud -"restore.cloud.title" = "Sicherung auswählen"; -"restore.cloud.description" = "Wählen Sie die Sicherungskopie der Wallet aus, die Sie wiederherstellen möchten."; -"restore.cloud.empty" = "Keine Sicherungen gefunden."; -"restore.cloud.imported" = "Importierte Wallet"; +"restore.cloud.title" = "Select Backup"; +"restore.cloud.description" = "Pick the mixtape (backup) of the wallet you wanna bring back to the spotlight." +"restore.cloud.empty" = "No backups found."; +"restore.cloud.imported" = "Imported wallets"; -"restore.cloud.password.title" = "Passwort eingeben"; -"restore.cloud.password.placeholder" = "Passwort sichern"; -"restore.cloud.password.description" = "Geben Sie das Backup-Passwort ein, um Ihre Brieftasche aus iCloud zu importieren."; +"restore.cloud.password.title" = "Enter Password"; +"restore.cloud.password.placeholder" = "Backup Password"; +"restore.cloud.password.description" = "Drop that secret beat (backup password) to get your wallet vibes from the iCloud stage." // Restore Cex -"restore.cex.title" = "CEX auswählen"; -"restore.cex.description" = "Wählen Sie den zentralen Austausch aus, zu dem Sie sich verbinden möchten."; +"restore.cex.title" = "Select CEX"; +"restore.cex.description" = "Pick the main stage (centralized exchange) you wanna rock with." // Restore Binance -"restore.binance.description" = "Bitte geben Sie den API-Schlüssel und das API-Geheimnis an, um Ihren Austausch zu verknüpfen."; -"restore.binance.api_key" = "API-Schlüssel"; -"restore.binance.secret_key" = "Geheimer Schlüssel"; -"restore.binance.connect" = "Verbinden"; -"restore.binance.connecting" = "Verbinde..."; -"restore.binance.get_api_keys" = "API-Schlüssel erhalten"; -"restore.binance.failed_to_connect" = "Verbindung zum API-Schlüssel fehlgeschlagen"; -"restore.binance.invalid_qr_code" = "Ungültiger QR-Code"; +"restore.binance.description" = "Drop those VIP passes (API Keys and API Secret) to get backstage with your exchange."; +"restore.binance.api_key" = "API Key"; +"restore.binance.secret_key" = "Secret Key"; +"restore.binance.connect" = "Connect"; +"restore.binance.connecting" = "Connecting..."; +"restore.binance.get_api_keys" = "Get API Keys"; +"restore.binance.failed_to_connect" = "Failed to connect your API Key"; +"restore.binance.invalid_qr_code" = "Invalid QR Code"; // Coin Settings -"coin_settings.title" = "Blockchain Einstellungen"; +"coin_settings.title" = "Blockchain Settings"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (empfohlen)"; -"sync_mode.from_blockchain" = "Von Blockchain"; -"blockchain_settings.description" = "Wählen Sie das Adressformat für den Empfang von Zahlungen. Ein korrektes Format sollte bei der Wiederherstellung einer bestehenden Brieftasche gewählt werden."; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; +"sync_mode.from_blockchain" = "From Blockchain"; +"blockchain_settings.description" = "Pick your style (address format) for catching those payments. When bringing back an old mix (restoring a wallet), make sure the beats match." // Coin Platforms -"coin_platforms.native" = "Nativ"; +"coin_platforms.native" = "Native"; // Copy Warning -"copy_warning.title" = "Kopierrisiko"; -"copy_warning.description" = "Als Sicherheitsmaßnahme empfehlen wir, die Kopierfunktion nicht für diesen Wert zu verwenden."; -"copy_warning.dont_copy" = "Nicht kopieren"; -"copy_warning.i_will_risk_it" = "Ich werde es riskieren"; +"copy_warning.title" = "Risk of Copying"; +"copy_warning.description" = "For safety, like rocking shades indoors, skip that copy action for this value, fam." +"copy_warning.dont_copy" = "Don't Copy"; +"copy_warning.i_will_risk_it" = "I will risk it"; // Recovery Phrase -"recovery_phrase.title" = "Wiederherstellungsphrase"; -"recovery_phrase.warning" = "Teilen Sie diesen Schlüssel niemals mit niemandem. %@ Wallet-Support-Team wird ihn niemals anfragen."; -"recovery_phrase.tap_to_show" = "Tippen, um Wiederherstellungsphrase anzuzeigen"; -"recovery_phrase.passphrase" = "Passwort"; -"recovery_phrase.copy_warning.title" = "Risiko der Wiederherstellungs-Phrase-Kopie"; -"recovery_phrase.copy_warning.description" = "Als Sicherheitsmaßnahme empfehlen wir, Kopiermaßnahmen nicht auf Wiederherstellungs-Phrase zu verwenden."; +"recovery_phrase.title" = "Recovery Phrase"; +"recovery_phrase.warning" = "Hold onto this key like a rare Yeezy drop. The %@ Wallet crew? We won't ever slide into your DMs for it." +"recovery_phrase.tap_to_show" = "Tap to show recovery phrase"; +"recovery_phrase.passphrase" = "Passphrase"; +"recovery_phrase.copy_warning.title" = "Risk of Recovery Phrase copy"; +"recovery_phrase.copy_warning.description" = "As a security measure,we recommend not using copy action on recovery phrase."; // EVM Private Key -"evm_private_key.title" = "EVM privater Schlüssel"; -"evm_private_key.tap_to_show" = "Tippen um privaten Schlüssel anzuzeigen"; +"evm_private_key.title" = "EVM Private Key"; +"evm_private_key.tap_to_show" = "Tap to show private key"; // Extended Key "extended_key.bip32_root_key" = "BIP32 Root Key"; "extended_key.account_extended_private_key" = "Account Extended Private Key"; "extended_key.account_extended_public_key" = "Account Extended Public Key"; -"extended_key.purpose" = "Zweck"; +"extended_key.purpose" = "Purpose"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; -"extended_key.tap_to_show" = "Zum Anzeigen des erweiterten privaten Schlüssels tippen"; +"extended_key.tap_to_show" = "Tap to show extended private key"; // Backup -"backup.title" = "Wiederherstellungsphrase"; -"backup.description" = "Schreibe diese Wörter in die richtige Reihenfolge und halte sie an einem sicheren Ort"; -"backup.tap_to_show" = "Tippen, um Wiederherstellungsphrase anzuzeigen"; -"backup.passphrase" = "Passwort"; -"backup.verify" = "Prüfen"; -"backup.verified" = "Verifiziert"; +"backup.title" = "Recovery Phrase"; +"backup.description" = "Jot these words down, in flow, and stash them somewhere no haters can find." +"backup.tap_to_show" = "Tap to show recovery phrase"; +"backup.passphrase" = "Passphrase"; +"backup.verify" = "Verify"; +"backup.verified" = "Verified"; // Backup Verify Words -"backup_verify_words.title" = "Prüfen"; -"backup_verify_words.description" = "Wählen Sie zwei angeforderte Wörter aus Ihrer Wallet-Wiederherstellungsphrase"; -"backup_verify_words.incorrect_word" = "Falsches Wort"; +"backup_verify_words.title" = "Verify"; +"backup_verify_words.description" = "Pick out those two spotlight words from your wallet's lyrical recovery phrase." +"backup_verify_words.incorrect_word" = "Incorrect Word"; // Backup Verify Passphrase -"backup_verify_passphrase.title" = "Prüfen"; -"backup_verify_passphrase.description" = "Geben Sie Ihre Passphrase ein"; -"backup_verify_passphrase.incorrect_passphrase" = "Falsches Passwort"; +"backup_verify_passphrase.title" = "Verify"; +"backup_verify_passphrase.description" = "Enter the passphrase"; +"backup_verify_passphrase.incorrect_passphrase" = "Incorrect passphrase"; // Backup Required -"backup_required.title" = "Sicherung erforderlich"; +"backup_required.title" = "Backup Required"; // Backup Prompt -"backup_prompt.title" = "Manuelle Sicherung"; -"backup_prompt.warning" = "Erstellen Sie eine Sicherungskopie der Wiederherstellungsphrase und des zugehörigen Passworts, mit dem Sie Ihre Wallet wiederherstellen können, wenn Ihr Telefon verloren geht, gestohlen, kaputt, etc."; -"backup_prompt.backup" = "Sicherung"; -"backup_prompt.backup_manual" = "Manuelle Sicherung"; -"backup_prompt.backup_cloud" = "Sicherung in iCloud"; -"backup_prompt.later" = "Später"; +"backup_prompt.title" = "Manual Backup"; +"backup_prompt.warning" = "Craft a backup mix of the recovery vibes and that secret code. It's your safety net if your phone goes MIA, gets jacked, or just breaks down." +"backup_prompt.backup" = "Backup"; +"backup_prompt.backup_manual" = "Manual Backup"; +"backup_prompt.backup_cloud" = "Backup to iCloud"; +"backup_prompt.later" = "Later"; // Backup to iCloud -"backup.cloud.title" = "Sicherung in iCloud"; -"backup.cloud.description" = "iCloud-Speicher ist ein von Apple bereitgestellter Cloud-Speicherdienst. Es ist wichtig zu wissen, dass Ihre Daten auf Apple-Servern gespeichert werden und nicht auf Ihren persönlichen Geräten. Das bedeutet, dass Sie Ihre Daten anvertrauen und die Sicherheit Ihrer Daten einem Drittanbieter überlassen."; +"backup.cloud.title" = "Backup to iCloud"; +"backup.cloud.description" = "Heads up: iCloud is Apple's stage, not yours. Your beats (data) are on their mics (servers), not your personal studio. So, you're passing the mic and trust to them." +"backup.cloud.terms.item.1" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; -"backup.cloud.terms.item.1" = "Ich verstehe, dass der Verlust des Zugriffs auf meine iCloud dazu führen wird, dass auch der Zugriff auf die Sicherung einer entsprechenden Brieftasche verloren geht."; - -"backup.cloud.name.title" = "Sicherungsname"; -"backup.cloud.name.description" = "Geben Sie den Namen für die Backup-Datei ein."; -"backup.cloud.name.empty" = "Sicherungsname darf nicht leer sein!"; -"backup.cloud.name.error.empty" = "Sicherungsname darf nicht leer sein"; -"backup.cloud.name.error.already_exist" = "Sicherungsname existiert bereits!"; +"backup.cloud.name.title" = "Backup Name"; +"backup.cloud.name.description" = "Enter name for the backup file."; +"backup.cloud.name.empty" = "Backup name can't be empty!"; +"backup.cloud.name.error.empty" = "Backup name must be not empty"; +"backup.cloud.name.error.already_exist" = "Backup name already exist!"; "backup.cloud.name.placeholder" = "Name"; -"backup.cloud.password.title" = "Passwort festlegen"; -"backup.cloud.password.description" = "Legen Sie ein Passwort fest, um das Entsperren zu ermöglichen. Das Passwort muss mindestens 8 Zeichen lang sein und einen Kleinbuchstaben, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten."; -"backup.cloud.password.highlighted_description" = "Vergessen Sie dieses Passwort nicht! Es ist getrennt von Ihrem Apple iCloud-Passwort und kann nicht wiederhergestellt oder zurückgesetzt werden."; -"backup.cloud.password.placeholder" = "Passwort"; -"backup.cloud.password.confirm.placeholder" = "Bestätigen"; -"backup.cloud.password.save" = "Speichern und sichern"; - -"backup.cloud.password.error.empty_passphrase" = "Passphrase darf nicht leer sein"; -"backup.cloud.password.error.forbidden_symbols" = "Bitte verwende nur unterstützte Symbole: \\ _ # @ | % ]]>"; -"backup.cloud.password.error.minimum_requirement" = "Das Passwort muss mindestens 8 Zeichen enthalten, darunter ein Großbuchstabe, ein Kleinbuchstabe, eine Zahl und ein Symbol."; -"backup.cloud.password.error.invalid_password" = "Falsches Passwort"; -"backup.cloud.password.error.invalid_backup" = "Backup ist beschädigt"; -"backup.cloud.password.confirm.error.doesnt_match" = "Passwort stimmt nicht überein"; -"backup.cloud.not_available" = "iCloud nicht verfügbar"; -"backup.cloud.cant_create_file" = "Die Datei kann nicht in iCloud gespeichert werden"; -"backup.cloud.cant_delete_file" = "Kann nicht von iCloud gelöscht werden"; -"backup.cloud.no_access.title" = "Zugriff auf iCloud"; -"backup.cloud.no_access.title" = "Zugriff auf iCloud"; -"backup.cloud.no_access.description" = "Um ein Backup zu erstellen, müssen Sie Zugriff auf iCloud-Speicher gewähren."; - +"backup.cloud.password.title" = "Set Password"; +"backup.cloud.password.description" = "Craft a password that's as iconic as a Kanye track. Need 8 beats (symbols) with a mix of highs (uppercase), lows (lowercase), numbers, and some flair (special character)." +"backup.cloud.password.highlighted_description" = "Don't forget this password! It is separate from your Apple iCloud password, and it cannot be recovered or reset."; +"backup.cloud.password.placeholder" = "Password"; +"backup.cloud.password.confirm.placeholder" = "Confirm"; +"backup.cloud.password.save" = "Save and Backup"; + +"backup.cloud.password.error.empty_passphrase" = "Passphrase cannot be empty"; +"backup.cloud.password.error.forbidden_symbols" = "Stick to the platinum hits: A-Z a-z 0-9 and these symbols:A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %. No remixes, please."; +"backup.cloud.password.error.minimum_requirement" = "Bring at least 8 characters to the stage, featuring a capital hit, a chill lowercase, a number, and some special effects." +"backup.cloud.password.error.invalid_password" = "Incorrect Password"; +"backup.cloud.password.error.invalid_backup" = "Backup is corrupted"; +"backup.cloud.password.confirm.error.doesnt_match" = "Password doesn’t match"; +"backup.cloud.not_available" = "iCloud not Available"; +"backup.cloud.cant_create_file" = "Can't save File to iCloud"; +"backup.cloud.cant_delete_file" = "Can't delete from iCloud"; +"backup.cloud.no_access.title" = "Access iCloud"; +"backup.cloud.no_access.description" = "Wanna drop a backup? Gotta pass the mic to iCloud storage first." // Errors -"error.send.self_transfer" = "Sich selbst zu senden wird nicht unterstützt"; -"error.send_binance.memo_required" = "Empfänger benötigt nicht-leeren Vermerk bei Transfer einer Transaktion"; -"error.send_binance.only_digits_allowed" = "Der Vermerk darf nur Zahlen enthalten"; -"error.send_z_cash.transparent_address" = "%@ unterstützt keine Zahlungen an eine transparente Adresse"; +"error.send.self_transfer" = "Sending to yourself is not supported"; +"error.send_binance.memo_required" = "Yo, the receiver's looking for a memo that's not silent. Drop some beats in that transfer note." +"error.send_binance.only_digits_allowed" = "The memo's gotta be numbers only, like chart-topping digits." +"error.send_z_cash.transparent_address" = "Hold up, %@ ain't vibing with payments to a see-through address." // Balance -"balance.title" = "Guthaben"; -"balance.tab_bar_item" = "Guthaben"; -"balance.send" = "Senden"; -"balance.withdraw" = "Abheben"; +"balance.title" = "Balance"; +"balance.tab_bar_item" = "Balance"; +"balance.send" = "Send"; +"balance.withdraw" = "Withdraw"; "balance.swap" = "Swap"; -"balance.receive" = "Empfangen"; -"balance.deposit" = "Anfordern"; -"balance.address" = "Adresse"; -"balance.rate_per_coin" = "%@ pro %@"; -"balance.syncing" = "Synchronisieren..."; -"balance.searching" = "Transaktionen werden gesucht..."; -"balance.downloading_sapling" = "Setze herunterladen... %d%%"; -"balance.downloading_blocks" = "Blöcke herunterladen"; -"balance.scanning_blocks" = "Scanne Blöcke"; -"balance.enhancing_transactions" = "Transaktionen verbessern"; +"balance.receive" = "Receive"; +"balance.deposit" = "Deposit"; +"balance.address" = "Address"; +"balance.rate_per_coin" = "%@ per %@"; +"balance.syncing" = "Syncing..."; +"balance.searching" = "Searching transactions..."; +"balance.downloading_sapling" = "Downloading Sapling... %d%%"; +"balance.downloading_blocks" = "Downloading Blocks"; +"balance.scanning_blocks" = "Scanning Blocks"; +"balance.enhancing_transactions" = "Enhancing Transactions"; "balance.searching.count" = "%@ tx"; -"balance.syncing_percent" = "Synchronisieren... %@"; -"balance.synced_through" = "bis %@"; -"balance.add_coin" = "Coin hinzufügen"; -"balance.invalid_api_key" = "Ungültiger API-Schlüssel"; -"balance.empty.add_coins" = "Coins hinzufügen"; -"balance.empty.description" = "Sie haben keine Coins zu diesem Wallet hinzugefügt."; -"balance.watch_empty.description" = "Diese Wallet-Adresse hat keinen Kontostand"; -"balance.sort_by" = "Sortieren nach"; -"balance.sort.header" = "Sortieren nach"; -"balance.sort.valueHighToLow" = "Guthaben"; +"balance.syncing_percent" = "Syncing... %@"; +"balance.synced_through" = "until %@"; +"balance.add_coin" = "Add Coin"; +"balance.invalid_api_key" = "Invalid API Key"; +"balance.empty.add_coins" = "Add Coins"; +"balance.empty.description" = "Looks like your wallet's still waiting for its debut album. No coins dropped in yet." +"balance.watch_empty.description" = "This wallet's address? Still waiting for its first hit single. No balance here." +"balance.sort_by" = "Sort By"; +"balance.sort.header" = "Sort by"; +"balance.sort.valueHighToLow" = "Balance"; "balance.sort.az" = "Name"; -"balance.sort.price_change" = "Preisänderung"; +"balance.sort.price_change" = "Price Change"; -"balance_error.change_source" = "Quelle ändern"; -"balance_error.sync_error" = "Sync-Fehler"; -"lost_accounts.warning_title" = "iOS-Keychain-Fehler"; -"lost_accounts.warning_message" = "Die verschlüsselten Daten Ihres Wallets wurden kürzlich ungültig, weil Ihr iOS-Sperrbildschirm geändert wurde"; +"balance_error.change_source" = "Change Source"; +"balance_error.sync_error" = "Sync Error"; +"lost_accounts.warning_title" = "iOS Keychain Error"; +"lost_accounts.warning_message" = "The encrypted data holding your wallet was recently invalidated because your iOS lock screen was changed"; // Token Balance Page "balance.token.locked" = "Locked"; "balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "Der Sender hat dieses Guthaben mit einer Ausgabesperre versehen, die zu dem angezeigten Datum abläuft.\n\nKeine Sorge, die erhaltenen Bitcoin sind bereits Ihre. Die Bitcoins sind nur zeitweise gesperrt, so dass Sie sie erst nach Ablauf der Sperrzeit im Bitcoin-Netzwerk ausgeben können."; -"balance.token.staked" = "Absteckung"; -"balance.token.staked.info.title" = "Absteckter Titel"; -"balance.token.staked.info.description" = "Absteckungstext"; +"balance.token.locked.info.description" = "The sender dropped these funds with a lil' hold - kinda like a track release date.\n\nChill, those Bitcoins are already in your studio, but you gotta wait for the drop before you can make it rain on the Bitcoin network." +"balance.token.staked" = "Staked"; +"balance.token.staked.info.title" = "Staked title"; +"balance.token.staked.info.description" = "Staked Description Text"; "balance.token.frozen" = "Frozen"; -"balance.token.frozen.info.title" = "gefrorener Titel"; -"balance.token.frozen.info.description" = "gefrorener Beschreibungstext"; +"balance.token.frozen.info.title" = "Frozen title"; +"balance.token.frozen.info.description" = "Frozen Description Text"; // Account switcher -"switch_account.title" = "Wallet wechseln"; +"switch_account.title" = "Switch Wallet"; "switch_account.wallets" = "Wallets"; -"switch_account.watch_wallets" = "Watch-Adresse"; +"switch_account.watch_wallets" = "Watch Wallets"; // Release notes -"release_notes.title" = "Das ist neu"; -"release_notes.follow_us" = "Folgen Sie uns"; +"release_notes.title" = "What's New"; +"release_notes.follow_us" = "Follow Us"; // Deposit -"deposit.receive_coin" = "%@ Anfordern "; -"deposit.address" = "Adresse"; -"deposit.your_address" = "Deine Adresse"; -"receive_alert.not_backed_up_description" = "Sie müssen %@ sichern, bevor Sie %@ empfangen können."; -"receive_alert.any_coins.not_backed_up_description" = "Du musst %@ sichern, bevor du Münzen erhalten kannst."; -"deposit.no_adapter.error" = "Kann keine Adresse angeben"; +"deposit.receive_coin" = "Receive %@"; +"deposit.address" = "Address"; +"deposit.your_address" = "Your Address"; +"receive_alert.any_coins.not_backed_up_description" = "Before you collect those coins, make sure you've laid down a backup of %@. "; +"receive_alert.not_backed_up_description" = "Before you catch those %@ vibes, make sure you've saved %@ like a classic track."; +"deposit.no_adapter.error" = "Can't provide address"; "deposit.address_format" = "Format"; -"deposit.address_network" = "Netzwerk"; -"deposit.qr_code_description" = "Ihre Adresse für %@"; -"deposit.qr_code_description.watch" = "Beobachtungsadresse von %@"; +"deposit.address_network" = "Network"; +"deposit.qr_code_description" = "Your address for depositing %@"; +"deposit.qr_code_description.watch" = "Watch address of %@"; "deposit.account" = "Account"; -"deposit.not_active" = "nicht aktiv"; -"deposit.not_active.title" = "Keine aktive Adresse"; -"deposit.not_active.tron_description" = "Neu erstellte Konten in der TRON-Blockchain sind inaktiv und können nicht abgefragt oder untersucht werden. Sie müssen aktiviert werden.\n\nDie Übertragung von TRX- oder TRC-10-Token an eine inaktive Konto-Adresse wird das Konto aktivieren. Zur Aktivierung eines neuen Kontos auf der Tron-Kette ist eine Gebühr von 1 TRX erforderlich"; +"deposit.not_active" = "Not active"; +"deposit.not_active.title" = "Not Active Address"; +"deposit.not_active.tron_description" = "Newly created accounts on the TRON blockchain are inactive and cannot be queried or explored. They need to be activated.\n\nTransferring TRX or TRC-10 tokens to an inactive account address will activate the account. Activating a new account on the Tron chain requires a fee of 1 TRX"; -"deposit.zcash.restore.description" = "Hast du schon einmal im Besitz von ZEC-Münzen?"; -"deposit.zcash.restore.already_own" = "Ja, ich besitze bereits"; -"deposit.zcash.restore.dont_have" = "Nein, ich habe nicht"; +"deposit.zcash.restore.description" = "Have you previously owned any ZEC coins?"; +"deposit.zcash.restore.already_own" = "Yes, I already own"; +"deposit.zcash.restore.dont_have" = "No, I don’t have"; -"deposit.warning" = "Senden Sie nur %@ an diese Adresse. Das Senden anderer Arten von Tokens an diese Adresse führt zu ihrem ultimativen Verlust."; +"deposit.warning" = "Send only %@ to this address. Sending other types of tokens to this address will result in their ultimate loss."; -"receive_network_select.title" = "Netzwerk"; -"receive_network_select.description" = "Wählen Sie ein Netzwerk aus und erhalten Sie eine Adresse."; +"receive_network_select.title" = "Network"; +"receive_network_select.description" = "Choose a network and get an address to receive."; -"receive_address_format_select.title" = "Adressformatierung"; -"receive_address_format_select.description" = "Wählen Sie ein Adressformat aus, um Ihre Adresse zu erhalten."; -"receive_address_format_select.bitcoin.bottom_description" = "Das Native SegWit Format wird in Bitcoin bevorzugt für einen verbesserten Durchsatz und mehr Sicherheit. Alle Adressformate (Taproot, SegWit, Legacy) können unabhängig vom Adressformat des Absenders zum Empfang von BTC verwendet werden ermöglicht nahtlose Transaktionen über verschiedene Münztypen."; -"receive_address_format_select.bitcoin_cash.bottom_description" = "Das Cash Address Format wird bevorzugt für den Erhalt von Bitcoin Cash (BCH) aufgrund seiner verbesserten Benutzerfreundlichkeit und Kompatibilität bevorzugt. Beide Adressformate können jedoch unabhängig vom Adressformat des Absenders austauschbar genutzt werden, um BCH zu empfangen."; +"receive_address_format_select.title" = "Address Format"; +"receive_address_format_select.description" = "Select an address format to receive your address."; +"receive_address_format_select.bitcoin.bottom_description" = "The Native SegWit format is preferred in Bitcoin for improved throughput and security. All address formats (Taproot, SegWit, Legacy) can be used interchangeably to receive BTC regardless of the sender's address format, enabling seamless transactions across different coin types."; +"receive_address_format_select.bitcoin_cash.bottom_description" = "The Cash Address format is preferred for receiving Bitcoin Cash (BCH) due to its improved user experience and compatibility. However, both address formats can be used interchangeably to receive BCH, regardless of the sender's address format."; -"blockchain_type.recommended" = " (empfohlen)"; +"blockchain_type.recommended" = " (recommended)"; // Send -"send.title" = "Senden %@"; -"send.send" = "Senden"; -"send.no_assets" = "Sie haben keine Assets zu senden."; -"send.amount_placeholder" = "Betrag"; -"send.address_placeholder" = "Adresse"; -"send.address_or_domain_placeholder" = "Adresse oder Domäne"; -"send.fee" = "Gebühr"; -"send.network_fee" = "Netzwerkgebühr"; -"send.estimated_fee" = "Geschätzte Gebühren"; -"send.max_fee" = "Max Gebühr"; -"send.duration.hours" = "%d Std."; -"send.duration.minutes" = "%d Min."; -"send.available_balance" = "Verfügbares Guthaben"; -"send.max_button" = "Max."; -"send.next_button" = "Weiter"; -"send.error.invalid" = "Ungültig"; -"send.error.address" = "Adresse"; +"send.title" = "Send %@"; +"send.send" = "Send"; +"send.no_assets" = "You have no assets to send."; +"send.amount_placeholder" = "Amount"; +"send.address_placeholder" = "Address"; +"send.address_or_domain_placeholder" = "Address or Domain"; +"send.fee" = "Fee"; +"send.network_fee" = "Network Fee"; +"send.estimated_fee" = "Estimated Fee"; +"send.max_fee" = "Max Fee"; +"send.duration.hours" = "%d h."; +"send.duration.minutes" = "%d min."; +"send.available_balance" = "Available Balance"; +"send.max_button" = "Max"; +"send.next_button" = "Next"; +"send.error.invalid" = "Invalid"; +"send.error.address" = "Address"; "send.hodler_locktime" = "TimeLock"; -"send.hodler_locktime_hour" = "1 Stunde"; -"send.hodler_locktime_month" = "1 Monat"; -"send.hodler_locktime_half_year" = "6 Monate"; -"send.hodler_locktime_year" = "1 Jahr"; -"send.hodler_locktime_off" = "Aus"; -"send.hodler_error.unsupported_address" = "Zeitsperre funktioniert nur mit P2PKH-Adressen (beginnend mit 1) "; -"send.fee_info.title" = "Gebührenrate"; -"send.fee_info.description" = "Blockchains verlangen, dass Nutzer Netzwerkgebühren beim Versenden von Transaktionen bezahlen. Die Gebühren sind höher, wenn viele Transaktionen im Netzwerk stattfinden.\n\nDie %@ Brieftasche schätzt die Gebühr basierend auf der aktuellen Blockchain Aktivität und empfiehlt den optimalen Wert, damit die Transaktion innerhalb einer angemessenen Zeit bearbeitet werden kann.\n\nDie empfohlene Gebührenrate, die als Betrag des satoshi Benutzers angezeigt wird, um für ein einziges Byte der Transaktion zu bezahlen. Daher hängt die Gesamtgebühr von der Gesamtgröße der Transaktion ab, die in Bytes gemessen wird.\n\nBenutzer können die zur Verfügung gestellten Steuerelemente verwenden, um den Gebührenwert zu erhöhen oder zu senken. Die Änderung des Gebührensatzes ändert die Gesamtgebühr für die Transaktion, die der Nutzer zu zahlen hat.\n\nSetzen der Gebühr unter dem empfohlenen Wert kann dazu führen, dass eine Transaktion stundenlang als ausstehend gehalten oder abgelehnt wird. Je niedriger der Wert, desto länger dauert die Bestätigung der Transaktion. Für Transaktionen, bei denen Priorität wichtig ist, empfehlen wir, höhere Gebührensätze festzulegen."; +"send.hodler_locktime_hour" = "1 hour"; +"send.hodler_locktime_month" = "1 month"; +"send.hodler_locktime_half_year" = "6 months"; +"send.hodler_locktime_year" = "1 year"; +"send.hodler_locktime_off" = "Off"; +"send.hodler_error.unsupported_address" = "Time locking works only when sending to payment addresses starting with 1... (aka BIP44 addresses)"; +"send.fee_info.title" = "Fee Rate"; +"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; "send.transaction_inputs_outputs_info.title" = "Transaction Inputs / Outputs"; -"send.transaction_inputs_outputs_info.description" = "Die meisten Bitcoin-Transaktionen sowie Transaktionen in Kryptowährungen wie Bitcoin-Cash, Dash, und Litecoin erzeugen zwei Ausgänge. Die eine Ausgabe ist der Betrag, der an den Empfänger geht, die andere ist die Umstellausgabe, die an den Absender zurückgegeben wird. Die Art und Weise, wie die meisten Brieftaschen Transaktionen erstellen, macht es für einen Dritten leicht zu verstehen, welche der Ausgaben an den Empfänger gingen und welcher der Änderungsbetrag an den Absender zurückgegeben wurde. Da die an den Absender zurückgegebene Ausgabe später bei zukünftigen Transaktionen verwendet wird, zeigt sich eine Verbindung zwischen diesen beiden Transaktionen.\n\nDie Brieftasche %@ implementiert Maßnahmen, um es jemandem zu erschweren, herauszufinden, welche Ausgabe wohin geht.\n\nEs gibt zwei Optionen für %@ Benutzer:"; +"send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "Die Reihenfolge der Transaktionsausgänge wird bei jeder Transaktion zufällig. Manchmal können Änderungen die erste Ausgabe sein, manchmal kann es die zweite sein. Wenn ein Benutzer dem Entwickler der App vertraut, dann erwäge dies eine empfohlene Option."; +"send.transaction_inputs_outputs_info.shuffle.description" = "The order of transaction outputs is randomized on every transaction. Sometimes change can be the first output, sometimes it can be the second. If a user trusts the developer of the app, then consider this a recommended option."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; -"send.transaction_inputs_outputs_info.deterministic.description" = "Es gibt einen allgemein vereinbarten Standard für die Bestellung von Transaktionsausgängen (bekannt als BIP69). In Open-Source-Brieftaschen stellt dieser Standard sicher, dass Wallet-Benutzer nicht darauf vertrauen müssen, wie Entwickler der App die Reihenfolge der Ausgaben implementieren. Da dieser Standard neu ist, haben noch nicht viele Brieftaschen ihn implementiert. Infolgedessen ist es einigermaßen möglich, auf der Blockchain zu sehen, ob eine Transaktion von einer Brieftasche gesendet wurde, die diesen Standard verwendet hat oder nicht."; +"send.transaction_inputs_outputs_info.deterministic.description" = "There is a commonly agreed standard for ordering transaction outputs (known as BIP69). In open-source wallets, that standard ensures wallet users do not need to trust how developers of the app implement the ordering of the outputs. As this standard is new, not many wallets have implemented it yet. As a result, it's somewhat possible to see on the blockchain whether a transaction was sent from a wallet that uses that standard or not."; -"send.confirmation.you_send" = "Sie Senden"; -"send.confirmation.to" = "An"; -"send.confirmation.contact_name" = "Kontaktname"; -"send.confirmation.domain" = "Domäne"; -"send.confirmation.address" = "Adresse"; +"send.confirmation.you_send" = "You Send"; +"send.confirmation.to" = "To"; +"send.confirmation.contact_name" = "Contact Name"; +"send.confirmation.domain" = "Domain"; +"send.confirmation.address" = "Address"; "send.confirmation.account" = "Account"; "send.confirmation.memo" = "Memo"; "send.confirmation.memo_placeholder" = "Memo"; -"send.confirmation.total" = "Insgesamt"; -"send.confirmation.fee" = "Gebühr"; -"send.confirmation.time_lock" = "TimeLock"; -"send.confirmation.slide_to_send" = "Zum Senden wischen"; -"send.confirmation.sending" = "Wird gesendet"; -"send.confirmation.resend_description" = "Mit dieser Aktion wird versucht, die vorherige Transaktion durch erneutes Senden mit einer höheren Gebühr ungültig zu machen. Wenn die ursprüngliche Transaktion noch aussteht, während eine neue gesendet wird, besteht eine hohe Wahrscheinlichkeit (nicht garantiert), dass sie für ungültig erklärt und ersetzt wird. Nur eine dieser beiden Transaktionen wird in die Blockchain aufgenommen."; -"send.confirmation.resend" = "Nochmal senden"; -"send.confirmation.cancel_description" = "Mit dieser Aktion wird versucht, die vorherige Transaktion durch eine neue Transaktion mit einem Betrag von 0 an sich selbst ungültig zu machen. Wenn die ursprüngliche Transaktion noch aussteht, während eine neue Transaktion gesendet wird, besteht eine hohe Wahrscheinlichkeit (nicht garantiert), dass sie für ungültig erklärt und ersetzt wird. Nur eine dieser beiden Transaktionen wird in die Blockchain aufgenommen."; -"send.confirmation.cancel" = "Transaktion abbrechen"; +"send.confirmation.total" = "Total"; +"send.confirmation.fee" = "Fee"; +"send.confirmation.time_lock" = "Time Lock"; +"send.confirmation.slide_to_send" = "Slide to Send"; +"send.confirmation.sending" = "Sending"; +"send.confirmation.resend_description" = "This action will attempt to invalidate the previous transaction by resending it with a higher fee. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; +"send.confirmation.resend" = "Resend"; +"send.confirmation.cancel_description" = "This action will attempt to invalidate previous transaction by resending as a new 0 amount transaction to self. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; +"send.confirmation.cancel" = "Cancel Transaction"; "send.confirmation.nonce" = "Nonce"; -"send.confirmation.method" = "Methode"; -"send.amount_error.balance" = "Nicht genügend Guthaben"; -"send.address_error.own_address" = "TRX kann nicht auf sich selbst übertragen werden"; -"send.amount_error.maximum_amount" = "Maximaler Betrag %@"; -"send.amount_error.minimum_amount" = "Minimaler Betrag %@"; -"send.amount_error.min_required_balance" = "Mindestens erforderlicher Rest %@"; -"send.amount_warning.coin_needed_for_fee" = "Überlegen Sie, ob Sie einige %@ auf dem Guthaben belassen wollen, um für zukünftige Transaktionen bezahlen zu können."; -"send.token.insufficient_fee_alert" = "Transaktionsgebühren für %@ %@ in %@ bezahlt. Du brauchst %@."; - -"send.fee_settings.amount_error.balance.title" = "Unzureichendes Guthaben"; -"send.fee_settings.amount_error.balance" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; - -"send.fee_settings.stuck_warning.title" = "Gefahr des Festklemmens"; -"send.fee_settings.stuck_warning" = "Die Transaktion kann festklemmen oder fehlschlagen."; -"send.fee_settings.fee_error.title" = "Gebührenfehler"; -"send.fee_settings.too_low" = "Die Gebühr ist zu niedrig."; -"send.fee_settings.fee_rate_unavailable" = "Gebührensatz nicht verfügbar. Bitte überprüfen Sie die Gebührensätze manuell"; - -"send.stuck_warning" = "Warnung! Gefahr des Festklemmens"; +"send.confirmation.method" = "Method"; +"send.amount_error.balance" = "Not Enough Balance"; +"send.address_error.own_address" = "Cannot transfer TRX to yourself"; +"send.amount_error.maximum_amount" = "Maximum amount %@"; +"send.amount_error.minimum_amount" = "Minimum amount %@"; +"send.amount_error.min_required_balance" = "Min. required remainder %@"; +"send.amount_warning.coin_needed_for_fee" = "Consider leaving some %@ on balance to be able to pay for future transactions."; +"send.token.insufficient_fee_alert" = "Transaction fees for transferring %@ (on %@) paid in %@. You need %@."; + +"send.fee_settings.amount_error.balance.title" = "Insufficient Balance"; +"send.fee_settings.amount_error.balance" = "The current %@ balance is below the amount required to process theis transaction, including transaction fee."; + +"send.fee_settings.stuck_warning.title" = "Risk of getting stuck"; +"send.fee_settings.stuck_warning" = "The transaction may get stuck or fail."; +"send.fee_settings.fee_error.title" = "Fee Error"; +"send.fee_settings.too_low" = "Fee Rate is too low."; +"send.fee_settings.fee_rate_unavailable" = "Fee Rate unavailable. Please, check fee rates manually"; + +"send.stuck_warning" = "Warning! Risk of getting stuck"; "send.lock_time" = "TimeLock"; -"approve.confirmation.you_approve" = "Sie genehmigen"; -"approve.confirmation.you_revoke" = "Du widerrufen"; +"approve.confirmation.you_approve" = "You Approve"; +"approve.confirmation.you_revoke" = "You Revoke"; "approve.confirmation.spender" = "Spender"; // Donate -"donate.list.title" = "Spenden mit"; -"donate.list.get_address" = "Adresse erhalten"; -"donate.list.get_address.title" = "Adresse"; -"donate.title" = "%@ Spenden"; -"donate.no_assets" = "Sie haben keine Assets zu spenden."; -"donate.support.description" = "Gemeinsam können wir mit Ihrer Unterstützung diese App noch besser machen!"; +"donate.list.title" = "Donate with"; +"donate.list.get_address" = "Get Address"; +"donate.list.get_address.title" = "Addresses"; +"donate.title" = "Donate %@"; +"donate.no_assets" = "You have no assets to donate."; +"donate.support.description" = "Together, with your support, we can make this app even better!"; // CoinSelector -"choose_coin.title" = "Coins auswählen"; +"choose_coin.title" = "Choose Coins"; // Swap "swap.title" = "Swap"; -"swap.no_assets" = "Du hast keine Vermögenswerte zum Tauschen."; -"swap.you_pay" = "Sie bezahlen"; -"swap.estimated" = "geschätzt"; -"swap.balance" = "Guthaben"; -"swap.allowance" = "Vergütung"; -"swap.you_get" = "Sie erhalten"; -"swap.token" = "Auswählen"; -"swap.advanced_settings" = "Swap-Einstellungen"; -"swap.proceed_button" = "Weiter"; -"swap.approve.title" = "Genehmigen tauschen"; -"swap.approve.description" = "Sie sollten einem intelligenten Vertrag die Erlaubnis erteilen, in Ihrem Namen gegebene Token einzutauschen. Diese Erlaubnis legt einen Betrag fest, der von einem intelligenten Vertrag verwendet werden kann. Sie wirkt sich nicht auf Ihr Guthaben aus, erfordert jedoch eine geringe Gebühr für die Ausführung der Genehmigungstransaktion.\n\nEs kann zwar vor jedem Handel auf Anfrage erfolgen, es ist jedoch günstiger, bei zukünftigen Handelsgeschäften einen höheren Betrag im Voraus für uns zu genehmigen."; -"swap.approve.amount_error.already_approved" = "Sie haben bereits eine Vergütung für diesen Betrag"; -"swap.approving_button" = "Wird bestätigt..."; -"swap.revoke_warning" = "Sie können %@tauschen, oder Sie müssen den neuen Betrag widerrufen und genehmigen"; -"swap.revoking_button" = "Widerrufen..."; -"swap.not_available_button" = "Saldo N/A"; -"swap.trade_error.not_found" = "Diese Token sind nicht tauschbar"; -"swap.trade_error.wrap_unwrap_not_allowed" = "Dieser Service erlaubt keine Verpackung/Auspackung. Bitte versuchen Sie einen anderen Swap-Service. 1Inch empfohlen"; -"swap.button_error.insufficient_balance" = "Unzureichendes Guthaben"; -"swap.switch_provider.title" = "Dienst wechseln"; +"swap.no_assets" = "You have no assets to swap."; +"swap.you_pay" = "You Pay"; +"swap.estimated" = "estimated"; +"swap.balance" = "Balance"; +"swap.allowance" = "Allowance"; +"swap.you_get" = "You Get"; +"swap.token" = "Select"; +"swap.advanced_settings" = "Swap Settings"; +"swap.proceed_button" = "Next"; +"swap.approve.title" = "Swap Approve"; +"swap.approve.description" = "You should grant permission to a smart contract to swap a given token on your behalf. This permission sets the amount that can be used by a smart contract. It doesn't affect your balance but requires a small fee to execute an approval transaction. \n\nWhile it may be done on-demand before each trade, it's cheaper to approve a higher amount in advance for future trades."; +"swap.approve.amount_error.already_approved" = "You already have an allowance for this amount"; +"swap.approving_button" = "Approving..."; +"swap.revoke_warning" = "You can exchange %@, or you need to revoke and approve the new amount"; +"swap.revoking_button" = "Revoking..."; +"swap.not_available_button" = "Balance N/A"; +"swap.trade_error.not_found" = "Can't swap these tokens"; +"swap.trade_error.wrap_unwrap_not_allowed" = "This service doesn't allow wrapping/unwrapping. Please, try another swap service. 1Inch recommended"; +"swap.button_error.insufficient_balance" = "Insufficient balance"; +"swap.switch_provider.title" = "Swap Service"; "swap.amount_type.coin" = "Coin"; -"swap.price" = "Preis"; -"swap.buy_price" = "Kaufpreis"; -"swap.sell_price" = "Verkaufspreis"; -"swap.price_impact" = "Preisauswirkungen"; -"swap.maximum_paid" = "Maximal bezahlt"; -"swap.minimum_got" = "Mindestens erhalten"; -"swap.estimate_short" = "(geschätzt)"; +"swap.price" = "Price"; +"swap.buy_price" = "Buy Price"; +"swap.sell_price" = "Sell Price"; +"swap.price_impact" = "Price Impact"; +"swap.maximum_paid" = "Maximum Amount"; +"swap.minimum_got" = "Guaranteed Amount"; +"swap.estimate_short" = "(est)"; "swap.minimum_short" = "(min)"; "swap.maximum_short" = "(max)"; // Swap Advanced Settings -"swap.advanced_settings.slippage" = "Slippage-Toleranz"; -"swap.advanced_settings.slippage.footer" = "Ihre Transaktion wird zurückgesetzt, wenn sich der Preis um mehr als diesen Prozentsatz ungünstig ändert."; -"swap.advanced_settings.deadline" = "Transaktionsfrist"; -"swap.advanced_settings.deadline.footer" = "Ihre Transaktion wird rückgängig gemacht, wenn sie länger als diese aussteht."; -"swap.advanced_settings.recipient.footer" = "Nach dem Umtauschvorgang wird der Betrag an die angegebene Adresse überwiesen"; -"swap.advanced_settings.deadline_minute" = "%@ Min"; -"swap.advanced_settings.recipient_address" = "Empfängeradresse"; -"swap.advanced_settings.warning.unusual_slippage" = "Ihre Transaktion kann vorzeitig ausgeführt werden"; -"swap.advanced_settings.service_fee_description" = "Eine Service-Gebühr für die Swap-Aktion auf der Plattform typischerweise entweder 0,3% oder 0,6%"; -"swap.advanced_settings.error.lower_slippage" = "Ihre Transaktion wird wahrscheinlich fehlschlagen."; -"swap.advanced_settings.error.higher_slippage" = "Die Slippage-Toleranz darf nicht mehr als %@%% betragen"; -"swap.advanced_settings.error.invalid_address" = "Ungültige Adresse"; -"swap.advanced_settings.error.invalid_slippage" = "Ungültige Slippage"; -"swap.advanced_settings.error.invalid_deadline" = "Ungültige Frist"; - -"swap.one_inch.error.cannot_estimate" = "Schätzungsfehler"; -"swap.one_inch.error.cannot_estimate.info" = "Fehler! Überprüfen Sie Ihr Guthaben und vergewissern Sie sich, dass genügend %@ zur Deckung der Gebühr vorhanden sind. Oder versuchen Sie, die Kursdifferenz zu erhöhen und versuchen Sie es erneut. Wird in 3 Sekunden erneut versucht..."; -"swap.one_inch.error.insufficient_liquidity" = "Unzureichende Liquidität"; -"swap.one_inch.error.insufficient_liquidity.info" = "Wahrscheinlich ist nicht genügend Liquidität vorhanden, um diesen Handel abzuwickeln. Versuchen Sie einen niedrigeren Betrag."; - -"swap.service" = "Dienst"; -"swap.service.title" = "Dienst"; +"swap.advanced_settings.slippage" = "Slippage Tolerance"; +"swap.advanced_settings.slippage.footer" = "Your transaction will revert if the price changes unfavorably by more than this percentage"; +"swap.advanced_settings.deadline" = "Transaction Deadline"; +"swap.advanced_settings.deadline.footer" = "Your transaction will revert if it is pending for more than this long."; +"swap.advanced_settings.recipient.footer" = "After the exchange operation, the amount will be transferred to the specified address"; +"swap.advanced_settings.deadline_minute" = "%@ min"; +"swap.advanced_settings.recipient_address" = "Recipient Address"; +"swap.advanced_settings.warning.unusual_slippage" = "Your transaction may be frontrun"; +"swap.advanced_settings.service_fee_description" = "A service fee for the swap action on the platform typicaly either 0.3% or 0.6%"; +"swap.advanced_settings.error.lower_slippage" = "Your transaction is likely to fail."; +"swap.advanced_settings.error.higher_slippage" = "Slippage Tolerance can’t be more than %@%%"; +"swap.advanced_settings.error.invalid_address" = "Invalid Address"; +"swap.advanced_settings.error.invalid_slippage" = "Invalid Slippage"; +"swap.advanced_settings.error.invalid_deadline" = "Invalid Deadline"; + +"swap.one_inch.error.cannot_estimate" = "Estimation Error"; +"swap.one_inch.error.cannot_estimate.info" = "Check the balance to ensure there is enough %@ to cover the fee. Or try increasing the price slippage amount and try again. Will auto-retry in 3 sec..."; +"swap.one_inch.error.insufficient_liquidity" = "Insufficient Liquidity"; +"swap.one_inch.error.insufficient_liquidity.info" = "Likely there is not enough liquidity available to process this trade. Try lowering the amount."; + +"swap.service" = "Service"; +"swap.service.title" = "Service"; // Swap Approving @@ -533,35 +529,34 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Wischen zum Tauschen"; +"swap.confirmation.slide_to_swap" = "Slide to Swap"; "swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.impact_too_high" = "%@ hat Swap-Aktion für diesen Handel deaktiviert, da du einen extrem ungünstigen Preis bekommst. Dies ist auf extrem niedrige Liquidität zurückzuführen.\nWenn Sie trotzdem wechseln möchten, verwenden Sie stattdessen %@ Webseite."; -"swap.confirmation.impact_warning" = "Wichtig! Du bekommst einen extrem ungünstigen Preis. Dies ist auf die extrem niedrige Liquidität zurückzuführen."; - -"swap.confirmation.minimum_received" = "Mindestens erhalten"; -"swap.confirmation.maximum_sent" = "Maximale Ausgaben"; - -"swap.dex_info.description" = "Dieser Tauschdienst wird von %@ betrieben, einem dezentralen Token-Tauschprotokoll, das auf der %@ Blockchain basiert.\n\n%@ ist vollständig automatisiert und wird von intelligenten Verträgen verwaltet, die den Token-Tausch in einer zuverlässigen Art und Weise ermöglichen, ohne dass Betrug betrieben werden kann."; - -"swap.dex_info.header_dex_related" = "%@ zugehörige"; -"swap.dex_info.header_allowance" = "Vergütung"; -"swap.dex_info.content_allowance" = "Der Betrag, den ein Austausch im Namen des Benutzers ausgeben kann, wenn Token Swaps ausgeführt werden. Bevor eine tatsächliche Swap-Transaktion durchgeführt werden kann, ist eine vorherige Transaktion erforderlich, die ausreichende Zertifikate festlegt."; -"swap.dex_info.header_price_impact" = "Preisauswirkungen"; -"swap.dex_info.content_price_impact" = "Erwartete Preisabweichung von einem angezeigten Preis, steigt normalerweise mit dem Tauschbetrag."; -"swap.dex_info.header_swap_fee" = "Swap-Gebühr"; -"swap.dex_info.content_swap_fee" = "Eine Servicegebühr für die Tauschaktion auf der Plattform, angegeben in der Währung, die der Benutzer verkauft. Für die meisten Aufträge beträgt diese entweder 0,3 % oder 0,6 %."; -"swap.dex_info.header_guaranteed_amount" = "Mindestens erhalten"; -"swap.dex_info.content_guaranteed_amount" = "Der Mindestbetrag, den ein Benutzer als Ergebnis einer Tauschaktion erhalten wird."; -"swap.dex_info.header_maximum_spend" = "Maximale Ausgaben"; -"swap.dex_info.content_maximum_spend" = "Der Maximalbetrag, den ein Benutzer als Ergebnis einer Tauschaktion ausgeben wird."; - -"swap.dex_info.header_other" = "Andere"; -"swap.dex_info.header_transaction_fee" = "Transaktionsgebühr"; -"swap.dex_info.content_transaction_fee" = "Die geschätzten Verarbeitungskosten für die angegebene Transaktion auf der %@ Blockchain. Transaktionen, die sich auf %@ beziehen, kosten in der Regel mehr als allgemeine Token-Transaktionen."; -"swap.dex_info.header_transaction_speed" = "Transaktionsgeschwindigkeit"; -"swap.dex_info.content_transaction_speed" = "Transaktionen mit höheren Transaktionsgebühren führen zu schnelleren Bearbeitungsgeschwindigkeiten. Auch das Gegenteil ist der Fall."; - -"swap.dex_info.link_button" = "%@ Seite"; +"swap.confirmation.impact_too_high" = "%@ has disabled swap action for this trade because you're getting an extremely unfavorable price. This is due to extremely low liquidity.\nIf you still want to swap please use %@ website instead."; +"swap.confirmation.impact_warning" = "Important! You're getting an extremely unfavorable price. This is due to extremely low liquidity."; + +"swap.confirmation.minimum_received" = "Minimum Received"; +"swap.confirmation.maximum_sent" = "Maximum Sent"; + +"swap.dex_info.description" = "This exchange service is powered by %@, a decentralized token exchange protocol built on the %@ blockchain. \n\n%@ is fully automated and managed by smart contracts that facilitate token exchanges in a reliable manner without any means to cheat."; +"swap.dex_info.header_dex_related" = "%@ Related"; +"swap.dex_info.header_allowance" = "Allowance"; +"swap.dex_info.content_allowance" = "The amount an exchange can spend on a user’s behalf when executing token swaps. A proceeding transaction setting sufficient allowance is required before an actual swap transaction can take place."; +"swap.dex_info.header_price_impact" = "Price Impact"; +"swap.dex_info.content_price_impact" = "The expected price deviation from a shown price typically increases with the swap amount."; +"swap.dex_info.header_swap_fee" = "Swap Fee"; +"swap.dex_info.content_swap_fee" = "A service fee for the swap action on the platform, shown in the currency the user sells. For most orders, it should be either 0.3 % or 0.6 %."; +"swap.dex_info.header_guaranteed_amount" = "Guaranteed Amount"; +"swap.dex_info.content_guaranteed_amount" = "The minimum amount a user is going to receive as a result of a swap action."; +"swap.dex_info.header_maximum_spend" = "Maximum Spend"; +"swap.dex_info.content_maximum_spend" = "The maximum amount a user is going to spend as a result of a swap action."; + +"swap.dex_info.header_other" = "Other"; +"swap.dex_info.header_transaction_fee" = "Transaction Fee"; +"swap.dex_info.content_transaction_fee" = "The estimated processing cost of the given transaction on %@ blockchain. %@ related transactions will typically cost more than generic token transfer transactions."; +"swap.dex_info.header_transaction_speed" = "Transaction Speed"; +"swap.dex_info.content_transaction_speed" = "Transactions with a higher transaction fee will result in faster processing speeds. The opposite is true as well."; + +"swap.dex_info.link_button" = "%@ Site"; // Market @@ -569,7 +564,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.title" = "Markets"; "market.category.overview" = "Overview"; "market.category.posts" = "News"; -"market.category.watchlist" = "Merkliste"; +"market.category.watchlist" = "Watchlist"; "market.total_market_cap" = "Total M.Cap"; "market.24h_volume" = "24h Volume"; "market.defi_cap" = "DeFi Cap"; @@ -577,7 +572,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.project_has_no_coin" = "This project doesn’t have a coin"; -"market.top.section.header.see_all" = "Alle anzeigen"; +"market.top.section.header.see_all" = "See All"; "market.top.section.header.top_gainers" = "Top Gainers"; "market.top.section.header.top_losers" = "Top Losers"; "market.top.section.header.top_sectors" = "Top Sectors"; @@ -594,29 +589,29 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.tvl.platform_field.all" = "All"; -"market.sort_by" = "Sortieren nach"; +"market.sort_by" = "Sort By"; "market.top.title" = "Top Coins"; "market.top.description" = "Top Coins by market cap rank"; -"market.top.highest_cap" = "Höchste Obergrenze"; -"market.top.lowest_cap" = "Niedrigste Obergrenze"; -"market.top.highest_volume" = "Höchste Lautstärke"; -"market.top.lowest_volume" = "Geringste Lautstärke"; +"market.top.highest_cap" = "Highest Cap"; +"market.top.lowest_cap" = "Lowest Cap"; +"market.top.highest_volume" = "Highest Volume"; +"market.top.lowest_volume" = "Lowest Volume"; "market.top.top_gainers" = "Top Gainers"; "market.top.top_losers" = "Top Losers"; "market.top.top_collections" = "Top NFT Collections"; -"market.top.floor_price" = "Stockwerk:"; +"market.top.floor_price" = "Floor:"; "market.top.top_platforms" = "Top Platforms"; "market.top.protocols" = "Protocols"; -"top_platforms.title" = "Plattform-Rang"; -"top_platforms.description" = "Leading blockchain platforms by the cumulative market of projects built on top."; +"top_platforms.title" = "Platforms Rank"; +"top_platforms.description" = "Leading blockchain platforms by cumulative market of projects built on top."; "top_platform.title" = "%@ Ecosystem"; "top_platform.description" = "Market cap of all protocols on the %@ chain"; -"market_discovery.title" = "Discovey"; +"market_discovery.title" = "Discovery"; "market_discovery.filters" = "Filters"; "market_discovery.browse_categories" = "Browse Categories"; "market_discovery.top_coins" = "TOP Coins"; @@ -624,11 +619,11 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market_watchlist.empty.caption" = "Your watchlist is empty."; -"market.search.title" = "Suche"; +"market.search.title" = "Search"; "market.search.empty_text" = "No results found"; "market.advanced_search.title" = "Filters"; -"market.advanced_search.show_results" = "Ergebnisse anzeigen"; +"market.advanced_search.show_results" = "Show Results"; "market.advanced_search.empty_results" = "Empty Results"; "market.advanced_search.dex_description" = "This setting applies to tokens traded on Ethereum (Uniswap DEX) and Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24h"; @@ -642,16 +637,16 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.advanced_search.liquidity" = "DEX Liquidity"; "market.advanced_search.blockchains" = "Blockchains"; "market.advanced_search.price_period" = "Price Period"; -"market.advanced_search.price_change" = "Preisänderung"; +"market.advanced_search.price_change" = "Price Change"; -"market.advanced_search.outperformed_btc" = "Übertraf BTC"; -"market.advanced_search.outperformed_eth" = "Übertraf ETH"; -"market.advanced_search.outperformed_bnb" = "Übertraf BNB"; +"market.advanced_search.outperformed_btc" = "Outperformed BTC"; +"market.advanced_search.outperformed_eth" = "Outperformed ETH"; +"market.advanced_search.outperformed_bnb" = "Outperformed BNB"; "market.advanced_search.price_close_to_ath" = "Price Close To ATH"; "market.advanced_search.price_close_to_atl" = "Price Close To ATL"; "market.advanced_search.top" = "Top %d"; -"market.advanced_search.reset_all" = "Zurücksetzen"; +"market.advanced_search.reset_all" = "Reset"; "market.advanced_search.less_5_m" = "< 5M"; "market.advanced_search.less_10_m" = "< 10M"; @@ -678,9 +673,9 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.advanced_search.more_500_b" = "> 500B"; "market.advanced_search.day" = "1 Day"; -"market.advanced_search.week" = "1Week"; +"market.advanced_search.week" = "1 Week"; "market.advanced_search.week2" = "2 Weeks"; -"market.advanced_search.month" = "1 Monat"; +"market.advanced_search.month" = "1 Month"; "market.advanced_search.month6" = "6 Months"; "market.advanced_search.year" = "1 Year"; @@ -702,7 +697,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Coin Page -"coin_page.overview" = "Preis"; +"coin_page.overview" = "Price"; "coin_page.analytics" = "Analytics"; "coin_page.markets" = "Markets"; "coin_page.tweets" = "Tweets"; @@ -720,9 +715,9 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_overview.trading_volume" = "Trading Volume"; "coin_overview.roi.hour24" = "1 Day"; -"coin_overview.roi.day7" = "1Week"; +"coin_overview.roi.day7" = "1 Week"; "coin_overview.roi.day14" = "2 Weeks"; -"coin_overview.roi.day30" = "1 Monat"; +"coin_overview.roi.day30" = "1 Month"; "coin_overview.roi.day200" = "6 Month"; "coin_overview.roi.year1" = "1 Year"; @@ -730,8 +725,8 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; -"coin_overview.coin_types" = "Coin Type"; -"coin_overview.show_more" = "Mehr anzeigen"; +"coin_overview.coin_types" = "Coin Types"; +"coin_overview.show_more" = "Show More"; "coin_overview.show_less" = "Show Less"; "coin_overview.links" = "Links"; @@ -751,12 +746,12 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.indicators.strong_sell" = "Strong Sell"; "coin_analytics.period" = "Period"; "coin_analytics.period.select_title" = "Select Period"; -"coin_analytics.period.1h" = "1 Stunde"; +"coin_analytics.period.1h" = "1 hour"; "coin_analytics.period.4h" = "4 hours"; "coin_analytics.period.1d" = "1 day"; "coin_analytics.period.1w" = "1 week"; -"coin_analytics.details" = "Info"; +"coin_analytics.details" = "Details"; "coin_analytics.not_available" = "This project has no analytics data"; @@ -768,15 +763,15 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.cex_volume" = "CEX Volume"; "coin_analytics.cex_volume_rank" = "CEX Volume Rank"; "coin_analytics.cex_volume_rank.description" = "Tokens ranked by trading volume for the token on centralized exchanges."; -"coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; +"coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; -"coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; +"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info4" = "List of all tokens ranked based on trading volume on centralized exchanges over 24H / 7D / 1M intervals."; "coin_analytics.dex_volume" = "DEX Volume"; "coin_analytics.dex_volume_rank" = "DEX Volume Rank"; "coin_analytics.dex_volume_rank.description" = "Tokens ranked by trading volume for the token on decentralized exchanges."; -"coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; +"coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading decentralized exchanges over a 30-day period."; "coin_analytics.dex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading decentralized exchanges over 1 year period."; "coin_analytics.dex_volume.info3" = "Token's rank based on trading volume on leading decentralized exchanges over 30-day period."; "coin_analytics.dex_volume.info4" = "List of all tokens ranked based on trading volume on decentralized exchanges over 24H / 7D / 1M intervals."; @@ -798,7 +793,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.active_addresses.30_day_unique_addresses" = "30-Day Unique Addresses"; "coin_analytics.active_addresses_rank" = "Active Addresses Rank"; "coin_analytics.active_addresses_rank.description" = "Tokens ranked by number of unique addresses transacting with the token."; -"coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; +"coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over a 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; "coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; "coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; @@ -821,7 +816,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.holders.tracked_blockchains" = "Tracked blockchains: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; "coin_analytics.holders.in_top_10_addresses" = "in top 10 holders"; "coin_analytics.holders.count" = "Total Holders: %@"; -"coin_analytics.holders.see_all" = "Alle anzeigen"; +"coin_analytics.holders.see_all" = "See All"; "coin_analytics.project_tvl" = "Project TVL"; "coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; @@ -918,152 +913,152 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Transactions -"transactions.title" = "Transaktionen"; -"transactions.tab_bar_item" = "Transaktionen"; +"transactions.title" = "Transactions"; +"transactions.tab_bar_item" = "Transactions"; "transactions.blockchain" = "Blockchain"; -"transactions.all_blockchains" = "Alle Blockchains"; -"transactions.all_coins" = "Alle Coins"; -"transactions.choose_coin" = "Coins auswählen"; +"transactions.all_blockchains" = "All Blockchains"; +"transactions.all_coins" = "All Coins"; +"transactions.choose_coin" = "Choose Coin"; "transactions.filter_all" = "All"; -"transactions.empty_text" = "Sie haben noch keine offenen oder früheren Transaktionen"; -"transactions.pending" = "In Bearbeitung"; -"transactions.processing" = "Verarbeite"; -"transactions.completed" = "Abgeschlossen"; -"transactions.failed" = "Fehlgeschlagen"; - -"transactions.receive" = "Empfangen"; -"transactions.send" = "Senden"; +"transactions.empty_text" = "You don't have any pending or past transactions yet"; +"transactions.pending" = "Pending"; +"transactions.processing" = "Processing"; +"transactions.completed" = "Completed"; +"transactions.failed" = "Failed"; + +"transactions.receive" = "Receive"; +"transactions.send" = "Send"; "transactions.burn" = "Burn"; "transactions.mint" = "Mint"; -"transactions.approve" = "Genehmigen"; +"transactions.approve" = "Approve"; "transactions.swap" = "Swap"; -"transactions.contract_call" = "Vertragsaufruf"; -"transactions.contract_creation" = "Vertragserstellung"; -"transactions.external_call" = "Externe Anrufe"; +"transactions.contract_call" = "Contract Call"; +"transactions.contract_creation" = "Contract Creation"; +"transactions.external_call" = "External Call"; -"transactions.to" = "An %@"; -"transactions.from" = "Von %@"; +"transactions.to" = "To %@"; +"transactions.from" = "From %@"; -"transactions.multiple" = "Mehrere"; +"transactions.multiple" = "Multiple"; -"transactions.value.unlimited" = "unbegrenzt"; +"transactions.value.unlimited" = "unlimited"; -"transactions.today" = "Heute"; -"transactions.yesterday" = "Gestern"; +"transactions.today" = "Today"; +"transactions.yesterday" = "Yesterday"; "transactions.types.all" = "All"; -"transactions.types.incoming" = "Erhalten"; -"transactions.types.outgoing" = "Gesendet"; +"transactions.types.incoming" = "Received"; +"transactions.types.outgoing" = "Sent"; "transactions.types.swap" = "Swaps"; -"transactions.types.approve" = "Genehmigungen"; +"transactions.types.approve" = "Approvals"; -"transactions.unknown_transaction.title" = "Unbekannte Transaktion"; -"transactions.unknown_transaction.description" = "Transaktion kann nicht analysiert werden"; +"transactions.unknown_transaction.title" = "Unknown Transaction"; +"transactions.unknown_transaction.description" = "Transaction can not be parsed"; // Transaction Info -"tx_info.title" = "Transaktionsdaten"; -"tx_info.date" = "Datum"; +"tx_info.title" = "Transaction Info"; +"tx_info.date" = "Date"; "tx_info.title_approval" = "Swap Approval"; -"tx_info.status.pending" = "In Bearbeitung"; -"tx_info.status.completed" = "Abgeschlossen"; -"tx_info.status.failed" = "Fehlgeschlagen"; -"tx_info.from_hash" = "Von"; -"tx_info.transaction_id" = "Transaktion ID"; -"tx_info.to_hash" = "An"; +"tx_info.status.pending" = "Pending"; +"tx_info.status.completed" = "Completed"; +"tx_info.status.failed" = "Failed"; +"tx_info.from_hash" = "From"; +"tx_info.transaction_id" = "ID"; +"tx_info.to_hash" = "To"; "tx_info.spender" = "Spender"; -"tx_info.contact_name" = "Kontaktname"; -"tx_info.button_explorer" = "Auf %@ ansehen"; -"tx_info.rate" = "Historischer Rate"; -"tx_info.options.speed_up" = "Beschleunigen"; -"tx_info.options.cancel" = "Transaktion abbrechen"; -"tx_info.transaction.already_in_block" = "Transaktion bereits im Block"; -"tx_info.fee" = "Gebühr"; -"tx_info.fee.estimated" = "Gebühr (est.)"; -"tx_info.to_self_note" = "Diese Transaktion wurde an die eigene Adresse gesendet"; -"tx_info.double_spent_note" = "Double-Spend-Risiko!"; -"tx_info.locked_until" = "Gesperrt bis %@"; -"tx_info.unlocked_at" = "Freigegeben am %@"; -"tx_info.recipient_hash" = "Empfänger"; -"tx_info.raw_transaction" = "Rohdaten der Transaktion"; +"tx_info.contact_name" = "Contact Name"; +"tx_info.button_explorer" = "View on %@"; +"tx_info.rate" = "Historical Rate"; +"tx_info.options.speed_up" = "Speed Up"; +"tx_info.options.cancel" = "Cancel Transaction"; +"tx_info.transaction.already_in_block" = "Transaction already in block"; +"tx_info.fee" = "Fee"; +"tx_info.fee.estimated" = "Fee (est.)"; +"tx_info.to_self_note" = "This transaction is sent to own address"; +"tx_info.double_spent_note" = "Double Spend Risk!"; +"tx_info.locked_until" = "Locked until %@"; +"tx_info.unlocked_at" = "Unlocked at %@"; +"tx_info.recipient_hash" = "Recipient"; +"tx_info.raw_transaction" = "Raw Transaction"; "tx_info.memo" = "Memo"; -"tx_info.service" = "Dienst"; -"tx_info.view_on" = "Auf %@ ansehen"; -"tx_info.you_pay" = "Sie bezahlen"; -"tx_info.you_get" = "Sie erhalten"; -"tx_info.you_paid" = "Du hast gezahlt"; -"tx_info.you_got" = "Du bekommst"; -"tx_info.price" = "Preis"; +"tx_info.service" = "Service"; +"tx_info.view_on" = "View on %@"; +"tx_info.you_pay" = "You Pay"; +"tx_info.you_get" = "You Get"; +"tx_info.you_paid" = "You Paid"; +"tx_info.you_got" = "You Got"; +"tx_info.price" = "Price"; // Settings -"settings.title" = "Einstellungen"; -"settings.tab_bar_item" = "Einstellungen"; -"settings.manage_accounts" = "Wallets verwalten"; -"settings.blockchain_settings" = "Blockchain Einstellungen"; -"settings.security" = "Sicherheit"; -"settings.experimental_features" = "Experimentell"; +"settings.title" = "Settings"; +"settings.tab_bar_item" = "Settings"; +"settings.manage_accounts" = "Manage Wallets"; +"settings.blockchain_settings" = "Blockchain Settings"; +"settings.security" = "Security"; +"settings.experimental_features" = "Experimental"; "settings.personal_support" = "Personal Support"; -"settings.base_currency" = "Währung"; -"settings.language" = "Sprache"; +"settings.base_currency" = "Base Currency"; +"settings.language" = "Language"; "settings.faq" = "FAQ"; -"settings.theme" = "Design"; -"settings.info_subtitle" = "decentralized"; -"settings.donate.description" = "Gemeinsam können wir mit Ihrer Unterstützung diese App noch besser machen!"; -"settings.donate.title" = "Spenden"; +"settings.theme" = "Theme"; +"settings.info_subtitle" = "decentralized app"; +"settings.donate.description" = "Together, with your support, we can make this app even better!"; +"settings.donate.title" = "Donate"; // Settings -> Base Currency -"settings.base_currency.title" = "Währung"; -"settings.base_currency.other" = "Andere"; -"settings.base_currency.disclaimer" = "Haftungsausschluss"; +"settings.base_currency.title" = "Base Currency"; +"settings.base_currency.other" = "Other"; +"settings.base_currency.disclaimer" = "Disclaimer"; "settings.base_currency.disclaimer.description" = "The exchange rate data is provided by a third party service - Coingecko.com. \n\nThe %@ wallet app doesn't guarantee these values are always correct and matches market data. The chance for inconsistency is higher if you select any base currency other than %@."; -"settings.base_currency.disclaimer.set" = "Übernehmen"; +"settings.base_currency.disclaimer.set" = "Set"; // Settings -> Manage Wallet -"manage_wallets.title" = "Kryptowährung"; -"manage_wallets.not_found" = "Keine Ergebnisse gefunden. Versuchen Sie, Token manuell hinzuzufügen."; -"manage_wallets.search_placeholder" = "Name, Code oder Vertragsadresse"; -"manage_wallets.contract_address" = "Vertragsadresse"; -"manage_wallets.derivation_description" = "Es gibt 4 gängige Adressformate %@ Brieftaschen zum Empfang eingehender Zahlungen:\n\n- BIP44 (ältes)\n- BIP49\n- BIP84 (empfohlen)\n- BIP86 (neueste)\n\nWährend %@ alle 4 unterstützt empfiehlt es, eine %@ Brieftasche im BIP84-Format zu verwenden."; -"manage_wallets.bitcoin_cash_coin_type_description" = "Es gibt 2 Adressformate Bitcoin Cash Brieftaschen zum Empfang eingehender Zahlungen:\n\n- TYPE 0 (älter)\n- TYPE 145 (neuer)\n\nWährend %@ Brieftasche beide unterstützt, empfiehlt es sich, eine Bitcoin Cash Brieftasche im TYPE 145 Format zu verwenden."; +"manage_wallets.title" = "Coin Manager"; +"manage_wallets.not_found" = "No results found. Try to add token manually."; +"manage_wallets.search_placeholder" = "Name, code or contract address"; +"manage_wallets.contract_address" = "Contract Address"; +"manage_wallets.derivation_description" = "There are 4 common address formats %@ wallets can use to receive incoming payments:\n\n- BIP44 (oldest)\n- BIP49\n- BIP84 (recommended)\n- BIP86 (newest)\n\nWhile %@ wallet supports all 4, it recommends to use a %@ wallet operating in BIP84 format."; +"manage_wallets.bitcoin_cash_coin_type_description" = "There are 2 address formats Bitcoin Cash wallets can use to receive incoming payments:\n\n- TYPE 0 (older)\n- TYPE 145 (newer)\n\nWhile %@ wallet supports both of them it recommends to use a Bitcoin Cash wallet operating in TYPE 145 format."; // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Account"; -"settings.personal_support.telegram_username.placeholder" = "@benutzername"; -"settings.personal_support.description" = "Geben Sie Ihren Telegram-Kontonamen ein, um einen persönlichen Support-Chat zu eröffnen und wir senden Ihnen eine Nachricht."; -"settings.personal_support.request" = "Anforderung"; -"settings.personal_support.requested" = "Angefragt"; -"settings.personal_support.failed" = "Anfrage fehlgeschlagen"; +"settings.personal_support.telegram_username.placeholder" = "@username"; +"settings.personal_support.description" = "Enter your Telegram account name to open a personal support chat and we'll send message to you."; +"settings.personal_support.request" = "Request"; +"settings.personal_support.requested" = "Requested"; +"settings.personal_support.failed" = "Request failed"; "settings.personal_support.need_subscription" = "This feature only for %@ Wallet premium users. More info in our official site."; -"settings.personal_support.requested.description" = "Sie haben bereits einen privaten Chat angefordert, finden Sie ihn auf Telegram"; -"settings.personal_support.requested.open_telegram" = "Telegram öffnen"; -"settings.personal_support.requested.new_request" = "Neue Zahlungsaufforderung"; +"settings.personal_support.requested.description" = "You've already requested a private chat, find it on Telegram"; +"settings.personal_support.requested.open_telegram" = "Open Telegram"; +"settings.personal_support.requested.new_request" = "New Request"; // Settings -> Experimental Features -"settings.experimental_features.title" = "Experimentell"; -"settings.experimental_features.description" = "Die unten aufgeführten Funktionen sind experimentell und sollten mit Vorsicht eingesetzt werden. Wir haben sie zwar ausführlich mit unserem eigenen Kryptoguthaben getestet, aber wir können noch nicht garantieren, dass sie in allen denkbaren Fällen funktionieren."; +"settings.experimental_features.title" = "Experimental"; +"settings.experimental_features.description" = "The features below are experimental and should be used with caution. While we have thoroughly tested these features using our own crypto funds, we cannot guarantee they will work as expected in all possible cases."; "settings.experimental_features.bitcoin_hodling" = "TimeLock"; // Settings -> Experimental Features -> Bitcoin HODLing "settings.bitcoin_hodling.title" = "TimeLock"; -"settings.bitcoin_hodling.lock_time" = "Aktivieren"; -"settings.bitcoin_hodling.description" = "Dadurch können Sie Bitcoins senden, die bis zu einem bestimmten Datum nicht ausgegeben werden können. \n\nDer Empfänger solcher Transaktionen sollte die %@ Wallet-App Version 0 verwenden. 0 oder neuer, mit dem BIP44 Adressformat für Bitcoin. \n\nNur die %@ Brieftasche kann solche Transaktionen im Bitcoin-Netzwerk korrekt identifizieren sowie den Empfänger in die Lage zu versetzen, diese Bitcoins nach Ablauf der Sperrfrist auszugeben. \n\nWenn Sie ein HODLer sind Sie können diese Funktion nutzen, um sich selbst zu erzwingen, Ihre Bitcoins zu hodlen, indem Sie solche Transaktionen an sich senden."; +"settings.bitcoin_hodling.lock_time" = "Activate"; +"settings.bitcoin_hodling.description" = "This enables you to send Bitcoins that cannot be spent until a specified date. \n\nThe receiver of such transactions should use the %@ wallet app version 0.10 or newer, with the BIP44 address format for Bitcoin. \n\nOnly the %@ wallet can correctly identify such transactions on the Bitcoin network, as well as enable the receiver to spend those Bitcoins after the lock period expires. \n\nIf you’re a HODLer, you may use this feature to force yourself into hodling your Bitcoins by sending such transactions to yourself."; // Settings -> Terms -"terms.title" = "Bedingungen"; -"terms.i_agree" = "Ich stimme zu"; +"terms.title" = "Terms"; +"terms.i_agree" = "I Agree"; -"terms.item.1" = "Sicheres Backup der Wiederherstellungsausdrücke für jede Wallet. Dies ist die einzige Möglichkeit, den Zugriff auf Gelder zurückzugewinnen, wenn die App nicht funktioniert."; -"terms.item.2" = "Die Wallet-Wiederherstellungs-Phrasen werden während des Setups zufällig auf dem Gerät generiert und werden an anderer Stelle nicht gespeichert."; -"terms.item.3" = "Das Deaktivieren der PIN (Code) auf dem Smartphone löscht alle Brieftaschen aus der App. Wiederherstellungs-Phrasen werden benötigt, um den Zugriff auf Gelder wiederherzustellen."; -"terms.item.4" = "Jailbreaking (Rooting), Verwendung veralteter Betriebssysteme und das Installieren von Apps aus unbekannten Quellen könnten die Sicherheit des Geldes gefährden."; -"terms.item.5" = "Möglicherweise gibt es unentdeckte Softwarefehler in dem Code, der diese App aktiviert, was zu Fehlfunktionen der App führen kann."; +"terms.item.1" = "Securely backup recovery phrases for each wallet. It's the only way to regain access to funds if the app malfunctions."; +"terms.item.2" = "The wallet recovery phrases are randomly generated on the device during setup and are not stored elsewhere."; +"terms.item.3" = "Disabling unlock PIN (code) on the smartphone deletes all wallets from the app. Recovery phrases will be needed to restore access to funds."; +"terms.item.4" = "Jailbreaking (rooting), using outdated OS, and installing apps from unknown sources may endanger the safety of funds."; +"terms.item.5" = "There may be undiscovered software issues in the code powering this app which may cause the app to malfunction."; // Settings -> Tell Friends @@ -1071,203 +1066,203 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Settings -> Blockchain Settings -"blockchain_settings.title" = "Blockchain Einstellungen"; +"blockchain_settings.title" = "Blockchain Settings"; // Settings -> Security -"settings_security.title" = "Sicherheit"; -"settings_security.passcode" = "Code"; -"settings_security.change_pin" = "Code bearbeiten"; +"settings_security.title" = "Security"; +"settings_security.passcode" = "Passcode"; +"settings_security.change_pin" = "Edit Passcode"; "settings_security.touch_id" = "Touch ID"; "settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Blockchain Einstellungen"; -"security_settings.delete_alert_button" = "Von Telefon löschen"; +"settings_security.blockchain_settings" = "Blockchain Settings"; +"security_settings.delete_alert_button" = "Delete from Phone"; -"btc_blockchain_settings.restore_source" = "Parameter wiederherstellen"; -"btc_blockchain_settings.restore_source.description" = "Wählen Sie die Datenquelle für die Wiederherstellung des Wallets mit Transaktionen aus."; -"btc_blockchain_settings.restore_source.alert" = "Nach dem Ändern der Quelle wiederherstellen muss sich die Wallet mit der %@ Blockchain synchronisieren."; +"btc_blockchain_settings.restore_source" = "Restore Source"; +"btc_blockchain_settings.restore_source.description" = "Select a data source for restoring a wallet with transactions."; +"btc_blockchain_settings.restore_source.alert" = "After changing Restore Source the wallet will have to resync itself with the %@ blockchain."; -"btc_restore_mode.recommended" = "Empfohlen"; -"btc_restore_mode.more_private" = "Mehr Privat"; +"btc_restore_mode.recommended" = "Recommended"; +"btc_restore_mode.more_private" = "More Private"; -"btc_transaction_sort_mode.shuffle" = "Zufällig"; -"btc_transaction_sort_mode.shuffle.description" = "Zufällige Indizierung"; -"btc_transaction_sort_mode.bip69" = "BIP69"; -"btc_transaction_sort_mode.bip69.description" = "Lexikographische Indizierung"; +"btc_transaction_sort_mode.shuffle" = "Shuffle"; +"btc_transaction_sort_mode.shuffle.description" = "Random Indexing"; +"btc_transaction_sort_mode.bip69" = "Deterministic Bip69"; +"btc_transaction_sort_mode.bip69.description" = "Lexicographical Indexing"; -"blockchain_settings.info.restore_source" = "Parameter wiederherstellen"; -"blockchain_settings.info.restore_source.content" = "Diese Einstellung ist nur relevant, wenn eine bestehende Brieftasche wiederhergestellt wird. Es ist ein Prozess, die Transaktionsgeschichte für eine bestimmte Kryptowährung abzurufen, so dass die Wallet-App in der Lage ist, vergangene Transaktionen anzuzeigen und den Kontostand des Benutzers zu berechnen. Dies muss nur einmal passieren, wenn der Benutzer die zuvor erstellten Wallets wiederherstellt.\n\nan diesem Punkt es gibt zwei mögliche Wege für eine mobile Brieftasche wie %@ , dies zu tun:\n\n1. vom API-Server: Es gibt einen vordefinierten Server von Dritten, der die gesamte Blockchain beherbergt und alle Daten verarbeitet und optimiert hat, um diese Daten schnell zur Verfügung zu stellen. Diese Methode ist schnell, aber möglicherweise (nicht notwendig) weniger privat. Es ist auch eine zentralisierte Methode, um eine Brieftasche wiederherzustellen, da sie von der Verfügbarkeit eines Drittanbieter-Servers abhängt. Diese Option wird aufgrund der Geschwindigkeit des Abrufs von Daten (5-10 Minuten) empfohlen.\n\n2. von Blockchain: Die App versucht direkt aus einem Netzwerk von Blockchain-Knoten wiederherzustellen. Dies ist ein dezentralisierter Weg, um das Gleichgewicht der Brieftasche und der vergangenen Transaktionen wiederherzustellen. Die App pingt viele Netzwerkknoten und fordert Daten von ihnen an, ohne einige Knoten speziell anzusprechen. Diese Option ist langsam und kann leicht 2-3 Stunden dauern. Die App muss während der Wiederherstellung geöffnet sein. Diese Wiederherstellungsmethode hängt nicht von einer Entität ab und sollte unter allen Bedingungen funktionieren."; -"blockchain_settings.info.rpc_source" = "RPC-Quelle"; -"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer.%@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; +"blockchain_settings.info.restore_source" = "Restore Source"; +"blockchain_settings.info.restore_source.content" = "This setting is only relevant when restoring an existing wallet. It is a process of getting transaction history for a given cryptocurrency so the wallet app is able to display past transactions and calculate the user's balance. This needs to happen only once when the user restores previously created wallets.\n\nAt this point, there are two potential ways for a mobile wallet like %@ to do this:\n\n1. from the API Server: There is a third-party predefined server that hosts the entire blockchain and has all the data processed and optimized to provide that data in a fast manner. This method is fast but potentially (not necessarily) less private. It's also a centralized method to restore a wallet as it depends on the availability of a 3rd party server. This option is recommended due to its speed of getting data (5-10 minutes).\n\n2. from Blockchain: The app tries to restore directly from a network of blockchain nodes. This is a decentralized way to restore wallet balance and past transactions. The app pings many of the network nodes and requests data from them without addressing some nodes specifically. This option is slow and can easily take 2-3 hours, the app needs to be open while restoring is happening. This restore method doesn't depend on any entity and should work in all conditions."; +"blockchain_settings.info.rpc_source" = "RPC Source"; +"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer. %@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; // Manage Accounts -"manage_accounts.migration_required" = "Migration erforderlich"; -"manage_accounts.migration_recommended" = "Migration empfohlen"; -"manage_accounts.backup_required" = "Sicherung erforderlich"; +"manage_accounts.migration_required" = "Migration Required"; +"manage_accounts.migration_recommended" = "Migration recommended"; +"manage_accounts.backup_required" = "Backup required"; // Manage Keys -"settings_manage_keys.title" = "Wallets verwalten"; -"settings_manage_keys.delete" = "Löschen"; -"settings_manage_keys.backup" = "Sicherung"; -"settings_manage_keys.delete.title" = "Wallet löschen"; -"settings_manage_keys.delete.confirmation_remove" = "Damit wird das Wallet von diesem Gerät gelöscht."; -"settings_manage_keys.delete.confirmation_loose" = "Wenn Sie den Private Key für dieses Wallet nicht gesichert haben, verlieren Sie den Zugang zu Ihrem Guthaben."; -"settings_manage_keys.delete.confirmation_watch" = "Möchten Sie die Beobachtung dieser Wallet-Adresse beenden?"; -"settings_manage_keys.delete.confirmation_watch.button" = "Beobachten stoppen"; +"settings_manage_keys.title" = "Manage Wallets"; +"settings_manage_keys.delete" = "Delete"; +"settings_manage_keys.backup" = "Backup"; +"settings_manage_keys.delete.title" = "Delete Wallet"; +"settings_manage_keys.delete.confirmation_remove" = "The action will delete this wallet from the device."; +"settings_manage_keys.delete.confirmation_loose" = "If you didn't back up the private key for this wallet, you will lose access to your funds."; +"settings_manage_keys.delete.confirmation_watch" = "Do you want to stop watching this wallet address?"; +"settings_manage_keys.delete.confirmation_watch.button" = "Stop Watching"; // Settings -> About App -"settings.about_app.title" = "Über die App"; +"settings.about_app.title" = "About App"; "settings.about_app.app_name" = "%@ Wallet"; -"settings.about_app.description" = "Die Brieftasche %@ wurde für diejenigen gebaut, die Kryptowährungen auf private und unabhängige Weise investieren und speichern möchten.\n\nEs handelt sich um eine nicht-Custodial, Peer-to-Peer-Wallet, bei der nur der Benutzer die Kontrolle über das Guthaben hat. Es sammelt keine Daten und hält den Benutzer unabhängig, indem er das Guthaben des Benutzers nicht an eine bestimmte Wallet-App sperrt.\n\nDie %@ Brieftasche ist vollständig Open-Source und jeder kann bestätigen, dass die App genau so funktioniert, wie sie es vorgibt."; -"settings.about_app.whats_new" = "Das ist neu"; +"settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; +"settings.about_app.whats_new" = "What's New"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Kontaktiere uns"; -"settings.about_app.rate_us" = "Bewerten Sie uns"; -"settings.about_app.tell_friends" = "Freunden erzählen"; +"settings.about_app.contact" = "Contact Us"; +"settings.about_app.rate_us" = "Rate Us"; +"settings.about_app.tell_friends" = "Tell Friends"; // Settings -> About App -> Contact -"settings.contact.title" = "Kontaktiere uns"; -"settings.contact.via_email" = "per e-Mail"; -"settings.contact.via_telegram" = "per Telegram"; +"settings.contact.title" = "Contact Us"; +"settings.contact.via_email" = "via E-mail"; +"settings.contact.via_telegram" = "via Telegram"; // Settings -> Privacy -"settings.privacy" = "Privatsphäre"; -"settings.privacy.description" = "%@ sammelt keine Daten oder nutzt Analysewerkzeuge, die möglicherweise Daten über ihre Benutzer enthüllen. Die Brieftasche ist so konzipiert, dass sie den Benutzern ein hohes Maß an Privatsphäre garantiert."; -"settings.privacy.statement.user_data_storage" = "Benutzerdaten bleiben immer auf dem Gerät des Benutzers."; -"settings.privacy.statement.data_usage" = "Das Wallet sammelt keine Daten über Benutzer."; -"settings.privacy.statement.data_privacy" = "Das Wallet teilt keine Daten über Benutzer."; -"settings.privacy.statement.user_account" = "Es gibt keine Benutzerkonten oder Datenbanken, die Benutzerdaten woanders speichern."; +"settings.privacy" = "Privacy"; +"settings.privacy.description" = "%@ doesn't collect any data or use analytics tools that may expose any data about its users. The wallet is designed to ensure a high level of privacy for its users."; +"settings.privacy.statement.user_data_storage" = "User data always remains on the user's device."; +"settings.privacy.statement.data_usage" = "The wallet doesn't collect any data about users."; +"settings.privacy.statement.data_privacy" = "The wallet doesn't share any data about users."; +"settings.privacy.statement.user_account" = "There are no user accounts or databases keeping user data elsewhere."; // Settings -> Appearance -"appearance.title" = "Darstellung"; +"appearance.title" = "Appearance"; -"appearance.theme" = "Design"; +"appearance.theme" = "Theme"; "appearance.theme.system" = "System"; -"appearance.theme.dark" = "Dunkel"; -"appearance.theme.light" = "Hell"; +"appearance.theme.dark" = "Dark"; +"appearance.theme.light" = "Light"; -"appearance.tab_settings" = "Tab-Einstellungen"; -"appearance.markets_tab" = "Markt-Tab"; -"appearance.launch_screen" = "Startbildschirm"; +"appearance.tab_settings" = "Tab Settings"; +"appearance.markets_tab" = "Markets Tab"; +"appearance.launch_screen" = "Launch Screen"; "appearance.launch_screen.auto" = "Auto"; -"appearance.launch_screen.balance" = "Guthaben"; -"appearance.launch_screen.market_overview" = "Marktübersicht"; -"appearance.launch_screen.watchlist" = "Merkliste"; +"appearance.launch_screen.balance" = "Balance"; +"appearance.launch_screen.market_overview" = "Market Overview"; +"appearance.launch_screen.watchlist" = "Watchlist"; -"appearance.app_icon" = "App-Symbol"; +"appearance.app_icon" = "App Icon"; -"appearance.balance_conversion" = "Saldokonvertierung"; +"appearance.balance_conversion" = "Balance Conversion"; -"appearance.balance_value" = "Saldo Wert"; +"appearance.balance_value" = "Balance Value"; "appearance.balance_value.coin_value" = "Coin Value"; "appearance.balance_value.fiat_value" = "Fiat Value"; -"appearance.balance_auto_hide" = "Auto-Ausblenden ausgleichen"; +"appearance.balance_auto_hide" = "Balance Auto Hide"; // Settings -> Contacts -"contacts.title" = "Kontakte"; -"contacts.list.search_placeholder" = "Nach Namen suchen"; -"contacts.list.not_found" = "Sie haben keinen neuen Kontakt"; +"contacts.title" = "Contacts"; +"contacts.list.search_placeholder" = "Search by name"; +"contacts.list.not_found" = "You do not have an added contact"; "contacts.list.not_found_search" = "No results found"; -"contacts.add_new_contact" = "Neuer Kontakt"; -"contacts.update_contact.already_has_address" = "Der ausgewählte Kontakt hat bereits eine Adresse auf %@. Diese Aktion wird die Adresse %@ durch %@ ersetzen."; -"contacts.update_contact.replace" = "Ersetzen"; -"contacts.list.addresses_count" = "Adresse: %d"; -"contacts.contact.new.title" = "Neuer Kontakt"; +"contacts.add_new_contact" = "Add New Contact"; +"contacts.update_contact.already_has_address" = "Selected contact already has an address on %@. This action will replace the address %@ with %@."; +"contacts.update_contact.replace" = "Replace"; +"contacts.list.addresses_count" = "Addresses: %d"; +"contacts.contact.new.title" = "New Contact"; "contacts.contact.name.placeholder" = "Name"; -"contacts.contact.update.error.name_already_exist" = "Name existiert bereits"; -"contacts.contact.add_address" = "Adresse hinzufügen"; -"contacts.contact.delete" = "Kontakt löschen"; +"contacts.contact.update.error.name_already_exist" = "Name Already Exist"; +"contacts.contact.add_address" = "Add Address"; +"contacts.contact.delete" = "Delete Contact"; "contacts.contact.address.blockchains" = "Blockchains"; "contacts.contact.address.blockchain" = "Blockchain"; -"contacts.contact.address.delete_address" = "Adresse löschen"; +"contacts.contact.address.delete_address" = "Delete Address"; -"contacts.restore.restored" = "Wiederhergestellt"; -"contacts.restore.parsing_error" = "Datei hat falsche Daten!"; -"contacts.restore.restore_error" = "Wiederherstellung der Kontakte fehlgeschlagen"; -"contacts.restore.overwrite_alert.description" = "Diese Aktion wird Ihre lokalen Zahlungskontakte sowie die iCloud-Kopie (falls vorhanden) überschreiben."; -"contacts.restore.overwrite_alert.replace" = "Ersetzen"; +"contacts.restore.restored" = "Restored"; +"contacts.restore.parsing_error" = "File has wrong data!"; +"contacts.restore.restore_error" = "Failed to restore contacts"; +"contacts.restore.overwrite_alert.description" = "This action will overwrite your local payment contacts as well as its iCloud copy (if there is one)."; +"contacts.restore.overwrite_alert.replace" = "Replace"; -"contacts.add_address.title" = "Adresse hinzufügen"; -"contacts.add_address.create_new" = "Neuen Kontakt erstellen"; -"contacts.add_address.add_to_contact" = "Zu vorhandenem Kontakt hinzufügen"; -"contacts.add_address.exist_address" = "Diese Adresse wird bereits für %@ verwendet"; +"contacts.add_address.title" = "Add Address"; +"contacts.add_address.create_new" = "Create New Contact"; +"contacts.add_address.add_to_contact" = "Add to Existing Contact"; +"contacts.add_address.exist_address" = "This address is already used for %@"; -"contacts.contact.delete_alert.title" = "Kontakt löschen"; -"contacts.contact.delete_alert.description" = "Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?"; -"contacts.contact.delete_alert.delete" = "Löschen"; +"contacts.contact.delete_alert.title" = "Delete Contact"; +"contacts.contact.delete_alert.description" = "Are you sure you want to delete this contact?"; +"contacts.contact.delete_alert.delete" = "Delete"; -"contacts.contact.dismiss_changes.description" = "Sind Sie sicher, dass Sie diese neuen Änderungen verwerfen möchten?"; -"contacts.contact.dismiss_changes.discard_changes" = "Änderungen verwerfen"; -"contacts.contact.dismiss_changes.keep_editing" = "Weiter bearbeiten"; +"contacts.contact.dismiss_changes.description" = "Are you sure you want to discard these new changes?"; +"contacts.contact.dismiss_changes.discard_changes" = "Discard Changes"; +"contacts.contact.dismiss_changes.keep_editing" = "Keep Editing"; -"contacts.add_address.delete_alert.title" = "Adresse löschen"; -"contacts.add_address.delete_alert.description" = "Sind Sie sicher, dass Sie diese Adresse löschen wollen?"; -"contacts.add_address.delete_alert.delete" = "Löschen"; +"contacts.add_address.delete_alert.title" = "Delete Address"; +"contacts.add_address.delete_alert.description" = "Are you sure you want to delete this address?"; +"contacts.add_address.delete_alert.delete" = "Delete"; // Contacts -> Settings -"contacts.settings.title" = "Einstellungen"; +"contacts.settings.title" = "Settings"; -"contacts.settings.restore_contacts" = "Kontakte wiederherstellen"; -"contacts.settings.backup_contacts" = "Kontakte speichern"; +"contacts.settings.restore_contacts" = "Restore Contacts"; +"contacts.settings.backup_contacts" = "Backup Contacts"; "contacts.settings.icloud_sync" = "iCloud Sync"; -"contacts.settings.description" = "Synchronisiere Zahlungskontakte mit iCloud für einfache Sicherung und Zugriff auf mehrere Geräte."; -"contacts.settings.lost_synchronization.description" = "iCloud-Synchronisation ist verloren. Bitte überprüfen Sie, ob iCloud-Speicher auf Ihrem Gerät aktiviert ist."; -"contacts.settings.merge_disclaimer" = "Ihre lokalen Zahlungskontakte werden mit den in iCloud gespeicherten zusammengeführt."; +"contacts.settings.description" = "Sync payment contacts to iCloud for easy backup and access across multiple devices."; +"contacts.settings.lost_synchronization.description" = "iCloud synchronization is lost. Please check that iCloud Storage is enabled on your device."; +"contacts.settings.merge_disclaimer" = "Your local payment contacts will be merged with ones stored on iCloud."; "contacts.settings.alert.title" = "iCloud Sync"; -"contacts.settings.alert.description" = "Bitte überprüfen Sie, dass der iCloud-Speicher auf Ihrem Gerät aktiviert ist."; +"contacts.settings.alert.description" = "Please check that iCloud Storage is enabled on your device."; -"contacts.settings.alert_error.title" = "iCloud Fehler"; +"contacts.settings.alert_error.title" = "iCloud Error"; // Set PIN -"set_pin.title" = "Code"; -"set_pin.info" = "Dein Code ist notwendig, um deinen Wallet zu entsperren und Geld zu schicken"; -"set_pin.wrong_confirmation" = "Der eingegebene Code scheint nicht richtig zu sein. Bitte versuchen Sie es erneut."; +"set_pin.title" = "Passcode"; +"set_pin.info" = "Your passcode will be used to unlock your wallet"; +"set_pin.wrong_confirmation" = "Passcode did not match. Try again"; // Edit PIN -"edit_pin.title" = "Code bearbeiten"; -"edit_pin.unlock_info" = "Aktuelle Code "; -"edit_pin.new_pin_info" = "Neue Code"; +"edit_pin.title" = "Edit Passcode"; +"edit_pin.unlock_info" = "Current Passcode"; +"edit_pin.new_pin_info" = "New Passcode"; // Unlock PIN -"unlock_pin.info" = "Code"; -"unlock_pin.cant_save_pin" = "Hoppla! Wir können den Code nicht speichern - bitte kontaktieren Sie uns so schnell wie möglich."; -"unlock_pin.blocked_until" = "Deaktiviert bis: %@ "; +"unlock_pin.info" = "Passcode"; +"unlock_pin.cant_save_pin" = "Ouch! We cannot save your passcode, please contact us asap!"; +"unlock_pin.blocked_until" = "Disabled until: %@"; // Key Types -"chart.time_duration.day" = "24Std"; -"chart.time_duration.week" = "7T"; +"chart.time_duration.day" = "24H"; +"chart.time_duration.week" = "7D"; "chart.time_duration.week2" = "2W"; "chart.time_duration.month" = "1M"; "chart.time_duration.month3" = "3M"; "chart.time_duration.halfyear" = "6M"; -"chart.time_duration.year" = "1J"; -"chart.time_duration.year2" = "2J"; +"chart.time_duration.year" = "1Y"; +"chart.time_duration.year2" = "2Y"; "chart.time_duration.all" = "ALL"; "chart.market_cap" = "Market Cap"; -"chart.volume" = "Lautstärke(24 St.)"; +"chart.volume" = "Volume (24h)"; "chart.circulation" = "In Circulation"; "chart.selected.volume" = "Vol."; "chart.market.header" = "Market"; "chart.market.market_cap" = "Market Cap"; -"chart.market.volume" = "Lautstärke(24 St.)"; +"chart.market.volume" = "Volume (24h)"; "chart.market.circulation" = "In Circulation"; "chart.market.total_supply" = "Total Supply"; @@ -1282,135 +1277,133 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Create Wallet -"create_wallet.title" = "Neues Wallet"; +"create_wallet.title" = "New Wallet"; "create_wallet.name" = "Name"; -"create_wallet.advanced_setup" = "Erweitert"; -"create_wallet.create" = "Erstellen"; -"create_wallet.advanced" = "Erweitert"; -"create_wallet.phrase_count" = "Wiederherstellungsphrase"; -"create_wallet.12_words" = "12 Wörter (empfohlen)"; -"create_wallet.n_words" = "%@ Wörter"; -"create_wallet.word_list" = "Wortliste"; -"create_wallet.passphrase" = "Passwort"; -"create_wallet.input.passphrase" = "Passwort"; -"create_wallet.input.confirm" = "Bestätigen"; -"create_wallet.passphrase_description" = "Passphrases fügen eine zusätzliche Sicherheitsschicht für Brieftaschen hinzu. Um eine solche Brieftasche wiederherzustellen, benötigt ein Benutzer sowohl mnemonische als auch eine Passphrase.\n\nPassphrasen erleichtern es Benutzern auch, mehrere Multicoin-Wallets mit einem einzigen mnemonischen, aber anderen Passwort zu haben."; -"create_wallet.error.empty_passphrase" = "Passphrase darf nicht leer sein"; -"create_wallet.error.forbidden_symbols" = "Bitte verwenden Sie nur unterstützte Symbole: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"create_wallet.error.invalid_confirmation" = "Passphrase-Bestätigung stimmt nicht überein"; +"create_wallet.advanced_setup" = "Advanced"; +"create_wallet.create" = "Create"; +"create_wallet.advanced" = "Advanced"; +"create_wallet.phrase_count" = "Recovery Phrase"; +"create_wallet.12_words" = "12 words (recommended)"; +"create_wallet.n_words" = "%@ words"; +"create_wallet.word_list" = "Word List"; +"create_wallet.passphrase" = "Passphrase"; +"create_wallet.input.passphrase" = "Passphrase"; +"create_wallet.input.confirm" = "Confirm"; +"create_wallet.passphrase_description" = "Passphrases add an additional security layer for wallets. To restore such a wallet a user required both a recovery phrase as well as a passphrase.\n\nPassphrases also make it easy for users to have many multi-coin wallets using a single mnemonic but different password."; +"create_wallet.error.empty_passphrase" = "Passphrase cannot be empty"; +"create_wallet.error.forbidden_symbols" = "Please use only supported symbols:A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"create_wallet.error.invalid_confirmation" = "Passphrase confirmation does not match"; // Restore Select -"restore_select.title" = "Blockchains auswählen"; +"restore_select.title" = "Choose Blockchains"; // Lock Info "lock_info.title" = "TimeLock"; -"lock_info.text" = "Der Sender hat dieses Guthaben mit einer Ausgabesperre versehen, die zu dem angezeigten Datum abläuft.\n\nKeine Sorge, die erhaltenen Bitcoin sind bereits Ihre. Die Bitcoins sind nur zeitweise gesperrt, so dass Sie sie erst nach Ablauf der Sperrzeit im Bitcoin-Netzwerk ausgeben können."; +"lock_info.text" = "The sender sent these funds with a spending lock that will expire on the shown date. \n\nNo worries, the received Bitcoins are already yours, but until the lock period expires you cannot spend them on the Bitcoin network."; // Double Spend Info -"double_spend_info.title" = "Double-Spend"; -"double_spend_info.header" = "Double-Spend-Risiko! Es gibt eine weitere Transaktion auf der Blockchain, die versucht, die in dieser Transaktion verwendeten Inputs auszugeben. Nur eine Transaktion wird vom Netzwerk akzeptiert"; -"double_spend_info.this_hash" = "Dieser Tx"; -"double_spend_info.conflicting_hash" = "Gegensätzliche Tx"; +"double_spend_info.title" = "Double Spend"; +"double_spend_info.header" = "Double Spend Risk! There is another transaction on the blockchain that is trying to spend inputs used in this transaction. Only one transaction will be accepted by the network"; +"double_spend_info.this_hash" = "This Tx"; +"double_spend_info.conflicting_hash" = "Conflicting Tx"; // Relative Date -"timestamp.days_ago" = "vor %luT"; -"timestamp.hours_ago" = "vor %luSt"; -"timestamp.min_ago" = "vor %luMin"; +"timestamp.days_ago" = "%lud ago"; +"timestamp.hours_ago" = "%luh ago"; +"timestamp.min_ago" = "%lum ago"; // Intro -"intro.unchain_assets.title" = "Assets lösen"; -"intro.unchain_assets.description" = "Sperren Sie sich nicht ein und lassen Sie nicht zu, dass andere das mit Ihnen tun"; -"intro.go_borderless.title" = "Jetzt loslegen"; -"intro.go_borderless.description" = "Umgehung der bedingten Schranken und globaler Marktzugang"; -"intro.stay_private.title" = "Privat bleiben"; -"intro.stay_private.description" = "Geben Sie Ihre privaten und finanziellen Daten nicht an die Welt weiter"; +"intro.unchain_assets.title" = "Unchain Assets"; +"intro.unchain_assets.description" = "Don't lock yourself in and don't let others do that to you"; +"intro.go_borderless.title" = "Go Borderless"; +"intro.go_borderless.description" = "Bypass conditional barriers and access markets globally"; +"intro.stay_private.title" = "Stay Private"; +"intro.stay_private.description" = "Do not leak your private and financial data to the world"; // Guides -"guides.tab_bar_item" = "Akademie"; -"guides.title" = "Akademie"; +"guides.tab_bar_item" = "Academy"; +"guides.title" = "Academy"; // Add Token -"add_token.title" = "Token hinzufügen"; +"add_token.title" = "Add Token"; "add_token.blockchain" = "Blockchain"; -"add_token.already_added" = "Dieser Token befindet sich bereits in der Coin-Verwaltungsliste"; -"add_token.invalid_contract_address" = "Ungültige Vertragsadresse"; +"add_token.already_added" = "This token is already in the Coin Manager list"; +"add_token.invalid_contract_address" = "Invalid contract address"; "add_token.invalid_bep2_symbol" = "Invalid BEP2 symbol"; -"add_token.contract_address_not_found" = "Vertragsadresse in %@ Blockchain nicht gefunden"; -"add_token.bep2_symbol_not_found" = "BEP2-Symbol nicht gefunden"; -"add_token.input_placeholder.contract_address" = "Vertragsadresse"; +"add_token.contract_address_not_found" = "Contract address not found in %@ blockchain"; +"add_token.bep2_symbol_not_found" = "BEP2 symbol not found"; +"add_token.input_placeholder.contract_address" = "Contract Address"; "add_token.input_placeholder.bep2_symbol" = "BEP2 Symbol"; "add_token.coin_name" = "Coin Name"; "add_token.symbol" = "Symbol"; -"add_token.decimals" = "Dezimalen"; +"add_token.decimals" = "Decimals"; // Wallet Connect "wallet_connect.title" = "WalletConnect"; -"wallet_connect.error.invalid_url" = "Ungültige URL-Adresse"; +"wallet_connect.error.invalid_url" = "Invalid URL Address"; "wallet_connect.url" = "URL"; -"wallet_connect.active_account" = "Aktive Wallet"; -"wallet_connect.address" = "Adresse"; -"wallet_connect.network" = "Netzwerk"; -"wallet_connect.address" = "Adresse"; -"wallet_connect.network" = "Netzwerk"; -"wallet_connect.list.pending_requests" = "Ausstehende Anfragen"; -"wallet_connect.main.no_any_supported_chains" = "Keine unterstützten Ketten!"; -"wallet_connect.main.unsupported_chains" = "Einige Ketten werden nicht unterstützt!"; -"wallet_connect.connect_description" = "Indem Sie auf \"Genehmigen\" klicken, erlauben Sie dieser App, Ihre öffentliche Adresse anzuzeigen. Dies ist ein wichtiger Sicherheitsschritt, um Ihre Daten vor potenziellen Phishing-Risiken zu schützen."; -"wallet_connect.usage_description" = "Sie können den Browser aufrufen. Schließen Sie diese Seite nicht, während Sie im Browser interagieren."; -"wallet_connect.no_connection" = "Verbindung konnte nicht hergestellt werden. Versuchen Sie, die Verbindung erneut herzustellen."; -"wallet_connect.button_reconnect" = "Wiederverbinden"; -"wallet_connect.button_disconnect" = "Verbindung trennen"; -"ethereum_transaction.error.title" = "Fehler"; -"ethereum_transaction.error.insufficient_balance" = "Die Transaktion erfordert %@ zum Senden."; -"ethereum_transaction.error.insufficient_balance_with_fee" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; -"ethereum_transaction.error.lower_than_base_gas_limit" = "Der gewählte Gebührenwert ist zu niedrig und wird abgelehnt!"; -"ethereum_transaction.error.nonce_already_in_block" = "Transaktion bereits im Block!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Gebühr nicht ausreichend, um die Transaktion zu ersetzen"; -"ethereum_transaction.error.transaction_underpriced" = "Gebühr nicht ausreichend, um die Transaktion zu ersetzen"; -"ethereum_transaction.error.tips_higher_than_max_fee" = "Maximale Gebühr kann nicht niedriger sein als die Tipps, da die Max-Gebühr die Tipps enthält."; -"ethereum_transaction.error.reverted" = "Die Transaktion kann nicht ausgeführt werden: %@"; -"wallet_connect.request_title" = "Vertragsaufruf"; -"wallet_connect.button.confirm" = "Bestätigen"; -"wallet_connect.sign.request_title" = "Signaturanfrage"; -"wallet_connect.sign.domain" = "Domäne"; +"wallet_connect.active_account" = "Active Wallet"; +"wallet_connect.address" = "Address"; +"wallet_connect.network" = "Network"; +"wallet_connect.list.pending_requests" = "Pending Requests"; +"wallet_connect.main.no_any_supported_chains" = "No any supported chains!"; +"wallet_connect.main.unsupported_chains" = "Some chains are unsupported!"; +"wallet_connect.connect_description" = "By clicking approve, you allow this app to view your public address. This is an important security step to protect your data from potential phishing risks."; +"wallet_connect.usage_description" = "You can go to the browser. Do not close this page while interacting in the browser."; +"wallet_connect.no_connection" = "Failed to establish a connection. Try reconnecting again."; +"wallet_connect.button_reconnect" = "Reconnect"; +"wallet_connect.button_disconnect" = "Disconnect"; +"ethereum_transaction.error.title" = "Error"; +"ethereum_transaction.error.insufficient_balance" = "The transaction requires %@ for sending."; +"ethereum_transaction.error.insufficient_balance_with_fee" = "The current %@ balance is below the amount required to process this transaction, including the transaction fee."; +"ethereum_transaction.error.lower_than_base_gas_limit" = "The selected fee value is too low and will be rejected!"; +"ethereum_transaction.error.nonce_already_in_block" = "The transaction is already in block!"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Fee not enough to replace the transaction"; +"ethereum_transaction.error.transaction_underpriced" = "Fee not enough to send the transaction"; +"ethereum_transaction.error.tips_higher_than_max_fee" = "Max fee cannot be lower than the tips, because Max fee includes the tips."; +"ethereum_transaction.error.reverted" = "The transaction cannot be executed: %@"; +"wallet_connect.request_title" = "Contract Call"; +"wallet_connect.button.confirm" = "Confirm"; +"wallet_connect.sign.request_title" = "Sign Request"; +"wallet_connect.sign.domain" = "Domain"; "wallet_connect.sign.dapp_name" = "dApp"; -"wallet_connect.sign.message" = "Zu unterzeichnende Nachricht"; +"wallet_connect.sign.message" = "Message to sign"; "wallet_connect_list.title" = "WalletConnect"; -"wallet_connect.list.empty_view_text" = "Keine aktiven Sitzungen"; +"wallet_connect.list.empty_view_text" = "No active sessions"; "wallet_connect.list.pairings" = "Pairings"; "wallet_connect.list.version_text" = "Version %@"; -"wallet_connect.list.v1_bottom_text" = "In der ersten Version von WalletConnect müssen Sie die Sitzungen aufrufen, um die Anfrage zu sehen und zu bestätigen"; -"wallet_connect_list.new_connection" = "Neue Verbindung"; -"wallet_connect_list.disconnecting" = "Verbindung wird getrennt"; +"wallet_connect.list.v1_bottom_text" = "In the first version of WalletConnect, you must go into sessions to see and confirm the request"; +"wallet_connect_list.new_connection" = "New Connection"; +"wallet_connect_list.disconnecting" = "Disconnecting"; -"wallet_connect.no_account.description" = "Sie müssen eine Wallet erstellen oder importieren bevor Sie WalletConnect verwenden können."; -"wallet_connect.unbackuped_account.description" = "Sie müssen %@ sichern bevor Sie WalletConnect verwenden können"; +"wallet_connect.no_account.description" = "You need to create or import a wallet before you can use WalletConnect."; +"wallet_connect.unbackuped_account.description" = "You need to backup %@ before you can use WalletConnect"; -"wallet_connect.non_supported_account.description" = "Ihr aktueller Wallet-Typ %@ unterstützt WalletConnect nicht"; -"wallet_connect.non_supported_account.switch" = "Wechseln"; +"wallet_connect.non_supported_account.description" = "Your current wallet type %@ does not support WalletConnect"; +"wallet_connect.non_supported_account.switch" = "Switch"; -"wallet_connect.pending_requests_title" = "Ausstehende Anfragen"; +"wallet_connect.pending_requests_title" = "Pending Requests"; "wallet_connect.paired_dapps.title" = "Paired dApps"; -"wallet_connect.paired_dapps.cant_disconnect" = "Kann nicht trennen"; -"wallet_connect.paired_dapps.disconnect_all" = "Alle löschen"; -"wallet_connect.pending_requests.nonactive_footer" = "Um eine Anfrage zu öffnen, müssen Sie die gewünschte Wallet aktivieren"; +"wallet_connect.paired_dapps.cant_disconnect" = "Can't Disconnect"; +"wallet_connect.paired_dapps.disconnect_all" = "Delete All"; +"wallet_connect.pending_requests.nonactive_footer" = "To open an request you must activate the desired wallet"; // App Status -"app_status.title" = "App-Status"; -"app_status.application_status" = "Antragsstatus"; -"app_status.linked_wallets" = "Verknüpfte Wallets"; -"app_status.version_history" = "Versionsverlauf"; -"app_status.blockchain_status" = "Blockchain-Status"; +"app_status.title" = "App Status"; +"app_status.application_status" = "Application status"; +"app_status.linked_wallets" = "Linked Wallets"; +"app_status.version_history" = "Version History"; +"app_status.blockchain_status" = "Blockchain status"; // FAQ @@ -1419,321 +1412,321 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Status Info "status_info.title" = "Status"; -"status_info.pending.title" = "In Bearbeitung"; -"status_info.pending.content" = "Die Transaktion wurde noch nicht in der Blockchain bestätigt. Transaktionen, die mit einer empfohlenen oder höheren Gebühreneinstellung gesendet werden, werden in der Regel innerhalb weniger Minuten abgewickelt. Transaktionen, die mit einer geringen Gebühr gesendet werden, können für einige Stunden oder sogar Tage ausstehend bleiben und sogar abgelehnt werden. Beachten Sie, dass der Status einer individuellen Transaktion in %@ Wallet-Schnittstelle typischerweise mit einer kurzen Verzögerung aktualisiert wird."; -"status_info.processing.title" = "Verarbeite"; -"status_info.processing.content" = "Die Transaktion wurde bereits in die Blockchain aufgenommen, ist aber noch nicht endgültig abgeschlossen. Zu diesem Zeitpunkt kann die Transaktion für kleinere Zahlungen als abgeschlossen betrachtet werden. Bei größeren Zahlungen wird empfohlen, zu warten, bis der Transaktionsstatus auf abgeschlossen wechselt."; -"status_info.completed.title" = "Abgeschlossen"; -"status_info.confirmed.content" = "Die Transaktion ist abgeschlossen und gilt als dauerhaft und unumkehrbar."; -"status_info.failed.title" = "Fehlgeschlagen"; -"status_info.failed.content" = "Die Transaktion wurde nicht verarbeitet und es wurde kein Werttransfer durchgeführt. Abhängig vom Grund des Scheiterns können einige fehlgeschlagene Transaktionen Transaktionsgebühren kosten. Transaktionen, die durch eine andere Transaktion ersetzt oder abgebrochen wurden, verbrauchen keine Transaktionsgebühren und erscheinen auch als fehlgeschlagen. Die %@ App kann den Grund für fehlgeschlagene Transaktionen nicht anzeigen. aber Benutzer können es selbst auf einem öffentlichen Block-Explorer i. e. etherscan.io."; +"status_info.pending.title" = "Pending"; +"status_info.pending.content" = "The transaction has not been confirmed on the blockchain yet. Transactions sent with a recommended or higher fee setting are generally processed within a few minutes. Transactions sent with a low fee may remain pending for a few hours or even days, and can even be rejected. Note that status of an individual transaction in %@ wallet interface typically updated with a short delay."; +"status_info.processing.title" = "Processing"; +"status_info.processing.content" = "The transaction has been already included in the blockchain but has not reached permanent finality. At this point, it's safe to consider the transaction as completed for smaller payments. For larger payments, it's recommended to wait until the transaction status changes to completed."; +"status_info.completed.title" = "Completed"; +"status_info.confirmed.content" = "Transaction is completed and considered permanent and irreversible."; +"status_info.failed.title" = "Failed"; +"status_info.failed.content" = "The transaction did not get processed and no value transfer took place. Depending on the fail reason, some failed transactions may consume transaction fees. Transactions that were replaced or canceled by another transaction do not consume transaction fees and will also appear as failed. The %@ app is unable to show the reason for failed transactions, but users are able to look it up themselves on a public block explorer i.e. etherscan.io."; // Onboarding -"onboarding.balance.create" = "Neues Wallet"; -"onboarding.balance.import" = "Wallet importieren"; -"onboarding.balance.watch" = "Wallet ansehen"; +"onboarding.balance.create" = "New Wallet"; +"onboarding.balance.import" = "Import Wallet"; +"onboarding.balance.watch" = "Watch Wallet"; // Manage Accounts -"manage_accounts.n_words" = "%@ Wörter"; -"manage_accounts.n_words_with_passphrase" = "%@ Wörter mit Passphrase"; +"manage_accounts.n_words" = "%@ words"; +"manage_accounts.n_words_with_passphrase" = "%@ words with passphrase"; // Manage Account "manage_account.name" = "Name"; -"manage_account.recovery_phrase" = "Wiederherstellungsphrase"; -"manage_account.public_keys" = "Öffentliche Schlüssel"; -"manage_account.private_keys" = "Private Schlüssel"; -"manage_account.backup_recovery_phrase" = "Manuelle Sicherung"; -"manage_account.cloud_backup_recovery_phrase" = "Sicherung in iCloud"; -"manage_account.cloud_delete_backup_recovery_phrase" = "Sicherung aus iCloud löschen"; -"manage_account.manual_backup_required" = "Manuelle Sicherung erforderlich"; -"manage_account.manual_backup_required.description" = "Um Ihre Sicherheitskopie auf iCloud sicher zu löschen, müssen Sie zuerst Ihre Wiederherstellungsphrase manuell sichern."; -"manage_account.manual_backup_required.button" = "Jetzt Sichern"; -"manage_account.unlink" = "Wallet entfernen"; -"manage_account.backup.no_backup_yet_description" = "Füllen Sie eine der Wallet-Sicherungsoptionen aus, um mit der Wallet zu beginnen."; -"manage_account.backup.has_backup_description" = "Es wird empfohlen, eine manuelle Sicherung für jede Wallet zu haben."; - -"manage_account.cloud_delete_backup_recovery_phrase.description" = "Sind Sie sicher, dass Sie Ihre Wallet-Sicherung aus iCloud löschen möchten?"; +"manage_account.recovery_phrase" = "Recovery Phrase"; +"manage_account.public_keys" = "Public Keys"; +"manage_account.private_keys" = "Private Keys"; +"manage_account.backup_recovery_phrase" = "Manual Backup"; +"manage_account.cloud_backup_recovery_phrase" = "Backup to iCloud"; +"manage_account.cloud_delete_backup_recovery_phrase" = "Delete Backup from iCloud"; +"manage_account.manual_backup_required" = "Manual Backup Required"; +"manage_account.manual_backup_required.description" = "In order to securely delete your backup on iCloud, you will need to manually backup your recovery phrase first."; +"manage_account.manual_backup_required.button" = "Backup Now"; +"manage_account.unlink" = "Unlink Wallet"; +"manage_account.backup.no_backup_yet_description" = "Complete one of the wallet backup options to start using the wallet."; +"manage_account.backup.has_backup_description" = "It's recommended to have a manual backup for each wallet."; + +"manage_account.cloud_delete_backup_recovery_phrase.description" = "Are you sure you want to delete your wallet backup from iCloud?"; // Manage Account -> Public Keys -"public_keys.title" = "Öffentliche Schlüssel"; -"public_keys.evm_address" = "EVM-Adresse"; -"public_keys.evm_address.description" = "Ermöglicht die bloße Überwachung von Brieftaschen mit Assets auf Ethereum, Binance Smart Chain und anderen EVM-basierten Blockchains."; +"public_keys.title" = "Public Keys"; +"public_keys.evm_address" = "EVM Address"; +"public_keys.evm_address.description" = "Allows read-only monitoring of wallets holding assets on Ethereum, Binance Smart Chain and other EVM based blockchains."; "public_keys.account_extended_public_key" = "Account Extended Public Key"; -"public_keys.account_extended_public_key.description" = "Ermöglicht die schreibgeschützte Überwachung von Brieftaschen mit Bitcoin und anderen auf UTXO basierenden Verschlüsselungen (z.B. Litecoin, Bitcoin Cash, Dash, etc.)."; +"public_keys.account_extended_public_key.description" = "Allows read-only monitoring of wallets holding Bitcoin and other UTXO based crypto (i.e. Litecoin, Bitcoin Cash, Dash, etc.)."; // Manage Account -> Private Keys -"private_keys.title" = "Private Schlüssel"; -"private_keys.evm_private_key" = "EVM privater Schlüssel"; -"private_keys.evm_private_key.description" = "Gewährt volle Kontrolle über EVM-basierte Kryptos, d.h. Ethereum, Binance Smart Chain etc innerhalb der jeweiligen Wallet."; +"private_keys.title" = "Private Keys"; +"private_keys.evm_private_key" = "EVM Private Key"; +"private_keys.evm_private_key.description" = "Grants full control over EVM based crypto i.e. Ethereum, Binance Smart Chain etc within respective wallet."; "private_keys.bip32_root_key" = "BIP32 Root Key"; -"private_keys.bip32_root_key.description" = "Gewährt die volle Kontrolle über die Vermögenswerte auf der jeweiligen Brieftasche."; +"private_keys.bip32_root_key.description" = "Grants full control over the assets on the respective wallet."; "private_keys.account_extended_private_key" = "Account Extended Private Key"; -"private_keys.account_extended_private_key.description" = "Gewährt volle Kontrolle über Bitcoin und andere auf UTXO basierende Kryptos, z.B. Litecoin, Bitcoin Cash, Dash, etc. innerhalb der jeweiligen Wallet."; +"private_keys.account_extended_private_key.description" = "Grants full control over Bitcoin and other UTXO based crypto i.e. Litecoin, Bitcoin Cash, Dash, etc. within respective wallet."; // Manage Account -> EVM Address -"evm_address.title" = "EVM-Adresse"; +"evm_address.title" = "EVM Address"; // Birthday Height -"birthday_height.title" = "Geburtstagshöhe"; +"birthday_height.title" = "Birthday Height"; // Birthday Input -"birthday_input.title" = "Geburtstagshöhe"; -"birthday_input.description" = "Geben Sie die Geburtstagshöhe der Brieftasche für eine schnellere Synchronisierung ein."; -"birthday_input.new_wallet" = "Neues Wallet"; -"birthday_input.new_wallet.description" = "Keine Transaktionen vorhanden"; -"birthday_input.old_wallet" = "Vorhandene Wallet"; -"birthday_input.old_wallet.description" = "Hat Transaktionen"; +"birthday_input.title" = "Birthday Height"; +"birthday_input.description" = "Enter wallet's birthday height for faster synchronization."; +"birthday_input.new_wallet" = "New Wallet"; +"birthday_input.new_wallet.description" = "Doesn't have any transactions"; +"birthday_input.old_wallet" = "Existing Wallet"; +"birthday_input.old_wallet.description" = "Has transactions"; "birthday_input.input_placeholder" = "%@ (optional)"; -"restore_setting.birthday_height" = "%@ Geburtstagshöhe"; -"restore_setting.download.disclaimer" = "Die erste Synchronisation mit der Blockchain kann viel Internet-Verkehr verbrauchen."; +"restore_setting.birthday_height" = "%@ Birthday Height"; +"restore_setting.download.disclaimer" = "The initial synchronization with the blockchain can consume a lot of internet traffic."; // EVM Network -"evm_network.rpc_source" = "RPC-Quelle"; -"evm_network.added" = "Hinzugefügt"; -"evm_network.add_new" = "Neu hinzufügen"; +"evm_network.rpc_source" = "RPC Source"; +"evm_network.added" = "Added"; +"evm_network.add_new" = "Add New"; // Add RPC Source -"add_evm_sync_source.title" = "RPC-Quelle hinzufügen"; +"add_evm_sync_source.title" = "Add RPC Source"; "add_evm_sync_source.name" = "Name"; "add_evm_sync_source.rpc_url" = "RPC URL"; -"add_evm_sync_source.basic_auth" = "Einfacher Auth (optional)"; -"add_evm_sync_source.warning.url_exists" = "RPC-Quelle mit dieser URL existiert bereits"; -"add_evm_sync_source.error.invalid_url" = "Die eingegebene URL ist ungültig. Gültige URL muss eines der folgenden Schemata haben: http, https, ws, wss"; +"add_evm_sync_source.basic_auth" = "Basic Auth (optional)"; +"add_evm_sync_source.warning.url_exists" = "RPC Source with this url already exists"; +"add_evm_sync_source.error.invalid_url" = "Entered url is invalid. Valid url must have one of the following schemes: https, wss"; // Send Settings -"evm_send_settings.nonce" = "Transaktion Nonce"; -"evm_send_settings.nonce.info" = "Das nonce ist ein eindeutiger Integer-Wert für eine Transaktion innerhalb der Wallet des Benutzers. Sie erhöht sich normalerweise mit jeder abgeschlossenen Transaktion und muss sich nicht ändern. Fortgeschrittene Benutzer können es einem Nonce einer ausstehenden Transaktion gleichen, um diese Transaktion zu stornieren und zu ersetzen solange die neue Transaktion eine ausreichend höhere Gebühr hat, um zu verhindern, dass die alte statt dessen bestätigt wird (zum Beispiel Sie möchten möglicherweise die Bestätigung beschleunigen oder die Transaktionsparameter vollständig ändern). Wenn mehrere ausstehende Transaktionen die gleiche Nonce haben, wird nur eine bestätigt, typischerweise die mit der höchsten Gebühr."; -"evm_send_settings.nonce.errors.already_in_use" = "Nonce verwendet"; -"evm_send_settings.nonce.errors.already_in_use.info" = "Eine ausgeführte Transaktion mit diesem nonce existiert bereits."; - -"fee_settings" = "Erweitert"; -"fee_settings.fee" = "Gebühr"; -"fee_settings.fee.info" = "Blockchains verlangen, dass Nutzer Netzwerkgebühren beim Versenden von Transaktionen bezahlen. Die Gebühren sind höher, wenn viele Transaktionen im Netzwerk stattfinden."; -"fee_settings.fee_rate" = "Gebührenrate"; -"fee_settings.fee_rate.description" = "Hier ist der empfohlene Wert, um die nächsten 2 Blöcke zu treffen"; -"fee_settings.inputs_outputs" = "Ein-/Ausgänge"; -"fee_settings.transaction_settings" = "Transaktionseinstellungen"; -"fee_settings.transaction_settings.description" = "Machen Sie Ihre Bitcoin-Transaktionen schwieriger nachvollziehbar, indem Sie die Transaktionsstruktur verändern."; -"fee_settings.time_lock" = "TimeLock"; -"fee_settings.time_lock.description" = "TimeLock funktioniert nur beim Senden an BIP44 Adressen (ab 1)"; - -"fee_settings.network_fee" = "Netzwerkgebühr"; -"fee_settings.network_fee.info" = "Die geschätzten Kosten für den Versand der Transaktion im Netzwerk."; -"fee_settings.gas_limit" = "Gaslimit"; -"fee_settings.gas_limit.info" = "Die Transaktionskomplexität wird in Einheiten mit dem Namen \"Gas\" gemessen, die je nach ausgeführten intelligenten Verträgen variieren. Das Gaslimit ist das geschätzte Höchstgas, das benötigt wird, um es auszuführen. Das tatsächliche Gas wird normalerweise niedriger sein."; +"evm_send_settings.nonce" = "Transaction Nonce"; +"evm_send_settings.nonce.info" = "The nonce is a unique integer value for a transaction within the user's wallet. It normally increments with each submitted transaction and does not need changing. Advanced users can set it equal to a nonce of a pending transaction in order to cancel and replace that transaction, as long as the new transaction has a sufficiently higher fee to prevent the old one from being confirmed instead (for example, they may want to speed up its confirmation, or to change transaction parameters entirely). When multiple pending transactions have the same nonce, only one gets confirmed, typically the one with the highest fee."; +"evm_send_settings.nonce.errors.already_in_use" = "Used Nonce"; +"evm_send_settings.nonce.errors.already_in_use.info" = "An executed transaction with this nonce already exists."; + +"fee_settings" = "Advanced"; +"fee_settings.fee" = "Fee"; +"fee_settings.fee.info" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network."; +"fee_settings.fee_rate" = "Fee Rate"; +"fee_settings.fee_rate.description" = "Here is the recommended value for hitting the next 2 blocks"; +"fee_settings.inputs_outputs" = "Inputs/Outputs"; +"fee_settings.transaction_settings" = "Transaction Settings"; +"fee_settings.transaction_settings.description" = "Make your Bitcoin transactions harder to trace by changing the way transactions are structured."; +"fee_settings.time_lock" = "Time Lock"; +"fee_settings.time_lock.description" = "TimeLock works only for sending to BIP44 addresses (starting with 1)"; + +"fee_settings.network_fee" = "Network Fee"; +"fee_settings.network_fee.info" = "The estimated cost of sending given transaction on the network."; +"fee_settings.gas_limit" = "Gas Limit"; +"fee_settings.gas_limit.info" = "Transaction complexity is measured in units called \"gas\". It varies depending on the smart contract being executed. The Gas Limit is the estimated maximum gas needed to execute it. The actual gas used will normally be lower."; "fee_settings.gas_price" = "Gas Price"; -"fee_settings.gas_price.info" = "Die Gebühr für Transaktionen im Netzwerk wird in Gaseinheiten gemessen. Der Gaspreis ist der Betrag, den ein Nutzer für eine Einheit Gas auszugeben bereit ist. Wenn das Netzwerk ausgelastet ist, sind die Gaspreise hoch, und niedrig, wenn es nicht genutzt wird. Ein unzureichender Gaspreis ist oft ein Grund dafür, dass eine Transaktion über einen längeren Zeitraum aussteht."; - -"fee_settings.base_fee" = "Basisgebühr"; -"fee_settings.base_fee.info" = "Das Netzwerkprotokoll bestimmt den Basispreis pro Gas für jeden Block, sogenannte Basisgebühren. Sie variiert je nach Netzwerkauslastung von Block zu Block. Sie kann im nächsten Block um nicht mehr als 12,5% steigen oder sinken, was die Gebühren berechenbarer macht. Der hier angezeigte Wert ist die Basisgebühr des aktuellen Blocks."; -"fee_settings.max_fee_rate" = "Max Gebührenrate"; -"fee_settings.max_fee_rate.info" = "Dies ist der maximale Gesamtpreis pro Gas, den der Nutzer zu zahlen bereit ist. Es muss den Basistarif des Netzwerks und den maximalen Prioritätstarif abdecken. Der hier angezeigte Wert wird anhand einer Schätzung der Basisgebühr des nächsten Blocks plus der vom Nutzer gewählten Maximalgebührengebühr vorgeschlagen. Der tatsächlich gezahlte Gebührensatz ist normalerweise niedriger. Wenn diese Einstellung niedriger als die aktuelle Basisgebühr ist, wird die gezahlte Gebühr begrenzt aber führt zu längeren Wartezeiten für die Bestätigung der Transaktion oder sogar zu einer festgefahrenen Transaktion."; -"fee_settings.tips" = "Max Prioritätsgebühr"; -"fee_settings.tips.info" = "Benutzer zahlen Prioritätsgebühren, um eine Transaktion schneller zu bestätigen. Sie werden manchmal als Tipps bezeichnet. Die maximale Prioritätsgebühr ist der maximale Zusatzpreis pro Gas, den der Nutzer zusätzlich zur Basisgebühr zahlen möchte. Der hier angezeigte Wert wird basierend auf vorausgesagten Netzwerkbedingungen vorgeschlagen. Die tatsächliche Prioritätsgebühr wird normalerweise niedriger sein. Die Einstellung auf Null kann zu einer langen Wartezeit führen, bis die Transaktion bestätigt wird, , da er am Ende der Warteschlange aller Benutzer platziert ist."; - -"fee_settings.errors.insufficient_balance" = "Unzureichendes Guthaben"; -"fee_settings.errors.unexpected_error" = "Unerwarteter Fehler"; -"fee_settings.errors.insufficient_balance.info" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; -"fee_settings.errors.low_max_fee" = "Niedrige Gebühr"; -"fee_settings.errors.low_max_fee.info" = "Der eingestellte Betrag der Transaktionsgebühr reicht nicht aus, um diese Transaktion jetzt zu bearbeiten."; -"fee_settings.errors.nonce_already_in_block" = "Transaktion kann nicht ersetzt werden."; -"fee_settings.errors.replacement_transaction_underpriced" = "Niedrige Gebühr für Ersatztransaktionen"; -"fee_settings.errors.transaction_underpriced" = "Niedrige Transaktionsgebühr"; -"fee_settings.errors.tips_higher_than_max_fee" = "Max Gebühr ist zu niedrig"; -"fee_settings.errors.zero_amount.info" = "Kann 0 TRX nicht übertragen"; -"fee_settings.warning.risk_of_getting_stuck" = "Riskant"; -"fee_settings.warning.risk_of_getting_stuck.info" = "Die Transaktion kann eine Zeit lang ausstehend bleiben oder ganz fehlschlagen."; -"fee_settings.warning.overpricing" = "Gebühr zu hoch"; -"fee_settings.warning.overpricing.info" = "Die festgelegte Transaktionsgebühr ist höher als für die Bearbeitung dieser Transaktion jetzt erforderlich."; +"fee_settings.gas_price.info" = "The fee for transacting on the network is measured in gas units. Gas Price is the amount a user is willing to spend per unit of gas. When the network is busy, gas prices are high, and low when it's idle. An insufficient gas price is often a reason for a transaction to remain pending for an extended period."; + +"fee_settings.base_fee" = "Base Fee"; +"fee_settings.base_fee.info" = "The network protocol determines the base price per gas for each block, called base fee rate. It varies according to the network utilization level from block to block. It can increase or decrease by no more than 12.5% in the next block, making fees more predictable. The value shown here is the current block's base fee rate."; +"fee_settings.max_fee_rate" = "Max Fee Rate"; +"fee_settings.max_fee_rate.info" = "This is the maximum total price per gas the user is willing to pay. It must cover the network's base fee rate and max priority fee rate. The value shown here is suggested based on an estimate of the next block's base fee rate plus the max priority fee rate chosen by the user. The actual fee rate paid will normally be lower. Setting this lower than the current base fee rate will limit the fee paid, but will result in longer waiting times for the transaction to be confirmed, or even in a stuck transaction."; +"fee_settings.tips" = "Max Priority Fee"; +"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; + +"fee_settings.errors.insufficient_balance" = "Insufficient balance"; +"fee_settings.errors.unexpected_error" = "Unexpected Error"; +"fee_settings.errors.insufficient_balance.info" = "The current %@ balance is below the amount required to process this transaction, including the transaction fee."; +"fee_settings.errors.low_max_fee" = "Low Fee"; +"fee_settings.errors.low_max_fee.info" = "The set transaction fee amount is insufficient for processing this transaction now."; +"fee_settings.errors.nonce_already_in_block" = "Can't replace transaction."; +"fee_settings.errors.replacement_transaction_underpriced" = "Low Fee for replacement transaction"; +"fee_settings.errors.transaction_underpriced" = "Low Fee for transaction"; +"fee_settings.errors.tips_higher_than_max_fee" = "Max Fee is too low"; +"fee_settings.errors.zero_amount.info" = "Cannot transfer 0 TRX"; +"fee_settings.warning.risk_of_getting_stuck" = "Risky"; +"fee_settings.warning.risk_of_getting_stuck.info" = "The transaction may remain pending for a while or fail entirely."; +"fee_settings.warning.overpricing" = "Fee Too High"; +"fee_settings.warning.overpricing.info" = "The set transaction fee is higher than necessary for processing this transaction now."; // Watch Address -"watch_address.title" = "Wallet ansehen"; -"watch_address.watch" = "Weiter"; -"watch_address.address" = "Adresse"; -"watch_address.by" = "Von"; -"watch_address.watch_by" = "Beobachten von"; -"watch_address.evm_address" = "EVM-Adresse"; -"watch_address.tron_address" = "TRON Adresse"; +"watch_address.title" = "Watch Wallet"; +"watch_address.watch" = "Next"; +"watch_address.address" = "Address"; +"watch_address.by" = "By"; +"watch_address.watch_by" = "Watch By"; +"watch_address.evm_address" = "EVM Address"; +"watch_address.tron_address" = "TRON Address"; "watch_address.public_key" = "Account xPubKey"; -"watch_address.public_key.placeholder" = "Account Extended Public Key eingeben"; -"watch_address.public_key.invalid_key" = "Ungültiger Schlüssel"; -"watch_address.choose_blockchain" = "Blockchain auswählen"; -"watch_address.choose_coin" = "Coins auswählen"; +"watch_address.public_key.placeholder" = "Enter Account Extended Public Key "; +"watch_address.public_key.invalid_key" = "Invalid Key"; +"watch_address.choose_blockchain" = "Choose Blockchain"; +"watch_address.choose_coin" = "Choose Coin"; // Nft Collections "nft_collections.title" = "NFTs"; -"nft_collections.price_mode" = "Preismodus"; -"nft_collections.last_sale" = "Letzter Verkauf"; -"nft_collections.average_7d" = "Durchschnitt 7D"; -"nft_collections.average_30d" = "Durchschnitt 30D"; -"nft_collections.on_sale" = "Im Verkauf"; -"nft_collections.empty" = "Sie haben keinen NFT in Ihrer Brieftasche"; +"nft_collections.price_mode" = "Price Mode"; +"nft_collections.last_sale" = "Last Sale"; +"nft_collections.average_7d" = "Average 7D"; +"nft_collections.average_30d" = "Average 30D"; +"nft_collections.on_sale" = "On Sale"; +"nft_collections.empty" = "You don’t have any NFTs in your wallet"; "top_nft_collections.title" = "Top NFT Collections"; -"top_nft_collections.description" = "Führen von NFT-Sammlungen durch Handelsvolumen."; +"top_nft_collections.description" = "Leading NFT collections by trading volume."; // Nft Asset "nft_asset.tab.overview" = "Overview"; -"nft_asset.tab.activity" = "Aktivität"; - -"nft_asset.last_sale" = "Letzter Verkauf"; -"nft_asset.average_7d" = "7-Tages-Durchschnitt"; -"nft_asset.average_30d" = "30-Tages-Durchschnitt"; -"nft_asset.floor_price" = "Mindestpreis"; -"nft_asset.on_sale" = "Im Verkauf"; -"nft_asset.on_auction" = "Auf Auktion"; -"nft_asset.until_date" = "bis %@"; -"nft_asset.buy_now" = "Jetzt kaufen"; -"nft_asset.minimum_bid" = "Mindestgebot"; -"nft_asset.best_offer" = "Bestes Angebot"; -"nft_asset.properties" = "Eigenschaften"; -"nft_asset.description" = "Beschreibung"; -"nft_asset.details" = "Info"; -"nft_asset.details.contract_address" = "Vertragsadresse"; +"nft_asset.tab.activity" = "Activity"; + +"nft_asset.last_sale" = "Last Sale"; +"nft_asset.average_7d" = "7 Day Average"; +"nft_asset.average_30d" = "30 Day Average"; +"nft_asset.floor_price" = "Floor Price"; +"nft_asset.on_sale" = "On Sale"; +"nft_asset.on_auction" = "On Auction"; +"nft_asset.until_date" = "until %@"; +"nft_asset.buy_now" = "Buy Now"; +"nft_asset.minimum_bid" = "Minimum Bid"; +"nft_asset.best_offer" = "Best Offer"; +"nft_asset.properties" = "Properties"; +"nft_asset.description" = "Description"; +"nft_asset.details" = "Details"; +"nft_asset.details.contract_address" = "Contract Address"; "nft_asset.details.token_id" = "Token ID"; -"nft_asset.details.token_standard" = "Token-Standard"; +"nft_asset.details.token_standard" = "Token Standard"; "nft_asset.details.blockchain" = "Blockchain"; "nft_asset.links" = "Links"; "nft_asset.links.website" = "Website"; -"nft_asset.options.save_to_photos" = "In Fotos speichern"; -"nft_asset.options.set_as_watch_face" = "Als Watch-Face festlegen"; -"nft_asset.save_to_photos.failed" = "NFT-Bild konnte nicht gespeichert werden"; +"nft_asset.options.save_to_photos" = "Save to Photos"; +"nft_asset.options.set_as_watch_face" = "Set as Watch Face"; +"nft_asset.save_to_photos.failed" = "Failed to save NFT image"; // Nft Collection "nft_collection.tab.overview" = "Overview"; -"nft_collection.tab.assets" = "Artikel"; -"nft_collection.tab.activity" = "Aktivität"; +"nft_collection.tab.assets" = "Items"; +"nft_collection.tab.activity" = "Activity"; -"nft_collection.overview.description" = "Beschreibung"; -"nft_collection.overview.contracts" = "Verträge"; +"nft_collection.overview.description" = "Description"; +"nft_collection.overview.contracts" = "Contracts"; "nft_collection.overview.links" = "Links"; "nft_collection.overview.links.website" = "Website"; -"nft_collection.overview.owners" = "Besitzer"; -"nft_collection.overview.items" = "Artikel"; +"nft_collection.overview.owners" = "Owners"; +"nft_collection.overview.items" = "Items"; "nft_collection.overview.24h_volume" = "24h Volume"; -"nft_collection.overview.today_sellers" = "Heutige Verkäufer"; -"nft_collection.overview.floor_price" = "Mindestpreis"; -"nft_collection.overview.all_time_average" = "Ganzzeitiger Durchschnitt"; +"nft_collection.overview.today_sellers" = "Today's Sellers"; +"nft_collection.overview.floor_price" = "Floor Price"; +"nft_collection.overview.all_time_average" = "All time average"; "nft_collection.overview.royalty" = "Royalty"; "nft_collection.overview.inception_date" = "Inception Date"; -"nft.activity.contracts" = "Verträge"; -"nft.activity.empty_list" = "Noch keine Artikel-Aktivität"; -"nft.activity.event_types" = "Ereignistypen"; -"nft.activity.event_type.all" = "Alle Ereignisse"; +"nft.activity.contracts" = "Contracts"; +"nft.activity.empty_list" = "No item activity yet"; +"nft.activity.event_types" = "Event Types"; +"nft.activity.event_type.all" = "All Events"; "nft.activity.event_type.sale" = "Sale"; "nft.activity.event_type.transfer" = "Transfer"; "nft.activity.event_type.mint" = "Mint"; -"nft.activity.event_type.list" = "Liste"; -"nft.activity.event_type.listCancel" = "Liste abbrechen"; -"nft.activity.event_type.offer" = "Angebot"; -"nft.activity.event_type.offerCancel" = "Angebot abbrechen"; +"nft.activity.event_type.list" = "List"; +"nft.activity.event_type.listCancel" = "List Cancel"; +"nft.activity.event_type.offer" = "Offer"; +"nft.activity.event_type.offerCancel" = "Offer Cancel"; // Subscription Info -"subscription_info.title" = "Premium-Funktionen"; +"subscription_info.title" = "Premium Features"; "subscription_info.info1.title" = "Cryptocurrency Analytics"; "subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.info2.title" = "Chart Indicators"; "subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.info3.title" = "Personal Support"; "subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.get_premium" = "Premium erhalten"; -"subscription_info.already_have" = "Ich habe bereits Premium"; +"subscription_info.get_premium" = "Get Premium"; +"subscription_info.already_have" = "I already have Premium"; // Activate Subscription -"activate_subscription.title" = "Aktivieren"; +"activate_subscription.title" = "Activate"; "activate_subscription.wallet" = "Wallet"; -"activate_subscription.address" = "Adresse"; +"activate_subscription.address" = "Address"; "activate_subscription.message" = "Message to Sign"; -"activate_subscription.sign" = "Signieren"; +"activate_subscription.sign" = "Sign"; "activate_subscription.activating" = "Activating..."; "activate_subscription.failed_to_activate" = "Failed to activate subscription"; -"activate_subscription.activated" = "Aktiviert"; -"activate_subscription.no_subscriptions" = "Ihre Wallet-Adresse hat kein Abonnement für Premium-Funktionen, Sie müssen sie kaufen, um das Abonnement zu aktivieren."; +"activate_subscription.activated" = "Activated"; +"activate_subscription.no_subscriptions" = "Your wallet address does not have a subscription to premium features, you need to purchase it to activate the subscription."; // Launch -"launch.failed_to_launch" = "Anwendung konnte aufgrund eines internen Fehlers nicht gestartet werden. Bitte versuchen Sie die App neu zu starten oder melden Sie den Fehler an unser Support-Team."; -"launch.failed_to_launch.report" = "Melden"; +"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting app or report the error to our support team."; +"launch.failed_to_launch.report" = "Report"; // Tron -"tron.send.activation_fee" = "Aktivierungsgebühr"; -"tron.send.resources_consumed" = "Verbrauchte Ressourcen"; +"tron.send.activation_fee" = "Activation Fee"; +"tron.send.resources_consumed" = "Resources Consumed"; "tron.send.bandwidth" = "Bandwidth"; "tron.send.energy" = "Energy"; -"tron.send.fee.info" = "Die geschätzten Kosten für den Versand einer Transaktion im Netzwerk (ohne Energie-, Bandbreiten- und Aktivierungsgebühren)"; -"tron.send.resources_consumed.info" = "Bandwidth ist die Einheit, die die Größe der in der Blockchain-Datenbank gespeicherten Transaktionsbytes misst. Je größer die Transaktion, desto mehr Ressourcen werden verbraucht.\n\nEnergie ist die Einheit, die die Anzahl der Berechnungen misst, die von der virtuellen TRON-Maschine benötigt werden, um bestimmte Operationen im TRON-Netzwerk durchzuführen.\n\nDa intelligente Vertragsabschlüsse die Verwendung von Rechnerressourcen erfordern, muss für jeden Smart Contract eine Energiegebühr bezahlt werden."; -"tron.send.activation_fee.info" = "Die Übertragung von TRX- oder TRC-10-Token an eine inaktive Konto-Adresse wird das Konto aktivieren."; -"tron.send.inactive_address" = "Diese Adresse ist nicht aktiv"; +"tron.send.fee.info" = "The estimated cost of sending given transaction on the network. (Without excluding Energy, Bandwidth and Activating Fee)"; +"tron.send.resources_consumed.info" = "Bandwidth is the unit that measures the size of the transaction bytes stored in the blockchain database. The larger the transaction, the more bandwidth resources will be consumed.\n\nEnergy is the unit that measures the amount of computation required by the TRON virtual machine to perform specific operations on the TRON network.\n\nSince smart contract transactions require computing resources to execute, each smart contract transaction requires to pay for the energy fee."; +"tron.send.activation_fee.info" = "Transferring TRX or TRC-10 tokens to an inactive account address will activate the account."; +"tron.send.inactive_address" = "This address is not active"; // Cex Coin Select -"cex_coin_select.title" = "Coins auswählen"; -"cex_coin_select.withdraw" = "Abheben"; -"cex_coin_select.withdraw.empty" = "Sie haben keine Assets zum Ausziehen."; -"cex_coin_select.search_placeholder" = "Münzcode oder Name"; -"cex_coin_select.suspended" = "Gesperrt"; +"cex_coin_select.title" = "Choose Coin"; +"cex_coin_select.withdraw" = "Withdraw"; +"cex_coin_select.withdraw.empty" = "You have no assets to withdraw."; +"cex_coin_select.search_placeholder" = "Coin Code or Name"; +"cex_coin_select.suspended" = "Suspended"; // Cex Deposit Network Select -"cex_deposit_network_select.title" = "Netzwerk auswählen"; -"cex_deposit_network_select.description" = "Wählen Sie ein Netzwerk aus und erhalten Sie eine Adresse zum Einzahlen."; +"cex_deposit_network_select.title" = "Choose Network"; +"cex_deposit_network_select.description" = "Choose a network and get an address to deposit."; // Cex Deposit -"cex_deposit.title" = "Auftragen %@"; -"cex_deposit.address" = "Adresse"; +"cex_deposit.title" = "Deposit %@"; +"cex_deposit.address" = "Address"; -"cex_deposit.network" = "Netzwerk"; +"cex_deposit.network" = "Network"; "cex_deposit.memo" = "Memo (Tag)"; "cex_deposit.min_amount" = "Min. Amount"; -"cex_deposit.warning_memo" = "Memo (Tag) ist erforderlich, sonst verlieren Sie Ihre Münzen."; -"cex_deposit.copy_address" = "Kopieren"; -"cex_deposit.share_address" = "Teilen"; -"cex_deposit.failed" = "Fehler beim Laden der Einzahlungsadresse. Bitte erneut versuchen."; -"cex_deposit.memo_warning.title" = "Wichtig"; -"cex_deposit.memo_warning.description" = "Sowohl eine Memo (Tag) als auch die Adresse werden benötigt, um sicherzustellen, dass Vermögenswerte empfangen werden. Andernfalls gehen Ihre Gelder verloren."; +"cex_deposit.warning_memo" = "Memo (tag) is required, or your will lose your coins."; +"cex_deposit.copy_address" = "Copy"; +"cex_deposit.share_address" = "Share"; +"cex_deposit.failed" = "Failed to load deposit address. Please retry."; +"cex_deposit.memo_warning.title" = "Important"; +"cex_deposit.memo_warning.description" = "Both a memo (tag) and the address are needed to ensure that assets are received. Otherwise your funds will be lost."; // Cex Widthdraw -"cex_withdraw.network" = "Netzwerk"; -"cex_withdraw.network_warning" = "Stellen Sie sicher, dass das Netzwerk mit der Auszahlungsadresse übereinstimmt und die Einzahlungsplattform sie unterstützt, oder es können Vermögenswerte verloren gehen."; -"cex_withdraw.fee" = "Gebühr"; -"cex_withdraw.fee_from_amount" = "Gebühr aus Betrag"; -"cex_withdraw.error.insufficient_funds" = "Nicht genügend verfügbares Guthaben"; -"cex_withdraw.error.max_amount_violated" = "Der maximale Betrag, den du abheben kannst, ist %@"; -"cex_withdraw.error.min_amount_violated" = "Der Mindestbetrag, den du abheben kannst, ist %@"; -"cex_withdraw.address_required" = "Adresse ist erforderlich"; +"cex_withdraw.network" = "Network"; +"cex_withdraw.network_warning" = "Ensure the network matches the withdrawal address and the deposit platform supports it, or assets may be lost."; +"cex_withdraw.fee" = "Fee"; +"cex_withdraw.fee_from_amount" = "Fee from amount"; +"cex_withdraw.error.insufficient_funds" = "Not enough available balance"; +"cex_withdraw.error.max_amount_violated" = "The maximum amount you can withdraw is %@"; +"cex_withdraw.error.min_amount_violated" = "The minimum amount you can withdraw is %@"; +"cex_withdraw.address_required" = "Address is required"; // Cex Withdraw Network Select -"cex_withdraw_network_select.title" = "Netzwerk"; -"cex_withdraw_network_select.description" = "Stellen Sie sicher, dass das Netzwerk mit der Auszahlungsadresse übereinstimmt und die Einzahlungsplattform unterstützt sie."; +"cex_withdraw_network_select.title" = "Network"; +"cex_withdraw_network_select.description" = "Ensure the network matches the withdrawal address and the deposit platform supports it."; // Cex Withdraw Confirm -"cex_withdraw_confirm.you_withdraw" = "Sie zurückziehen"; -"cex_withdraw_confirm.network" = "Netzwerk"; -"cex_withdraw_confirm.withdraw" = "Abheben"; -"cex_withdraw_confirm.withdraw_failed" = "Fehler bei Auszahlung"; +"cex_withdraw_confirm.you_withdraw" = "You Withdraw"; +"cex_withdraw_confirm.network" = "Network"; +"cex_withdraw_confirm.withdraw" = "Withdraw"; +"cex_withdraw_confirm.withdraw_failed" = "Withdraw failed"; \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 142033878d..42b3305af7 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -46,7 +46,6 @@ "alert.removed_from_wallet" = "Removed from Wallet"; "alert.already_added_to_wallet" = "Already added to Wallet"; "alert.not_supported_yet" = "Not Supported Yet"; -"alert.copied" = "Copied"; "alert.created" = "Created"; "alert.imported" = "Imported"; "alert.wallet_added" = "Wallet Added"; @@ -1353,8 +1352,6 @@ Go to Settings - > %@ and allow access to the camera."; "wallet_connect.active_account" = "Active Wallet"; "wallet_connect.address" = "Address"; "wallet_connect.network" = "Network"; -"wallet_connect.address" = "Address"; -"wallet_connect.network" = "Network"; "wallet_connect.list.pending_requests" = "Pending Requests"; "wallet_connect.main.no_any_supported_chains" = "No any supported chains!"; "wallet_connect.main.unsupported_chains" = "Some chains are unsupported!"; diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index a418495d4d..7ce43f5aac 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -107,12 +107,12 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "restore.advanced" = "Advanced"; "restore.import_by" = "Import By"; "restore.restore_type.mnemonic" = "Recovery Phrase"; -"restore.restore_type.private_key" = "Top secret! Private Key."; +"restore.restore_type.private_key" = "Private Key."; "restore.mnemonic.placeholder" = "Give me those magic words! Recovery Phrase, come on!"; "restore.private_key.placeholder" = "Enter EVM Private Key, BIP32 Root Key or Account Extended Private Key"; -"restore.private_key.invalid_key" = "This key? Not the best. It's wrong!"; +"restore.private_key.invalid_key" = "Invalid Key"; "restore_error.mnemonic_word_count" = "Listen, we need 12 to 24 words. Not more, not less. Perfect balance. You gave me: %@"; -"restore.checksum_error" = "Checksum? Fake news! It's wrong!"; +"restore.checksum_error" = "Invalid checksum"; "restore.passphrase" = "Passphrase"; "restore.input.passphrase" = "Passphrase"; "restore.passphrase_description" = "You know, we have this tremendous password. It's for your wallet backup, not your Apple thing. It's so good, if you lose it, even I can't get it back for you!"; @@ -124,11 +124,11 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Restore Type -"restore_type.title" = "Importing The Best Wallet Ever!"; +"restore_type.title" = "Import Wallet"; -"restore_type.recovery.title" = "The Tremendous Recovery Phrase!"; -"restore_type.cloud.title" = "iCloud"; -"restore_type.cex.title" = "From the Exchange Wallet – The Best!"; +"restore_type.recovery.title" = "from Recovery Phrase"; +"restore_type.cloud.title" = "from iCloud"; +"restore_type.cex.title" = "from Exchange Wallet"; "restore_type.recovery.description" = "Importing with the mightiest recovery phrase or a super private key!"; "restore_type.cloud.description" = "Importing from the backup – straight outta your keychain!"; @@ -136,30 +136,30 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Restore Cloud -"restore.cloud.title" = "Backup? The Best Backup!"; +"restore.cloud.title" = "Select Backup"; "restore.cloud.description" = "Pick your wallet's backup, the one you really wanna bring back!"; -"restore.cloud.empty" = "Backups? None found. Sad!"; -"restore.cloud.imported" = "Wallets that we've made great again!"; +"restore.cloud.empty" = "No backups found"; +"restore.cloud.imported" = "Imported wallets"; -"restore.cloud.password.title" = "Type in the most secret password!"; -"restore.cloud.password.placeholder" = "Your fantastic backup password here!"; +"restore.cloud.password.title" = "Enter Password"; +"restore.cloud.password.placeholder" = "Backup Password"; "restore.cloud.password.description" = "Key in your majestic backup password to get that wallet out of iCloud!"; // Restore Cex -"restore.cex.title" = "Pick Your CEX – The Best CEX!"; +"restore.cex.title" = "Select CEX"; "restore.cex.description" = "Choose your centralized exchange, the greatest of them all, believe me!"; // Restore Binance "restore.binance.description" = "Hand over those API Keys and Secret! Only the best for our exchange!"; -"restore.binance.api_key" = "The Unbeatable API Key!"; +"restore.binance.api_key" = "API Key"; "restore.binance.secret_key" = "Secret Key"; "restore.binance.connect" = "Connect"; "restore.binance.connecting" = "Connecting..."; "restore.binance.get_api_keys" = "Get API Keys"; -"restore.binance.failed_to_connect" = "Couldn't connect your API Key. Sad! But we'll try again!"; -"restore.binance.invalid_qr_code" = "That QR Code? Not the best. Try another!"; +"restore.binance.failed_to_connect" = "Failed to connect your API key"; +"restore.binance.invalid_qr_code" = "Invalid QR Code"; // Coin Settings @@ -175,7 +175,7 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Copy Warning -"copy_warning.title" = "Woah! Risky copying ahead."; +"copy_warning.title" = "Risk of Copying"; "copy_warning.description" = "For safety, folks, I'd say don't copy this! I have the best safety tips."; "copy_warning.dont_copy" = "Don't Copy"; "copy_warning.i_will_risk_it" = "I'm feeling brave today!"; @@ -192,7 +192,7 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // EVM Private Key "evm_private_key.title" = "EVM Private Key"; -"evm_private_key.tap_to_show" = "Give it a little tap! The big reveal."; +"evm_private_key.tap_to_show" = "Tap to show private key"; // Extended Key @@ -202,61 +202,61 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "extended_key.purpose" = "Purpose"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; - "extended_key.tap_to_show" = "Drumroll, please... Tap to unveil!"; + "extended_key.tap_to_show" = "Tap to show extended private key"; // Backup -"backup.title" = "Yuuuge Recovery Phrase"; +"backup.title" = "Recovery Phrase"; "backup.description" = "Jot these words down, in the right order, just like my best speeches. Keep them safe, maybe next to your tax returns!"; "backup.tap_to_show" = "Give it a tap to reveal the best recovery phrase!"; "backup.passphrase" = "Passphrase"; -"backup.verify" = "Double-check"; -"backup.verified" = "Boom! Verified, and you know I love verifying things."; +"backup.verify" = "Verify"; +"backup.verified" = "Verified"; // Backup Verify Words -"backup_verify_words.title" = "Double-check Time"; +"backup_verify_words.title" = "Verify"; "backup_verify_words.description" = "Pick the two words I'm asking for, from your top-notch recovery phrase"; -"backup_verify_words.incorrect_word" = "Oops! Not the word we wanted."; +"backup_verify_words.incorrect_word" = "Incorrect Word"; // Backup Verify Passphrase -"backup_verify_passphrase.title" = "Verification Station"; +"backup_verify_passphrase.title" = "Verify"; "backup_verify_passphrase.description" = "Type in the passphrase. Make sure it's terrific!"; -"backup_verify_passphrase.incorrect_passphrase" = "That's not the right passphrase, but we'll get there!"; +"backup_verify_passphrase.incorrect_passphrase" = "Incorrect passphrase"; // Backup Required -"backup_required.title" = "Backup is a Must, Folks"; +"backup_required.title" = "Backup Required"; // **Backup Prompt** -"backup_prompt.title" = "Backup Bonanza"; +"backup_prompt.title" = "Manual Backup"; "backup_prompt.warning" = "You want to make a yuuuge backup of this recovery phrase, trust me. Don't lose out if your phone goes missing, or breaks!"; -"backup_prompt.backup" = "Backup Time!"; -"backup_prompt.backup_manual" = "Do It the Old Fashioned Way"; -"backup_prompt.backup_cloud" = "Send It Up to iCloud!"; -"backup_prompt.later" = "Not now, but don't wait too long!"; +"backup_prompt.backup" = "Backup"; +"backup_prompt.backup_manual" = "Manual Backup"; +"backup_prompt.backup_cloud" = "Backup to iCloud!"; +"backup_prompt.later" = "Later"; // **Backup to iCloud** -"backup.cloud.title" = "Sky High iCloud Backup"; +"backup.cloud.title" = "Backup to iCloud"; "backup.cloud.description" = "So, iCloud's an Apple thing. They'll keep your stuff, not on your gadgets. You're trusting them, not us. Just so we're clear!"; "backup.cloud.terms.item.1" = "Listen up! Lose access to iCloud, and you say goodbye to this backup."; -"backup.cloud.name.title" = "What's in a Name?"; +"backup.cloud.name.title" = "Backup Name"; "backup.cloud.name.description" = "Name your backup, maybe after one of my hotels?"; "backup.cloud.name.empty" = "Give it a name, c'mon!"; "backup.cloud.name.error.empty" = "A name's what we need!"; "backup.cloud.name.error.already_exist" = "Been there, named that. Pick another!"; -"backup.cloud.name.placeholder" = "Fabulous Name"; +"backup.cloud.name.placeholder" = "Name"; -"backup.cloud.password.title" = "The Best Password"; +"backup.cloud.password.title" = "Set Password"; "backup.cloud.password.description" = "Craft a password that's tremendous! Needs 8 characters and a mix of the alphabet, numbers, and those little symbols."; "backup.cloud.password.highlighted_description" = "Remember this one! It's not your Apple password, and it's irreplaceable!"; -"backup.cloud.password.placeholder" = "Amazing Password"; -"backup.cloud.password.confirm.placeholder" = "Confirm the Brilliance"; -"backup.cloud.password.save" = "Seal the Deal & Backup"; +"backup.cloud.password.placeholder" = "Password"; +"backup.cloud.password.confirm.placeholder" = "Confirm"; +"backup.cloud.password.save" = "Save and Backup"; "backup.cloud.password.error.empty_passphrase" = "Hey, we need that passphrase!"; "backup.cloud.password.error.forbidden_symbols" = "Stick to the basics, folks: A-Z, a-z, 0-9, and those symbols!"; @@ -267,92 +267,92 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "backup.cloud.not_available" = "iCloud's taking a break!"; "backup.cloud.cant_create_file" = "Can't ship that file to iCloud!"; "backup.cloud.cant_delete_file" = "Can't kick it out of iCloud!"; -"backup.cloud.no_access.title" = "Knock on iCloud's Door"; +"backup.cloud.no_access.title" = "Access iCloud"; "backup.cloud.no_access.description" = "Need your green light for iCloud storage!"; -// **Errors** +// Errors "error.send.self_transfer" = "C'mon now, sending to yourself? We don't do that here!"; "error.send_binance.memo_required" = "The receiver's like 'I need a memo!' Don't leave it empty!"; "error.send_binance.only_digits_allowed" = "Memo's gotta have digits only! Numbers, always winning!"; "error.send_z_cash.transparent_address" = "%@ says 'No way!' to payments to a transparent address."; -// **Balance** - -"balance.title" = "Big League Balance"; -"balance.tab_bar_item" = "Yuuuge Balance"; -"balance.send" = "Ship It"; -"balance.withdraw" = "Pull Out"; -"balance.swap" = "Switcheroo"; -"balance.receive" = "Gimme"; -"balance.deposit" = "Throw In"; -"balance.address" = "Prime Location"; -"balance.rate_per_coin" = "%@ for each %@"; -"balance.syncing" = "Getting in sync..."; -"balance.searching" = "On the hunt for transactions..."; -"balance.downloading_sapling" = "Grabbing Sapling... %d%%"; -"balance.downloading_blocks" = "Fetching those Blocks"; -"balance.scanning_blocks" = "Eyeing those Blocks"; -"balance.enhancing_transactions" = "Sprucing up Transactions"; - -"balance.searching.count" = "%@ big deals"; +// Balance + +"balance.title" = "Balance"; +"balance.tab_bar_item" = "Balance"; +"balance.send" = "Send"; +"balance.withdraw" = "Withdraw"; +"balance.swap" = "Swap"; +"balance.receive" = "Receive"; +"balance.deposit" = "Deposit"; +"balance.address" = "Address"; +"balance.rate_per_coin" = "%@ per %@"; +"balance.syncing" = "Syncing..."; +"balance.searching" = "Searching transactions..."; +"balance.downloading_sapling" = "Downloading Sapling... %d%%"; +"balance.downloading_blocks" = "Downloading Blocks"; +"balance.scanning_blocks" = "Scanning Blocks"; +"balance.enhancing_transactions" = "Enhancing Transactions"; + +"balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Syncing... %@"; -"balance.synced_through" = "all the way to %@"; -"balance.add_coin" = "Add some Bling"; -"balance.invalid_api_key" = "API Key's a no-go!"; -"balance.empty.add_coins" = "More Bling!"; +"balance.synced_through" = "until %@"; +"balance.add_coin" = "Add Coin"; +"balance.invalid_api_key" = "Invalid API Key"; +"balance.empty.add_coins" = "Add Coins!"; "balance.empty.description" = "No coins in this wallet yet. Sad!"; "balance.watch_empty.description" = "This wallet address is feeling a bit light!"; -"balance.sort_by" = "Rank 'em"; -"balance.sort.header" = "Rank them like"; -"balance.sort.valueHighToLow" = "By the Numbers"; -"balance.sort.az" = "By Name"; -"balance.sort.price_change" = "Who's Moving Up"; - -"balance_error.change_source" = "Switch it Up!"; -"balance_error.sync_error" = "Whoops, sync hiccup!"; -"lost_accounts.warning_title" = "Oh No, iOS Keychain Error!"; +"balance.sort_by" = "Sort By"; +"balance.sort.header" = "Sort by"; +"balance.sort.valueHighToLow" = "Balance"; +"balance.sort.az" = "Name"; +"balance.sort.price_change" = "Price Change"; + +"balance_error.change_source" = "Change Source"; +"balance_error.sync_error" = "Sync Error"; +"lost_accounts.warning_title" = "iOS Keychain Error!"; "lost_accounts.warning_message" = "The encrypted treasure holding your wallet got messed up 'cause you changed your iOS lock screen."; // Token Balance Page "balance.token.locked" = "On Lockdown"; -"balance.token.locked.info.title" = "Hold On!"; +"balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "The sender's put a timer on this. The Bitcoins are yours, but you gotta wait a tad to spend them on the Bitcoin network."; -"balance.token.staked" = "All In"; -"balance.token.staked.info.title" = "In The Game"; +"balance.token.staked" = "Staked"; +"balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Let's talk staking!"; -"balance.token.frozen" = "Chilled"; +"balance.token.frozen" = "Frozen"; "balance.token.frozen.info.title" = "Cool Off"; "balance.token.frozen.info.description" = "Let's chat about why it's so cool."; // **Account switcher** -"switch_account.title" = "Swap Wallets"; -"switch_account.wallets" = "Fat Wallets"; -"switch_account.watch_wallets" = "Wallet Watchlist"; +"switch_account.title" = "Switch Wallets"; +"switch_account.wallets" = "Wallets"; +"switch_account.watch_wallets" = "Watch Wallets"; // **Release notes** -"release_notes.title" = "The Latest & Greatest"; -"release_notes.follow_us" = "Join the Trump Unstoppable Train"; +"release_notes.title" = "What's New"; +"release_notes.follow_us" = "Follow Us"; // Deposit -"deposit.receive_coin" = "Look, folks, get ready to grab your %@!"; -"deposit.address" = "Big beautiful Address!"; -"deposit.your_address" = "Your very own Address!"; +"deposit.receive_coin" = "Receive %@!"; +"deposit.address" = "Address!"; +"deposit.your_address" = "Your Address!"; "receive_alert.not_backed_up_description" = "Hold on! You've got to back up %@ before we're winning with %@."; "receive_alert.any_coins.not_backed_up_description" = "You've gotta backup %@ before making any yuge coin deals!"; -"deposit.no_adapter.error" = "Whoops! Can't get that address right now."; +"deposit.no_adapter.error" = "Can't provide address"; -"deposit.address_format" = "Flashy Format"; -"deposit.address_network" = "The Best Network"; +"deposit.address_format" = "Format"; +"deposit.address_network" = "Network"; "deposit.qr_code_description" = "Your VIP ticket to deposit %@"; "deposit.qr_code_description.watch" = "Keep an eye on this %@ address!"; -"deposit.account" = "Your Gold Account"; +"deposit.account" = "Account"; -"deposit.not_active" = "Taking a nap"; -"deposit.not_active.title" = "Sleepy Address"; +"deposit.not_active" = "Not active"; +"deposit.not_active.title" = "Not Active Address"; "deposit.not_active.tron_description" = "Just so you know, new accounts on TRON aren't awake yet. To get them going, send over some TRX or TRC-10 tokens. It'll cost you just 1 TRX."; "deposit.zcash.restore.description" = "Ever had a little ZEC action before?"; @@ -361,43 +361,43 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "deposit.warning" = "Only send %@ here, or you'll be losing bigly!"; -"receive_network_select.title" = "Pick your Network"; +"receive_network_select.title" = "Network"; "receive_network_select.description" = "Choose the best network and grab an address!"; -"receive_address_format_select.title" = "Prestige Address Format"; +"receive_address_format_select.title" = "Address Format"; "receive_address_format_select.description" = "Pick a network, get the finest address!"; "receive_address_format_select.bitcoin.bottom_description" = "For the best deals in Bitcoin, Native SegWit is the way. Any address will do – Taproot, SegWit, Legacy. It's all great!"; "receive_address_format_select.bitcoin_cash.bottom_description" = "Want the smoothest experience for Bitcoin Cash? Go with the Cash Address."; -"blockchain_type.recommended" = " (Total winner)"; +"blockchain_type.recommended" = " (recommended)"; // Send -"send.title" = "Dispatch some %@!"; -"send.send" = "Ship it!"; +"send.title" = "Send %@!"; +"send.send" = "Send"; "send.no_assets" = "Looks like your vault's empty, sad!"; -"send.amount_placeholder" = "How many chips?"; -"send.address_placeholder" = "Where to?"; -"send.address_or_domain_placeholder" = "Address or fancy Domain"; -"send.fee" = "Little Charge"; -"send.network_fee" = "Network’s Tiny Fee"; -"send.estimated_fee" = "Ballpark Fee"; -"send.max_fee" = "Maximum Fee"; -"send.duration.hours" = "%d tremendous hours"; -"send.duration.minutes" = "%d quick mins"; -"send.available_balance" = "What's in the kitty?"; -"send.max_button" = "Go Yuge!"; -"send.next_button" = "Onward!"; -"send.error.invalid" = "No good!"; -"send.error.address" = "Wonky Address!"; -"send.hodler_locktime" = "TimeVault"; -"send.hodler_locktime_hour" = "Just an hour"; -"send.hodler_locktime_month" = "1 big month"; -"send.hodler_locktime_half_year" = "Solid 6 months"; -"send.hodler_locktime_year" = "1 winning year"; -"send.hodler_locktime_off" = "Shut it!"; +"send.amount_placeholder" = "Amount"; +"send.address_placeholder" = "Address"; +"send.address_or_domain_placeholder" = "Address or Domain"; +"send.fee" = "Fee"; +"send.network_fee" = "Network Fee"; +"send.estimated_fee" = "Estimated Fee"; +"send.max_fee" = "Max Fee"; +"send.duration.hours" = "%d h"; +"send.duration.minutes" = "%d min."; +"send.available_balance" = "Available Balance"; +"send.max_button" = "Max"; +"send.next_button" = "Next"; +"send.error.invalid" = "Invalid"; +"send.error.address" = "Address!"; +"send.hodler_locktime" = "TimeLock"; +"send.hodler_locktime_hour" = "1 hour"; +"send.hodler_locktime_month" = "1 month"; +"send.hodler_locktime_half_year" = "6 months"; +"send.hodler_locktime_year" = "1 year"; +"send.hodler_locktime_off" = "Off"; "send.hodler_error.unsupported_address" = "Time vaulting? Only for top-tier addresses starting with 1...!"; -"send.fee_info.title" = "The Fee Game"; +"send.fee_info.title" = "Fee Rate"; "send.fee_info.description" = "Everybody pays a bit to keep things moving on the blockchain. Busy days mean higher fees. But hey, we'll suggest the best deal for you!"; "send.transaction_inputs_outputs_info.title" = "In's and Out's of Transactions"; "send.transaction_inputs_outputs_info.description" = "Most transactions have two parts: what you send and what you get back. We're smart, making it tricky for anyone trying to snoop."; @@ -406,97 +406,98 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Classic Play"; "send.transaction_inputs_outputs_info.deterministic.description" = "We've got standards, like the BIP69. It's a new thing, so not everyone's on board yet."; -"send.confirmation.you_send" = "You're parting with"; -"send.confirmation.to" = "To the lucky one"; -"send.confirmation.contact_name" = "Who's getting rich?"; -"send.confirmation.domain" = "Fancy Domain"; -"send.confirmation.address" = "Top-tier Address"; -"send.confirmation.account" = "Lucky Account"; -"send.confirmation.memo" = "Your two cents"; -"send.confirmation.memo_placeholder" = "Add a note"; -"send.confirmation.total" = "The whole enchilada"; -"send.confirmation.fee" = "A small fee"; -"send.confirmation.time_lock" = "Time Vault"; -"send.confirmation.slide_to_send" = "Swipe to make it rain!"; -"send.confirmation.sending" = "Making it rain!"; +"send.confirmation.you_send" = "You Send"; +"send.confirmation.to" = "To"; +"send.confirmation.contact_name" = "Contact Name"; +"send.confirmation.domain" = "Domain"; +"send.confirmation.address" = "Address"; +"send.confirmation.account" = "Account"; +"send.confirmation.memo" = "Memo"; +"send.confirmation.memo_placeholder" = "Memo"; +"send.confirmation.total" = "Total"; +"send.confirmation.fee" = "Fee"; +"send.confirmation.time_lock" = "TimeLock"; +"send.confirmation.slide_to_send" = "Slide to Send"; +"send.confirmation.sending" = "Sending"; "send.confirmation.resend_description" = "Let's try ousting that old transaction with a bigger, better fee. Only the best one stays!"; -"send.confirmation.resend" = "Do-over!"; +"send.confirmation.resend" = "Resend"; "send.confirmation.cancel_description" = "We'll try sneaking a new zero-amount transaction past the old one. Only the top dog will make it!"; -"send.confirmation.cancel" = "Bail on it!"; -"send.confirmation.nonce" = "Secret Sauce"; -"send.confirmation.method" = "The Plan"; -"send.amount_error.balance" = "You're short, buddy!"; +"send.confirmation.cancel" = "Cancel Transaction"; +"send.confirmation.nonce" = "Nonce"; +"send.confirmation.method" = "Method"; +"send.amount_error.balance" = "Not Enough Balance"; "send.address_error.own_address" = "Why send TRX to yourself? That's just showing off!"; -"send.amount_error.maximum_amount" = "That's a bit much, even for %@!"; -"send.amount_error.minimum_amount" = "Come on, %@ deserves more!"; +"send.amount_error.maximum_amount" = "Maximum amount %@"; +"send.amount_error.minimum_amount" = "Minimum amount %@"; "send.amount_error.min_required_balance" = "Gotta leave some %@ behind for the party!"; "send.amount_warning.coin_needed_for_fee" = "Keep some %@ around, it's always good for the bill!"; "send.token.insufficient_fee_alert" = "Heads up! You'll need %@ to cover the %@ transfer on %@."; -"send.fee_settings.amount_error.balance.title" = "Uh-oh, low balance!"; +"send.fee_settings.amount_error.balance.title" = "Insufficient Balance"; "send.fee_settings.amount_error.balance" = "Your %@ is a bit low for this grand move."; + "send.fee_settings.stuck_warning.title" = "Could get sticky!"; "send.fee_settings.stuck_warning" = "Might get tangled up or just flop."; -"send.fee_settings.fee_error.title" = "Ошибка комиссии"; -"send.fee_settings.too_low" = "Ставка комиссии слишком низкая."; -"send.fee_settings.fee_rate_unavailable" = "Ставка комиссии недоступна. Пожалуйста, проверьте ставки комиссии вручную"; +"send.fee_settings.fee_error.title" = "Fee error"; +"send.fee_settings.too_low" = "Fee Rate is too low"; +"send.fee_settings.fee_rate_unavailable" = "Listen folks, our fee rate's taking a little vacation. Tremendous idea to check them manually. Believe me, I've done it!"; -"send.stuck_warning" = "Внимание! Транзакция может застрять в сети"; +"send.stuck_warning" = "Warning! Risk of getting stuck"; -"send.lock_time" = "Time's Locked! Trust me, nobody locks time better than this app."; +"send.lock_time" = "TimeLock"; -"approve.confirmation.you_approve" = "You just approved, bigly. Everyone's talking about it!"; -"approve.confirmation.you_revoke" = "You revoked it! I've always said, you're the best decision-maker!"; -"approve.confirmation.spender" = "This spender, very talented person, believe me!"; +"approve.confirmation.you_approve" = "You Approve"; +"approve.confirmation.you_revoke" = "You Revoke"; +"approve.confirmation.spender" = "Spender"; // Donate -"donate.list.title" = "Wanna give a little?"; -"donate.list.get_address" = "Gimme the Address!"; -"donate.list.get_address.title" = "Where's the money going?"; -"donate.title" = "Let's Make Donations Great Again, Donate %@!"; +"donate.list.title" = "Donate with"; +"donate.list.get_address" = "Get Address"; +"donate.list.get_address.title" = "Address"; +"donate.title" = "Donate %@!"; "donate.no_assets" = "Looks like you're a bit short, buddy."; "donate.support.description" = "Let's work together to make this app HUGE! The best app ever."; // CoinSelector -"choose_coin.title" = "Pick your winning coin!"; +"choose_coin.title" = "Choose Coins"; // Swap -"swap.title" = "Let's Swap!"; +"swap.title" = "Swap!"; "swap.no_assets" = "You're empty! Fill it up."; -"swap.you_pay" = "What you're giving"; -"swap.estimated" = "around-ish"; -"swap.balance" = "Your stash"; -"swap.allowance" = "Permission slip"; -"swap.you_get" = "The good stuff you get"; -"swap.token" = "Choose"; -"swap.advanced_settings" = "Big brain settings"; -"swap.proceed_button" = "Onward!"; -"swap.approve.title" = "Give the thumbs up!"; +"swap.you_pay" = "You Pay"; +"swap.estimated" = "estimated"; +"swap.balance" = "Balance"; +"swap.allowance" = "Allowance"; +"swap.you_get" = "You Get"; +"swap.token" = "Select"; +"swap.advanced_settings" = "Swap Settings"; +"swap.proceed_button" = "Next"; +"swap.approve.title" = "Swap Approve"; "swap.approve.description" = "Time to grant permission! But don't worry, it doesn't touch your stash. Just a little fee, and you're golden!"; "swap.approve.amount_error.already_approved" = "Been there, done that! You're set."; -"swap.approving_button" = "Doing the thing..."; +"swap.approving_button" = "Approving..."; "swap.revoke_warning" = "Trade or revoke and try again!"; -"swap.revoking_button" = "Taking it back..."; -"swap.not_available_button" = "No cash here!"; +"swap.revoking_button" = "Revoking..."; +"swap.not_available_button" = "Balance N/A"; "swap.trade_error.not_found" = "This swap's a no-go!"; "swap.trade_error.wrap_unwrap_not_allowed" = "No wrapping or unwrapping here! 1Inch is the way to go!"; -"swap.button_error.insufficient_balance" = "Need more green!"; -"swap.switch_provider.title" = "Who's the dealer?"; -"swap.amount_type.coin" = "Shiny Coin"; - -"swap.price" = "The tag"; -"swap.buy_price" = "What it'll cost ya"; -"swap.sell_price" = "Cashing in!"; -"swap.price_impact" = "Price rollercoaster"; -"swap.maximum_paid" = "Top Dollar"; -"swap.minimum_got" = "The least you'll get"; -"swap.estimate_short" = "(ballpark)"; -"swap.minimum_short" = "(low end)"; -"swap.maximum_short" = "(high end)"; +"swap.button_error.insufficient_balance" = "Insufficient Balance"; +"swap.switch_provider.title" = "Swap Service"; +"swap.amount_type.coin" = "Coin"; + +"swap.price" = "Price"; +"swap.buy_price" = "Buy Price"; +"swap.sell_price" = "Sell Price!"; +"swap.price_impact" = "Price Impact"; +"swap.maximum_paid" = "Maximum Amount"; +"swap.minimum_got" = "Guaranteed Amount"; +"swap.estimate_short" = "(est)"; +"swap.minimum_short" = "(min)"; +"swap.maximum_short" = "(max)"; // Swap Advanced Settings @@ -505,7 +506,7 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "swap.advanced_settings.deadline" = "Transaction Deadline"; "swap.advanced_settings.deadline.footer" = "No dilly-dallying! Time's ticking."; "swap.advanced_settings.recipient.footer" = "Where's the money going after the swap?"; -"swap.advanced_settings.deadline_minute" = "Hurry up, %@ min!"; +"swap.advanced_settings.deadline_minute" = "%@ min"; "swap.advanced_settings.recipient_address" = "Recipient Address"; "swap.advanced_settings.warning.unusual_slippage" = "Watch out! Others might jump the line!"; "swap.advanced_settings.service_fee_description" = "A small thank you fee for our service. Usually just a tiny 0.3% or 0.6%!"; @@ -515,9 +516,9 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "swap.advanced_settings.error.invalid_slippage" = "Invalid Slippage"; "swap.advanced_settings.error.invalid_deadline" = "Invalid Deadline"; -"swap.one_inch.error.cannot_estimate" = "Math's a bit tricky."; +"swap.one_inch.error.cannot_estimate" = "Estimation Error"; "swap.one_inch.error.cannot_estimate.info" = "Check your stash. Maybe up the wiggle room and try again. Give it another go in 3..."; -"swap.one_inch.error.insufficient_liquidity" = "Pool's a bit shallow!"; +"swap.one_inch.error.insufficient_liquidity" = "Insufficient Liquidity"; "swap.one_inch.error.insufficient_liquidity.info" = "Not enough in the pot. Lower the ante."; "swap.service" = "Service"; @@ -525,42 +526,42 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Swap Approving -"swap.approve.subtitle" = "Swap it out!"; +"swap.approve.subtitle" = "Swap"; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Slide to the deal!"; -"swap.confirmation.swapping" = "Making magic!"; +"swap.confirmation.slide_to_swap" = "Slide to Swap"; +"swap.confirmation.swapping" = "Swapping"; "swap.confirmation.impact_too_high" = "Hold up! %@ says this deal's a dud. Check out %@ instead!"; "swap.confirmation.impact_warning" = "Watch out! It's a wild ride!"; -"swap.confirmation.minimum_received" = "You'll at least get"; -"swap.confirmation.maximum_sent" = "At most you'll send"; +"swap.confirmation.minimum_received" = "Minimum Received"; +"swap.confirmation.maximum_sent" = "Maximum Sent"; "swap.dex_info.description" = "This swanky service is by %@, the big shots in decentralized trading on the %@ chain. 100% automated, 100% reliable!"; -"swap.dex_info.header_dex_related" = "All about %@"; -"swap.dex_info.header_allowance" = "Spending Limit"; +"swap.dex_info.header_dex_related" = "%@Related"; +"swap.dex_info.header_allowance" = "Allowance"; "swap.dex_info.content_allowance" = "How much the exchange can use on your behalf. Gotta get this before the main event."; -"swap.dex_info.header_price_impact" = "Price Wave"; +"swap.dex_info.header_price_impact" = "Price Impact"; "swap.dex_info.content_price_impact" = "How wild the price might get. Bigger swaps, bigger waves."; -"swap.dex_info.header_swap_fee" = "Thank you fee"; +"swap.dex_info.header_swap_fee" = "Swap Fee"; "swap.dex_info.content_swap_fee" = "Our little thank you note. Usually 0.3% or 0.6%."; -"swap.dex_info.header_guaranteed_amount" = "Sure thing amount"; +"swap.dex_info.header_guaranteed_amount" = " Guaranteed Amount"; "swap.dex_info.content_guaranteed_amount" = "The least you're gonna get after swapping."; -"swap.dex_info.header_maximum_spend" = "Max Spend"; +"swap.dex_info.header_maximum_spend" = "Maximum Spend"; "swap.dex_info.content_maximum_spend" = "The most you're gonna use in the swap."; -"swap.dex_info.header_other" = "Other stuff"; -"swap.dex_info.header_transaction_fee" = "The cost of doing business"; +"swap.dex_info.header_other" = "Other"; +"swap.dex_info.header_transaction_fee" = "Transaction Fee"; "swap.dex_info.content_transaction_fee" = "What you're paying to use the %@ chain. %@ stuff might be pricier."; -"swap.dex_info.header_transaction_speed" = "How fast it goes"; +"swap.dex_info.header_transaction_speed" = "Transaction Speed"; "swap.dex_info.content_transaction_speed" = "Pay more, go faster. It's that simple."; -"swap.dex_info.link_button" = "Check out %@"; +"swap.dex_info.link_button" = "%@ Site"; // Market -"market.tab_bar_item" = "YUGE Markets!"; +"market.tab_bar_item" = "Markets!"; "market.title" = "Best Markets Ever!"; "market.category.overview" = "The Best View"; "market.category.posts" = "Real News, Not Fake!"; @@ -572,40 +573,40 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "market.project_has_no_coin" = "No coin? Sad!"; -"market.top.section.header.see_all" = "See Everything!"; -"market.top.section.header.top_gainers" = "Total Winners"; -"market.top.section.header.top_losers" = "Not Winning… Yet!"; -"market.top.section.header.top_sectors" = "Top of the Tops"; -"market.top.section.header.news" = "The News You Need"; -"market.top.volume.title" = "Volume? Huge!"; -"market.top.market_cap.title" = "Massive MCap"; -"market.top.diluted_market_cap.title" = "Even Bigger MCap"; +"market.top.section.header.see_all" = "See All"; +"market.top.section.header.top_gainers" = "Top Gainers"; +"market.top.section.header.top_losers" = "Top Losers"; +"market.top.section.header.top_sectors" = "Top Sectors"; +"market.top.section.header.news" = "News"; +"market.top.volume.title" = "Vol"; +"market.top.market_cap.title" = "MCap"; +"market.top.diluted_market_cap.title" = "Dilluted MCap"; -"market.market_field.mcap" = "Huge Cap"; -"market.market_field.vol" = "Big Volumes"; +"market.market_field.mcap" = "MCap"; +"market.market_field.vol" = "Vol"; -"market.tvl.market_field.value" = "Big Bucks"; -"market.tvl.market_field.diff" = "Up or Down?"; +"market.tvl.market_field.value" = "USD"; +"market.tvl.market_field.diff" = "Percent"; -"market.tvl.platform_field.all" = "All of 'em"; +"market.tvl.platform_field.all" = "All"; -"market.sort_by" = "Pick Your Winners"; +"market.sort_by" = "Sort By"; -"market.top.title" = "Best Coins Ever"; +"market.top.title" = "Top Coins"; "market.top.description" = "Top Coins, because we only deal with the best!"; -"market.top.highest_cap" = "The Richest Cap"; -"market.top.lowest_cap" = "Room for Growth Cap"; -"market.top.highest_volume" = "Lots of Noise"; -"market.top.lowest_volume" = "Quiet Winners"; -"market.top.top_gainers" = "On Fire Gainers"; -"market.top.top_losers" = "They'll Bounce Back"; -"market.top.top_collections" = "Top Art Stash"; -"market.top.floor_price" = "Bottom Price? Maybe!"; -"market.top.top_platforms" = "Best Stages"; -"market.top.protocols" = "The Rules"; - -"top_platforms.title" = "Platform Kings"; +"market.top.highest_cap" = "Highest Cap"; +"market.top.lowest_cap" = "Lowest Cap"; +"market.top.highest_volume" = "Highest Volume"; +"market.top.lowest_volume" = "Lowest Volume"; +"market.top.top_gainers" = "Top Gainers"; +"market.top.top_losers" = "Top Losers"; +"market.top.top_collections" = "Top NFT Collections"; +"market.top.floor_price" = "Floor"; +"market.top.top_platforms" = "Top Platforms"; +"market.top.protocols" = "Protocols"; + +"top_platforms.title" = "Platform Ranks"; "top_platforms.description" = "The best places to build greatness."; "top_platform.title" = "%@ Ecosystem"; @@ -622,28 +623,28 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "market.search.title" = "Search"; "market.search.empty_text" = "No results found"; -"market.advanced_search.title" = "Nitty Gritty Filters"; -"market.advanced_search.show_results" = "Show Me The Gold"; -"market.advanced_search.empty_results" = "Where'd they go?"; +"market.advanced_search.title" = "Filters"; +"market.advanced_search.show_results" = "Show Results"; +"market.advanced_search.empty_results" = "Empty Results"; "market.advanced_search.dex_description" = "This is for the Ethereum (Uniswap) and Binance (Pancake) hotshots."; -"market.advanced_search.24h" = "A Day in the Life"; - -"market.advanced_search.market_parameters" = "Market Magic"; -"market.advanced_search.network_parameters" = "Network Know-How"; -"market.advanced_search.price_parameters" = "Price Party!"; -"market.advanced_search.choose_set" = "Pick and Choose"; -"market.advanced_search.market_cap" = "How Big's the Hat?"; -"market.advanced_search.volume" = "Making Waves"; -"market.advanced_search.liquidity" = "Liquid Gold"; -"market.advanced_search.blockchains" = "The Chains That Bind"; -"market.advanced_search.price_period" = "Pricey Times"; -"market.advanced_search.price_change" = "Up or Down?"; - -"market.advanced_search.outperformed_btc" = "Beat the Bitcoin!"; -"market.advanced_search.outperformed_eth" = "Crushed Ethereum!"; -"market.advanced_search.outperformed_bnb" = "Bounced Binance!"; -"market.advanced_search.price_close_to_ath" = "Almost At The Top!"; -"market.advanced_search.price_close_to_atl" = "Low, But Not For Long!"; +"market.advanced_search.24h" = "24h"; + +"market.advanced_search.market_parameters" = "Market Parameters"; +"market.advanced_search.network_parameters" = "Network Parameters"; +"market.advanced_search.price_parameters" = "Price Parameters!"; +"market.advanced_search.choose_set" = "Choose Set"; +"market.advanced_search.market_cap" = "Market Cap"; +"market.advanced_search.volume" = "Trading Volume"; +"market.advanced_search.liquidity" = "DEX Liquidity"; +"market.advanced_search.blockchains" = "Blockchains"; +"market.advanced_search.price_period" = "Price Period"; +"market.advanced_search.price_change" = "Price Change"; + +"market.advanced_search.outperformed_btc" = "Outperformed BTC"; +"market.advanced_search.outperformed_eth" = "Outperformed ETH!"; +"market.advanced_search.outperformed_bnb" = "Outperformed BNB!"; +"market.advanced_search.price_close_to_ath" = "Price Close To ATH"; +"market.advanced_search.price_close_to_atl" = "Price Close To ATL"; "market.advanced_search.top" = "Top %d"; "market.advanced_search.reset_all" = "Reset"; @@ -680,19 +681,20 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "market.advanced_search.year" = "1 Year"; "market.advanced_search_results.title" = "Results"; -"market.global.total_market_cap.title" = "All the Money"; + +"market.global.total_market_cap.title" = "Total Market Cap"; "market.global.total_market_cap.description" = "Every penny, because we're that good."; -"market.global.volume_24h.title" = "A Day's Worth"; +"market.global.volume_24h.title" = "24h Volume"; "market.global.volume_24h.description" = "All the moves in 24 hours"; -"market.global.defi_cap.title" = "DeFi Dynasty"; +"market.global.defi_cap.title" = "DeFi Cap"; "market.global.defi_cap.description" = "Where DeFi makes its mark."; -"market.global.tvl_in_defi.title" = "Locked Up in DeFi"; +"market.global.tvl_in_defi.title" = "TVL in DeFi"; "market.global.tvl_in_defi.description" = "Where the real money sleeps."; -"market.global.tvl_in_defi.multi_chain" = "All the Chains, One Place"; -"market.global.tvl_in_defi.filter_by_chain" = "Pick Your Chain"; +"market.global.tvl_in_defi.multi_chain" = "Multi-Chain"; +"market.global.tvl_in_defi.filter_by_chain" = "Filtered by Chain"; // Coin Page @@ -761,7 +763,7 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "coin_analytics.technical_indicators.info3" = "Oscillators: They fluctuate, and they're tremendous. Helps you spot the best deals in the market. The very best!\n\nRelative Strength Index (RSI): Measures everything super fast! Used by the best to spot those overbought and oversold markets.\n\nMoving Average Convergence Divergence (MACD): This is the future! Points out the best times to buy or sell. Super smart, trust me."; "coin_analytics.cex_volume" = "CEX Volume"; -"coin_analytics.cex_volume_rank" = "CEX Volume Rank - we're always number one!"; +"coin_analytics.cex_volume_rank" = "CEX Volume Rank"; "coin_analytics.cex_volume_rank.description" = "We rank tokens better than anyone else, especially based on trading volume. Everyone wants to be on top!"; "coin_analytics.cex_volume.info1" = "Just look at our trading volume on the top centralized exchanges. Leading the game for 30 days straight!"; "coin_analytics.cex_volume.info2" = "This chart? It's a masterpiece. Shows the trading volume changes like you've never seen before. Top-notch stuff!"; @@ -769,6 +771,7 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "coin_analytics.cex_volume.info4" = "List of tokens? Only the winners, ranked by their top trading volumes. We're always on top, 24/7, every month."; "coin_analytics.dex_volume" = "DEX Volume"; +"coin_analytics.dex_volume_rank" = "DEX Volume Rank"; "coin_analytics.dex_volume_rank.description" = "Tokens ranked? We've got the best rankings, especially for decentralized exchanges. Nobody does it better!"; "coin_analytics.dex_volume.info1" = "The numbers are huge! Unbelievable trading volume for the token on the greatest decentralized exchanges. We're leading for 30 days, and everyone's talking about it!"; "coin_analytics.dex_volume.info2" = "Ever seen a chart like this? Didn't think so. It shows the incredible variation in daily trading volume. The best chart for the best decentralized exchanges over a whole year!"; @@ -804,7 +807,6 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "coin_analytics.transaction_count" = "Transaction Count"; "coin_analytics.transaction_count_rank" = "Tx Count Rank"; - "coin_analytics.transaction_count_rank.description" = "Tokens ranked by transactions? We're the gold standard!"; "coin_analytics.transaction_count.info1" = "Count 'em! Massive number of transactions with our token in 30 days!"; "coin_analytics.transaction_count.info5" = "Look at this! A sheer number of tokens moved across the blockchain in just 30 days. Big league!"; @@ -816,7 +818,6 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "coin_analytics.holders" = "Holders"; "coin_analytics.holders_rank" = "Holders Rank"; - "coin_analytics.holders_rank.description" = "Rankings by holders? Our token is held by the best people on multiple blockchains!"; "coin_analytics.holders.info1" = "The numbers? Only growing! So many unique addresses holding our amazing token across various blockchains."; "coin_analytics.holders.info2" = "Top 10 wallets? Everyone's asking! Holding our token on each and every blockchain!"; @@ -837,9 +838,11 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "coin_analytics.project_fee" = "Project Fee"; "coin_analytics.project_fee_rank" = "Project Fee Rank"; "coin_analytics.project_fee_rank.description" = "We're ranking tokens on who's really making money here, folks. Look at these fees! And every project has its own way, its own secret sauce. Some tokens are just better at it than others, believe me!"; + "coin_analytics.project_revenue" = "Project Revenue"; "coin_analytics.project_revenue_rank" = "Project Revenue Rank"; "coin_analytics.project_revenue_rank.description" = "Let's rank tokens on who's actually putting money in the pockets of their people! Some of these tokens have brilliant strategies, like staking or even burning their tokens to create value. Not all tokens do it, but the smart ones do, and they're winning bigly for their holders!"; + "coin_analytics.other_data" = "Other Data"; "coin_analytics.reports" = "Reports"; @@ -1027,39 +1030,40 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "manage_wallets.title" = "Coin Manager"; "manage_wallets.not_found" = "Hmm, no luck. Maybe try adding the token by hand?"; "manage_wallets.search_placeholder" = "Whatcha looking for? Name, code, or maybe contract address?"; -"manage_wallets.contract_address" = "The Contract's Address"; +"manage_wallets.contract_address" = "Contract Address"; "manage_wallets.derivation_description" = "Fun fact: there are 4 cool address formats %@ wallets can use. They are BIP44, BIP49, BIP84 (our fav!), and BIP86. But no stress, we got you covered with all of them!"; "manage_wallets.bitcoin_cash_coin_type_description" = "For Bitcoin Cash, there are 2 formats - TYPE 0 and TYPE 145. We say TYPE 145 is the way to go!"; // Settings -> Personal Support -"settings.personal_support.telegram_username.title" = "Your TG Handle"; -"settings.personal_support.telegram_username.placeholder" = "It's the @username one!"; +"settings.personal_support.telegram_username.title" = "Account"; +"settings.personal_support.telegram_username.placeholder" = "@username!"; "settings.personal_support.description" = "Pop in your Telegram username, and we'll slide into your DMs for some one-on-one chat."; -"settings.personal_support.request" = "Hit me up!"; -"settings.personal_support.requested" = "Message sent!"; -"settings.personal_support.failed" = "Oops! That didn't go as planned."; +"settings.personal_support.request" = "Request"; +"settings.personal_support.requested" = "Requested"; +"settings.personal_support.failed" = "Request failed"; "settings.personal_support.need_subscription" = "Heads up! This cool feature? It's for the %@ Wallet VIPs. Wanna know more? Check our website!"; "settings.personal_support.requested.description" = "You've already pinged us! Check your Telegram for our message."; -"settings.personal_support.requested.open_telegram" = "Jump to Telegram"; -"settings.personal_support.requested.new_request" = "Ping again?"; +"settings.personal_support.requested.open_telegram" = "Open Telegram"; +"settings.personal_support.requested.new_request" = "New Request"; // Settings -> Experimental Features -"settings.experimental_features.title" = "Lab Stuff"; +"settings.experimental_features.title" = "Experimental"; "settings.experimental_features.description" = "Below are some fun (but experimental) features. We've played with them, and while we think they're cool, use them at your own risk!"; "settings.experimental_features.bitcoin_hodling" = "TimeLock"; // Settings -> Experimental Features -> Bitcoin HODLing -"settings.bitcoin_hodling.title" = "The TimeLock Trick"; -"settings.bitcoin_hodling.lock_time" = "Switch it on!"; +"settings.bitcoin_hodling.title" = "TimeLock"; +"settings.bitcoin_hodling.lock_time" = "Activate"; "settings.bitcoin_hodling.description" = "So this? It's a cool trick to lock up your Bitcoins till a certain date. But make sure the receiver's using %@ wallet app version 0.10 or newer. It's a fun way to HODL, especially if you're sending to yourself."; // Settings -> Terms -"terms.title" = "The Fine Print"; -"terms.i_agree" = "Alright, I'm in!"; +"terms.title" = "Terms"; +"terms.i_agree" = "I Agree"; + "terms.item.1" = "Always backup your wallet recovery phrases. It's your key to the kingdom if things go sideways."; "terms.item.2" = "Your wallet recovery phrases are made just for you, right here, and we don't keep them anywhere else."; "terms.item.3" = "Heads up! If you switch off the unlock PIN on your phone, all wallets in the app will vanish. You'll need those recovery phrases to get back in."; @@ -1084,38 +1088,39 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "settings_security.blockchain_settings" = "Blockchain Settings"; "security_settings.delete_alert_button" = "Delete from Phone"; +"btc_blockchain_settings.restore_source" = "Restore Source"; "btc_blockchain_settings.restore_source.description" = "Where do you wanna pull your wallet's past deeds from?"; "btc_blockchain_settings.restore_source.alert" = "Just so you know, if you tweak the Restore Source, the wallet's gotta get in sync with the %@ blockchain again."; -"btc_restore_mode.recommended" = "What We Suggest"; -"btc_restore_mode.more_private" = "On the Down Low"; +"btc_restore_mode.recommended" = "Recommended"; +"btc_restore_mode.more_private" = "More Private"; "btc_transaction_sort_mode.shuffle" = "Shuffle"; "btc_transaction_sort_mode.shuffle.description" = "Random Indexing"; "btc_transaction_sort_mode.bip69" = "Deterministic Bip69"; "btc_transaction_sort_mode.bip69.description" = "Lexicographical Indexing"; -"blockchain_settings.info.restore_source" = "Source of the Comeback"; +"blockchain_settings.info.restore_source" = "Restore Source"; "blockchain_settings.info.restore_source.content" = "Alright, here's the deal. If you're bringing back an old wallet, you've got to know where it's been. %@ can tap into two ways: 1. A super-fast API server (think 5-10 minutes) that's kinda like asking a friend who knows everything. But it’s not as private and leans on that friend always being around. 2. Straight from the blockchain network, which is more private and independent but also a slow poke (could take 2-3 hours)."; -"blockchain_settings.info.rpc_source" = "Chatting with Blockchains"; +"blockchain_settings.info.rpc_source" = "RPC Source"; "blockchain_settings.info.rpc_source.content" = "How do we chat with the blockchains when money's moving? With some (like Bitcoin), it's like chatting in a big group. With others (like Ethereum), we gotta use a middleman like Infura.io. No worries about your coins though, just how we connect. We're brainstorming more freestyle ways to link up. Stay tuned!"; // Manage Accounts -"manage_accounts.migration_required" = "Time to Move!"; -"manage_accounts.migration_recommended" = "Might Wanna Consider Moving"; -"manage_accounts.backup_required" = "Need a Safety Net"; +"manage_accounts.migration_required" = "Migration Required"; +"manage_accounts.migration_recommended" = "Migration Recommended"; +"manage_accounts.backup_required" = "Backup Required"; // Manage Keys -"settings_manage_keys.title" = "Wallet Wizardry"; -"settings_manage_keys.delete" = "Toss It"; -"settings_manage_keys.backup" = "Safekeep"; -"settings_manage_keys.delete.title" = "Ditch the Wallet"; +"settings_manage_keys.title" = "Manage Wallets"; +"settings_manage_keys.delete" = "Delete"; +"settings_manage_keys.backup" = "Backup"; +"settings_manage_keys.delete.title" = "Delete Wallet"; "settings_manage_keys.delete.confirmation_remove" = "Heads up! This will kick the wallet off the device."; "settings_manage_keys.delete.confirmation_loose" = "Hey, if you haven't stashed the key for this wallet, you could be saying bye to your bucks."; "settings_manage_keys.delete.confirmation_watch" = "Done watching this wallet's every move?"; -"settings_manage_keys.delete.confirmation_watch.button" = "Eyes Off"; +"settings_manage_keys.delete.confirmation_watch.button" = "Stop Watching"; // Settings -> About App @@ -1322,11 +1327,11 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Intro -"intro.unchain_assets.title" = "Break Those Chains!"; +"intro.unchain_assets.title" = "Unchain Assets"; "intro.unchain_assets.description" = "Why cage yourself in? And don’t let any losers do that to you. Be free!"; -"intro.go_borderless.title" = "No Walls Here!"; +"intro.go_borderless.title" = "Go Borderless"; "intro.go_borderless.description" = "Skip those silly barriers. Access markets everywhere, like a boss!"; -"intro.stay_private.title" = "Keep it Secret, Keep it Safe!"; +"intro.stay_private.title" = "Stay Private"; "intro.stay_private.description" = "Why broadcast your business? Keep your secrets, just like I do!"; // Guides @@ -1526,27 +1531,31 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; "fee_settings.network_fee.info" = "The estimated cost of sending given transaction on the network."; "fee_settings.gas_limit" = "Gas Limit"; "fee_settings.gas_limit.info" = "You know, transactions have their own kind of 'energy', and they call it 'gas'. Think of the Gas Limit as the biggest energy drink you'd need for a transaction. But usually, you'll sip less than what you think."; + "fee_settings.gas_price" = "Gas Price"; "fee_settings.gas_price.info" = "So here's the deal: doing business on the network costs 'gas'. The Gas Price? It's like your bid at an auction - how much you're willing to drop for each gas unit. If the network's buzzing like one of my rallies, prices soar. When it's calm, they dip. And hey, if you're stingy on the gas price, don't be surprised if your transaction hangs around longer than expected!"; + "fee_settings.base_fee" = "Base Fee"; "fee_settings.base_fee.info" = "So, every block has a ticket price set by the big network – they call it the base fee rate. Think of it as a fluctuating stock; sometimes up, sometimes down, but never more than 12.5% change. This ticket price changes based on how packed the network is. The number you're seeing? That's the current rate. Very transparent!"; -"fee_settings.max_fee_rate" = "Top Dollar Rate"; +"fee_settings.max_fee_rate" = "Max Fee Rate"; "fee_settings.max_fee_rate.info" = "This is your ceiling - the max you're willing to spend per gas unit. It's gotta cover the basic fee and the special priority fee. The number here? It's a clever estimate, but usually, you'll end up spending less. Cut corners on this and you might be waiting a long time, or even get your transaction stuck in limbo!"; +"fee_settings.tips" = "Max Priority Fee"; +"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; + -"fee_settings.tips" = "The Big Tipper's Fee"; -"fee_settings.errors.insufficient_balance" = "Running on empty!"; -"fee_settings.errors.unexpected_error" = "Whoops, something went Trump-sized wrong!"; +"fee_settings.errors.insufficient_balance" = "Insufficient balance"; +"fee_settings.errors.unexpected_error" = "Unexpected Error"; "fee_settings.errors.insufficient_balance.info" = "Listen, your %@ stash is looking a bit thin for this transaction, especially when we throw in the fee."; -"fee_settings.errors.low_max_fee" = "Your fee's too puny!"; +"fee_settings.errors.low_max_fee" = "Low Fee"; "fee_settings.errors.low_max_fee.info" = "That fee you set? Way too low for the big league transaction you're trying to do!"; "fee_settings.errors.nonce_already_in_block" = "Too late, already made the move!"; "fee_settings.errors.replacement_transaction_underpriced" = "Trying to switch things up? Your fee's still too skimpy!"; "fee_settings.errors.transaction_underpriced" = "That's a bargain-bin fee!"; "fee_settings.errors.tips_higher_than_max_fee" = "Your max fee? It's not gonna cut it!"; -"fee_settings.errors.zero_amount.info" = "You can't just send thin air!"; -"fee_settings.warning.risk_of_getting_stuck" = "Living on the edge!"; +"fee_settings.errors.zero_amount.info" = "Cannot transfer 0 TRX"; +"fee_settings.warning.risk_of_getting_stuck" = "Risky"; "fee_settings.warning.risk_of_getting_stuck.info" = "This might take a while... or, who knows, might not even happen at all!"; -"fee_settings.warning.overpricing" = "Throwing your money away!"; +"fee_settings.warning.overpricing" = "Fee Too High"; "fee_settings.warning.overpricing.info" = "Your fee is looking more lavish than Mar-a-Lago! Way more than what you need for this."; // Watch Address From d0bac97024aea713dbbca7e47d148a3216b0daf6 Mon Sep 17 00:00:00 2001 From: _imadia Date: Mon, 25 Sep 2023 14:30:35 +0600 Subject: [PATCH 12/63] Fix DE localizable strings --- .../de.lproj/Localizable.strings | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index 2307a3e8b8..3c7a7278db 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -97,7 +97,7 @@ // Access Camera -"access_camera.message" = "Yo, %@ wants a lil' peek with that camera to catch that flashy QR code. 📸 +"access_camera.message" = "Yo, %@ wants a lil' peek with that camera to catch that flashy QR code. Swag your way to Settings -> %@ and let it shine!"; "access_camera.settings" = "Settings"; @@ -116,12 +116,12 @@ Swag your way to Settings -> %@ and let it shine!"; "restore.checksum_error" = "Invalid checksum"; "restore.passphrase" = "Passphrase"; "restore.input.passphrase" = "Passphrase"; -"restore.passphrase_description" = "This is the password that locks up your wallet backup, like a secret track. Ain't no iCloud vibes here. Lose it and it's gone, like old Kanye mixtapes." +"restore.passphrase_description" = "This is the password that locks up your wallet backup, like a secret track. Ain't no iCloud vibes here. Lose it and it's gone, like old Kanye mixtapes."; "restore.error.empty_passphrase" = "The passphrase cannot be empty"; "restore.non_standard_import" = "Non-Standard Import"; -"restore.non_standard_import.description" = "Yo, %@ fam, this page is for y'all with that unique Yeezy-style wallet. Maybe you were vibing in the %@ 0.27 - 0.28 era with some international lyrics or fancy symbols in your wallet. Seeing a big zero on that balance after the 0.29 drop? We got you. Dive in here to get back to the beat. And after? Consider rocking a brand-new, standard-compliant wallet and make those money moves." -"restore.warning.non_recommended.description" = "Hold up! Your wallet's got that avant-garde character style. Not seeing the cash or the flow? Dive deep for the deets below. 🎤\n\nTAP IN FOR THE SCOOP" -"restore.error.non_standard.description" = "Yo, this wallet's got its own wave. It's non-standard. 🕶️\n\nTAP TO UNRAVEL THE MYSTERY" +"restore.non_standard_import.description" = "Yo, %@ fam, this page is for y'all with that unique Yeezy-style wallet. Maybe you were vibing in the %@ 0.27 - 0.28 era with some international lyrics or fancy symbols in your wallet. Seeing a big zero on that balance after the 0.29 drop? We got you. Dive in here to get back to the beat. And after? Consider rocking a brand-new, standard-compliant wallet and make those money moves."; +"restore.warning.non_recommended.description" = "Hold up! Your wallet's got that avant-garde character style. Not seeing the cash or the flow? Dive deep for the deets below. 🎤\n\nTAP IN FOR THE SCOOP"; +"restore.error.non_standard.description" = "Yo, this wallet's got its own wave. It's non-standard. 🕶️\n\nTAP TO UNRAVEL THE MYSTERY"; // Restore Type "restore_type.title" = "Import Wallet"; @@ -137,18 +137,18 @@ Swag your way to Settings -> %@ and let it shine!"; // Restore Cloud "restore.cloud.title" = "Select Backup"; -"restore.cloud.description" = "Pick the mixtape (backup) of the wallet you wanna bring back to the spotlight." +"restore.cloud.description" = "Pick the mixtape (backup) of the wallet you wanna bring back to the spotlight."; "restore.cloud.empty" = "No backups found."; "restore.cloud.imported" = "Imported wallets"; "restore.cloud.password.title" = "Enter Password"; "restore.cloud.password.placeholder" = "Backup Password"; -"restore.cloud.password.description" = "Drop that secret beat (backup password) to get your wallet vibes from the iCloud stage." +"restore.cloud.password.description" = "Drop that secret beat (backup password) to get your wallet vibes from the iCloud stage."; // Restore Cex "restore.cex.title" = "Select CEX"; -"restore.cex.description" = "Pick the main stage (centralized exchange) you wanna rock with." +"restore.cex.description" = "Pick the main stage (centralized exchange) you wanna rock with."; // Restore Binance @@ -167,7 +167,7 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; "coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; "sync_mode.from_blockchain" = "From Blockchain"; -"blockchain_settings.description" = "Pick your style (address format) for catching those payments. When bringing back an old mix (restoring a wallet), make sure the beats match." +"blockchain_settings.description" = "Pick your style (address format) for catching those payments. When bringing back an old mix (restoring a wallet), make sure the beats match."; // Coin Platforms @@ -176,14 +176,14 @@ Swag your way to Settings -> %@ and let it shine!"; // Copy Warning "copy_warning.title" = "Risk of Copying"; -"copy_warning.description" = "For safety, like rocking shades indoors, skip that copy action for this value, fam." +"copy_warning.description" = "For safety, like rocking shades indoors, skip that copy action for this value, fam."; "copy_warning.dont_copy" = "Don't Copy"; "copy_warning.i_will_risk_it" = "I will risk it"; // Recovery Phrase "recovery_phrase.title" = "Recovery Phrase"; -"recovery_phrase.warning" = "Hold onto this key like a rare Yeezy drop. The %@ Wallet crew? We won't ever slide into your DMs for it." +"recovery_phrase.warning" = "Hold onto this key like a rare Yeezy drop. The %@ Wallet crew? We won't ever slide into your DMs for it."; "recovery_phrase.tap_to_show" = "Tap to show recovery phrase"; "recovery_phrase.passphrase" = "Passphrase"; "recovery_phrase.copy_warning.title" = "Risk of Recovery Phrase copy"; @@ -207,7 +207,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Backup "backup.title" = "Recovery Phrase"; -"backup.description" = "Jot these words down, in flow, and stash them somewhere no haters can find." +"backup.description" = "Jot these words down, in flow, and stash them somewhere no haters can find."; "backup.tap_to_show" = "Tap to show recovery phrase"; "backup.passphrase" = "Passphrase"; "backup.verify" = "Verify"; @@ -216,7 +216,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Backup Verify Words "backup_verify_words.title" = "Verify"; -"backup_verify_words.description" = "Pick out those two spotlight words from your wallet's lyrical recovery phrase." +"backup_verify_words.description" = "Pick out those two spotlight words from your wallet's lyrical recovery phrase."; "backup_verify_words.incorrect_word" = "Incorrect Word"; // Backup Verify Passphrase @@ -232,7 +232,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Backup Prompt "backup_prompt.title" = "Manual Backup"; -"backup_prompt.warning" = "Craft a backup mix of the recovery vibes and that secret code. It's your safety net if your phone goes MIA, gets jacked, or just breaks down." +"backup_prompt.warning" = "Craft a backup mix of the recovery vibes and that secret code. It's your safety net if your phone goes MIA, gets jacked, or just breaks down."; "backup_prompt.backup" = "Backup"; "backup_prompt.backup_manual" = "Manual Backup"; "backup_prompt.backup_cloud" = "Backup to iCloud"; @@ -241,7 +241,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Backup to iCloud "backup.cloud.title" = "Backup to iCloud"; -"backup.cloud.description" = "Heads up: iCloud is Apple's stage, not yours. Your beats (data) are on their mics (servers), not your personal studio. So, you're passing the mic and trust to them." +"backup.cloud.description" = "Heads up: iCloud is Apple's stage, not yours. Your beats (data) are on their mics (servers), not your personal studio. So, you're passing the mic and trust to them."; "backup.cloud.terms.item.1" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; "backup.cloud.name.title" = "Backup Name"; @@ -252,7 +252,7 @@ Swag your way to Settings -> %@ and let it shine!"; "backup.cloud.name.placeholder" = "Name"; "backup.cloud.password.title" = "Set Password"; -"backup.cloud.password.description" = "Craft a password that's as iconic as a Kanye track. Need 8 beats (symbols) with a mix of highs (uppercase), lows (lowercase), numbers, and some flair (special character)." +"backup.cloud.password.description" = "Craft a password that's as iconic as a Kanye track. Need 8 beats (symbols) with a mix of highs (uppercase), lows (lowercase), numbers, and some flair (special character)."; "backup.cloud.password.highlighted_description" = "Don't forget this password! It is separate from your Apple iCloud password, and it cannot be recovered or reset."; "backup.cloud.password.placeholder" = "Password"; "backup.cloud.password.confirm.placeholder" = "Confirm"; @@ -260,7 +260,7 @@ Swag your way to Settings -> %@ and let it shine!"; "backup.cloud.password.error.empty_passphrase" = "Passphrase cannot be empty"; "backup.cloud.password.error.forbidden_symbols" = "Stick to the platinum hits: A-Z a-z 0-9 and these symbols:A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %. No remixes, please."; -"backup.cloud.password.error.minimum_requirement" = "Bring at least 8 characters to the stage, featuring a capital hit, a chill lowercase, a number, and some special effects." +"backup.cloud.password.error.minimum_requirement" = "Bring at least 8 characters to the stage, featuring a capital hit, a chill lowercase, a number, and some special effects."; "backup.cloud.password.error.invalid_password" = "Incorrect Password"; "backup.cloud.password.error.invalid_backup" = "Backup is corrupted"; "backup.cloud.password.confirm.error.doesnt_match" = "Password doesn’t match"; @@ -268,14 +268,14 @@ Swag your way to Settings -> %@ and let it shine!"; "backup.cloud.cant_create_file" = "Can't save File to iCloud"; "backup.cloud.cant_delete_file" = "Can't delete from iCloud"; "backup.cloud.no_access.title" = "Access iCloud"; -"backup.cloud.no_access.description" = "Wanna drop a backup? Gotta pass the mic to iCloud storage first." +"backup.cloud.no_access.description" = "Wanna drop a backup? Gotta pass the mic to iCloud storage first."; // Errors "error.send.self_transfer" = "Sending to yourself is not supported"; -"error.send_binance.memo_required" = "Yo, the receiver's looking for a memo that's not silent. Drop some beats in that transfer note." -"error.send_binance.only_digits_allowed" = "The memo's gotta be numbers only, like chart-topping digits." -"error.send_z_cash.transparent_address" = "Hold up, %@ ain't vibing with payments to a see-through address." +"error.send_binance.memo_required" = "Yo, the receiver's looking for a memo that's not silent. Drop some beats in that transfer note."; +"error.send_binance.only_digits_allowed" = "The memo's gotta be numbers only, like chart-topping digits."; +"error.send_z_cash.transparent_address" = "Hold up, %@ ain't vibing with payments to a see-through address."; // Balance @@ -301,8 +301,8 @@ Swag your way to Settings -> %@ and let it shine!"; "balance.add_coin" = "Add Coin"; "balance.invalid_api_key" = "Invalid API Key"; "balance.empty.add_coins" = "Add Coins"; -"balance.empty.description" = "Looks like your wallet's still waiting for its debut album. No coins dropped in yet." -"balance.watch_empty.description" = "This wallet's address? Still waiting for its first hit single. No balance here." +"balance.empty.description" = "Looks like your wallet's still waiting for its debut album. No coins dropped in yet."; +"balance.watch_empty.description" = "This wallet's address? Still waiting for its first hit single. No balance here."; "balance.sort_by" = "Sort By"; "balance.sort.header" = "Sort by"; "balance.sort.valueHighToLow" = "Balance"; @@ -317,7 +317,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Token Balance Page "balance.token.locked" = "Locked"; "balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "The sender dropped these funds with a lil' hold - kinda like a track release date.\n\nChill, those Bitcoins are already in your studio, but you gotta wait for the drop before you can make it rain on the Bitcoin network." +"balance.token.locked.info.description" = "The sender dropped these funds with a lil' hold - kinda like a track release date.\n\nChill, those Bitcoins are already in your studio, but you gotta wait for the drop before you can make it rain on the Bitcoin network."; "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; From 9b766bd7495a9f6c9b7d0349aa36af46ab12ac2c Mon Sep 17 00:00:00 2001 From: ant013 Date: Tue, 26 Sep 2023 11:20:20 +0400 Subject: [PATCH 13/63] Update Zcash library to 2.0.0 version --- .../project.pbxproj | 2 +- .../Core/Adapters/ZcashAdapter.swift | 465 +++++++++--------- .../Core/Adapters/ZcashTransactionPool.swift | 4 +- .../Adapters/ZcashTransactionWrapper.swift | 2 - .../Core/Managers/DownloadService.swift | 60 +-- .../Models/AdapterState.swift | 2 + .../TransactionsTableViewDataSource.swift | 10 +- .../Token/DataSources/DataSourceChain.swift | 60 +-- .../WalletTokenBalanceDataSource.swift | 168 ++++--- .../WalletTokenBalanceViewItemFactory.swift | 12 +- .../TokenList/WalletTokenListDataSource.swift | 7 +- .../WalletTokenListViewItemFactory.swift | 6 + .../Wallet/WalletViewItemFactory.swift | 6 + .../en.lproj/Localizable.strings | 1 + 14 files changed, 397 insertions(+), 408 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 6782e6ca86..d66391ebe2 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; requirement = { kind = exactVersion; - version = "0.22.0-beta"; + version = "2.0.0"; }; }; D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index 91ac321f15..0b7482ce23 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -1,19 +1,18 @@ +import Combine import Foundation -import UIKit -import ZcashLightClientKit -import RxSwift -import RxRelay import HdWalletKit +import HsExtensions import HsToolKit import MarketKit -import HsExtensions -import Combine +import RxRelay +import RxSwift +import UIKit +import ZcashLightClientKit class ZcashAdapter { private static let endPoint = "mainnet.lightwalletd.com" // "lightwalletd.electriccoin.co" private let queue = DispatchQueue(label: "\(AppConfig.label).zcash-adapter", qos: .userInitiated) - private let disposeBag = DisposeBag() private var cancellables: [AnyCancellable] = [] private let token: Token @@ -29,6 +28,7 @@ class ZcashAdapter { private let uniqueId: String private let seedData: [UInt8] private let birthday: BlockHeight + private let initMode: WalletInitMode private var viewingKey: UnifiedFullViewingKey? // this being a single account does not need to be an array private var spendingKey: UnifiedSpendingKey? private var logger: HsToolKit.Logger? @@ -43,12 +43,12 @@ class ZcashAdapter { private let depositAddressSubject = PassthroughSubject, Never>() private var started = false - private var preparing: Bool = false private var lastBlockHeight: Int = 0 private var waitForStart: Bool = false { didSet { - if waitForStart && zAddress != nil { // already prepared and has address + print("Change waitForStart to \(waitForStart) : zAddress : \(zAddress != nil)") + if waitForStart, zAddress != nil { // already prepared and has address syncMain() } } @@ -89,7 +89,7 @@ class ZcashAdapter { } init(wallet: Wallet, restoreSettings: RestoreSettings) throws { - logger = App.shared.logger.scoped(with: "ZCashKit") //HsToolKit.Logger(minLogLevel: .debug) + logger = HsToolKit.Logger(minLogLevel: .debug) // App.shared.logger.scoped(with: "ZCashKit") // guard let seed = wallet.account.type.mnemonicSeed else { throw AdapterError.unsupportedAccount @@ -102,113 +102,120 @@ class ZcashAdapter { transactionSource = wallet.transactionSource uniqueId = wallet.account.id - let birthday: BlockHeight switch wallet.account.origin { - case .created: birthday = Self.newBirthdayHeight(network: network) + case .created: + birthday = Self.newBirthdayHeight(network: network) + initMode = .newWallet case .restored: if let height = restoreSettings.birthdayHeight { birthday = max(height, network.constants.saplingActivationHeight) } else { birthday = network.constants.saplingActivationHeight } + initMode = .restoreWallet } - self.birthday = birthday let seedData = [UInt8](seed) self.seedData = seedData let initializer = try ZcashAdapter.initializer(network: network, uniqueId: uniqueId) synchronizer = SDKSynchronizer(initializer: initializer) - // subscribe on sync states synchronizer - .stateStream - .throttle(for: .seconds(0.3), scheduler: DispatchQueue.main, latest: true) - .sink(receiveValue: { [weak self] state in self?.sync(state: state) }) - .store(in: &cancellables) + .stateStream + .throttle(for: .seconds(0.3), scheduler: DispatchQueue.main, latest: true) + .sink(receiveValue: { [weak self] state in self?.sync(state: state) }) + .store(in: &cancellables) // subscribe on new transactions synchronizer - .eventStream - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] event in self?.sync(event: event) }) - .store(in: &cancellables) + .eventStream + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] event in self?.sync(event: event) }) + .store(in: &cancellables) + + saplingDownloader + .$state + .sink(receiveValue: { [weak self] in self?.downloaderStatusUpdated(state: $0) }) + .store(in: &cancellables) // subscribe on background and events from sapling downloader NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - subscribe(disposeBag, saplingDownloader.stateObservable) { [weak self] in self?.downloaderStatusUpdated(state: $0) } - - prepare(initializer: initializer, seedData: seedData, walletBirthday: birthday) + prepare(seedData: seedData, walletBirthday: birthday, for: initMode) } - private func prepare(initializer: Initializer, seedData: [UInt8], walletBirthday: BlockHeight) { - preparing = true + private func prepare(seedData: [UInt8], walletBirthday: BlockHeight, for initMode: WalletInitMode) { + guard !state.isPrepairing else { + return + } state = .preparing depositAddressSubject.send(.loading) - Task { + Task { [weak self, synchronizer] in do { let tool = DerivationTool(networkType: .mainnet) guard let unifiedSpendingKey = try? tool.deriveUnifiedSpendingKey(seed: seedData, accountIndex: 0), - let unifiedViewingKey = try? tool.deriveUnifiedFullViewingKey(from: unifiedSpendingKey) else { - + let unifiedViewingKey = try? tool.deriveUnifiedFullViewingKey(from: unifiedSpendingKey) + else { throw AppError.ZcashError.cantCreateKeys } - spendingKey = unifiedSpendingKey - viewingKey = unifiedViewingKey - + self?.spendingKey = unifiedSpendingKey + self?.viewingKey = unifiedViewingKey - let result = try await synchronizer.prepare(with: seedData, viewingKeys: [unifiedViewingKey], walletBirthday: walletBirthday) + let result = try await synchronizer.prepare(with: seedData, walletBirthday: walletBirthday, for: initMode) if case .seedRequired = result { throw AppError.ZcashError.seedRequired } - logger?.log(level: .debug, message: "Successful prepared!") + self?.logger?.log(level: .debug, message: "Successful prepared!") guard let address = try? await synchronizer.getUnifiedAddress(accountIndex: 0), - let saplingAddress = try? address.saplingReceiver() else { + let saplingAddress = try? address.saplingReceiver() + else { throw AppError.ZcashError.noReceiveAddress } - zAddress = saplingAddress.stringEncoded - depositAddressSubject.send(.completed(DepositAddress(saplingAddress.stringEncoded))) + self?.zAddress = saplingAddress.stringEncoded + self?.depositAddressSubject.send(.completed(DepositAddress(saplingAddress.stringEncoded))) - logger?.log(level: .debug, message: "Successful get address for 0 account! \(saplingAddress.stringEncoded)") + self?.logger?.log(level: .debug, message: "Successful get address for 0 account! \(saplingAddress.stringEncoded)") let transactionPool = ZcashTransactionPool(receiveAddress: saplingAddress, synchronizer: synchronizer) - self.transactionPool = transactionPool + self?.transactionPool = transactionPool - logger?.log(level: .debug, message: "Starting fetch transactions.") + self?.logger?.log(level: .debug, message: "Starting fetch transactions.") await transactionPool.initTransactions() let wrapped = transactionPool.all if !wrapped.isEmpty { - logger?.log(level: .debug, message: "Send to pool all transactions \(wrapped.count)") - transactionRecordsSubject.onNext(wrapped.map { - transactionRecord(fromTransaction: $0) + self?.logger?.log(level: .debug, message: "Send to pool all transactions \(wrapped.count)") + self?.transactionRecordsSubject.onNext(wrapped.compactMap { + self?.transactionRecord(fromTransaction: $0) }) } let shielded = await (try? synchronizer.getShieldedBalance(accountIndex: 0).decimalValue.decimalValue) ?? 0 let shieldedVerified = await (try? synchronizer.getShieldedVerifiedBalance(accountIndex: 0).decimalValue.decimalValue) ?? 0 - balanceSubject.onNext(BalanceData( + self?.balanceSubject.onNext( + BalanceData( balance: shieldedVerified, locked: shielded - shieldedVerified - )) + ) + ) + self?.lastBlockHeight = try await synchronizer.latestHeight() + self?.lastBlockUpdatedSubject.onNext(()) - finishPrepare() + self?.finishPrepare() } catch { - setPreparing(error: error) + self?.setPreparing(error: error) } } } private func setPreparing(error: Error) { - preparing = false state = .notSynced(error: error) logger?.log(level: .error, message: "Has preparing error! \(error)") } private func finishPrepare() { - preparing = false state = .idle if waitForStart { @@ -217,25 +224,21 @@ class ZcashAdapter { } } - @objc private func didEnterBackground(_ notification: Notification) { + @objc private func didEnterBackground(_: Notification) { stop() } private func downloaderStatusUpdated(state: DownloadService.State) { switch state { case .idle: + () + case .success: syncMain() - case .inProgress(let progress): + case let .inProgress(progress): self.state = .downloadingSapling(progress: Int(progress * 100)) } } - private func progress(p: BlockProgress) -> Double { - let overall = p.targetHeight - birthday - - return Double(overall > 0 ? Float((p.progressHeight - birthday)) / Float(overall) : 0) - } - private func sync(state: SynchronizerState) { synchronizerState = state @@ -249,28 +252,31 @@ class ZcashAdapter { } else { syncStatus = .idle } + case .stopped: + logger?.log(level: .debug, message: "State: Disconnected") + syncStatus = .syncing(progress: nil, lastBlockDate: nil) case .upToDate: if !started { started = true } logger?.log(level: .debug, message: "State: Synced") syncStatus = .synced - lastBlockHeight = max(state.latestScannedHeight, lastBlockHeight) + lastBlockHeight = max(state.latestBlockHeight, lastBlockHeight) logger?.log(level: .debug, message: "Update BlockHeight = \(lastBlockHeight)") checkFailingTransactions() - case .syncing(let progress): + case let .syncing(progress): if !started { started = true } logger?.log(level: .debug, message: "State: Syncing") logger?.log(level: .debug, message: "State progress: \(progress)") - lastBlockHeight = max(state.latestScannedHeight, lastBlockHeight) + lastBlockHeight = max(state.latestBlockHeight, lastBlockHeight) logger?.log(level: .debug, message: "Update BlockHeight = \(lastBlockHeight)") lastBlockUpdatedSubject.onNext(()) - syncStatus = .downloadingBlocks(number: state.latestScannedHeight, lastBlock: state.latestBlockHeight) - case .error(let error): + syncStatus = .downloadingBlocks(progress: progress, lastBlock: state.latestBlockHeight) + case let .error(error): if !started, case .synchronizerDisconnected = error as? ZcashError { syncStatus = .idle } else { @@ -287,7 +293,7 @@ class ZcashAdapter { private func sync(event: SynchronizerEvent) { switch event { - case .foundTransactions(let transactions, let inRange): + case let .foundTransactions(transactions, inRange): logger?.log(level: .debug, message: "found \(transactions.count) mined txs in range: \(inRange)") transactions.forEach { overview in logger?.log(level: .debug, message: "tx: v =\(overview.value.decimalValue.decimalString) : fee = \(overview.fee?.decimalString() ?? "N/A") : height = \(overview.minedHeight?.description ?? "N/A")") @@ -299,7 +305,7 @@ class ZcashAdapter { transactionRecord(fromTransaction: $0) }) } - case .minedTransaction(let pendingEntity): + case let .minedTransaction(pendingEntity): logger?.log(level: .debug, message: "found pending tx: v =\(pendingEntity.value.decimalValue.decimalString) : fee = \(pendingEntity.fee?.decimalString() ?? "N/A")") Task { try await update(transactions: [pendingEntity]) @@ -315,7 +321,7 @@ class ZcashAdapter { private func reSyncPending() { Task { - let pending = await synchronizer.pendingTransactions + let pending = await synchronizer.transactions.filter { overview in overview.minedHeight == nil } logger?.log(level: .debug, message: "Resync pending txs: \(pending.count)") pending.forEach { entity in logger?.log(level: .debug, message: "TX : \(entity.value.decimalValue.description)") @@ -340,51 +346,51 @@ class ZcashAdapter { // TODO: Should have it's own transactions with memo if !transaction.isSentTransaction { return BitcoinIncomingTransactionRecord( - token: token, - source: transactionSource, - uid: transaction.transactionHash, - transactionHash: transaction.transactionHash, - transactionIndex: transaction.transactionIndex, - blockHeight: transaction.minedHeight, - confirmationsThreshold: ZcashSDK.defaultRewindDistance, - date: Date(timeIntervalSince1970: Double(transaction.timestamp)), - fee: defaultFeeDecimal(network: network, height: transaction.minedHeight), - failed: transaction.failed, - lockInfo: nil, - conflictingHash: nil, - showRawTransaction: showRawTransaction, - amount: abs(transaction.value.decimalValue.decimalValue), - from: transaction.recipientAddress, - memo: transaction.memo + token: token, + source: transactionSource, + uid: transaction.transactionHash, + transactionHash: transaction.transactionHash, + transactionIndex: transaction.transactionIndex, + blockHeight: transaction.minedHeight, + confirmationsThreshold: ZcashSDK.defaultRewindDistance, + date: Date(timeIntervalSince1970: Double(transaction.timestamp)), + fee: defaultFeeDecimal(network: network, height: transaction.minedHeight), + failed: transaction.failed, + lockInfo: nil, + conflictingHash: nil, + showRawTransaction: showRawTransaction, + amount: abs(transaction.value.decimalValue.decimalValue), + from: transaction.recipientAddress, + memo: transaction.memo ) } else { return BitcoinOutgoingTransactionRecord( - token: token, - source: transactionSource, - uid: transaction.transactionHash, - transactionHash: transaction.transactionHash, - transactionIndex: transaction.transactionIndex, - blockHeight: transaction.minedHeight, - confirmationsThreshold: ZcashSDK.defaultRewindDistance, - date: Date(timeIntervalSince1970: Double(transaction.timestamp)), - fee: defaultFeeDecimal(network: self.network, height: transaction.minedHeight), - failed: transaction.failed, - lockInfo: nil, - conflictingHash: nil, - showRawTransaction: showRawTransaction, - amount: abs(transaction.value.decimalValue.decimalValue), - to: transaction.recipientAddress, - sentToSelf: false, - memo: transaction.memo + token: token, + source: transactionSource, + uid: transaction.transactionHash, + transactionHash: transaction.transactionHash, + transactionIndex: transaction.transactionIndex, + blockHeight: transaction.minedHeight, + confirmationsThreshold: ZcashSDK.defaultRewindDistance, + date: Date(timeIntervalSince1970: Double(transaction.timestamp)), + fee: defaultFeeDecimal(network: network, height: transaction.minedHeight), + failed: transaction.failed, + lockInfo: nil, + conflictingHash: nil, + showRawTransaction: showRawTransaction, + amount: abs(transaction.value.decimalValue.decimalValue), + to: transaction.recipientAddress, + sentToSelf: false, + memo: transaction.memo ) } } - static private var cloudSpendParamsURL: URL? { + private static var cloudSpendParamsURL: URL? { URL(string: ZcashSDK.cloudParameterURL + ZcashSDK.spendParamFilename) } - static private var cloudOutputParamsURL: URL? { + private static var cloudOutputParamsURL: URL? { URL(string: ZcashSDK.cloudParameterURL + ZcashSDK.outputParamFilename) } @@ -393,14 +399,16 @@ class ZcashAdapter { if let cloudSpendParamsURL = Self.cloudOutputParamsURL, let destinationURL = try? Self.outputParamsURL(uniqueId: uniqueId), - !DownloadService.existing(url: destinationURL) { + !DownloadService.existing(url: destinationURL) + { isExist = false saplingDownloader.download(source: cloudSpendParamsURL, destination: destinationURL) } if let cloudSpendParamsURL = Self.cloudSpendParamsURL, let destinationURL = try? Self.spendParamsURL(uniqueId: uniqueId), - !DownloadService.existing(url: destinationURL) { + !DownloadService.existing(url: destinationURL) + { isExist = false saplingDownloader.download(source: cloudSpendParamsURL, destination: destinationURL) } @@ -408,7 +416,7 @@ class ZcashAdapter { return isExist } - func fixPendingTransactionsIfNeeded(completion: (() -> ())? = nil) { + func fixPendingTransactionsIfNeeded(completion: (() -> Void)? = nil) { // check if we need to perform the fix or leave // get all the pending transactions guard !App.shared.localStorage.zcashAlwaysPendingRewind else { @@ -417,7 +425,7 @@ class ZcashAdapter { } Task { - let txs = await synchronizer.pendingTransactions + let txs = await synchronizer.transactions.filter { overview in overview.minedHeight == nil } // fetch the first one that's reported to be unmined guard let firstUnmined = txs.filter({ $0.minedHeight == nil }).first else { App.shared.localStorage.zcashAlwaysPendingRewind = true @@ -429,41 +437,39 @@ class ZcashAdapter { } } - private func rewind(unmined: ZcashTransaction.Overview, completion: (() -> ())? = nil) { + private func rewind(unmined: ZcashTransaction.Overview, completion: (() -> Void)? = nil) { synchronizer - .rewind(.transaction(unmined)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - App.shared.localStorage.zcashAlwaysPendingRewind = true - completion?() - case .failure: - self.rewindQuick() - } - }, - receiveValue: { _ in } - ) - .store(in: &cancellables) - } - - private func rewindQuick(completion: (() -> ())? = nil) { + .rewind(.transaction(unmined)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + App.shared.localStorage.zcashAlwaysPendingRewind = true + completion?() + case .failure: + self.rewindQuick() + } + }, + receiveValue: { _ in }) + .store(in: &cancellables) + } + + private func rewindQuick(completion: (() -> Void)? = nil) { synchronizer - .rewind(.quick) - .sink(receiveCompletion: { [weak self] result in - switch result { - case .finished: - App.shared.localStorage.zcashAlwaysPendingRewind = true - self?.logger?.log(level: .debug, message: "rewind Successful") - completion?() - case let .failure(error): - self?.state = .notSynced(error: error) - completion?() - self?.logger?.log(level: .error, message: "attempt to fix pending transactions failed with error: \(error)") - } - }, - receiveValue: { _ in } - ) - .store(in: &cancellables) + .rewind(.quick) + .sink(receiveCompletion: { [weak self] result in + switch result { + case .finished: + App.shared.localStorage.zcashAlwaysPendingRewind = true + self?.logger?.log(level: .debug, message: "rewind Successful") + completion?() + case let .failure(error): + self?.state = .notSynced(error: error) + completion?() + self?.logger?.log(level: .error, message: "attempt to fix pending transactions failed with error: \(error)") + } + }, + receiveValue: { _ in }) + .store(in: &cancellables) } private var _balanceData: BalanceData { @@ -476,8 +482,8 @@ class ZcashAdapter { let diff = balance - verifiedBalance return BalanceData( - balance: verifiedBalance.decimalValue.decimalValue, - locked: diff.decimalValue.decimalValue + balance: verifiedBalance.decimalValue.decimalValue, + locked: diff.decimalValue.decimalValue ) } @@ -488,7 +494,6 @@ class ZcashAdapter { self?.logger?.log(level: .debug, message: "Synchronizer Was Stopped") } } - } extension ZcashAdapter { @@ -497,17 +502,18 @@ extension ZcashAdapter { } static func initializer(network: ZcashNetwork, uniqueId: String) throws -> Initializer { - Initializer( - cacheDbURL: nil, - fsBlockDbRoot: try fsBlockDbRootURL(uniqueId: uniqueId, network: network), - dataDbURL: try dataDbURL(uniqueId: uniqueId, network: network), - endpoint: LightWalletEndpoint(address: endPoint, port: 9067, secure: true, streamingCallTimeoutInMillis: 10 * 60 * 60 * 1000), - network: network, - spendParamsURL: try spendParamsURL(uniqueId: uniqueId), - outputParamsURL: try outputParamsURL(uniqueId: uniqueId), - saplingParamsSourceURL: SaplingParamsSourceURL.default, - alias: .custom(uniqueId), - loggingPolicy: .default(.debug) + try Initializer( + cacheDbURL: nil, + fsBlockDbRoot: fsBlockDbRootURL(uniqueId: uniqueId, network: network), + generalStorageURL: generalStorageURL(uniqueId: uniqueId, network: network), + dataDbURL: dataDbURL(uniqueId: uniqueId, network: network), + endpoint: LightWalletEndpoint(address: endPoint, port: 9067, secure: true, streamingCallTimeoutInMillis: 10 * 60 * 60 * 1000), + network: network, + spendParamsURL: spendParamsURL(uniqueId: uniqueId), + outputParamsURL: outputParamsURL(uniqueId: uniqueId), + saplingParamsSourceURL: SaplingParamsSourceURL.default, + alias: .custom(uniqueId), + loggingPolicy: .default(.debug) ) } @@ -515,8 +521,8 @@ extension ZcashAdapter { let fileManager = FileManager.default let url = try fileManager - .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - .appendingPathComponent("z-cash-kit", isDirectory: true) + .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + .appendingPathComponent("z-cash-kit", isDirectory: true) try fileManager.createDirectory(at: url, withIntermediateDirectories: true) @@ -527,6 +533,10 @@ extension ZcashAdapter { try dataDirectoryUrl().appendingPathComponent(network.networkType.chainName + uniqueId + ZcashSDK.defaultFsCacheName, isDirectory: true) } + private static func generalStorageURL(uniqueId: String, network: ZcashNetwork) throws -> URL { + try dataDirectoryUrl().appendingPathComponent(network.networkType.chainName + uniqueId + "general_storage", isDirectory: true) + } + private static func cacheDbURL(uniqueId: String, network: ZcashNetwork) throws -> URL { try dataDirectoryUrl().appendingPathComponent(network.constants.defaultDbNamePrefix + uniqueId + ZcashSDK.defaultCacheDbName, isDirectory: false) } @@ -553,38 +563,31 @@ extension ZcashAdapter { } } } - } extension ZcashAdapter: IAdapter { - var isMainNet: Bool { network.networkType == .mainnet } func start() { - guard !preparing else { // postpone start library until preparing will finish + guard !state.isPrepairing else { // postpone start library until preparing will finish logger?.log(level: .debug, message: "Can't start because preparing!") waitForStart = true return } - if zAddress == nil { // else we need to try prepare library again + if zAddress == nil { // else we need to try prepare library again logger?.log(level: .debug, message: "No address, try to prepare kit again!") - do { - let initializer = try Self.initializer(network: network, uniqueId: uniqueId) - prepare(initializer: initializer, seedData: seedData, walletBirthday: birthday) - } catch { - logger?.log(level: .error, message: "Can't start adapter: \(error.localizedDescription)") - } + prepare(seedData: seedData, walletBirthday: birthday, for: initMode) return } - waitForStart = false // if we has address just start syncing library or downloading sapling data + waitForStart = false // if we has address just start syncing library or downloading sapling data if saplingDataExist() { logger?.log(level: .debug, message: "Start syncing kit!") - syncMain(retry: true) + syncMain() } } @@ -597,17 +600,16 @@ extension ZcashAdapter: IAdapter { start() } - private func syncMain(retry: Bool = false) { + private func syncMain() { DispatchQueue.main.async { [weak self] in - self?.sync(retry: true) + self?.sync() } } - private func sync(retry: Bool = false) { + private func sync() { balanceSubject.onNext(_balanceData) fixPendingTransactionsIfNeeded { [weak self] in - self?.logger?.log(level: .debug, message: "\(Date()) Try to start synchronizer : retry = \(retry), by Thread:\(Thread.current)") - + self?.logger?.log(level: .debug, message: "\(Date()) Try to start synchronizer :by Thread:\(Thread.current)") Task { [weak self] in do { try await self?.synchronizer.start(retry: true) @@ -626,28 +628,26 @@ extension ZcashAdapter: IAdapter { let zAddress = zAddress ?? "No Info" var balanceState = "No Balance Information yet" - if let status = self.synchronizerState { + if let status = synchronizerState { balanceState = """ - shielded balance - total: \(balanceData.balanceTotal.description) - verified: \(balanceData.balance) - transparent balance - total: \(String(describing: status.transparentBalance.total)) - verified: \(String(describing: status.transparentBalance.verified)) - """ + shielded balance + total: \(balanceData.balanceTotal.description) + verified: \(balanceData.balance) + transparent balance + total: \(String(describing: status.transparentBalance.total)) + verified: \(String(describing: status.transparentBalance.verified)) + """ } return """ - ZcashAdapter - z-address: \(String(describing: zAddress)) - spendingKeys: \(spendingKey?.description ?? "N/A") - balanceState: \(balanceState) - """ + ZcashAdapter + z-address: \(String(describing: zAddress)) + spendingKeys: \(spendingKey?.description ?? "N/A") + balanceState: \(balanceState) + """ } - } extension ZcashAdapter: ITransactionsAdapter { - var lastBlockInfo: LastBlockInfo? { LastBlockInfo(height: lastBlockHeight, timestamp: nil) } @@ -668,22 +668,22 @@ extension ZcashAdapter: ITransactionsAdapter { network.networkType == .mainnet ? "https://blockchair.com/zcash/transaction/" + transactionHash : nil } - func transactionsObservable(token: Token?, filter: TransactionTypeFilter) -> Observable<[TransactionRecord]> { + func transactionsObservable(token _: Token?, filter: TransactionTypeFilter) -> Observable<[TransactionRecord]> { transactionRecordsSubject.asObservable() - .map { transactions in - transactions.compactMap { transaction -> TransactionRecord? in - switch (transaction, filter) { - case (_, .all): return transaction - case (is BitcoinIncomingTransactionRecord, .incoming): return transaction - case (is BitcoinOutgoingTransactionRecord, .outgoing): return transaction - default: return nil - } + .map { transactions in + transactions.compactMap { transaction -> TransactionRecord? in + switch (transaction, filter) { + case (_, .all): return transaction + case (is BitcoinIncomingTransactionRecord, .incoming): return transaction + case (is BitcoinOutgoingTransactionRecord, .outgoing): return transaction + default: return nil } } - .filter { !$0.isEmpty } + } + .filter { !$0.isEmpty } } - func transactionsSingle(from: TransactionRecord?, token: Token?, filter: TransactionTypeFilter, limit: Int) -> Single<[TransactionRecord]> { + func transactionsSingle(from: TransactionRecord?, token _: Token?, filter: TransactionTypeFilter, limit: Int) -> Single<[TransactionRecord]> { transactionPool?.transactionsSingle(from: from, filter: filter, limit: limit).map { [weak self] txs in txs.compactMap { self?.transactionRecord(fromTransaction: $0) } } ?? .just([]) @@ -692,11 +692,9 @@ extension ZcashAdapter: ITransactionsAdapter { func rawTransaction(hash: String) -> String? { transactionPool?.transaction(by: hash)?.raw?.hs.hex } - } extension ZcashAdapter: IBalanceAdapter { - var balanceStateUpdatedObservable: Observable { balanceStateSubject.asObservable() } @@ -708,11 +706,9 @@ extension ZcashAdapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { balanceSubject.asObservable() } - } extension ZcashAdapter: IDepositAdapter { - var receiveAddress: DepositAddress { // only first account DepositAddress(zAddress ?? "n/a".localized) @@ -721,7 +717,6 @@ extension ZcashAdapter: IDepositAdapter { var receiveAddressPublisher: AnyPublisher, Never> { depositAddressSubject.eraseToAnyPublisher() } - } extension ZcashAdapter: ISendZcashAdapter { @@ -731,7 +726,7 @@ extension ZcashAdapter: ISendZcashAdapter { } var availableBalance: Decimal { - max(0, balanceData.balance - fee) //TODO: check + max(0, balanceData.balance - fee) // TODO: check } func validate(address: String) throws -> AddressType { @@ -740,19 +735,19 @@ extension ZcashAdapter: ISendZcashAdapter { } do { - switch try Recipient(address, network: self.network.networkType) { + switch try Recipient(address, network: network.networkType) { case .transparent: return .transparent case .sapling, .unified: // I'm keeping changes to the minimum. Unified Address should be treated as a different address type which will include some shielded pool and possibly others as well. return .shielded } } catch { - //FIXME: Should this be handled another way? logged? how? + // FIXME: Should this be handled another way? logged? how? throw AppError.addressInvalid } } - func sendSingle(amount: Decimal, address: Recipient, memo: Memo?) -> Single<()> { + func sendSingle(amount: Decimal, address: Recipient, memo: Memo?) -> Single { guard let spendingKey else { return .error(AppError.ZcashError.noReceiveAddress) } @@ -765,10 +760,11 @@ extension ZcashAdapter: ISendZcashAdapter { Task { do { let pendingEntity = try await self.synchronizer.sendToAddress( - spendingKey: spendingKey, - zatoshi: Zatoshi.from(decimal: amount), - toAddress: address, - memo: memo) + spendingKey: spendingKey, + zatoshi: Zatoshi.from(decimal: amount), + toAddress: address, + memo: memo + ) self.logger?.log(level: .debug, message: "Successful send TX: : \(pendingEntity.value.decimalValue.description):") self.reSyncPending() observer(.success(())) @@ -783,7 +779,6 @@ extension ZcashAdapter: ISendZcashAdapter { func recipient(from stringEncodedAddress: String) -> ZcashLightClientKit.Recipient? { try? Recipient(stringEncodedAddress, network: network.networkType) } - } class ZcashAddressValidator { @@ -797,11 +792,10 @@ class ZcashAddressValidator { do { _ = try Recipient(address, network: network.networkType) } catch { - //FIXME: Should this be handled another way? logged? how? + // FIXME: Should this be handled another way? logged? how? throw AppError.addressInvalid } } - } extension EnhancementProgress { @@ -809,7 +803,7 @@ extension EnhancementProgress { guard totalTransactions <= 0 else { return 0 } - return Int(Double(self.enhancedTransactions)/Double(self.totalTransactions)) * 100 + return Int(Double(enhancedTransactions) / Double(totalTransactions)) * 100 } } @@ -819,21 +813,21 @@ enum ZCashAdapterState: Equatable { case synced case syncing(progress: Int?, lastBlockDate: Date?) case downloadingSapling(progress: Int) - case downloadingBlocks(number: Int, lastBlock: Int) + case downloadingBlocks(progress: Float, lastBlock: Int) case scanningBlocks(number: Int, lastBlock: Int) case enhancingTransactions(number: Int, count: Int) case notSynced(error: Error) - public static func ==(lhs: ZCashAdapterState, rhs: ZCashAdapterState) -> Bool { + public static func == (lhs: ZCashAdapterState, rhs: ZCashAdapterState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): return true case (.preparing, .preparing): return true case (.synced, .synced): return true - case (.syncing(let lProgress, let lLastBlockDate), .syncing(let rProgress, let rLastBlockDate)): return lProgress == rProgress && lLastBlockDate == rLastBlockDate - case (.downloadingSapling(let lProgress), .downloadingSapling(let rProgress)): return lProgress == rProgress - case (.downloadingBlocks(let lNumber, let lLast), .downloadingBlocks(let rNumber, let rLast)): return lNumber == rNumber && lLast == rLast - case (.scanningBlocks(let lNumber, let lLast), .scanningBlocks(let rNumber, let rLast)): return lNumber == rNumber && lLast == rLast - case (.enhancingTransactions(let lNumber, let lCount), .enhancingTransactions(let rNumber, let rCount)): return lNumber == rNumber && lCount == rCount + case let (.syncing(lProgress, lLastBlockDate), .syncing(rProgress, rLastBlockDate)): return lProgress == rProgress && lLastBlockDate == rLastBlockDate + case let (.downloadingSapling(lProgress), .downloadingSapling(rProgress)): return lProgress == rProgress + case let (.downloadingBlocks(lNumber, lLast), .downloadingBlocks(rNumber, rLast)): return lNumber == rNumber && lLast == rLast + case let (.scanningBlocks(lNumber, lLast), .scanningBlocks(rNumber, rLast)): return lNumber == rNumber && lLast == rLast + case let (.enhancingTransactions(lNumber, lCount), .enhancingTransactions(rNumber, rCount)): return lNumber == rNumber && lCount == rCount case (.notSynced, .notSynced): return true default: return false } @@ -841,20 +835,21 @@ enum ZCashAdapterState: Equatable { var adapterState: AdapterState { switch self { - case .idle: return .customSyncing(main: "Stopped", secondary: nil, progress: nil) + case .idle: return .customSyncing(main: "Starting...", secondary: nil, progress: nil) case .preparing: return .customSyncing(main: "Preparing...", secondary: nil, progress: nil) case .synced: return .synced - case .syncing(let progress, let lastDate): return .syncing(progress: progress, lastBlockDate: lastDate) - case .downloadingSapling(let progress): - return .customSyncing(main: "Downloading Sapling... \(progress)%", secondary: nil, progress: progress) - case .downloadingBlocks(let number, let lastBlock): - return .customSyncing(main: "Downloading Blocks", secondary: lastBlock == 0 ? nil : "\(number)/\(lastBlock)", progress: nil) - case .scanningBlocks(let number, let lastBlock): + case let .syncing(progress, lastDate): return .syncing(progress: progress, lastBlockDate: lastDate) + case let .downloadingSapling(progress): + return .customSyncing(main: "balance.downloading_sapling".localized(progress), secondary: nil, progress: progress) + case let .downloadingBlocks(progress, _): + let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress)), showSign: false) + return .customSyncing(main: "balance.downloading_blocks".localized, secondary: percentValue, progress: Int(progress * 100)) + case let .scanningBlocks(number, lastBlock): return .customSyncing(main: "Scanning Blocks", secondary: "\(number)/\(lastBlock)", progress: nil) - case .enhancingTransactions(let number, let count): + case let .enhancingTransactions(number, count): let progress: String? = count == 0 ? nil : "\(number)/\(count)" return .customSyncing(main: "Enhancing Transactions", secondary: progress, progress: nil) - case .notSynced(let error): return .notSynced(error: error) + case let .notSynced(error): return .notSynced(error: error) } } @@ -872,11 +867,17 @@ enum ZCashAdapterState: Equatable { } } + var isPrepairing: Bool { + switch self { + case .preparing: return true + default: return false + } + } + var lastProcessedBlockHeight: Int? { switch self { - case .downloadingBlocks(_, let last), .scanningBlocks(_, let last): return last + case let .downloadingBlocks(_, last), let .scanningBlocks(_, last): return last default: return nil } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift index e0a1a2c893..f3e77497de 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift @@ -55,9 +55,9 @@ class ZcashTransactionPool { func initTransactions() async { let overviews = await synchronizer.transactions - let pending = await synchronizer.pendingTransactions +// let pending = await synchronizer.pendingTransactions - pendingTransactions = await Set(zcashTransactions(pending, lastBlockHeight: 0)) +// pendingTransactions = await Set(zcashTransactions(pending, lastBlockHeight: 0)) confirmedTransactions = Set(await zcashTransactions(overviews, lastBlockHeight: 0)) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift index b1b13bb83c..5c334a9c63 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift @@ -3,7 +3,6 @@ import ZcashLightClientKit import HsExtensions class ZcashTransactionWrapper { - let id: String? let raw: Data? let transactionHash: String let transactionIndex: Int @@ -18,7 +17,6 @@ class ZcashTransactionWrapper { let failed: Bool init?(tx: ZcashTransaction.Overview, memo: Memo?, recipient: TransactionRecipient?, lastBlockHeight: Int) { - id = tx.id.description raw = tx.raw transactionHash = tx.rawID.hs.reversedHex transactionIndex = tx.index ?? 0 diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DownloadService.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DownloadService.swift index 32457e39a1..8809c9b9fb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DownloadService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DownloadService.swift @@ -1,51 +1,44 @@ -import Foundation import Alamofire -import RxSwift -import RxRelay +import Combine +import Foundation class DownloadService { private let queue: DispatchQueue private var downloads = [String: Double]() - private let stateRelay = PublishRelay() - private(set) var state: State = .idle { - didSet { - if state != oldValue { - stateRelay.accept(state) - } - } - } + @Published private(set) var state: State = .idle init(queueLabel: String = "io.SynchronizedDownloader") { queue = DispatchQueue(label: queueLabel, qos: .background) } - private func request(source: URLConvertible, destination: @escaping DownloadRequest.Destination, progress: ((Double) -> ())? = nil, completion: ((Bool) -> ())? = nil) { + private func request(source: URLConvertible, destination: @escaping DownloadRequest.Destination, progress: ((Double) -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { guard let key = try? source.asURL().path else { return } let alreadyDownloading = queue.sync { - downloads.contains(where: { (existKey, _) in key == existKey }) + downloads.contains(where: { existKey, _ in key == existKey }) } guard !alreadyDownloading else { + state = .success return } handle(progress: 0, key: key) AF.download(source, to: destination) - .downloadProgress(queue: DispatchQueue.global(qos: .background)) { [weak self] progressValue in - self?.handle(progress: progressValue.fractionCompleted, key: key) - progress?(progressValue.fractionCompleted) - } - .responseData(queue: DispatchQueue.global(qos: .background)) { [weak self] response in - self?.handle(response: response, key: key) - switch response.result { // extend errors/data to completion if needed - case .success: completion?(true) - case .failure: completion?(false) - } + .downloadProgress(queue: DispatchQueue.global(qos: .background)) { [weak self] progressValue in + self?.handle(progress: progressValue.fractionCompleted, key: key) + progress?(progressValue.fractionCompleted) + } + .responseData(queue: DispatchQueue.global(qos: .background)) { [weak self] response in + self?.handle(response: response, key: key) + switch response.result { // extend errors/data to completion if needed + case .success: completion?(true) + case .failure: completion?(false) } + } } private func handle(progress: Double, key: String) { @@ -55,7 +48,7 @@ class DownloadService { } } - private func handle(response: AFDownloadResponse, key: String) { + private func handle(response _: AFDownloadResponse, key: String) { queue.async { self.downloads[key] = nil self.syncState() @@ -70,34 +63,26 @@ class DownloadService { } guard downloads.count != 0 else { - state = .idle + state = .success return } let minimalProgress = downloads.min(by: { a, b in a.value < b.value })?.value ?? lastProgress state = .inProgress(value: max(minimalProgress, lastProgress)) } - } extension DownloadService { - - public func download(source: URLConvertible, destination: URL, progress: ((Double) -> ())? = nil, completion: ((Bool) -> ())? = nil) { + public func download(source: URLConvertible, destination: URL, progress: ((Double) -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { let destination: DownloadRequest.Destination = { _, _ in (destination, [.removePreviousFile, .createIntermediateDirectories]) } request(source: source, destination: destination, progress: progress, completion: completion) } - - public var stateObservable: Observable { - stateRelay.asObservable() - } - } extension DownloadService { - public static func existing(url: URL) -> Bool { (try? FileManager.default.attributesOfItem(atPath: url.path)) != nil } @@ -105,14 +90,15 @@ extension DownloadService { public enum State: Equatable { case idle case inProgress(value: Double) + case success - public static func ==(lhs: State, rhs: State) -> Bool { + public static func == (lhs: State, rhs: State) -> Bool { switch (lhs, rhs) { case (.idle, .idle): return true - case (.inProgress(let lhsValue), .inProgress(let rhsValue)): return lhsValue == rhsValue + case (.success, .success): return true + case let (.inProgress(lhsValue), .inProgress(rhsValue)): return lhsValue == rhsValue default: return false } } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift index 0ed8210580..8e96f93e52 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift @@ -5,6 +5,7 @@ enum AdapterState { case syncing(progress: Int?, lastBlockDate: Date?) case customSyncing(main: String, secondary: String?, progress: Int?) case notSynced(error: Error) + case stopped var isSynced: Bool { switch self { @@ -29,6 +30,7 @@ extension AdapterState: Equatable { case (.syncing(let lProgress, let lLastBlockDate), .syncing(let rProgress, let rLastBlockDate)): return lProgress == rProgress && lLastBlockDate == rLastBlockDate case (.customSyncing(let lMain, let lSecondary, let lProgress), .customSyncing(let rMain, let rSecondary, let rProgress)): return lMain == rMain && lSecondary == rSecondary && lProgress == rProgress case (.notSynced, .notSynced): return true + case (.stopped, .stopped): return true default: return false } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsTableViewDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsTableViewDataSource.swift index a3adea1df4..ab5c84703c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsTableViewDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsTableViewDataSource.swift @@ -47,22 +47,22 @@ class TransactionsTableViewDataSource: NSObject { self.allLoaded = allLoaded } - guard loaded else { + guard let tableView, loaded else { return } if let updateInfo = viewData.updateInfo { // print("Update Item: \(updateInfo.sectionIndex)-\(updateInfo.index)") let indexPath = IndexPath(row: updateInfo.index, section: updateInfo.sectionIndex) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath - if let tableView, - let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath), - let cell = tableView.cellForRow(at: originalIndexPath) as? BaseThemeCell { + if let cell = tableView.cellForRow(at: originalIndexPath) as? BaseThemeCell { cell.bind(rootElement: rootElement(viewItem: sectionViewItems[updateInfo.sectionIndex].viewItems[updateInfo.index])) } } else { // print("RELOAD TABLE VIEW") - tableView?.reloadData() + tableView.reloadData() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/DataSourceChain.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/DataSourceChain.swift index 82748c60ae..46cb09af8e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/DataSourceChain.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/DataSourceChain.swift @@ -14,19 +14,17 @@ protocol ISectionDataSourceDelegate: AnyObject { } extension ISectionDataSourceDelegate { - - func originalIndexPath(tableView: UITableView, dataSource: ISectionDataSource, indexPath: IndexPath) -> IndexPath { + func originalIndexPath(tableView _: UITableView, dataSource _: ISectionDataSource, indexPath: IndexPath) -> IndexPath { indexPath } - func height(tableView: UITableView, before dataSource: ISectionDataSource) -> CGFloat { + func height(tableView _: UITableView, before _: ISectionDataSource) -> CGFloat { .zero } - func height(tableView: UITableView, except dataSource: ISectionDataSource) -> CGFloat { + func height(tableView _: UITableView, except _: ISectionDataSource) -> CGFloat { .zero } - } class DataSourceChain: NSObject { @@ -42,12 +40,12 @@ class DataSourceChain: NSObject { private func sectionCount(tableView: UITableView, before section: Int) -> Int { dataSources - .prefix(section) - .map { $0.numberOfSections?(in: tableView) ?? 0 } - .reduce(0, +) + .prefix(section) + .map { $0.numberOfSections?(in: tableView) ?? 0 } + .reduce(0, +) } - private func sourceIndex(_ tableView: UITableView, `for` section: Int) -> Int { + private func sourceIndex(_ tableView: UITableView, for section: Int) -> Int { var shift = 0 for (index, dataSource) in dataSources.enumerated() { let count = dataSource.numberOfSections?(in: tableView) ?? 0 @@ -70,26 +68,24 @@ class DataSourceChain: NSObject { private func height(_ tableView: UITableView, dataSource: ISectionDataSource, section: Int) -> CGFloat { let numberOfRows = dataSource.tableView(tableView, numberOfRowsInSection: section) - return (0.. CGFloat { let sections = dataSource.numberOfSections?(in: tableView) ?? 0 - return (0.. IndexPath { guard let dataSourceIndex = dataSources.firstIndex(where: { $0.isEqual(dataSource) }) else { return indexPath @@ -105,9 +101,9 @@ extension DataSourceChain: ISectionDataSourceDelegate { } return dataSources - .prefix(dataSourceIndex) - .map { height(tableView, dataSource: $0) } - .reduce(0, +) + .prefix(dataSourceIndex) + .map { height(tableView, dataSource: $0) } + .reduce(0, +) } func height(tableView: UITableView, except dataSource: ISectionDataSource) -> CGFloat { @@ -118,23 +114,19 @@ extension DataSourceChain: ISectionDataSourceDelegate { let sources = dataSources.prefix(dataSourceIndex) + dataSources.suffix(from: dataSourceIndex + 1) return sources - .prefix(dataSourceIndex) - .map { height(tableView, dataSource: $0) } - .reduce(0, +) + .prefix(dataSourceIndex) + .map { height(tableView, dataSource: $0) } + .reduce(0, +) } - } extension DataSourceChain: ISectionDataSource { - func prepare(tableView: UITableView) { dataSources.forEach { $0.prepare(tableView: tableView) } } - } extension DataSourceChain: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { sectionCount(tableView: tableView, before: dataSources.count) } @@ -150,11 +142,9 @@ extension DataSourceChain: UITableViewDataSource { let sourcePath = sourcePath(tableView, forRowAt: indexPath) return dataSources[sourcePath.source].tableView(tableView, cellForRowAt: sourcePath.indexPath) } - } extension DataSourceChain: UITableViewDelegate { - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { let sourcePath = sourcePath(tableView, forRowAt: indexPath) dataSources[sourcePath.source].tableView?(tableView, willDisplay: cell, forRowAt: sourcePath.indexPath) @@ -185,15 +175,11 @@ extension DataSourceChain: UITableViewDelegate { let sourcePath = sourcePath(tableView, forRowAt: IndexPath(row: 0, section: section)) dataSources[sourcePath.source].tableView?(tableView, willDisplayHeaderView: view, forSection: sourcePath.indexPath.section) } - - } extension DataSourceChain { - private struct SourceIndexPath { let source: Int let indexPath: IndexPath } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift index 4ab8f8ea6e..974d75567f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift @@ -1,11 +1,11 @@ import Combine -import Foundation -import UIKit import ComponentKit +import Foundation import HUD import MarketKit -import ThemeKit import SectionsTableView +import ThemeKit +import UIKit class WalletTokenBalanceDataSource: NSObject { private let viewModel: WalletTokenBalanceViewModel @@ -24,58 +24,58 @@ class WalletTokenBalanceDataSource: NSObject { super.init() viewModel.playHapticPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.playHaptic() - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.playHaptic() + } + .store(in: &cancellables) viewModel.noConnectionErrorPublisher - .receive(on: DispatchQueue.main) - .sink { HudHelper.instance.show(banner: .noInternet) } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { HudHelper.instance.show(banner: .noInternet) } + .store(in: &cancellables) viewModel.openSyncErrorPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.openSyncError(wallet: $0, error: $1) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.openSyncError(wallet: $0, error: $1) + } + .store(in: &cancellables) viewModel.openReceivePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.openReceive(wallet: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.openReceive(wallet: $0) + } + .store(in: &cancellables) viewModel.openBackupRequiredPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.openBackupRequired(wallet: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.openBackupRequired(wallet: $0) + } + .store(in: &cancellables) viewModel.openCoinPagePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.openCoinPage(coin: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.openCoinPage(coin: $0) + } + .store(in: &cancellables) viewModel.$viewItem - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.sync(headerViewItem: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.sync(headerViewItem: $0) + } + .store(in: &cancellables) viewModel.$buttons - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.sync(buttons: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.sync(buttons: $0) + } + .store(in: &cancellables) sync(headerViewItem: viewModel.viewItem) sync(buttons: viewModel.buttons) @@ -91,14 +91,20 @@ class WalletTokenBalanceDataSource: NSObject { } if let tableView { - if let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: IndexPath(row: 0, section: 0)), - let headerCell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenBalanceCell { + let firstIndexPath = IndexPath(row: 0, section: 0) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: firstIndexPath) ?? firstIndexPath + + if let headerCell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenBalanceCell { bind(cell: headerCell) } headerViewItem?.customStates.enumerated().forEach { index, _ in - if let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: IndexPath(row: index, section: 1)), - let cell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenBalanceCustomAmountCell { + let indexPath = IndexPath(row: index, section: 1) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath + + if let cell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenBalanceCustomAmountCell { bind(cell: cell, row: index) } } @@ -108,9 +114,14 @@ class WalletTokenBalanceDataSource: NSObject { private func sync(buttons: [WalletModule.Button: ButtonState]) { self.buttons = buttons - if let tableView, - let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: IndexPath(row: 1, section: 0)), - let cell = tableView.cellForRow(at: originalIndexPath) as? BalanceButtonsCell { + guard let tableView else { + return + } + let indexPath = IndexPath(row: 1, section: 0) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath + + if let cell = tableView.cellForRow(at: originalIndexPath) as? BalanceButtonsCell { bind(cell: cell) } } @@ -136,7 +147,8 @@ class WalletTokenBalanceDataSource: NSObject { private func bind(cell: WalletTokenBalanceCustomAmountCell, row: Int) { guard let count = headerViewItem?.customStates.count, - let item = headerViewItem?.customStates.at(index: row) else { + let item = headerViewItem?.customStates.at(index: row) + else { return } cell.set(backgroundStyle: .externalBorderOnly, cornerRadius: .margin12, isFirst: row == 0, isLast: row == count - 1) @@ -145,7 +157,7 @@ class WalletTokenBalanceDataSource: NSObject { private func bindActions(cell: BalanceButtonsCell) { switch viewModel.element { - case .cexAsset(let cexAsset): + case let .cexAsset(cexAsset): cell.actions[.deposit] = { [weak self] in if let viewController = CexDepositModule.viewController(cexAsset: cexAsset) { let navigationController = ThemeNavigationController(rootViewController: viewController) @@ -158,7 +170,7 @@ class WalletTokenBalanceDataSource: NSObject { self?.parentViewController?.present(navigationController, animated: true) } } - case .wallet(let wallet): + case let .wallet(wallet): cell.actions[.send] = { [weak self] in if let viewController = SendModule.controller(wallet: wallet) { self?.parentViewController?.present(ThemeNavigationController(rootViewController: viewController), animated: true) @@ -188,7 +200,6 @@ class WalletTokenBalanceDataSource: NSObject { parentViewController?.present(viewController, animated: true) } - private func openReceive(wallet: Wallet) { guard let viewController = ReceiveAddressModule.viewController(wallet: wallet) else { return @@ -205,34 +216,32 @@ class WalletTokenBalanceDataSource: NSObject { private func openBackupRequired(wallet: Wallet) { let viewController = BottomSheetModule.viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "backup_required.title".localized, - items: [ - .highlightedDescription(text: "receive_alert.not_backed_up_description".localized(wallet.account.name, wallet.coin.name)) - ], - buttons: [ - .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [ weak self] in - guard let viewController = BackupModule.manualViewController(account: wallet.account) else { - return - } - - self?.parentViewController?.present(viewController, animated: true) - }, - .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [ weak self] in - let viewController = BackupModule.cloudViewController(account: wallet.account) - self?.parentViewController?.present(viewController, animated: true) - }, - .init(style: .transparent, title: "button.cancel".localized) - ] + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), + title: "backup_required.title".localized, + items: [ + .highlightedDescription(text: "receive_alert.not_backed_up_description".localized(wallet.account.name, wallet.coin.name)), + ], + buttons: [ + .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [weak self] in + guard let viewController = BackupModule.manualViewController(account: wallet.account) else { + return + } + + self?.parentViewController?.present(viewController, animated: true) + }, + .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak self] in + let viewController = BackupModule.cloudViewController(account: wallet.account) + self?.parentViewController?.present(viewController, animated: true) + }, + .init(style: .transparent, title: "button.cancel".localized), + ] ) parentViewController?.present(viewController, animated: true) } - } extension WalletTokenBalanceDataSource: ISectionDataSource { - func prepare(tableView: UITableView) { tableView.registerCell(forClass: WalletTokenBalanceCell.self) tableView.registerCell(forClass: BalanceButtonsCell.self) @@ -240,16 +249,14 @@ extension WalletTokenBalanceDataSource: ISectionDataSource { tableView.registerHeaderFooter(forClass: SectionColorHeader.self) self.tableView = tableView } - } extension WalletTokenBalanceDataSource: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { + func numberOfSections(in _: UITableView) -> Int { 1 + ((headerViewItem?.customStates.isEmpty ?? true) ? 0 : 1) } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: return 2 case 1: return headerViewItem?.customStates.count ?? 0 @@ -286,12 +293,10 @@ extension WalletTokenBalanceDataSource: UITableViewDataSource { return UITableViewCell() } - } extension WalletTokenBalanceDataSource: UITableViewDelegate { - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if let cell = cell as? WalletTokenBalanceCell { bind(cell: cell) } @@ -325,7 +330,7 @@ extension WalletTokenBalanceDataSource: UITableViewDelegate { } } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat { switch section { case 0: return .margin12 case 1: return .margin8 @@ -346,5 +351,4 @@ extension WalletTokenBalanceDataSource: UITableViewDelegate { default: () } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index 7776682656..e996178c80 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -70,6 +70,8 @@ class WalletTokenBalanceViewItemFactory { } else if case let .customSyncing(main, secondary, _) = item.state { let text = [main, secondary].compactMap { $0 }.joined(separator: " - ") return (text: text, dimmed: failedImageViewVisible(state: item.state)) + } else if case .stopped = item.state { + return (text: "balance.stopped".localized, dimmed: failedImageViewVisible(state: item.state)) } else { return secondaryValue(item: item, balanceHidden: balanceHidden) } @@ -84,14 +86,8 @@ class WalletTokenBalanceViewItemFactory { private func syncSpinnerProgress(state: AdapterState) -> Int? { switch state { - case let .syncing(progress, _): - if let progress = progress { - return max(minimumProgress, progress) - } else { - return infiniteProgress - } - case .customSyncing: - return infiniteProgress + case let .syncing(progress, _), .customSyncing(_, _, let progress): + return progress.map { max(minimumProgress, $0) } ?? infiniteProgress default: return nil } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift index 3b42ca122a..381254ab34 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift @@ -103,8 +103,11 @@ class WalletTokenListDataSource: NSObject { if let tableView { updateIndexes.forEach { - if let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: IndexPath(row: $0, section: 0)), - let cell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenCell { + let indexPath = IndexPath(row: $0, section: 0) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath + + if let cell = tableView.cellForRow(at: originalIndexPath) as? WalletTokenCell { let hideTopSeparator = originalIndexPath.row == 0 && originalIndexPath.section != 0 bind(cell: cell, index: $0, hideTopSeparator: hideTopSeparator, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift index 13641776e6..ce6b087df8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift @@ -29,6 +29,12 @@ class WalletTokenListViewItemFactory { return .syncing(progress: progress, syncedUntil: lastBlockDate.map { DateHelper.instance.formatSyncedThroughDate(from: $0) }) } else if case let .customSyncing(main, secondary, _) = item.state { return .customSyncing(main: main, secondary: secondary) + } else if case .stopped = item.state { + return .amount(viewItem: BalanceSecondaryAmountViewItem( + descriptionValue: (text: "balance.stopped".localized, dimmed: false), + secondaryValue: nil, + diff: nil + )) } else { return .amount(viewItem: BalanceSecondaryAmountViewItem( descriptionValue: (text: item.element.coin?.name, dimmed: false), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index da6e27e4a6..bcddb0eb6c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -29,6 +29,12 @@ class WalletViewItemFactory { return .syncing(progress: progress, syncedUntil: lastBlockDate.map { DateHelper.instance.formatSyncedThroughDate(from: $0) }) } else if case let .customSyncing(main, secondary, _) = item.state { return .customSyncing(main: main, secondary: secondary) + } else if case .stopped = item.state { + return .amount(viewItem: BalanceSecondaryAmountViewItem( + descriptionValue: (text: "balance.stopped".localized, dimmed: false), + secondaryValue: nil, + diff: nil + )) } else { return .amount(viewItem: BalanceSecondaryAmountViewItem( descriptionValue: rateValue(rateItem: item.priceItem), diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 42b3305af7..efb7dbaa66 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -290,6 +290,7 @@ Go to Settings - > %@ and allow access to the camera."; "balance.rate_per_coin" = "%@ per %@"; "balance.syncing" = "Syncing..."; "balance.searching" = "Searching transactions..."; +"balance.stopped" = "Stopped"; "balance.downloading_sapling" = "Downloading Sapling... %d%%"; "balance.downloading_blocks" = "Downloading Blocks"; "balance.scanning_blocks" = "Scanning Blocks"; From fe75e752bf51520d3df7786bd235f6b568dc398b Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 26 Sep 2023 19:28:47 +0600 Subject: [PATCH 14/63] New implementation of Passcode related features --- .../project.pbxproj | 441 +++++++++++------- .../Icons/backspace_24.imageset/Contents.json | 22 + .../backspace_24.imageset/backspace@2x.png | Bin 0 -> 723 bytes .../backspace_24.imageset/backspace@3x.png | Bin 0 -> 1028 bytes .../UnstoppableWallet/Core/App.swift | 68 +-- .../Core/Managers/AppManager.swift | 31 +- .../Core/Managers/BiometryManager.swift | 43 ++ .../Core/Managers/BlurManager.swift | 22 +- .../Core/Managers/LockManager.swift | 62 +++ .../Core/Managers/LockoutManager.swift | 86 ++++ .../Core/Managers/PasscodeManager.swift | 132 ++++++ .../Models/BiometryType.swift | 18 + .../Modules/Launch/LaunchModule.swift | 19 +- .../Modules/Launch/LaunchService.swift | 15 +- .../Modules/LockScreen/LockScreenModule.swift | 25 - .../LockScreen/LockScreenViewController.swift | 25 - .../Modules/Main/MainBadgeService.swift | 85 ++-- .../Modules/Main/MainModule.swift | 7 +- .../Modules/Main/MainService.swift | 22 +- .../ManageAccount/ManageAccountModule.swift | 24 +- .../ManageAccount/ManageAccountService.swift | 41 +- .../ManageAccountViewController.swift | 225 +++++---- .../ManageAccountViewModel.swift | 34 +- .../PrivateKeys/PrivateKeysModule.swift | 4 +- .../PrivateKeys/PrivateKeysService.swift | 19 +- .../PrivateKeysViewController.swift | 119 +++-- .../PrivateKeys/PrivateKeysViewModel.swift | 6 +- .../CreateDuressPasscodeViewModel.swift | 24 + .../Manage/CreatePasscodeModule.swift | 40 ++ .../Manage/CreatePasscodeViewModel.swift | 41 ++ .../Manage/EditDuressPasscodeViewModel.swift | 24 + .../Passcode/Manage/EditPasscodeModule.swift | 13 + .../Manage/EditPasscodeViewModel.swift | 24 + .../Passcode/Manage/SetPasscodeView.swift | 35 ++ .../Manage/SetPasscodeViewModel.swift | 59 +++ .../Modules/Passcode/NumPadView.swift | 68 +++ .../Modules/Passcode/PasscodeView.swift | 110 +++++ .../Passcode/Unlock/AppUnlockViewModel.swift | 42 ++ .../Passcode/Unlock/BaseUnlockViewModel.swift | 111 +++++ .../Passcode/Unlock/ModuleUnlockView.swift | 18 + .../Unlock/ModuleUnlockViewModel.swift | 29 ++ .../Passcode/Unlock/UnlockModule.swift | 30 ++ .../Modules/Passcode/Unlock/UnlockView.swift | 53 +++ .../Settings/Main/MainSettingsModule.swift | 24 +- .../Settings/Main/MainSettingsService.swift | 33 +- .../Settings/Main/MainSettingsViewModel.swift | 4 +- .../Security/SecuritySettingsModule.swift | 6 +- .../Security/SecuritySettingsView.swift | 177 +++---- .../Security/SecuritySettingsViewModel.swift | 119 ++--- .../WalletConnectAppShowModule.swift | 2 +- .../WalletConnectAppShowService.swift | 9 +- .../UserInterface/LockDelegate.swift | 11 + .../UserInterface/PinKitDelegate.swift | 14 - .../SwiftUI/InteractiveDismiss.swift | 52 +++ .../SwiftUI/SecondaryButtonStyle.swift | 35 ++ .../en.lproj/Localizable.strings | 71 ++- 56 files changed, 2025 insertions(+), 848 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/Contents.json create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/backspace@2x.png create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/backspace@3x.png create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Managers/BiometryManager.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Models/BiometryType.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenModule.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenViewController.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/PinKitDelegate.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InteractiveDismiss.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index d66391ebe2..589cee4219 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -45,11 +45,13 @@ 11B35068706B5C13888F7E22 /* EvmSyncSourceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */; }; 11B35068E05BC58C6C9A93D7 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35872950C107E4810AB6B /* AccountManager.swift */; }; 11B3506ECD6D4D8D0C7717B6 /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */; }; + 11B3507578AF3163AAC8C494 /* EditPasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */; }; 11B35076A96AD17809CE1F62 /* ClickableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D179817528224E926D1 /* ClickableRow.swift */; }; 11B350778F2EA9FF364558E4 /* RestoreBinanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2F781F7EDA04E955BB /* RestoreBinanceViewModel.swift */; }; 11B3507DDEBA587C023CE898 /* DashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358CA18471A93188933B4 /* DashAdapter.swift */; }; 11B3507F17791BC895872490 /* BrandFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E930EFEBA58B8F1FDC4 /* BrandFooterView.swift */; }; 11B3507F2690EB4A67271333 /* CexWithdrawConfirmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A36FFDA63E9668F1B24 /* CexWithdrawConfirmService.swift */; }; + 11B35083FB285F6692754E9B /* BiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */; }; 11B35085F61E874613B2B882 /* CoinInvestorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A0F912218FEC2A196C0 /* CoinInvestorsViewController.swift */; }; 11B350860CB79E9C5F032166 /* ManageAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355949F6D268EF1977DC9 /* ManageAccountViewModel.swift */; }; 11B3508846C7EB6EDC26E52C /* ManageAccountsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350911E00460DA8925165 /* ManageAccountsService.swift */; }; @@ -61,6 +63,7 @@ 11B3509603841DEF2FC02F24 /* PublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5A3CC8C229C0849756 /* PublicKeysViewController.swift */; }; 11B3509F4BE82888A15D88E8 /* AddEvmSyncSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F8A77664848396B7567 /* AddEvmSyncSourceService.swift */; }; 11B350A24C41C436EA7DC598 /* PasteInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3597E2B288ECD850C1DFE /* PasteInputCell.swift */; }; + 11B350A27335B798701EE7B3 /* InteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */; }; 11B350A41E2B2A6DF2E9B4FF /* NftCollectionOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */; }; 11B350ACC851B0F8C911AC3E /* ActiveAccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354C4B46DF1A50103F026 /* ActiveAccountStorage.swift */; }; 11B350ADB6D0D0C3E97F73D6 /* NftCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3580953728946194D1187 /* NftCollectionViewController.swift */; }; @@ -83,6 +86,7 @@ 11B350E3383E4435B0F919D7 /* CexWithdrawConfirmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A36FFDA63E9668F1B24 /* CexWithdrawConfirmService.swift */; }; 11B350E34285A392F34198D0 /* UnlinkModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529D276325D741CAEEF5 /* UnlinkModule.swift */; }; 11B350E923A4E51AAF9D2828 /* BarsProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A686DD5BA335FEB6BEB /* BarsProgressView.swift */; }; + 11B350EA36A2113C23047911 /* BiometryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6223272C5B3E261A24 /* BiometryManager.swift */; }; 11B350ECEE8748562D27249F /* CexWithdrawNetworkSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351C522855F29C6B038D3 /* CexWithdrawNetworkSelectViewController.swift */; }; 11B350EDDD1C8A1506043C4D /* BaseCurrencySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352034B036C9CB7A52724 /* BaseCurrencySettingsViewController.swift */; }; 11B350F12C3CA54080C16031 /* ManageWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D645B4468F84EADB7 /* ManageWalletsViewController.swift */; }; @@ -138,6 +142,7 @@ 11B3518578A4531274D73A21 /* UnlinkWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35592753D3F2A9CCA5809 /* UnlinkWatchViewController.swift */; }; 11B3518B594ECB199242C5CB /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B3518BEA8865CADA5DA684 /* LaunchScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEC0AB0B09C7E4209A /* LaunchScreenManager.swift */; }; + 11B3518C9B837CB6C740AABB /* CreatePasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */; }; 11B351909FE0FA637B5B1EC5 /* CoinValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3513A7417C236F56E5383 /* CoinValue.swift */; }; 11B35191E1A9626DF75D6A51 /* MarkdownViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359B9C1E0BB4D32599695 /* MarkdownViewModel.swift */; }; 11B35193A8E75B6D6117FBC7 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352978EC570F59F442BD5 /* View.swift */; }; @@ -159,6 +164,7 @@ 11B351DB86D936CC17C4A635 /* PrivateKeysModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55BE7717A87DA6FC43 /* PrivateKeysModule.swift */; }; 11B351DDFD1A7BC393EFA6E1 /* CustomToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356E4E27F5C12FC3859D1 /* CustomToken.swift */; }; 11B351DED0D2632D24084263 /* EvmUpdateStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E5C80435645132BCDD2 /* EvmUpdateStatus.swift */; }; + 11B351E088F87C02C870DDB8 /* SetPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */; }; 11B351E31777084419C3B2C0 /* CreateAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358145A0D9F93ACBC0301 /* CreateAccountModule.swift */; }; 11B351E4BD2180A5D6D59F23 /* PoolGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F4747A6B256C31EC7C /* PoolGroup.swift */; }; 11B351E6C8EF6B22C5F8B98D /* NftCollectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529499CD211CC5A21CA2 /* NftCollectionService.swift */; }; @@ -202,6 +208,7 @@ 11B35249B578809A019A2327 /* FaqViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B70808A7D2484859EFD /* FaqViewController.swift */; }; 11B3524E749AEC08F13CF4AE /* BlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573A58F426A72669A948 /* BlockchainSettingsViewModel.swift */; }; 11B3524EBBAC04F202622104 /* PrivateKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350D8D6E2FAD43FFCA8BB /* PrivateKeysViewController.swift */; }; + 11B35251E1B11235D00E6565 /* UnlockModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354506A9B41DCD49B2807 /* UnlockModule.swift */; }; 11B3525364EADC132A262166 /* WatchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3531B7AC10796F8D26455 /* WatchModule.swift */; }; 11B35258C072691B5BD7C41E /* ExtendedKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A774105F0F012935845 /* ExtendedKeyViewController.swift */; }; 11B3525DD29BC2286526669F /* PoolGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F4747A6B256C31EC7C /* PoolGroup.swift */; }; @@ -210,6 +217,7 @@ 11B3526AA8E758606BC0CE38 /* CexWithdrawService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3548F0E1223B08D3B7F0C /* CexWithdrawService.swift */; }; 11B3526D1747C11291F2D998 /* CoinTreasuriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3580ECB328146E94D4359 /* CoinTreasuriesService.swift */; }; 11B352712EC6F2C7F6965443 /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */; }; + 11B3527C3BD088DCCA6959C3 /* ModuleUnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */; }; 11B3527D20636D21F0F45C80 /* CurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */; }; 11B3527F2E2D46DC307E6D3D /* RestoreSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E343901BA7DE01181CB /* RestoreSettingsViewModel.swift */; }; 11B35281808DE30B8D717B73 /* PublicKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C285327E4099656DBA8 /* PublicKeysViewModel.swift */; }; @@ -259,11 +267,14 @@ 11B352FF1C3FA152E2EEFE67 /* EvmMethodLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E3F01D5A5CFE5A4E94B /* EvmMethodLabel.swift */; }; 11B3530088E70831A648EC63 /* CexDepositNetworkRaw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502198C667A95C21DCF3 /* CexDepositNetworkRaw.swift */; }; 11B35307AE70D7996F483DAE /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */; }; + 11B353096900F82EDF084F3B /* SetPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */; }; 11B3530E7755A4882F7E0C0A /* SelectorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353C09FE554834C760777 /* SelectorModule.swift */; }; 11B35311CEEC40EA3089293D /* SubscriptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351CD91AE01747F66E746 /* SubscriptionInfoViewController.swift */; }; 11B35313AC2978EE7DBC3EA9 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354712C102B954BCEE258 /* FilterView.swift */; }; + 11B3531640EE1F9D29B63325 /* AppUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BC07CC9E523971ED20E /* AppUnlockViewModel.swift */; }; 11B35317F0BD30311142EA5D /* EvmPrivateKeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D652AE2C3D9E084AC0F /* EvmPrivateKeyModule.swift */; }; 11B353198C1F47A679D3CAAC /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359DAB464176D8EBFC8A0 /* MarkdownTextView.swift */; }; + 11B3531D97E44DA1D8280C35 /* EditDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */; }; 11B3531F75BB6113B49DC088 /* BarPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3512EF5B66B852F5E05FB /* BarPageControl.swift */; }; 11B35321C9FCFD1DFA4401A3 /* SendEvmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3540BDD94203AFD41C6C7 /* SendEvmService.swift */; }; 11B353260AE7B998C07955E6 /* MarketCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */; }; @@ -284,6 +295,7 @@ 11B3534B12C5E7596E4953F0 /* RestoreSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35336293A4473DD9F5C8B /* RestoreSettingsModule.swift */; }; 11B3534B567884E30A871F32 /* AddTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */; }; 11B3534EF58DAC9E15DC49A5 /* BackupVerifyWordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6399E5264BFFA32F08 /* BackupVerifyWordsViewController.swift */; }; + 11B35353A5C1E254839CD61B /* InteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */; }; 11B35355FF6481B50773C868 /* NftCollectionOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4F17D4CC8E89F7DC3B /* NftCollectionOverviewService.swift */; }; 11B35356AA508971CA689290 /* CoinAnalyticsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3540B41309A446C1DDB83 /* CoinAnalyticsViewController.swift */; }; 11B35357032B368120BA1C06 /* TestNetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EE072CE5471B0DFF841 /* TestNetManager.swift */; }; @@ -311,6 +323,7 @@ 11B353AE1D1D9A8E5CF8E7A2 /* BaseTransactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35935EF1B2237E0289669 /* BaseTransactionsViewModel.swift */; }; 11B353B085BD167026DE4B5B /* CustomToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356E4E27F5C12FC3859D1 /* CustomToken.swift */; }; 11B353C149EC597A051E8310 /* BinanceWithdrawHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576C0D8464F74D44EE92 /* BinanceWithdrawHandler.swift */; }; + 11B353C7553F40CEEA28678B /* PasscodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */; }; 11B353C8EE86F13C1BBD601C /* BaseCurrencySettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D26C9E9E47E4FD46772 /* BaseCurrencySettingsService.swift */; }; 11B353CB3021FA5266D07607 /* MarketWatchlistToggleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3562819DF141457837340 /* MarketWatchlistToggleService.swift */; }; 11B353CBE0B92639753A9591 /* FaqRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358D98E1FBA6909D352DA /* FaqRepository.swift */; }; @@ -371,6 +384,7 @@ 11B3547FBABFB1F67D778E10 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C996B6E9B084C05A087 /* TextFieldCell.swift */; }; 11B35480339EC26092522079 /* BalanceTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356E71050EDF5C82FEFD9 /* BalanceTopView.swift */; }; 11B35480CA91E0A62617B83A /* EvmNftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E4B97A593E898724335 /* EvmNftRecord.swift */; }; + 11B35481F59793CD9C95B324 /* CreatePasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */; }; 11B35494E4BA9BF6A3DAA6D6 /* NftMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E2ACF02E2C35EFAE9FA /* NftMetadataService.swift */; }; 11B354A1F79EDA1E58E50418 /* WalletTokenListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */; }; 11B354A3BC2968C083771FC1 /* BlockchainSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F0A42CE7144C82D634 /* BlockchainSettingsModule.swift */; }; @@ -420,6 +434,7 @@ 11B3553109794AE192BF7591 /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; 11B35531B3F80D06EF040301 /* CoinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B2D07C69579BAEC997 /* CoinType.swift */; }; 11B355342F86DF79AE7000B9 /* Cex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D6718A1DEB73A0CEC02 /* Cex.swift */; }; + 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */; }; 11B3553D2E9EEC05401B724A /* CexDepositViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0A0EC524FBC663BEA5 /* CexDepositViewItemFactory.swift */; }; 11B3553D410457FB50DEFAE4 /* ManageAccountsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A8342513D5834B2145A /* ManageAccountsViewModel.swift */; }; 11B3553ED96875D0B6E5B5C4 /* BirthdayInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35239B1D2F94B326FC703 /* BirthdayInputViewController.swift */; }; @@ -481,6 +496,7 @@ 11B3560586CBAB617211F003 /* Caution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D96CF03878016FC38FD /* Caution.swift */; }; 11B35608F7D19B3E6318CB22 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352972B14FA6EBEFD6904 /* Text.swift */; }; 11B3560E158C55624C466E27 /* GuidesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E511F9D2B6C65792324 /* GuidesViewController.swift */; }; + 11B3561679C05C31F16EDC77 /* BaseUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */; }; 11B3561A469C906B67F24459 /* FeeRateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359BBFCD82C3C6DC06F96 /* FeeRateProvider.swift */; }; 11B3561E7DF566A274210E01 /* EvmSyncSourceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */; }; 11B3562466F0ADD109244158 /* NftCollectionAssetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35100DD6E2DBF905FD19B /* NftCollectionAssetsModule.swift */; }; @@ -488,10 +504,12 @@ 11B3562EE896D758066FEECB /* CexDepositNetworkSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35850DF16D11D45C44A60 /* CexDepositNetworkSelectService.swift */; }; 11B35631BD5C6570C9359BEC /* RowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButton.swift */; }; 11B35631E5455A54854A2A6F /* RestoreMnemonicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55DCC92BED4FA87CA0 /* RestoreMnemonicService.swift */; }; + 11B356330572A72E56DC2FEA /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */; }; 11B35633B952154A098532A4 /* NftCollectionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35708A630D70385F34A8B /* NftCollectionModule.swift */; }; 11B3563B5D19C7A4EDFC8FC1 /* EvmAccountRestoreStateStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35999E6C5518115365410 /* EvmAccountRestoreStateStorage.swift */; }; 11B3563BF84B730CB695FAB4 /* FaqViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C09B59EF5DEB6D7EB07 /* FaqViewModel.swift */; }; 11B3563E71C4AC16DFE8AB76 /* ActiveAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F98E89F83A30870F404 /* ActiveAccount.swift */; }; + 11B3564236FEF4E5ACC8C838 /* UnlockModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354506A9B41DCD49B2807 /* UnlockModule.swift */; }; 11B356448BC036CD117EB7DC /* BrandFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355DF40EB498107EDAA4A /* BrandFooterCell.swift */; }; 11B3564706CBCF6F6A30FF65 /* EnabledWalletCacheStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35025FD5E96FD1AB359E9 /* EnabledWalletCacheStorage.swift */; }; 11B356476D5E88F21C297B52 /* ManageAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A38C734DF3157C84678 /* ManageAccountViewController.swift */; }; @@ -510,12 +528,14 @@ 11B356762C9C6E62706B874B /* BottomSheetModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FF2DB6F840D867FD2F /* BottomSheetModule.swift */; }; 11B35678A2523AEDBE824743 /* FormCautionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F5D363E9B1D6C9120F /* FormCautionCell.swift */; }; 11B35683BF79A4A5AECA616F /* CoinAuditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566146F353C8B6C919CA /* CoinAuditsViewController.swift */; }; + 11B3568483AFF7864F050E0F /* LockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F57D462E2C9E9AEF67C /* LockManager.swift */; }; 11B3568EFCE57D12D377F7E4 /* ManageAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D805327837A9E81801C /* ManageAccountsViewController.swift */; }; 11B35696E9CD808522BEFCD6 /* BlockchainSettingRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353FA8AE18587D516754B /* BlockchainSettingRecordStorage.swift */; }; 11B356A19A721D3557D7213C /* CoinReportsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E767DA0B5D7C0DAF203 /* CoinReportsViewModel.swift */; }; 11B356A2666F52C272B4E465 /* WalletTokenListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35136653741E9703E61DE /* WalletTokenListViewModel.swift */; }; 11B356A300ED689602ACD35D /* BalanceCoinIconHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3560C1FC3F73833FA4439 /* BalanceCoinIconHolder.swift */; }; 11B356A4B22FA16BE27AFAB1 /* LogRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35822E26E7298100CD69D /* LogRecordStorage.swift */; }; + 11B356A5B50D4E6EF2282398 /* EditDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */; }; 11B356A8E75C3F3C9FC4E530 /* ShortcutInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1C200EC15159154E3F /* ShortcutInputCell.swift */; }; 11B356B1E072D1621D38499E /* EvmAddressLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353282C7000D3BDFC7FD0 /* EvmAddressLabel.swift */; }; 11B356B6BEB9562F670DFAC5 /* Caution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D96CF03878016FC38FD /* Caution.swift */; }; @@ -583,6 +603,7 @@ 11B3577BDCF978797E6C283E /* RestoreService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3596ECFEECF17ADB3BAEF /* RestoreService.swift */; }; 11B35780A8D573216864D763 /* InputSecondaryButtonWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D5C4EEEAABF83A67D95 /* InputSecondaryButtonWrapperView.swift */; }; 11B35783103DBC24D9EB7E85 /* Guide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B4C04282FDBB1B6563 /* Guide.swift */; }; + 11B35787F5BA973364784F3B /* LockoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576F224007FD4154EBE8 /* LockoutManager.swift */; }; 11B357975E11BDDCEAA491B4 /* EnabledWallet_v_0_10.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A0B705D8EABC5B6827 /* EnabledWallet_v_0_10.swift */; }; 11B3579C9B49D3B2F1DB389F /* TransactionsCoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D7E1CB9D978EE1BC15 /* TransactionsCoinSelectViewController.swift */; }; 11B357A607396E857705024F /* WalletTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B64097CCFA552310E3D /* WalletTokenCell.swift */; }; @@ -612,12 +633,14 @@ 11B357FDC1C6BD6C39FE6853 /* MarketAdvancedSearchResultModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */; }; 11B357FE4C2E1EC8E26ED68F /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */; }; 11B357FF94D326846E12B940 /* WalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D547F1BB38D2AD6AD5 /* WalletManager.swift */; }; + 11B358006AEB85BBE0BF47A7 /* EditPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */; }; 11B358033DAB0FF23CF0E309 /* NftActivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E034126F57DB7B4263 /* NftActivityService.swift */; }; 11B35804545D048C0EDB8089 /* EvmSyncSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502637A858E6DDF9471B /* EvmSyncSource.swift */; }; 11B3580696C6391D4C125245 /* EvmAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B143F359BE790EC392B /* EvmAddressService.swift */; }; 11B358092D442440DAAE8AC0 /* CoinSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353F1E3B5875396F03E0D /* CoinSelectService.swift */; }; 11B3580ABF4CD61826D0FCBB /* ReceiveBitcoinCashCoinTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A81AD46F48B63E59ED3 /* ReceiveBitcoinCashCoinTypeViewModel.swift */; }; 11B3580B9C21B55ACC07B043 /* AdapterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4E49ED2D2BF8E60863 /* AdapterManager.swift */; }; + 11B3580CD18A931ABAA6C122 /* BiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */; }; 11B3580E4A964C65BF8EDDE9 /* StackViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AAE4114A56DF13ECF0F /* StackViewCell.swift */; }; 11B358122FE64E16EC25F095 /* MarketOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */; }; 11B358137165A40C30218032 /* TermsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B462980B0617E11FB05 /* TermsModule.swift */; }; @@ -644,6 +667,7 @@ 11B3584FC09B4448EB4DFBFD /* CoinMarketsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353CFE743D50B049A3390 /* CoinMarketsViewModel.swift */; }; 11B35854327D3A8CC787E985 /* WatchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3531B7AC10796F8D26455 /* WatchModule.swift */; }; 11B35854532EEA0AE6AC2010 /* RateAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FA360A91FDE3EB0B85C /* RateAppManager.swift */; }; + 11B3585461729AD144448426 /* NumPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E1284B381BE56AC663 /* NumPadView.swift */; }; 11B35858954659DEE0C44618 /* TransactionsViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D89546EBA13B01A1ED /* TransactionsViewItemFactory.swift */; }; 11B3585AC6E5D92F98A71758 /* RestoreSettingRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3546480B733000550BEB6 /* RestoreSettingRecord.swift */; }; 11B3585E88319E5BBBB9CD3F /* EvmPrivateKeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D652AE2C3D9E084AC0F /* EvmPrivateKeyModule.swift */; }; @@ -653,6 +677,7 @@ 11B358657FCC50C9B3A10294 /* ManageWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D645B4468F84EADB7 /* ManageWalletsViewController.swift */; }; 11B3586BF6AC0538272E71A4 /* NftCollectionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35708A630D70385F34A8B /* NftCollectionModule.swift */; }; 11B35871BA700133050E9241 /* CexWithdrawViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2465CB748311AF03D5 /* CexWithdrawViewModel.swift */; }; + 11B3587D9E89A97F63CD0C5A /* EditPasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */; }; 11B3587DEC9342190880D3C3 /* TransactionsCoinSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF9B3B86F74961FADE1 /* TransactionsCoinSelectModule.swift */; }; 11B3587EF674C1E8EEE61DE7 /* MarkdownBlockQuoteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3552D3F84BA594EFE964C /* MarkdownBlockQuoteCell.swift */; }; 11B358807588598C4815BCE0 /* WalletCexElementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7CCC41913AA8D36CBC /* WalletCexElementService.swift */; }; @@ -675,6 +700,7 @@ 11B358AA46441AF0A7DCAA89 /* NftActivityModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35252F90F25774BDD2CB3 /* NftActivityModule.swift */; }; 11B358AE1CCD292DF2D2AC42 /* PrivateKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A9DB4112F41D7FCAC12 /* PrivateKeysViewModel.swift */; }; 11B358AE5241256C9AAFB588 /* SyncMode_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB98A27269A510F40EE /* SyncMode_v_0_24.swift */; }; + 11B358B004B48988A1F6D888 /* AppUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BC07CC9E523971ED20E /* AppUnlockViewModel.swift */; }; 11B358B0576F63BE43947DD5 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35614C6E244926AF48701 /* Account.swift */; }; 11B358B0A260AC250BFE65DE /* CexDepositNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35799B0DCCF655F0766BF /* CexDepositNetwork.swift */; }; 11B358B0C17F5F8F7764BDBE /* CellComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355436F62829DBE3C92B4 /* CellComponent.swift */; }; @@ -690,14 +716,17 @@ 11B358D913A404C1DA7D4E0E /* CoinInvestorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */; }; 11B358DC6827FC6035BF3225 /* TokenQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353684493AFDF3711DF2B /* TokenQuery.swift */; }; 11B358DC90F3372DB98BD4A5 /* CexDepositNetworkSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DDED1BC5B541DB6B4B3 /* CexDepositNetworkSelectViewModel.swift */; }; + 11B358E12CBE7D1B687AE788 /* NumPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E1284B381BE56AC663 /* NumPadView.swift */; }; 11B358E4C529863AFFF8806F /* ReceiveAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532A1DC90E3D0E3403F8 /* ReceiveAddressViewModel.swift */; }; 11B358E508ECA92493A9A3FD /* CoinPageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BFAAAE3B1357B5CE944 /* CoinPageService.swift */; }; 11B358E7A9BC36B1B562A5B4 /* NftMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */; }; + 11B358EC0A19773B1455CF62 /* LockoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576F224007FD4154EBE8 /* LockoutManager.swift */; }; 11B358F135917294A49D75F0 /* CoinMarketsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353CFE743D50B049A3390 /* CoinMarketsViewModel.swift */; }; 11B358F1E7C72C1F42EC456F /* CoinToggleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CDE31673BA1673B620 /* CoinToggleViewModel.swift */; }; 11B358F2CD17616038016E59 /* NftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354B32BD428041237570A /* NftRecord.swift */; }; 11B358F9D6842ECD84E80752 /* MarketCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352AC4F5BE70D055293D7 /* MarketCategoryViewModel.swift */; }; 11B3590189E28D408E207E19 /* CexDepositService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356B9F833E1AEE0D6D589 /* CexDepositService.swift */; }; + 11B35902128F12FB06B0CA5E /* BaseUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */; }; 11B359029FFF4106B703694C /* CexDepositNetworkSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BBC5BBCC258824A80F3 /* CexDepositNetworkSelectModule.swift */; }; 11B35907781848EB0C20759A /* RestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A64E88BD68714D4D07 /* RestoreViewController.swift */; }; 11B3590B327A6CC55E64B8B2 /* HsToolKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D747108CE6727D3103D /* HsToolKit.swift */; }; @@ -705,6 +734,7 @@ 11B3591204E6399DA9AA203E /* WatchEvmAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A8370C726989F4F456E /* WatchEvmAddressViewModel.swift */; }; 11B359131D838F3191A8C520 /* BtcBlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358830357DB1F87FCA006 /* BtcBlockchainSettingsViewModel.swift */; }; 11B3591854D77701EB7218BC /* CoinInvestorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */; }; + 11B3591C77EE71054BF819D0 /* SetPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */; }; 11B3591DF0CC1D367C1241AF /* ExtendedKeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F1248EDA20F7141AB8 /* ExtendedKeyModule.swift */; }; 11B35920CB7EA5E3322F6D7F /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */; }; 11B359257D417D73971FF400 /* MarketOverviewNftCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351CEB402BC8F806365D9 /* MarketOverviewNftCollectionsService.swift */; }; @@ -719,6 +749,7 @@ 11B3594DD9B54E11190B4CD5 /* PoolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359636E1AA1BC72CF7B11 /* PoolProvider.swift */; }; 11B3594FCD35038663CD4FEF /* ReceiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFED85A9315089223E3 /* ReceiveViewModel.swift */; }; 11B359515EE181B7C3D773D3 /* MarketOverviewTopPlatformsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3582259AD3A0C55CF6D2C /* MarketOverviewTopPlatformsService.swift */; }; + 11B35951600F986F1C424E24 /* PasscodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */; }; 11B35953182487E864EB4946 /* ActivateSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E1107158B6A2BF2149 /* ActivateSubscriptionService.swift */; }; 11B35953404F5C8903DDA70D /* RecipientAddressCautionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358B22BAF021E8FA028BF /* RecipientAddressCautionCell.swift */; }; 11B35959AAF414186CE39698 /* AddTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E8892971578502EF33 /* AddTokenViewModel.swift */; }; @@ -957,6 +988,7 @@ 11B35C3418D6E7D94CB5C2AF /* RecoveryPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A0B1AE5F612E6A5FEE /* RecoveryPhraseService.swift */; }; 11B35C343013C028B5D20B4A /* EvmKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E9873D262F88015F120 /* EvmKit.swift */; }; 11B35C357E406E8BC5BF1D94 /* NftMetadataSyncRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B58E21336A3DF5A9B45 /* NftMetadataSyncRecord.swift */; }; + 11B35C3A0B6DE83A66371224 /* SetPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */; }; 11B35C3AFFA5B40481AF15B9 /* AccountRecord_v_0_19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F6C5F6ABC288511AF0 /* AccountRecord_v_0_19.swift */; }; 11B35C43886D9A0F0C69EF33 /* EvmAccountRestoreStateStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35999E6C5518115365410 /* EvmAccountRestoreStateStorage.swift */; }; 11B35C47A06C0A4F7231C511 /* NftCollectionAssetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35100DD6E2DBF905FD19B /* NftCollectionAssetsModule.swift */; }; @@ -968,6 +1000,7 @@ 11B35C5388370450DAF65C5B /* EvmUpdateStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E5C80435645132BCDD2 /* EvmUpdateStatus.swift */; }; 11B35C59583AFCDFD828B9D1 /* TextFieldStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35921FBDF6F9BBAA88803 /* TextFieldStackView.swift */; }; 11B35C5E7A90AA7B302EB0CD /* MarketListMarketFieldDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B060BDF272932D3522 /* MarketListMarketFieldDecorator.swift */; }; + 11B35C5F856FB531028F8C0A /* CreateDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3501625BDD3F7D9BEA2F5 /* CreateDuressPasscodeViewModel.swift */; }; 11B35C68D57727AB0DAC7753 /* NftAddressMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEB24CDB82D3F4E7C0 /* NftAddressMetadata.swift */; }; 11B35C7425B861D2F32384E8 /* ListSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350C0CB7083E2738D356C /* ListSectionHeader.swift */; }; 11B35C78C16D1F89FBC8F222 /* ReceiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D1A38D53951CEE6F84 /* ReceiveViewController.swift */; }; @@ -983,6 +1016,7 @@ 11B35C8D53F838E7E5CA6EEC /* NftStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502AEB7EF95A590A7B1B /* NftStorage.swift */; }; 11B35C8E09922F59B200E347 /* MarketListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3596381A93F3A3D2575D6 /* MarketListViewController.swift */; }; 11B35C906C5278060BD5A04C /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E61AB3FB570A4F7C66 /* Wallet.swift */; }; + 11B35C9570D3C283E9C943D5 /* CreatePasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */; }; 11B35C95EA77972246D5F3BD /* CexAssetRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD211091A7C8619CEA2 /* CexAssetRecordStorage.swift */; }; 11B35CA25E02E397E167EEC3 /* QrCodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356F4578E266268264021 /* QrCodeCell.swift */; }; 11B35CA6259EDA3708695416 /* FaqUrlHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35759E226171A4969E66E /* FaqUrlHelper.swift */; }; @@ -1037,6 +1071,7 @@ 11B35D46B65772A1CC17B099 /* MarketGlobalTvlMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8E1106E31D68FD9181D /* MarketGlobalTvlMetricViewController.swift */; }; 11B35D4B4DE7C4620C34AA11 /* AddEvmSyncSourceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35504934CE3C31D523F82 /* AddEvmSyncSourceModule.swift */; }; 11B35D4CAE7D1CD1C169EDD4 /* TextDropDownAndSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C62F476065C11EE049 /* TextDropDownAndSettingsView.swift */; }; + 11B35D4CF0FBE2496CED70E4 /* EditPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */; }; 11B35D4E566D1F5D24825050 /* FeeRateProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D88585F2BBFA56CB77 /* FeeRateProviderFactory.swift */; }; 11B35D51B52EF0000711CE05 /* MultiTextMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359824DCDF3B05413CDD2 /* MultiTextMetricsView.swift */; }; 11B35D54818399B4BCE9F2C2 /* UnlinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */; }; @@ -1084,6 +1119,7 @@ 11B35DD9C17FDD3ED40BA321 /* CoinInvestorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EA2F51B9257D036D3E6 /* CoinInvestorsModule.swift */; }; 11B35DDA6B6FB48499F6E0D3 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3531E4476F43B9C2BA5A0 /* ExperimentalFeaturesView.swift */; }; 11B35DDB41FFB254E91B6019 /* MarkdownTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3536DB4D3D3D7771B3EA4 /* MarkdownTextCell.swift */; }; + 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */; }; 11B35DDC98FFF447333278FF /* MarketAdvancedSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */; }; 11B35DDD77B56489D1EB72C5 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3569F2E6BD5E9CBCFCA1F /* Token.swift */; }; 11B35DDFAF0532881A4F68B0 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; @@ -1099,6 +1135,8 @@ 11B35DFFC539A1E72382C8F7 /* ManageAccountsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350911E00460DA8925165 /* ManageAccountsService.swift */; }; 11B35DFFD52E10918F760DD5 /* InputSecondaryButtonWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D5C4EEEAABF83A67D95 /* InputSecondaryButtonWrapperView.swift */; }; 11B35E001107369BB1153649 /* CreateAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56BE1EA9891306D6EB /* CreateAccountViewModel.swift */; }; + 11B35E04C504E2C268F53B66 /* CreateDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3501625BDD3F7D9BEA2F5 /* CreateDuressPasscodeViewModel.swift */; }; + 11B35E051C3D3534E88BEB3D /* CreatePasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */; }; 11B35E075BBD2BBCF2F650D4 /* EvmAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353885F7A93DF25F5023B /* EvmAddressViewController.swift */; }; 11B35E08C957B79CF373E9FB /* BackupVerifyWordsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35785DD2AF78CEBD800F5 /* BackupVerifyWordsModule.swift */; }; 11B35E09BB62E1B486F213D2 /* WalletTokenListViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C6E5282F55B88042F8D /* WalletTokenListViewItemFactory.swift */; }; @@ -1134,6 +1172,7 @@ 11B35E57C6406D2249A23E6F /* SendEvmTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7F043B6C41E53D43BC /* SendEvmTransactionService.swift */; }; 11B35E584C30C56AE18DE076 /* TopPlatformHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */; }; 11B35E5DDFA437BD43717962 /* WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35269B569B8588DB9A23C /* WalletViewController.swift */; }; + 11B35E5EFE34BE1A3760F81D /* BiometryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6223272C5B3E261A24 /* BiometryManager.swift */; }; 11B35E5F3C6070DF6E1F6BAD /* BlockchainType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B185E8FECB3924FDF2 /* BlockchainType.swift */; }; 11B35E600B85B3D1F142D886 /* TransactionsCoinSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D885FDB31F5A920A98A /* TransactionsCoinSelectService.swift */; }; 11B35E61083F8A098D458EBC /* SyncerStateStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FA71AA140CD3764C6BC /* SyncerStateStorage.swift */; }; @@ -1148,6 +1187,7 @@ 11B35E87DDBCD81A36436A13 /* ExternalContractCallTransactionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F42A8CA942DF400A928 /* ExternalContractCallTransactionRecord.swift */; }; 11B35E8BF94FD52708DBB0E1 /* CoinRecord_v19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2C6C103AFF4CCC6E91 /* CoinRecord_v19.swift */; }; 11B35E8D7BC94103A4ABD91C /* WalletHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E859456CF982321B46F /* WalletHeaderView.swift */; }; + 11B35E8DED55EE76CE1F943D /* ModuleUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */; }; 11B35E8E0F5E5F43E65B8A98 /* GuidesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352CFEDEBF0A01CC7073D /* GuidesModule.swift */; }; 11B35E94A7BCB0FEE8E144A9 /* GuidesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E6CB5B964E2A1521CC /* GuidesViewModel.swift */; }; 11B35E99BBF6DCCA72BDA4D1 /* CoinTreasuriesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3522CBA84677E00D44983 /* CoinTreasuriesViewModel.swift */; }; @@ -1175,6 +1215,7 @@ 11B35EC7F06AEAB8E555B833 /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3525406D0B011EB76ACE6 /* AppStatusViewModel.swift */; }; 11B35ECCE5D888A506D7144A /* RestoreBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354D96A80987DAB3B64A6 /* RestoreBinanceViewController.swift */; }; 11B35ED22837284580055F0A /* BalanceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDEB703708795B71C4E /* BalanceData.swift */; }; + 11B35ED81BCE008EE5A71DE8 /* LockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F57D462E2C9E9AEF67C /* LockManager.swift */; }; 11B35ED89BE760771022E8A8 /* BlockchainTokensService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35219C4AB26DC0D104E30 /* BlockchainTokensService.swift */; }; 11B35ED9D5F95988E9335440 /* CoinAnalyticsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3545402F742FE641B9B6C /* CoinAnalyticsModule.swift */; }; 11B35EDC3703B04ED8B72BA8 /* CoinTreasuriesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */; }; @@ -1192,12 +1233,14 @@ 11B35F134E5EF8572BF330CB /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3578FB80AA013BD351A26 /* NavigationRow.swift */; }; 11B35F1440C5946E9C3D94ED /* Auditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D8AF9D337A98530548D /* Auditor.swift */; }; 11B35F173689829256427A34 /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */; }; + 11B35F1949F7203F34347550 /* ModuleUnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */; }; 11B35F20127C070137781ED5 /* AddTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */; }; 11B35F21E9C45606F6D05AC1 /* ManageAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D805327837A9E81801C /* ManageAccountsViewController.swift */; }; 11B35F2474FF811217F48132 /* WalletHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E859456CF982321B46F /* WalletHeaderView.swift */; }; 11B35F25D1209C6DB33ADA55 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3583932F270503C1DF3F0 /* AdapterFactory.swift */; }; 11B35F27274120E53E2C1ADE /* Faq.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35450456BE5E3EE8F7391 /* Faq.swift */; }; 11B35F28C21E228AB3158716 /* MarketOverviewMetricsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCCC2D8CD00EF6A9A77 /* MarketOverviewMetricsCell.swift */; }; + 11B35F29DCAF273D1092C0A4 /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */; }; 11B35F2F1770FB757E6FDCD8 /* NftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354B32BD428041237570A /* NftRecord.swift */; }; 11B35F3409AEFC534DC52137 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352978EC570F59F442BD5 /* View.swift */; }; 11B35F3525372880BC7B47DB /* EvmCoinServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35410733A35D1558E55B2 /* EvmCoinServiceFactory.swift */; }; @@ -1212,6 +1255,7 @@ 11B35F594D24B0B55FD169D7 /* MarketWideCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354AFC10A63BDF4E86EE0 /* MarketWideCardCell.swift */; }; 11B35F5EBB5E8CDDD488314C /* AddressInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A382720D6531AE92F72 /* AddressInputView.swift */; }; 11B35F6092E0950714E277E4 /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C2A54889447CE58B377 /* PostCell.swift */; }; + 11B35F655F8C5ECDB870712D /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D36E5D47264AE07D729 /* UnlockView.swift */; }; 11B35F65EE6333AA07636055 /* NftCollectionAssetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35340910590E6FCF05A90 /* NftCollectionAssetsViewController.swift */; }; 11B35F663F7E12BFDDE3C88B /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35872950C107E4810AB6B /* AccountManager.swift */; }; 11B35F66D2561CD9555C8857 /* UnlinkModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529D276325D741CAEEF5 /* UnlinkModule.swift */; }; @@ -1230,6 +1274,7 @@ 11B35F8BF4BD6481E6AF72AF /* TestNetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EE072CE5471B0DFF841 /* TestNetManager.swift */; }; 11B35F8FB24AB02560A1D018 /* MarketAdvancedSearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */; }; 11B35F91E53BA1F835DD4B4F /* HorizontalDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0EBAF33901578520E1 /* HorizontalDivider.swift */; }; + 11B35F98393E6F3B76381ECF /* ModuleUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */; }; 11B35F9CC94DB2BC7B43BB59 /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35F9E1AF528B31C6F383C /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B35F9F489F4B358FCCE893 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543968337A40168D3EB0 /* MarkdownParser.swift */; }; @@ -1261,6 +1306,7 @@ 11B35FF681C01782693B3C4A /* SendEvmConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354950B1534AD045FDA3A /* SendEvmConfirmationViewController.swift */; }; 11B35FF6D36153F372C16C32 /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */; }; 11B35FF84A61FFBEC01CE15E /* BlockchainTokensViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29B000CD809F81228 /* BlockchainTokensViewModel.swift */; }; + 11B35FFC8C3E4CF638397650 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D36E5D47264AE07D729 /* UnlockView.swift */; }; 11B35FFD159D864F6D914F08 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357511F8F17D8221B64E2 /* AppearanceView.swift */; }; 11B35FFE6FBDA949184E2BF2 /* AmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F2BE131B969BBEABDB9 /* AmountInputViewModel.swift */; }; 179E746F1E3D7BC613BD0AFC /* FavoriteCoinRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */; }; @@ -1665,7 +1711,7 @@ 58AAA0D14CD9EDAE2DBF7540 /* SwapApproveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA681BF5F2CBDCD0D8898 /* SwapApproveViewController.swift */; }; 58AAA10B748931BA5FA867DA /* SwapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAEB2257174A64DE5E51B /* SwapViewModel.swift */; }; 58AAA1152EEEBC93FCC3CAAC /* SwapConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC5B35D17E82D59F7183 /* SwapConfirmationViewController.swift */; }; - 58AAA12167F3BC03D0FA55DF /* PinKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16E4AB334B67FFD891A /* PinKitDelegate.swift */; }; + 58AAA12167F3BC03D0FA55DF /* LockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */; }; 58AAA124FE17B1CDAB5689BA /* CoinPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6CBFBB0EA959466977D /* CoinPageViewModel.swift */; }; 58AAA126020F429D94D77A76 /* OneInchSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA75A0580C45CC08D89E8 /* OneInchSettingsService.swift */; }; 58AAA1283FC7F83F62FC5961 /* FeeSliderValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA3C555FBFB5423CCF8E0 /* FeeSliderValueView.swift */; }; @@ -1691,7 +1737,6 @@ 58AAA2CEB9DB7E34921D7778 /* SwapDeadlineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFF25BF263B5EC4188F7 /* SwapDeadlineViewModel.swift */; }; 58AAA2EBAFC1C443C48BA857 /* CoinChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */; }; 58AAA31D8AD811C0C5434426 /* MarketPostModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */; }; - 58AAA3241CA9440B7366F7DD /* LockScreenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB5515ECA96D506F56C3 /* LockScreenModule.swift */; }; 58AAA331B4A743D9183F8449 /* MarketListTvlDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */; }; 58AAA34F0F6195DF86596A41 /* ChartConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9D5D29115C5F435CF1B /* ChartConfiguration.swift */; }; 58AAA35AF4F4454E0E9C7C60 /* MarketSingleSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */; }; @@ -1779,7 +1824,7 @@ 58AAA8D2A6FD519EFC668EC5 /* CoinPageModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA331E9A43EB0C7F186B1 /* CoinPageModule.swift */; }; 58AAA8D40B3399937CFEB0F2 /* PaymentRequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7A94D25C20240FD75C6 /* PaymentRequestAddress.swift */; }; 58AAA8D67EB6C19719BD760B /* MarketWatchlistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */; }; - 58AAA8E5EA8901CF69DDE43D /* PinKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16E4AB334B67FFD891A /* PinKitDelegate.swift */; }; + 58AAA8E5EA8901CF69DDE43D /* LockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */; }; 58AAA900E2644527A2C78863 /* MetricChartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8CB22CEF84A71CF044F /* MetricChartService.swift */; }; 58AAA9053CD38F13CD944E2A /* SwapApproveAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA1233617C06AC975285A /* SwapApproveAmountView.swift */; }; 58AAA926E1D95F61CA06EFB8 /* SwapConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB39CAE1453B9ED024E4 /* SwapConfirmationModule.swift */; }; @@ -1797,7 +1842,6 @@ 58AAA9AEFE1043B01BEC2D6A /* CoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9BBAB97C2D21A83956C /* CoinSelectViewController.swift */; }; 58AAA9B29938CA65FA3CB3F0 /* AdditionalDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9E19A578FD13792D2B7 /* AdditionalDataView.swift */; }; 58AAA9E3B53672B3C37B727E /* StepBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA422A0530C0C07E19F2F /* StepBadgeView.swift */; }; - 58AAA9F1B2A551603B6C9B6F /* LockScreenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB5515ECA96D506F56C3 /* LockScreenModule.swift */; }; 58AAAA07DC05EF7F912EA184 /* MarketListTvlDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */; }; 58AAAA19D6C7812306A164EA /* SwapCoinCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFB203A455CB53996F97 /* SwapCoinCardCell.swift */; }; 58AAAA2323F7CFCAB96FCF04 /* CoinPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA353DAC061C2123948FC /* CoinPageViewController.swift */; }; @@ -1807,7 +1851,6 @@ 58AAAA6AF87DE0EE337BB8AA /* GradientLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAE622FCAB8C2400A3149 /* GradientLayer.swift */; }; 58AAAA71882CB345D56BBA00 /* CoinChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */; }; 58AAAA7B0493F3790D49AA14 /* SwapAllowanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACE967F3E51A57A38835 /* SwapAllowanceViewModel.swift */; }; - 58AAAA80D6341A7D0773A0D5 /* LockScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA4A027BD92BD062748CC /* LockScreenViewController.swift */; }; 58AAAA8975F5B63340672D00 /* MarketWatchlistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */; }; 58AAAAC61D3D8AD1AC4BEEAE /* AdditionalDataWithErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8483BC1E85730F04CA3 /* AdditionalDataWithErrorView.swift */; }; 58AAAAC777502E0C331C109F /* MarketGlobalDefiMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD81E45666E783B8B2EA /* MarketGlobalDefiMetricService.swift */; }; @@ -1838,7 +1881,6 @@ 58AAACA5A8EC9B2A3182395F /* MarketGlobalTvlFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAEA0582FFB81EB6C6263 /* MarketGlobalTvlFetcher.swift */; }; 58AAACCD229B4D4D525A8182 /* CoinPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6CBFBB0EA959466977D /* CoinPageViewModel.swift */; }; 58AAACD7A57AA93736CDB54D /* SwapApproveAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA1233617C06AC975285A /* SwapApproveAmountView.swift */; }; - 58AAACEDCB8C71F78A4EE72D /* LockScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA4A027BD92BD062748CC /* LockScreenViewController.swift */; }; 58AAACF322E073F1DDA1FBDC /* MarketTvlSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA78BB269FEBB430092A3 /* MarketTvlSortHeaderViewModel.swift */; }; 58AAAD10078FF803A3C27F7C /* MetricChartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8CB22CEF84A71CF044F /* MetricChartService.swift */; }; 58AAAD15C27B67A91EA11F76 /* AddressUriParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD40C9A99F0EDEFCAD14 /* AddressUriParser.swift */; }; @@ -2490,7 +2532,6 @@ D3604E6C28F02E3F0066C366 /* LitecoinKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E6B28F02E3F0066C366 /* LitecoinKit */; }; D3604E7028F03AC80066C366 /* MarketKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E6F28F03AC80066C366 /* MarketKit */; }; D3604E7328F03B0A0066C366 /* ScanQrKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E7228F03B0A0066C366 /* ScanQrKit */; }; - D3604E7628F03B5E0066C366 /* PinKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E7528F03B5E0066C366 /* PinKit */; }; D3604E7928F03B9F0066C366 /* ModuleKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E7828F03B9F0066C366 /* ModuleKit */; }; D3604E7C28F03BD20066C366 /* CurrencyKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E7B28F03BD20066C366 /* CurrencyKit */; }; D3604E7F28F03C1D0066C366 /* Chart in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E7E28F03C1D0066C366 /* Chart */; }; @@ -2502,7 +2543,6 @@ D3604E8E28F03DBF0066C366 /* LitecoinKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E8D28F03DBF0066C366 /* LitecoinKit */; }; D3604E9028F03DC00066C366 /* MarketKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E8F28F03DC00066C366 /* MarketKit */; }; D3604E9228F03DC00066C366 /* ScanQrKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9128F03DC00066C366 /* ScanQrKit */; }; - D3604E9428F03DC00066C366 /* PinKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9328F03DC00066C366 /* PinKit */; }; D3604E9628F03DC00066C366 /* ModuleKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9528F03DC00066C366 /* ModuleKit */; }; D3604E9828F03DC00066C366 /* CurrencyKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9728F03DC00066C366 /* CurrencyKit */; }; D3604E9A28F03DC00066C366 /* Chart in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9928F03DC00066C366 /* Chart */; }; @@ -2666,6 +2706,7 @@ /* Begin PBXFileReference section */ 11B35011026CE084AE40FE6F /* CexDepositModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositModule.swift; sourceTree = ""; }; + 11B3501625BDD3F7D9BEA2F5 /* CreateDuressPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDuressPasscodeViewModel.swift; sourceTree = ""; }; 11B3502198C667A95C21DCF3 /* CexDepositNetworkRaw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkRaw.swift; sourceTree = ""; }; 11B35025FD5E96FD1AB359E9 /* EnabledWalletCacheStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWalletCacheStorage.swift; sourceTree = ""; }; 11B3502637A858E6DDF9471B /* EvmSyncSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmSyncSource.swift; sourceTree = ""; }; @@ -2753,6 +2794,7 @@ 11B351EC6F1B4D72D52B4D16 /* NftActivityHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityHeaderView.swift; sourceTree = ""; }; 11B351F1248EDA20F7141AB8 /* ExtendedKeyModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyModule.swift; sourceTree = ""; }; 11B351F33517C6DDA1E7AF59 /* AddEvmSyncSourceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEvmSyncSourceViewController.swift; sourceTree = ""; }; + 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUnlockViewModel.swift; sourceTree = ""; }; 11B352034B036C9CB7A52724 /* BaseCurrencySettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsViewController.swift; sourceTree = ""; }; 11B352044BCE494491257933 /* LocalStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = ""; }; 11B35216E1F4300730E08C5D /* CheckboxCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxCell.swift; sourceTree = ""; }; @@ -2775,10 +2817,12 @@ 11B3528090862B6792A76DA4 /* FaqCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqCell.swift; sourceTree = ""; }; 11B352884D47E0B23DCF2C2C /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; 11B3529499CD211CC5A21CA2 /* NftCollectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionService.swift; sourceTree = ""; }; + 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePasscodeModule.swift; sourceTree = ""; }; 11B352970EA9924258E5BB75 /* ListSectionFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionFooter.swift; sourceTree = ""; }; 11B352972B14FA6EBEFD6904 /* Text.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; 11B352978EC570F59F442BD5 /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 11B3529B6C7C426755CE9E14 /* ManageWalletsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageWalletsService.swift; sourceTree = ""; }; + 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPasscodeModule.swift; sourceTree = ""; }; 11B3529D276325D741CAEEF5 /* UnlinkModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkModule.swift; sourceTree = ""; }; 11B352A41EC99ADCC8F3E3E9 /* FormAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormAmountInputView.swift; sourceTree = ""; }; 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkViewController.swift; sourceTree = ""; }; @@ -2842,6 +2886,7 @@ 11B353CFE743D50B049A3390 /* CoinMarketsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMarketsViewModel.swift; sourceTree = ""; }; 11B353D752983424F341F2FC /* NftAssetTitleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetTitleCell.swift; sourceTree = ""; }; 11B353E0AC6E1DE4F81BDEF5 /* ReceiveAddressViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressViewItemFactory.swift; sourceTree = ""; }; + 11B353E1284B381BE56AC663 /* NumPadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumPadView.swift; sourceTree = ""; }; 11B353E80D544DAF20B12B56 /* AboutModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutModule.swift; sourceTree = ""; }; 11B353F1E3B5875396F03E0D /* CoinSelectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinSelectService.swift; sourceTree = ""; }; 11B353FA8AE18587D516754B /* BlockchainSettingRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingRecordStorage.swift; sourceTree = ""; }; @@ -2854,6 +2899,7 @@ 11B3543968337A40168D3EB0 /* MarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = ""; }; 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketHeaderCell.swift; sourceTree = ""; }; 11B35450456BE5E3EE8F7391 /* Faq.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Faq.swift; sourceTree = ""; }; + 11B354506A9B41DCD49B2807 /* UnlockModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockModule.swift; sourceTree = ""; }; 11B3545402F742FE641B9B6C /* CoinAnalyticsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsModule.swift; sourceTree = ""; }; 11B3546480B733000550BEB6 /* RestoreSettingRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingRecord.swift; sourceTree = ""; }; 11B35464B8D90CBE6E864B92 /* NftKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftKey.swift; sourceTree = ""; }; @@ -2951,6 +2997,7 @@ 11B3570624266AA63F869105 /* WalletViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletViewItemFactory.swift; sourceTree = ""; }; 11B35708A630D70385F34A8B /* NftCollectionModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionModule.swift; sourceTree = ""; }; 11B35711A471C5A45DD87108 /* EvmNetworkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmNetworkViewController.swift; sourceTree = ""; }; + 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryButtonStyle.swift; sourceTree = ""; }; 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryService.swift; sourceTree = ""; }; 11B3572B7C2F16CD51F37FF0 /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 11B3572F134D41A670EE9244 /* CexWithdrawNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawNetwork.swift; sourceTree = ""; }; @@ -2967,6 +3014,7 @@ 11B35759E226171A4969E66E /* FaqUrlHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqUrlHelper.swift; sourceTree = ""; }; 11B35763ED14419B9EE4C6F9 /* EnabledWalletStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWalletStorage.swift; sourceTree = ""; }; 11B3576C0D8464F74D44EE92 /* BinanceWithdrawHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceWithdrawHandler.swift; sourceTree = ""; }; + 11B3576F224007FD4154EBE8 /* LockoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockoutManager.swift; sourceTree = ""; }; 11B3576FCFC9394BA37975FC /* BackupVerifyWordsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupVerifyWordsViewModel.swift; sourceTree = ""; }; 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopService.swift; sourceTree = ""; }; 11B357736B8C29DF38F5DCBA /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; @@ -3007,6 +3055,7 @@ 11B358556C8FC5368E14D81E /* AccountRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecordStorage.swift; sourceTree = ""; }; 11B3585EF1DA625D906AF9B5 /* BalanceButtonsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceButtonsView.swift; sourceTree = ""; }; 11B358604E6B530C5DB22B92 /* RestoreMnemonicHintCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreMnemonicHintCell.swift; sourceTree = ""; }; + 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveDismiss.swift; sourceTree = ""; }; 11B3586E4CACC415DC6404C7 /* TokenProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProtocol.swift; sourceTree = ""; }; 11B3586FDC91E3742847B7E0 /* AppConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; 11B35872950C107E4810AB6B /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; @@ -3027,6 +3076,7 @@ 11B358D98E1FBA6909D352DA /* FaqRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqRepository.swift; sourceTree = ""; }; 11B358DFD25E8DC35F689D5C /* CoinAnalyticsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsViewModel.swift; sourceTree = ""; }; 11B358E9F753650F0B6BD4B9 /* CoinMajorHolderChartCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMajorHolderChartCell.swift; sourceTree = ""; }; + 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePasscodeViewModel.swift; sourceTree = ""; }; 11B3590EB4E34B278277E8E4 /* CexCoinService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexCoinService.swift; sourceTree = ""; }; 11B359198B26152903D5CA14 /* CexAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAccount.swift; sourceTree = ""; }; 11B3591AD106DAC0D18FEDD7 /* BalanceViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceViewItem.swift; sourceTree = ""; }; @@ -3037,6 +3087,7 @@ 11B35932B642378F85D6ACCD /* CoinAuditsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAuditsService.swift; sourceTree = ""; }; 11B35935EF1B2237E0289669 /* BaseTransactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsViewModel.swift; sourceTree = ""; }; 11B3593FBD158050C9FEF6B9 /* Misc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Misc.swift; sourceTree = ""; }; + 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditDuressPasscodeViewModel.swift; sourceTree = ""; }; 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankViewModel.swift; sourceTree = ""; }; 11B35957968B4D79EC406D4D /* BottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchScreen.swift; sourceTree = ""; }; @@ -3061,6 +3112,7 @@ 11B35996D668B9ADC60E6B9B /* CoinAnalyticsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsService.swift; sourceTree = ""; }; 11B35997A9E413878F48313B /* ActivateSubscriptionModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionModule.swift; sourceTree = ""; }; 11B35999E6C5518115365410 /* EvmAccountRestoreStateStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAccountRestoreStateStorage.swift; sourceTree = ""; }; + 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometryType.swift; sourceTree = ""; }; 11B359B9C1E0BB4D32599695 /* MarkdownViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownViewModel.swift; sourceTree = ""; }; 11B359BBFCD82C3C6DC06F96 /* FeeRateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeRateProvider.swift; sourceTree = ""; }; 11B359C5AF7EE92A5756CCFF /* CoinManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinManager.swift; sourceTree = ""; }; @@ -3077,12 +3129,14 @@ 11B359E546B8F1E572E695F4 /* AmountTypeSwitchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountTypeSwitchService.swift; sourceTree = ""; }; 11B359F01A63378AFAAEE113 /* ManageAccountsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountsModule.swift; sourceTree = ""; }; 11B359FB85F826A825CB401D /* AboutViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = ""; }; + 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = ""; }; 11B359FE5BB60FB12BB24F3E /* NonSpamPoolProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonSpamPoolProvider.swift; sourceTree = ""; }; 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataManager.swift; sourceTree = ""; }; 11B359FF2DB6F840D867FD2F /* BottomSheetModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetModule.swift; sourceTree = ""; }; 11B35A05B93CB243B6404C4A /* WelcomeTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTextView.swift; sourceTree = ""; }; 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryService.swift; sourceTree = ""; }; 11B35A0F912218FEC2A196C0 /* CoinInvestorsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestorsViewController.swift; sourceTree = ""; }; + 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetPasscodeView.swift; sourceTree = ""; }; 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchViewController.swift; sourceTree = ""; }; 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageMigrator.swift; sourceTree = ""; }; 11B35A1C200EC15159154E3F /* ShortcutInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutInputCell.swift; sourceTree = ""; }; @@ -3101,6 +3155,7 @@ 11B35A4E49ED2D2BF8E60863 /* AdapterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterManager.swift; sourceTree = ""; }; 11B35A5B004015DEA52AD5C9 /* WalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletService.swift; sourceTree = ""; }; 11B35A5DE20DD6DD486FAFC0 /* Protocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Protocols.swift; sourceTree = ""; }; + 11B35A6223272C5B3E261A24 /* BiometryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometryManager.swift; sourceTree = ""; }; 11B35A6399E5264BFFA32F08 /* BackupVerifyWordsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupVerifyWordsViewController.swift; sourceTree = ""; }; 11B35A686DD5BA335FEB6BEB /* BarsProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsProgressView.swift; sourceTree = ""; }; 11B35A6DE18A1E6E837DFB21 /* ContactBookManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookManager.swift; sourceTree = ""; }; @@ -3137,6 +3192,8 @@ 11B35B451378835F7F060012 /* NftPriceRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftPriceRecord.swift; sourceTree = ""; }; 11B35B462980B0617E11FB05 /* TermsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsModule.swift; sourceTree = ""; }; 11B35B4D1E2433F5439D9F9A /* TransactionsCoinSelectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsCoinSelectViewModel.swift; sourceTree = ""; }; + 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleUnlockViewModel.swift; sourceTree = ""; }; + 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeManager.swift; sourceTree = ""; }; 11B35B56BE1EA9891306D6EB /* CreateAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountViewModel.swift; sourceTree = ""; }; 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmSyncSourceRecord.swift; sourceTree = ""; }; 11B35B57AFCEAA3AA071F07F /* BaseCurrencySettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsModule.swift; sourceTree = ""; }; @@ -3156,6 +3213,7 @@ 11B35BBC5BBCC258824A80F3 /* CexDepositNetworkSelectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectModule.swift; sourceTree = ""; }; 11B35BBC9FB99B388F1A388F /* AccountRecord_v_0_10.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_10.swift; sourceTree = ""; }; 11B35BBEA6AF9464C818389E /* WalletStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorage.swift; sourceTree = ""; }; + 11B35BC07CC9E523971ED20E /* AppUnlockViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUnlockViewModel.swift; sourceTree = ""; }; 11B35BC10B98A0770A2AC342 /* BlockchainTokensModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainTokensModule.swift; sourceTree = ""; }; 11B35BCBAD15E32459826712 /* RecoveryPhraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseViewModel.swift; sourceTree = ""; }; 11B35BD9A836C953CCF8D077 /* MainSettingsFooterCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsFooterCell.swift; sourceTree = ""; }; @@ -3201,6 +3259,7 @@ 11B35CEBC4B32E57AA2469AA /* PasteboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasteboardManager.swift; sourceTree = ""; }; 11B35CEE91732D3F18290263 /* HighlightedDescriptionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightedDescriptionCell.swift; sourceTree = ""; }; 11B35CF031BC81E4D401CA01 /* ReceiveModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveModule.swift; sourceTree = ""; }; + 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPasscodeViewModel.swift; sourceTree = ""; }; 11B35CFED85A9315089223E3 /* ReceiveViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveViewModel.swift; sourceTree = ""; }; 11B35D04F465245548A31205 /* CexCoinSelectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexCoinSelectModule.swift; sourceTree = ""; }; 11B35D0672D73C973EBE5E1B /* BinanceKitManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceKitManager.swift; sourceTree = ""; }; @@ -3210,6 +3269,7 @@ 11B35D26C9E9E47E4FD46772 /* BaseCurrencySettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsService.swift; sourceTree = ""; }; 11B35D2FC6A2DABFE73D1025 /* EnabledWalletCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWalletCache.swift; sourceTree = ""; }; 11B35D31D3EC415789CFA160 /* MarkdownHeader1Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownHeader1Cell.swift; sourceTree = ""; }; + 11B35D36E5D47264AE07D729 /* UnlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockView.swift; sourceTree = ""; }; 11B35D49F0C58558CA8E5109 /* RecieveSelectorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecieveSelectorViewModel.swift; sourceTree = ""; }; 11B35D55BE7717A87DA6FC43 /* PrivateKeysModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateKeysModule.swift; sourceTree = ""; }; 11B35D55DCC92BED4FA87CA0 /* RestoreMnemonicService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreMnemonicService.swift; sourceTree = ""; }; @@ -3289,6 +3349,7 @@ 11B35F48B66071EEE1AA9574 /* WalletConnectSendEthereumTransactionRequestViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSendEthereumTransactionRequestViewModel.swift; sourceTree = ""; }; 11B35F4A3C8D3D2C6579FD94 /* AlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; 11B35F4B9522FCCD91582AAF /* WalletElementServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletElementServiceFactory.swift; sourceTree = ""; }; + 11B35F57D462E2C9E9AEF67C /* LockManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockManager.swift; sourceTree = ""; }; 11B35F5A3CC8C229C0849756 /* PublicKeysViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicKeysViewController.swift; sourceTree = ""; }; 11B35F60AFA103D0CD2369C3 /* BlockchainTokensView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainTokensView.swift; sourceTree = ""; }; 11B35F6B511DA5E0C60ED156 /* SendEvmViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmViewModel.swift; sourceTree = ""; }; @@ -3297,6 +3358,7 @@ 11B35F95A84DD0F232E5A9CD /* ExtendedKeyViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyViewModel.swift; sourceTree = ""; }; 11B35F980B34E005B9F02B8F /* EvmAccountManagerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAccountManagerFactory.swift; sourceTree = ""; }; 11B35F98E89F83A30870F404 /* ActiveAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveAccount.swift; sourceTree = ""; }; + 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetPasscodeViewModel.swift; sourceTree = ""; }; 11B35F9BA41AC15436A4B977 /* DropdownSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropdownSortHeaderView.swift; sourceTree = ""; }; 11B35F9DA79410E7B9C1B0F8 /* MarketTopModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopModule.swift; sourceTree = ""; }; 11B35FA360A91FDE3EB0B85C /* RateAppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateAppManager.swift; sourceTree = ""; }; @@ -3306,6 +3368,7 @@ 11B35FC207C703EBF63FD56A /* DonutChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonutChartView.swift; sourceTree = ""; }; 11B35FDC67CE58FBE44A4107 /* CoinAnalyticsRatingScaleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsRatingScaleViewController.swift; sourceTree = ""; }; 11B35FEC3027F45085959FBB /* NftDoubleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftDoubleCell.swift; sourceTree = ""; }; + 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleUnlockView.swift; sourceTree = ""; }; 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestorsViewModel.swift; sourceTree = ""; }; 11B35FF9B3B86F74961FADE1 /* TransactionsCoinSelectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsCoinSelectModule.swift; sourceTree = ""; }; 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecord.swift; sourceTree = ""; }; @@ -3530,7 +3593,7 @@ 58AAA13C7C5B258310BA61AF /* CoinChartService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinChartService.swift; sourceTree = ""; }; 58AAA15F4FA7B9EC091EDFF3 /* MarketSingleSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketSingleSortHeaderView.swift; sourceTree = ""; }; 58AAA16C7E337511638808E5 /* DebugInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugInteractor.swift; sourceTree = ""; }; - 58AAA16E4AB334B67FFD891A /* PinKitDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinKitDelegate.swift; sourceTree = ""; }; + 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockDelegate.swift; sourceTree = ""; }; 58AAA18F732998DCAA76E47C /* UniswapSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettings.swift; sourceTree = ""; }; 58AAA18F75B95ACBBAE94DF3 /* DebugLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogger.swift; sourceTree = ""; }; 58AAA19E66FCA0575AE33FAA /* RecipientAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipientAddressViewModel.swift; sourceTree = ""; }; @@ -3552,7 +3615,6 @@ 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketPostModule.swift; sourceTree = ""; }; 58AAA43491E0E4F17D020455 /* SwapApproveViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveViewModel.swift; sourceTree = ""; }; 58AAA444C885BCC354F1B7B3 /* CoinPageMarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageMarkdownParser.swift; sourceTree = ""; }; - 58AAA4A027BD92BD062748CC /* LockScreenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenViewController.swift; sourceTree = ""; }; 58AAA4A4F31EAB9164B33299 /* MarketTvlSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTvlSortHeaderView.swift; sourceTree = ""; }; 58AAA50A504CFA74CA19A415 /* MarketMetricView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketMetricView.swift; sourceTree = ""; }; 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketSingleSortHeaderViewModel.swift; sourceTree = ""; }; @@ -3613,7 +3675,6 @@ 58AAAAD2AA132E9B13726D8B /* MetricChartViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartViewController.swift; sourceTree = ""; }; 58AAAB126AA1B83DD40C426F /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = ""; }; 58AAAB39CAE1453B9ED024E4 /* SwapConfirmationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapConfirmationModule.swift; sourceTree = ""; }; - 58AAAB5515ECA96D506F56C3 /* LockScreenModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenModule.swift; sourceTree = ""; }; 58AAAB692B7C326319D186E4 /* CoinSelectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinSelectViewModel.swift; sourceTree = ""; }; 58AAAB934A3F1B6490245F1D /* MetricChartModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartModule.swift; sourceTree = ""; }; 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistService.swift; sourceTree = ""; }; @@ -4023,7 +4084,6 @@ D3C187E4290FD00E00FE1900 /* HUD in Frameworks */, D3604E9C28F03DC00066C366 /* FeeRateKit in Frameworks */, D3C187E2290FD00E00FE1900 /* ComponentKit in Frameworks */, - D3604E9428F03DC00066C366 /* PinKit in Frameworks */, D339A93F29126D2A00B895BE /* HsCryptoKit in Frameworks */, D3E675702AA9A24900F2BF60 /* SDWebImageSwiftUI in Frameworks */, D3604E9628F03DC00066C366 /* ModuleKit in Frameworks */, @@ -4080,7 +4140,6 @@ D3C187D2290FCF3D00FE1900 /* ComponentKit in Frameworks */, D3E1D00B2990D9BE00C68F00 /* Hodler in Frameworks */, 6BDA29B029D6F934003847ED /* HsToolKit in Frameworks */, - D3604E7628F03B5E0066C366 /* PinKit in Frameworks */, 6B423FD42913785800EE5E70 /* BitcoinCore in Frameworks */, D339A93D29126D0F00B895BE /* HsCryptoKit in Frameworks */, D3604E7928F03B9F0066C366 /* ModuleKit in Frameworks */, @@ -4444,7 +4503,7 @@ 11B35EFB45ECC2D403CA6C89 /* ValueFormatter.swift */, 58AAAE622FCAB8C2400A3149 /* GradientLayer.swift */, 58AAA8CCC7252ECFFDC51578 /* ChartIntervalConverter.swift */, - 58AAA16E4AB334B67FFD891A /* PinKitDelegate.swift */, + 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */, 1A564BCC9DD29DB5455669A5 /* HighlightedDescriptionBaseView.swift */, 1A564CB28708314AE0A69424 /* TitledHighlightedDescriptionView.swift */, 1A564730E8F235240D62124B /* HighlightedDescriptionView.swift */, @@ -4542,6 +4601,8 @@ 11B35B23F86488FDB41CC862 /* ListSectionInfoHeader.swift */, 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */, 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */, + 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */, + 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */, ); path = SwiftUI; sourceTree = ""; @@ -4618,6 +4679,10 @@ ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */, D023D26C2A24CD4F004F65B0 /* TronKitManager.swift */, D05E96A82A28657F002CCD71 /* TronAccountManager.swift */, + 11B35A6223272C5B3E261A24 /* BiometryManager.swift */, + 11B35F57D462E2C9E9AEF67C /* LockManager.swift */, + 11B3576F224007FD4154EBE8 /* LockoutManager.swift */, + 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */, ); path = Managers; sourceTree = ""; @@ -4676,7 +4741,6 @@ D0F7675026BA8E2900093AFF /* Transactions */, 2FA5D3D74F4E4BFA79E729B2 /* TransactionInfo */, 3C7B9BF3170D4C25D5293D33 /* Welcome */, - 58AAA2D1EAFC2C4D0251DD82 /* LockScreen */, 58AAAB6A314C9C062F5707AB /* Debug */, 1A564DFE0406B2AFCA4CAEC9 /* AppStatus */, 58AAAB709D0435465FC5BD99 /* DoubleSpendInfo */, @@ -4736,6 +4800,7 @@ 11B35FF3B50575193B455B17 /* Cex */, 11B35B00CE3E5752412A35AF /* Binance */, 11B35789C06F06FFAB1F4D6A /* BlockchainTokens */, + 11B356791A9FB33F6AF7409E /* Passcode */, ); path = Modules; sourceTree = ""; @@ -4978,10 +5043,24 @@ 11B3502198C667A95C21DCF3 /* CexDepositNetworkRaw.swift */, 11B35799B0DCCF655F0766BF /* CexDepositNetwork.swift */, 11B35B617A9CE668EEF4978B /* AmountData.swift */, + 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */, ); path = Models; sourceTree = ""; }; + 11B3566C587E62F8E154C9BC /* Unlock */ = { + isa = PBXGroup; + children = ( + 11B35D36E5D47264AE07D729 /* UnlockView.swift */, + 11B354506A9B41DCD49B2807 /* UnlockModule.swift */, + 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */, + 11B35BC07CC9E523971ED20E /* AppUnlockViewModel.swift */, + 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */, + 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */, + ); + path = Unlock; + sourceTree = ""; + }; 11B3566CCA730149B8DA7B0E /* Cells */ = { isa = PBXGroup; children = ( @@ -5001,6 +5080,17 @@ path = MarketAdvancedSearch; sourceTree = ""; }; + 11B356791A9FB33F6AF7409E /* Passcode */ = { + isa = PBXGroup; + children = ( + 11B35B91426D30FF5D5ED53A /* Manage */, + 11B3566C587E62F8E154C9BC /* Unlock */, + 11B353E1284B381BE56AC663 /* NumPadView.swift */, + 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */, + ); + path = Passcode; + sourceTree = ""; + }; 11B356A9BF5A101F8B2ABA7F /* Receive */ = { isa = PBXGroup; children = ( @@ -5413,6 +5503,21 @@ path = CexDeposit; sourceTree = ""; }; + 11B35B91426D30FF5D5ED53A /* Manage */ = { + isa = PBXGroup; + children = ( + 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */, + 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */, + 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */, + 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */, + 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */, + 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */, + 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */, + 11B3501625BDD3F7D9BEA2F5 /* CreateDuressPasscodeViewModel.swift */, + ); + path = Manage; + sourceTree = ""; + }; 11B35B9A31858F53A2490110 /* CoinMajorHolders */ = { isa = PBXGroup; children = ( @@ -6349,15 +6454,6 @@ path = Uniswap; sourceTree = ""; }; - 58AAA2D1EAFC2C4D0251DD82 /* LockScreen */ = { - isa = PBXGroup; - children = ( - 58AAAB5515ECA96D506F56C3 /* LockScreenModule.swift */, - 58AAA4A027BD92BD062748CC /* LockScreenViewController.swift */, - ); - path = LockScreen; - sourceTree = ""; - }; 58AAA3C250248F52467618E4 /* SwapConfirmation */ = { isa = PBXGroup; children = ( @@ -7624,7 +7720,6 @@ D3604E8D28F03DBF0066C366 /* LitecoinKit */, D3604E8F28F03DC00066C366 /* MarketKit */, D3604E9128F03DC00066C366 /* ScanQrKit */, - D3604E9328F03DC00066C366 /* PinKit */, D3604E9528F03DC00066C366 /* ModuleKit */, D3604E9728F03DC00066C366 /* CurrencyKit */, D3604E9928F03DC00066C366 /* Chart */, @@ -7688,7 +7783,6 @@ D3604E6B28F02E3F0066C366 /* LitecoinKit */, D3604E6F28F03AC80066C366 /* MarketKit */, D3604E7228F03B0A0066C366 /* ScanQrKit */, - D3604E7528F03B5E0066C366 /* PinKit */, D3604E7828F03B9F0066C366 /* ModuleKit */, D3604E7B28F03BD20066C366 /* CurrencyKit */, D3604E7E28F03C1D0066C366 /* Chart */, @@ -7767,44 +7861,43 @@ D3BF1E61274CBBCE00229A00 /* XCRemoteSwiftPackageReference "DeepDiff" */, 500F1D0F27AA87BC002AA419 /* XCRemoteSwiftPackageReference "AlignedCollectionViewFlowLayout" */, D36E0C2828D084AB00B622B9 /* XCRemoteSwiftPackageReference "CollectionViewCenteredFlowLayout" */, - D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */, - D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */, - D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */, - D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */, - D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */, - D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */, - D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */, - D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */, - D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */, - D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */, - D3604E7428F03B5E0066C366 /* XCRemoteSwiftPackageReference "PinKit.Swift" */, - D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */, - D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */, - D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */, - D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */, - D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */, - D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */, + D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */, + D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */, + D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */, + D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */, + D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */, + D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */, + D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */, + D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */, + D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */, + D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */, + D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */, + D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */, + D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */, + D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */, + D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */, + D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */, D3993D9C28F41F5C008720FB /* XCRemoteSwiftPackageReference "wallet-connect-swift" */, D3993DA328F4229F008720FB /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */, D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */, - D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */, + D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */, D3993DC028F42992008720FB /* XCRemoteSwiftPackageReference "resolution-swift" */, - D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */, + D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */, D3C187B82907CFAB00FE1900 /* XCRemoteSwiftPackageReference "Checkpoints" */, - D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */, - D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */, - D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */, - D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */, - D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */, - D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */, - D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */, - 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */, + D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */, + D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */, + D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */, + D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */, + D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */, + D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */, + D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */, + 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */, 6BDA29A929D6EA9B003847ED /* XCRemoteSwiftPackageReference "ECashKit.Swift" */, 6BDA29AE29D6F934003847ED /* XCRemoteSwiftPackageReference "HsToolKit.Swift" */, D3AF5A8729FFD85800C1399E /* XCRemoteSwiftPackageReference "RxSwift" */, D023D2612A249E59004F65B0 /* XCRemoteSwiftPackageReference "TronKit.Swift" */, D0EC34D92A4450B100BB308B /* XCRemoteSwiftPackageReference "HCaptcha-ios-sdk" */, - 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */, + 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */, D3E6756C2AA9A21300F2BF60 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, ); productRefGroup = D3285F4320BD158E00644076 /* Products */; @@ -7952,8 +8045,6 @@ 11B350B22CCCFCD466BEB808 /* FeeRateProvider.swift in Sources */, 6BCD53172A161F4800993F20 /* BackupViewController.swift in Sources */, 11B35B3F384758B223A7218C /* MainSettingsFooterCell.swift in Sources */, - 58AAA9F1B2A551603B6C9B6F /* LockScreenModule.swift in Sources */, - 58AAACEDCB8C71F78A4EE72D /* LockScreenViewController.swift in Sources */, 58AAAA6AF87DE0EE337BB8AA /* GradientLayer.swift in Sources */, 58AAA82CE1738BCC5B426CB8 /* DebugModule.swift in Sources */, 58AAA93B19192D8AE2590A4F /* DebugRouter.swift in Sources */, @@ -7987,7 +8078,7 @@ 58AAA4A4D0D7398E7184E7AB /* UITextView.swift in Sources */, 11B359CC4B492477E14339B2 /* KeychainKitDelegate.swift in Sources */, 5039F973269C5A9B004711B8 /* ReleaseNotesViewController.swift in Sources */, - 58AAA12167F3BC03D0FA55DF /* PinKitDelegate.swift in Sources */, + 58AAA12167F3BC03D0FA55DF /* LockDelegate.swift in Sources */, 58AAA50C2D93E909A98CFCFF /* DataStatus.swift in Sources */, 11B3556C12B91FD86A72A193 /* LitecoinAdapter.swift in Sources */, D3447DEB25E38300009928D9 /* WalletConnectManager.swift in Sources */, @@ -9124,6 +9215,29 @@ ABC9AA39ED35D6EF41A5353D /* SettingsBackup.swift in Sources */, ABC9AE1E60CABA0101D62738 /* FullCoin.swift in Sources */, ABC9A6A9C28C95352232B062 /* ThemeMode.swift in Sources */, + 11B35C3A0B6DE83A66371224 /* SetPasscodeView.swift in Sources */, + 11B3507578AF3163AAC8C494 /* EditPasscodeModule.swift in Sources */, + 11B3518C9B837CB6C740AABB /* CreatePasscodeModule.swift in Sources */, + 11B353096900F82EDF084F3B /* SetPasscodeViewModel.swift in Sources */, + 11B35D4CF0FBE2496CED70E4 /* EditPasscodeViewModel.swift in Sources */, + 11B35481F59793CD9C95B324 /* CreatePasscodeViewModel.swift in Sources */, + 11B3531D97E44DA1D8280C35 /* EditDuressPasscodeViewModel.swift in Sources */, + 11B35E04C504E2C268F53B66 /* CreateDuressPasscodeViewModel.swift in Sources */, + 11B35FFC8C3E4CF638397650 /* UnlockView.swift in Sources */, + 11B3564236FEF4E5ACC8C838 /* UnlockModule.swift in Sources */, + 11B3527C3BD088DCCA6959C3 /* ModuleUnlockView.swift in Sources */, + 11B3531640EE1F9D29B63325 /* AppUnlockViewModel.swift in Sources */, + 11B3561679C05C31F16EDC77 /* BaseUnlockViewModel.swift in Sources */, + 11B35F98393E6F3B76381ECF /* ModuleUnlockViewModel.swift in Sources */, + 11B358E12CBE7D1B687AE788 /* NumPadView.swift in Sources */, + 11B35F29DCAF273D1092C0A4 /* PasscodeView.swift in Sources */, + 11B35083FB285F6692754E9B /* BiometryType.swift in Sources */, + 11B35E5EFE34BE1A3760F81D /* BiometryManager.swift in Sources */, + 11B35ED81BCE008EE5A71DE8 /* LockManager.swift in Sources */, + 11B358EC0A19773B1455CF62 /* LockoutManager.swift in Sources */, + 11B353C7553F40CEEA28678B /* PasscodeManager.swift in Sources */, + 11B35353A5C1E254839CD61B /* InteractiveDismiss.swift in Sources */, + 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9227,8 +9341,6 @@ 11B35AF1B38CDF6728158514 /* AppConfig.swift in Sources */, 11B3561A469C906B67F24459 /* FeeRateProvider.swift in Sources */, 11B3540F182F3EDE74245EC7 /* MainSettingsFooterCell.swift in Sources */, - 58AAA3241CA9440B7366F7DD /* LockScreenModule.swift in Sources */, - 58AAAA80D6341A7D0773A0D5 /* LockScreenViewController.swift in Sources */, 6BCD53162A161F4800993F20 /* BackupViewController.swift in Sources */, 58AAAD33B32694AFA2E954D6 /* GradientLayer.swift in Sources */, 58AAAE430A2184D5A12202EA /* DebugModule.swift in Sources */, @@ -9265,7 +9377,7 @@ 58AAA39A983D2E97066C3959 /* LastBlockInfo.swift in Sources */, 58AAA550B894B6F8FC8DA1B1 /* UITextView.swift in Sources */, 11B3503093D40D5FA0675FA7 /* KeychainKitDelegate.swift in Sources */, - 58AAA8E5EA8901CF69DDE43D /* PinKitDelegate.swift in Sources */, + 58AAA8E5EA8901CF69DDE43D /* LockDelegate.swift in Sources */, 58AAA6A77A2B953931A1D7FC /* DataStatus.swift in Sources */, 11B354B8BD1C3C036F6DE16A /* LitecoinAdapter.swift in Sources */, D3447DEA25E38300009928D9 /* WalletConnectManager.swift in Sources */, @@ -10404,6 +10516,29 @@ ABC9A99861B1F83A19EA370D /* SettingsBackup.swift in Sources */, ABC9A3EA19771B14B0502A0A /* FullCoin.swift in Sources */, ABC9A5A4C6213D58CDA2EB73 /* ThemeMode.swift in Sources */, + 11B3591C77EE71054BF819D0 /* SetPasscodeView.swift in Sources */, + 11B3587D9E89A97F63CD0C5A /* EditPasscodeModule.swift in Sources */, + 11B35E051C3D3534E88BEB3D /* CreatePasscodeModule.swift in Sources */, + 11B351E088F87C02C870DDB8 /* SetPasscodeViewModel.swift in Sources */, + 11B358006AEB85BBE0BF47A7 /* EditPasscodeViewModel.swift in Sources */, + 11B35C9570D3C283E9C943D5 /* CreatePasscodeViewModel.swift in Sources */, + 11B356A5B50D4E6EF2282398 /* EditDuressPasscodeViewModel.swift in Sources */, + 11B35C5F856FB531028F8C0A /* CreateDuressPasscodeViewModel.swift in Sources */, + 11B35F655F8C5ECDB870712D /* UnlockView.swift in Sources */, + 11B35251E1B11235D00E6565 /* UnlockModule.swift in Sources */, + 11B35F1949F7203F34347550 /* ModuleUnlockView.swift in Sources */, + 11B358B004B48988A1F6D888 /* AppUnlockViewModel.swift in Sources */, + 11B35902128F12FB06B0CA5E /* BaseUnlockViewModel.swift in Sources */, + 11B35E8DED55EE76CE1F943D /* ModuleUnlockViewModel.swift in Sources */, + 11B3585461729AD144448426 /* NumPadView.swift in Sources */, + 11B356330572A72E56DC2FEA /* PasscodeView.swift in Sources */, + 11B3580CD18A931ABAA6C122 /* BiometryType.swift in Sources */, + 11B350EA36A2113C23047911 /* BiometryManager.swift in Sources */, + 11B3568483AFF7864F050E0F /* LockManager.swift in Sources */, + 11B35787F5BA973364784F3B /* LockoutManager.swift in Sources */, + 11B35951600F986F1C424E24 /* PasscodeManager.swift in Sources */, + 11B350A27335B798701EE7B3 /* InteractiveDismiss.swift in Sources */, + 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10706,7 +10841,7 @@ kind = branch; }; }; - 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */ = { + 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinCore.Swift"; requirement = { @@ -10714,7 +10849,7 @@ version = 2.0.2; }; }; - 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */ = { + 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/UIExtensions.Swift"; requirement = { @@ -10762,7 +10897,7 @@ version = 1.0.0; }; }; - D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */ = { + D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HsCryptoKit.Swift"; requirement = { @@ -10770,7 +10905,7 @@ version = 1.2.1; }; }; - D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */ = { + D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/EvmKit.Swift"; requirement = { @@ -10778,7 +10913,7 @@ version = 2.0.7; }; }; - D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */ = { + D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Eip20Kit.Swift"; requirement = { @@ -10786,7 +10921,7 @@ version = 2.0.0; }; }; - D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */ = { + D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/NftKit.Swift"; requirement = { @@ -10794,7 +10929,7 @@ version = 2.0.0; }; }; - D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */ = { + D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/UniswapKit.Swift"; requirement = { @@ -10802,7 +10937,7 @@ version = 2.0.5; }; }; - D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */ = { + D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/OneInchKit.Swift"; requirement = { @@ -10810,7 +10945,7 @@ version = 2.0.1; }; }; - D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */ = { + D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinKit.Swift"; requirement = { @@ -10818,7 +10953,7 @@ version = 2.0.0; }; }; - D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */ = { + D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinCashKit.Swift"; requirement = { @@ -10826,7 +10961,7 @@ version = 2.0.0; }; }; - D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */ = { + D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/LitecoinKit.Swift"; requirement = { @@ -10834,7 +10969,7 @@ version = 2.0.0; }; }; - D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */ = { + D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/MarketKit.Swift"; requirement = { @@ -10842,7 +10977,7 @@ version = 2.2.4; }; }; - D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */ = { + D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ScanQrKit.Swift"; requirement = { @@ -10850,15 +10985,7 @@ version = 2.0.0; }; }; - D3604E7428F03B5E0066C366 /* XCRemoteSwiftPackageReference "PinKit.Swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/horizontalsystems/PinKit.Swift"; - requirement = { - kind = exactVersion; - version = 2.0.4; - }; - }; - D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */ = { + D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ModuleKit.Swift"; requirement = { @@ -10866,7 +10993,7 @@ version = 2.0.0; }; }; - D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */ = { + D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/CurrencyKit.Swift"; requirement = { @@ -10874,7 +11001,7 @@ version = 2.0.1; }; }; - D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */ = { + D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Chart.Swift"; requirement = { @@ -10882,7 +11009,7 @@ version = 2.1.3; }; }; - D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */ = { + D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/FeeRateKit.Swift"; requirement = { @@ -10890,7 +11017,7 @@ version = 2.1.0; }; }; - D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */ = { + D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BinanceChainKit.Swift"; requirement = { @@ -10898,7 +11025,7 @@ version = 2.0.0; }; }; - D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */ = { + D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/DashKit.Swift"; requirement = { @@ -10927,7 +11054,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; requirement = { kind = exactVersion; - version = "2.0.0"; + version = 2.0.0; }; }; D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */ = { @@ -10938,7 +11065,7 @@ version = 1.6.10; }; }; - D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */ = { + D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ActionSheet.Swift"; requirement = { @@ -10978,7 +11105,7 @@ minimumVersion = 2.3.3; }; }; - D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */ = { + D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HdWalletKit.Swift"; requirement = { @@ -10994,7 +11121,7 @@ version = 1.0.13; }; }; - D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */ = { + D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ThemeKit.Swift"; requirement = { @@ -11002,7 +11129,7 @@ version = 2.0.2; }; }; - D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */ = { + D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ComponentKit.Swift"; requirement = { @@ -11010,7 +11137,7 @@ version = 2.0.11; }; }; - D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */ = { + D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HUD.Swift"; requirement = { @@ -11018,7 +11145,7 @@ version = 2.0.0; }; }; - D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */ = { + D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/LanguageKit.Swift"; requirement = { @@ -11026,7 +11153,7 @@ version = 1.0.0; }; }; - D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */ = { + D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/SectionsTableView.Swift"; requirement = { @@ -11034,7 +11161,7 @@ version = 1.0.0; }; }; - D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */ = { + D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/StorageKit.Swift"; requirement = { @@ -11042,7 +11169,7 @@ version = 2.0.0; }; }; - D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */ = { + D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Hodler.Swift"; requirement = { @@ -11073,12 +11200,12 @@ }; 6B423FD32913785800EE5E70 /* BitcoinCore */ = { isa = XCSwiftPackageProductDependency; - package = 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */; + package = 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */; productName = BitcoinCore; }; 6B5546202A6E73190054B524 /* UIExtensions */ = { isa = XCSwiftPackageProductDependency; - package = 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */; + package = 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */; productName = UIExtensions; }; 6BDA29AA29D6F37C003847ED /* ECashKit */ = { @@ -11138,182 +11265,172 @@ }; D339A93C29126D0F00B895BE /* HsCryptoKit */ = { isa = XCSwiftPackageProductDependency; - package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */; + package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */; productName = HsCryptoKit; }; D339A93E29126D2A00B895BE /* HsCryptoKit */ = { isa = XCSwiftPackageProductDependency; - package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */; + package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */; productName = HsCryptoKit; }; D3604E4228F02A020066C366 /* EvmKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */; + package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */; productName = EvmKit; }; D3604E4428F02A260066C366 /* EvmKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */; + package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */; productName = EvmKit; }; D3604E4928F02A8C0066C366 /* Eip20Kit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */; + package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */; productName = Eip20Kit; }; D3604E4C28F02AB40066C366 /* NftKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */; + package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */; productName = NftKit; }; D3604E4F28F02AE70066C366 /* UniswapKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */; + package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */; productName = UniswapKit; }; D3604E5228F02B150066C366 /* OneInchKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */; + package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */; productName = OneInchKit; }; D3604E5428F02B280066C366 /* Eip20Kit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */; + package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */; productName = Eip20Kit; }; D3604E5628F02B280066C366 /* NftKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */; + package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */; productName = NftKit; }; D3604E5828F02B280066C366 /* UniswapKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */; + package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */; productName = UniswapKit; }; D3604E5A28F02B280066C366 /* OneInchKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */; + package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */; productName = OneInchKit; }; D3604E6528F02D9A0066C366 /* BitcoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */; + package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */; productName = BitcoinKit; }; D3604E6828F02DF30066C366 /* BitcoinCashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */; + package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */; productName = BitcoinCashKit; }; D3604E6B28F02E3F0066C366 /* LitecoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */; + package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */; productName = LitecoinKit; }; D3604E6F28F03AC80066C366 /* MarketKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */; + package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */; productName = MarketKit; }; D3604E7228F03B0A0066C366 /* ScanQrKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */; + package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */; productName = ScanQrKit; }; - D3604E7528F03B5E0066C366 /* PinKit */ = { - isa = XCSwiftPackageProductDependency; - package = D3604E7428F03B5E0066C366 /* XCRemoteSwiftPackageReference "PinKit.Swift" */; - productName = PinKit; - }; D3604E7828F03B9F0066C366 /* ModuleKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */; + package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */; productName = ModuleKit; }; D3604E7B28F03BD20066C366 /* CurrencyKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */; + package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */; productName = CurrencyKit; }; D3604E7E28F03C1D0066C366 /* Chart */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */; + package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */; productName = Chart; }; D3604E8128F03C6B0066C366 /* FeeRateKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */; + package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */; productName = FeeRateKit; }; D3604E8428F03CDC0066C366 /* BinanceChainKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */; + package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */; productName = BinanceChainKit; }; D3604E8728F03D9E0066C366 /* DashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */; + package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */; productName = DashKit; }; D3604E8928F03DBF0066C366 /* BitcoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */; + package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */; productName = BitcoinKit; }; D3604E8B28F03DBF0066C366 /* BitcoinCashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */; + package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */; productName = BitcoinCashKit; }; D3604E8D28F03DBF0066C366 /* LitecoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */; + package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */; productName = LitecoinKit; }; D3604E8F28F03DC00066C366 /* MarketKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */; + package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */; productName = MarketKit; }; D3604E9128F03DC00066C366 /* ScanQrKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */; + package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */; productName = ScanQrKit; }; - D3604E9328F03DC00066C366 /* PinKit */ = { - isa = XCSwiftPackageProductDependency; - package = D3604E7428F03B5E0066C366 /* XCRemoteSwiftPackageReference "PinKit.Swift" */; - productName = PinKit; - }; D3604E9528F03DC00066C366 /* ModuleKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */; + package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */; productName = ModuleKit; }; D3604E9728F03DC00066C366 /* CurrencyKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */; + package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */; productName = CurrencyKit; }; D3604E9928F03DC00066C366 /* Chart */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */; + package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */; productName = Chart; }; D3604E9B28F03DC00066C366 /* FeeRateKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */; + package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */; productName = FeeRateKit; }; D3604E9D28F03DC00066C366 /* BinanceChainKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */; + package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */; productName = BinanceChainKit; }; D3604E9F28F03DC00066C366 /* DashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */; + package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */; productName = DashKit; }; D36E0C2928D084AB00B622B9 /* CollectionViewCenteredFlowLayout */ = { @@ -11358,12 +11475,12 @@ }; D3993DBA28F4277E008720FB /* ActionSheet */ = { isa = XCSwiftPackageProductDependency; - package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */; + package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */; productName = ActionSheet; }; D3993DBC28F4278F008720FB /* ActionSheet */ = { isa = XCSwiftPackageProductDependency; - package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */; + package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */; productName = ActionSheet; }; D3993DC128F42992008720FB /* UnstoppableDomainsResolution */ = { @@ -11428,12 +11545,12 @@ }; D3C187B22907A60800FE1900 /* HdWalletKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */; + package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */; productName = HdWalletKit; }; D3C187B42907A63600FE1900 /* HdWalletKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */; + package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */; productName = HdWalletKit; }; D3C187B92907CFAB00FE1900 /* Checkpoints */ = { @@ -11448,72 +11565,72 @@ }; D3C187CE290FCF2D00FE1900 /* ThemeKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */; + package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */; productName = ThemeKit; }; D3C187D1290FCF3D00FE1900 /* ComponentKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */; + package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */; productName = ComponentKit; }; D3C187D4290FCF7D00FE1900 /* HUD */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */; + package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */; productName = HUD; }; D3C187D7290FCF9C00FE1900 /* LanguageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */; + package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */; productName = LanguageKit; }; D3C187DA290FCFBC00FE1900 /* SectionsTableView */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */; + package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */; productName = SectionsTableView; }; D3C187DD290FCFE400FE1900 /* StorageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */; + package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */; productName = StorageKit; }; D3C187DF290FD00E00FE1900 /* ThemeKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */; + package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */; productName = ThemeKit; }; D3C187E1290FD00E00FE1900 /* ComponentKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */; + package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */; productName = ComponentKit; }; D3C187E3290FD00E00FE1900 /* HUD */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */; + package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */; productName = HUD; }; D3C187E5290FD00E00FE1900 /* LanguageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */; + package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */; productName = LanguageKit; }; D3C187E7290FD00E00FE1900 /* SectionsTableView */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */; + package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */; productName = SectionsTableView; }; D3C187E9290FD00E00FE1900 /* StorageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */; + package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */; productName = StorageKit; }; D3E1D00A2990D9BE00C68F00 /* Hodler */ = { isa = XCSwiftPackageProductDependency; - package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */; + package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */; productName = Hodler; }; D3E1D00C2990DA0400C68F00 /* Hodler */ = { isa = XCSwiftPackageProductDependency; - package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */; + package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */; productName = Hodler; }; D3E6756D2AA9A21300F2BF60 /* SDWebImageSwiftUI */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/Contents.json b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/Contents.json new file mode 100644 index 0000000000..8682a3c46d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "backspace@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "backspace@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/backspace@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/backspace_24.imageset/backspace@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ed6d5d68cc5907dbad535f924424e43d7af9cada GIT binary patch literal 723 zcmV;^0xbQBP)Qz>y}Hl#mj9OZ^$B+27>mSr<+ z?!k6Q0ltqp=c(*yEIpu^ya5x~w=KJ0=cy5Oi~+88aAFv7I)h zvO*_9<42oqbTA8}gv*@2R6A4=20sZnvTI?~X~EK1&L61xQtTHZ)@@%u5|%r?KMQG8 zyfabz$w%m{CZ-U34r!Foi3sh7<;Bv!gGi7(Zz00lPh}d#e4IfbOtwP!VR^MjhlCAv z`P-Jdb3+I};u}~_LL0X|^!WM_S?F#Knyk^pxG5z=KtIHK4VY4&i}AYsB$OXXX%UAo zE@~l_QnoSp@vnsfh)}OsDpRG#k6$fRW878*)k3KV#+c^|e&l%PTF7`Wgf-0tC^gOM zr6{^^t=oOUShDi-%_??gektsNYlGS|aonoT%f}dvh1$(1Yz4Dwfcl@ z0v_2&vSi`0z8NRjGUo9i+fT9t5C{YUfj}S-2uNU11Bkc}@S+dEJG6R4%Rcz!K3U>B z#LG$9j?M**Mpu2#V+8+FikJZ`j_F67$qJncLi}<=|9Y_d%xNJ>gZq4eGeHfKlR=Ue zZ}K%M0tYluj({!!&u{V@tqW?XoQw|Qr_TC@J+R1jIebdXi0&Ly@PxRda=zDNj9tt3 zh)(IxG+VQF4N{PEi+It49f4{Jl8m9GXhs+7!)(RMDb_SO3IDzo6dbItR2I}sIm=UR zuCiYxK~0sTgR!7C$PpL`YKt6!wxBl25oif&n;e01qUb^n;#bN;_*t^X#)u!z!2nyw zk)c$B9Lb?fmLnwHAp7DArk|rl@}B&0L=tF+t%BU0R=g+K*wc|z99z@}OGIV?WH`Cz zC`qcTQ5=~EP*qO+y`->I()QTmTd+iA7C`lyLoIp~$0^xGDoHK2%QLVttYlj(4T^Gt<>KJ z8X0mev1NC$iSIxJ2F(ZOv88u0@tn#$^|wKVww2S^*76{cp6jRnHc&}WeZ{uX)X+`+ zZJ?UH`-n{-X^Yhth;Gxyhopkmy!${*n>K2()zG8pUXrv&F)J%g0=LIjTaTiT^Ik1O zHgCJ>q0lGoN|G$C&}%*v$L6?4Q9Y0q+LDqU#igWqvcw~kwpH?;{Bi8{C~A!lqK>U` zNynC8j5v4?4S{}CF5f;_dC`2b>L(ZRiNUk^c1bcE-O(ON6~+g(LsG>NX3dp^h)Vle z7^B!&NlJ4^ERAw)jwDfY5U+u;m%V~rHs)(sovWdcCrxQNmrWY64L!;#_-=o*saeB< zw{GNe)+e(lNZM%_<0r%6#ocNh9ME7%zp3`TTHoMkwog4RZOc3WEV}uP&%hDW>CZ{0 z(>() - private let willEnterForegroundSubject = PublishSubject<()>() + private let didBecomeActiveSubject = PublishSubject() + private let willEnterForegroundSubject = PublishSubject() - init(accountManager: AccountManager, walletManager: WalletManager, adapterManager: AdapterManager, pinKit: PinKit.Kit, + init(accountManager: AccountManager, walletManager: WalletManager, adapterManager: AdapterManager, lockManager: LockManager, keychainKit: IKeychainKit, blurManager: BlurManager, kitCleaner: KitCleaner, debugLogger: DebugLogger?, appVersionManager: AppVersionManager, rateAppManager: RateAppManager, logRecordManager: LogRecordManager, deepLinkManager: DeepLinkManager, evmLabelManager: EvmLabelManager, balanceHiddenManager: BalanceHiddenManager, - walletConnectSocketConnectionService: WalletConnectSocketConnectionService, nftMetadataSyncer: NftMetadataSyncer - ) { + walletConnectSocketConnectionService: WalletConnectSocketConnectionService, nftMetadataSyncer: NftMetadataSyncer) + { self.accountManager = accountManager self.walletManager = walletManager self.adapterManager = adapterManager - self.pinKit = pinKit + self.lockManager = lockManager self.keychainKit = keychainKit self.blurManager = blurManager self.kitCleaner = kitCleaner - self.debugBackgroundLogger = debugLogger + debugBackgroundLogger = debugLogger self.appVersionManager = appVersionManager self.rateAppManager = rateAppManager self.logRecordManager = logRecordManager @@ -49,18 +48,15 @@ class AppManager { self.walletConnectSocketConnectionService = walletConnectSocketConnectionService self.nftMetadataSyncer = nftMetadataSyncer } - } extension AppManager { - func didFinishLaunching() { debugBackgroundLogger?.logFinishLaunching() keychainKit.handleLaunch() accountManager.handleLaunch() walletManager.preloadWallets() - pinKit.didFinishLaunching() kitCleaner.clear() rateAppManager.onLaunch() @@ -84,7 +80,7 @@ extension AppManager { func didEnterBackground() { debugBackgroundLogger?.logEnterBackground() - pinKit.didEnterBackground() + lockManager.didEnterBackground() walletConnectSocketConnectionService.didEnterBackground() balanceHiddenManager.didEnterBackground() } @@ -97,7 +93,7 @@ extension AppManager { willEnterForegroundSubject.onNext(()) keychainKit.handleForeground() - pinKit.willEnterForeground() + lockManager.willEnterForeground() adapterManager.refresh() walletConnectSocketConnectionService.willEnterForeground() @@ -111,17 +107,14 @@ extension AppManager { func didReceive(url: URL) -> Bool { deepLinkManager.handle(url: url) } - } extension AppManager: IAppManager { - - var didBecomeActiveObservable: Observable<()> { + var didBecomeActiveObservable: Observable { didBecomeActiveSubject.asObservable() } - var willEnterForegroundObservable: Observable<()> { + var willEnterForegroundObservable: Observable { willEnterForegroundSubject.asObservable() } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BiometryManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BiometryManager.swift new file mode 100644 index 0000000000..8bf4d2fc58 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BiometryManager.swift @@ -0,0 +1,43 @@ +import Combine +import HsExtensions +import LocalAuthentication +import StorageKit + +class BiometryManager { + private let biometricOnKey = "biometric_on_key" + + private let localStorage: ILocalStorage + private var tasks = Set() + + @PostPublished var biometryType: BiometryType? + @PostPublished var biometryEnabled: Bool { + didSet { + localStorage.set(value: biometryEnabled, for: biometricOnKey) + } + } + + init(localStorage: ILocalStorage) { + self.localStorage = localStorage + + biometryEnabled = localStorage.value(for: biometricOnKey) ?? false + + refreshBiometry() + } + + private func refreshBiometry() { + Task { [weak self] in + var authError: NSError? + let localAuthenticationContext = LAContext() + + if localAuthenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) { + switch localAuthenticationContext.biometryType { + case .faceID: self?.biometryType = .faceId + case .touchID: self?.biometryType = .touchId + default: self?.biometryType = .none + } + } else { + self?.biometryType = .none + } + }.store(in: &tasks) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BlurManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BlurManager.swift index 32ab59d291..678817c772 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BlurManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BlurManager.swift @@ -1,17 +1,18 @@ -import UIKit -import UIExtensions import ThemeKit -import PinKit +import UIExtensions +import UIKit class BlurManager { private let coverView = UIView() private let logoImageView = UIImageView() - private let pinKit: PinKit.Kit + private let lockManager: LockManager private var shown = false - init(pinKit: PinKit.Kit) { - self.pinKit = pinKit + var isEnabled = true + + init(lockManager: LockManager) { + self.lockManager = lockManager coverView.backgroundColor = .themeTyler @@ -61,17 +62,11 @@ class BlurManager { window?.addSubview(coverView) shown = true } - - private var unlockShown: Bool { - (UIViewController.visibleController as? PinViewController) != nil - } - } extension BlurManager { - func willResignActive() { - if !pinKit.isLocked && !unlockShown { + if !lockManager.isLocked, isEnabled { show() } } @@ -94,5 +89,4 @@ extension BlurManager { shown = false coverView.removeFromSuperview() } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift new file mode 100644 index 0000000000..bebedf0b70 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift @@ -0,0 +1,62 @@ +import Combine +import Foundation +import HsExtensions +import StorageKit + +class LockManager { + private let lastExitDateKey = "last_exit_date_key" + + private let passcodeManager: PasscodeManager + private let localStorage: ILocalStorage + private let delegate: LockDelegate + + private let lockTimeout: Double = 60 + private(set) var isLocked: Bool + + + init(passcodeManager: PasscodeManager, localStorage: ILocalStorage, delegate: LockDelegate) { + self.passcodeManager = passcodeManager + self.localStorage = localStorage + self.delegate = delegate + + isLocked = passcodeManager.isPasscodeSet + } +} + +extension LockManager { + func didEnterBackground() { + guard !isLocked else { + return + } + + localStorage.set(value: Date().timeIntervalSince1970, for: lastExitDateKey) + } + + func willEnterForeground() { + guard !isLocked else { + return + } + + let exitTimestamp: TimeInterval = localStorage.value(for: lastExitDateKey) ?? 0 + let now = Date().timeIntervalSince1970 + + guard now - exitTimestamp > lockTimeout else { + return + } + + lock() + } + + func lock() { + guard passcodeManager.isPasscodeSet else { + return + } + + isLocked = true + delegate.onLock() + } + + func onUnlock() { + isLocked = false + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift new file mode 100644 index 0000000000..0c740ac7e8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift @@ -0,0 +1,86 @@ +import Foundation +import HsExtensions +import StorageKit + +class LockoutManager { + private let unlockAttemptsKey = "unlock_attempts_keychain_key" + private let lockTimestampKey = "lock_timestamp_keychain_key" + private let maxAttempts = 5 + + private var secureStorage: ISecureStorage + + @PostPublished private(set) var lockoutState: LockoutState = .unlocked(attemptsLeft: 0, maxAttempts: 0) + + private var unlockAttempts: Int { + didSet { + try? secureStorage.set(value: unlockAttempts, for: unlockAttemptsKey) + } + } + + private var lockTimestamp: TimeInterval { + didSet { + try? secureStorage.set(value: lockTimestamp, for: lockTimestampKey) + } + } + + init(secureStorage: ISecureStorage) { + self.secureStorage = secureStorage + + unlockAttempts = secureStorage.value(for: unlockAttemptsKey) ?? 0 + lockTimestamp = secureStorage.value(for: lockTimestampKey) ?? Self.uptime + + syncState() + } + + private static var uptime: TimeInterval { + var uptime = timespec() + clock_gettime(CLOCK_MONOTONIC_RAW, &uptime) + return TimeInterval(uptime.tv_sec) + } + + private var lockoutInterval: TimeInterval { + if unlockAttempts == maxAttempts { + return 5 * 60 + } else if unlockAttempts == maxAttempts + 1 { + return 10 * 60 + } else if unlockAttempts == maxAttempts + 2 { + return 15 * 60 + } else { + return 30 * 60 + } + } +} + +extension LockoutManager { + + func syncState() { + if unlockAttempts < maxAttempts { + lockoutState = .unlocked(attemptsLeft: maxAttempts - unlockAttempts, maxAttempts: maxAttempts) + } else { + let timePast = max(0, Self.uptime - lockTimestamp) + let lockoutInterval = lockoutInterval + + if timePast > lockoutInterval { + lockoutState = .unlocked(attemptsLeft: 1, maxAttempts: maxAttempts) + } else { + lockoutState = .locked(unlockDate: Date().addingTimeInterval(lockoutInterval - timePast)) + } + } + } + + func didUnlock() { + unlockAttempts = 0 + syncState() + } + + func didFailUnlock() { + unlockAttempts += 1 + lockTimestamp = Self.uptime + syncState() + } +} + +enum LockoutState { + case unlocked(attemptsLeft: Int, maxAttempts: Int) + case locked(unlockDate: Date) +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift new file mode 100644 index 0000000000..18ce8b5025 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift @@ -0,0 +1,132 @@ +import Combine +import HsExtensions +import StorageKit + +class PasscodeManager { + private let separator = "|" + private let passcodeKey = "pin_keychain_key" + + private let biometryManager: BiometryManager + private let secureStorage: ISecureStorage + + private var passcodes = [String]() + + @PostPublished private(set) var currentPasscodeLevel: Int + @PostPublished private(set) var isPasscodeSet = false + @PostPublished private(set) var isDuressPasscodeSet = false + + init(biometryManager: BiometryManager, secureStorage: ISecureStorage) { + self.biometryManager = biometryManager + self.secureStorage = secureStorage + + if let rawPasscodes: String = secureStorage.value(for: passcodeKey), !rawPasscodes.isEmpty { + passcodes = rawPasscodes.components(separatedBy: separator) + } else { + passcodes = [""] + } + + currentPasscodeLevel = passcodes.count - 1 + + syncState() + } + + private func syncState() { + isPasscodeSet = passcodes.last.map { !$0.isEmpty } ?? false + isDuressPasscodeSet = passcodes.count > currentPasscodeLevel + 1 + + if !isPasscodeSet, biometryManager.biometryEnabled { + biometryManager.biometryEnabled = false + } + } + + private func save(passcodes: [String]) throws { + try secureStorage.set(value: passcodes.joined(separator: separator), for: passcodeKey) + } +} + +extension PasscodeManager { + func isValid(passcode: String) -> Bool { + passcodes[currentPasscodeLevel] == passcode + } + + func isValid(duressPasscode: String) -> Bool { + let duressLevel = currentPasscodeLevel + 1 + + guard passcodes.count > duressLevel else { + return false + } + + return passcodes[duressLevel] == duressPasscode + } + + func has(passcode: String) -> Bool { + passcodes.contains(passcode) + } + + func setLastPasscode() { + guard !passcodes.isEmpty else { + return + } + + currentPasscodeLevel = passcodes.count - 1 + syncState() + } + + func set(currentPasscode: String) { + guard let level = passcodes.firstIndex(of: currentPasscode) else { + return + } + + guard currentPasscodeLevel != level else { + return + } + + currentPasscodeLevel = level + syncState() + } + + func set(passcode: String) throws { + var newPasscodes = passcodes + + newPasscodes[currentPasscodeLevel] = passcode + + try save(passcodes: newPasscodes) + passcodes = newPasscodes + syncState() + } + + func removePasscode() throws { + var newPasscodes = passcodes + + newPasscodes[currentPasscodeLevel] = "" + newPasscodes = Array(newPasscodes.prefix(currentPasscodeLevel + 1)) + + try save(passcodes: newPasscodes) + passcodes = newPasscodes + syncState() + } + + func set(duressPasscode: String) throws { + var newPasscodes = passcodes + + if newPasscodes.count > currentPasscodeLevel + 1 { + newPasscodes[currentPasscodeLevel + 1] = duressPasscode + } else { + newPasscodes.append(duressPasscode) + } + + try save(passcodes: newPasscodes) + passcodes = newPasscodes + syncState() + } + + func removeDuressPasscode() throws { + var newPasscodes = passcodes + + newPasscodes = Array(newPasscodes.prefix(currentPasscodeLevel + 1)) + + try save(passcodes: newPasscodes) + passcodes = newPasscodes + syncState() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BiometryType.swift b/UnstoppableWallet/UnstoppableWallet/Models/BiometryType.swift new file mode 100644 index 0000000000..271ebe6727 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/BiometryType.swift @@ -0,0 +1,18 @@ +enum BiometryType { + case faceId + case touchId + + var title: String { + switch self { + case .faceId: return "face_id".localized + case .touchId: return "touch_id".localized + } + } + + var iconName: String { + switch self { + case .faceId: return "face_id_24" + case .touchId: return "touch_id_2_24" + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift index 1468309b12..dec28b9251 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift @@ -1,29 +1,29 @@ -import UIKit import StorageKit +import UIKit class LaunchModule { - static func viewController() -> UIViewController { let service = LaunchService( - accountManager: App.shared.accountManager, - pinKit: App.shared.pinKit, - keychainKit: App.shared.keychainKit, - localStorage: App.shared.localStorage + accountManager: App.shared.accountManager, + passcodeManager: App.shared.passcodeManager, + keychainKit: App.shared.keychainKit, + localStorage: App.shared.localStorage ) switch service.launchMode { case .passcodeNotSet: return NoPasscodeViewController(mode: .noPasscode) case .cannotCheckPasscode: return NoPasscodeViewController(mode: .cannotCheckPasscode) case .intro: return WelcomeScreenViewController() - case .unlock: return LockScreenModule.viewController(pinKit: App.shared.pinKit, appStart: true) + case .unlock: return UnlockModule.appUnlockView { + UIApplication.shared.windows.first { $0.isKeyWindow }?.set(newRootController: MainModule.instance()) + } + .toViewController() case .main: return MainModule.instance() } } - } extension LaunchModule { - enum LaunchMode { case passcodeNotSet case cannotCheckPasscode @@ -31,5 +31,4 @@ extension LaunchModule { case unlock case main } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchService.swift index f8c03936d1..022699ed16 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchService.swift @@ -1,23 +1,20 @@ import StorageKit -import PinKit class LaunchService { private let accountManager: AccountManager - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager private let keychainKit: IKeychainKit private let localStorage: LocalStorage - init(accountManager: AccountManager, pinKit: PinKit.Kit, keychainKit: IKeychainKit, localStorage: LocalStorage) { + init(accountManager: AccountManager, passcodeManager: PasscodeManager, keychainKit: IKeychainKit, localStorage: LocalStorage) { self.accountManager = accountManager - self.pinKit = pinKit + self.passcodeManager = passcodeManager self.keychainKit = keychainKit self.localStorage = localStorage } - } extension LaunchService { - var launchMode: LaunchModule.LaunchMode { let passcodeLockState = keychainKit.passcodeLockState @@ -25,14 +22,12 @@ extension LaunchService { return .passcodeNotSet } else if passcodeLockState == .unknown { return .cannotCheckPasscode - } else if pinKit.isPinSet { + } else if passcodeManager.isPasscodeSet { return .unlock } else if accountManager.accounts.isEmpty && !localStorage.mainShownOnce { - return .intro + return .intro } else { return .main } - } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenModule.swift deleted file mode 100644 index 18b27c014e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenModule.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit -import PinKit - -struct LockScreenModule { - - static func viewController(pinKit: PinKit.Kit, appStart: Bool) -> UIViewController { - let unlockController = pinKit.unlockPinModule( - biometryUnlockMode: .auto, - insets: UIEdgeInsets(top: 0, left: 0, bottom: .margin12x, right: 0), - cancellable: false, - autoDismiss: !appStart, - onUnlock: { - if appStart { - UIApplication.shared.windows.first { $0.isKeyWindow }?.set(newRootController: MainModule.instance()) - } - } - ) - - let viewController = LockScreenViewController(unlockViewController: unlockController) - viewController.modalPresentationStyle = .fullScreen - - return viewController - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenViewController.swift deleted file mode 100644 index ac6e512472..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/LockScreen/LockScreenViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit -import ThemeKit - -class LockScreenViewController: ThemeViewController { - private let unlockViewController: UIViewController - - init(unlockViewController: UIViewController) { - self.unlockViewController = unlockViewController - - super.init() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(unlockViewController) - view.addSubview(unlockViewController.view) - unlockViewController.didMove(toParent: self) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift index c52c0a0fd9..9593cb5c38 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift @@ -1,7 +1,6 @@ import Combine -import RxSwift import RxRelay -import PinKit +import RxSwift class MainBadgeService { private let disposeBag = DisposeBag() @@ -10,67 +9,66 @@ class MainBadgeService { private let backupManager: BackupManager private let accountRestoreWarningManager: AccountRestoreWarningManager - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager private let termsManager: TermsManager private let walletConnectSessionManager: WalletConnectSessionManager private let contactBookManager: ContactBookManager private let settingsBadgeRelay = BehaviorRelay<(Bool, Int)>(value: (false, 0)) - init(backupManager: BackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, pinKit: PinKit.Kit, termsManager: TermsManager, walletConnectSessionManager: WalletConnectSessionManager, contactBookManager: ContactBookManager) { + init(backupManager: BackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, passcodeManager: PasscodeManager, termsManager: TermsManager, walletConnectSessionManager: WalletConnectSessionManager, contactBookManager: ContactBookManager) { self.backupManager = backupManager self.accountRestoreWarningManager = accountRestoreWarningManager - self.pinKit = pinKit + self.passcodeManager = passcodeManager self.termsManager = termsManager self.walletConnectSessionManager = walletConnectSessionManager self.contactBookManager = contactBookManager accountRestoreWarningManager.hasNonStandardObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in - self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onNext: { [weak self] _ in + self?.syncSettingsBadge() + }) + .disposed(by: disposeBag) backupManager.allBackedUpObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in - self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) - - pinKit.isPinSetPublisher - .sink { [weak self] _ in - self?.syncSettingsBadge() - } - .store(in: &cancellables) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onNext: { [weak self] _ in + self?.syncSettingsBadge() + }) + .disposed(by: disposeBag) + + passcodeManager.$isPasscodeSet + .sink { [weak self] _ in + self?.syncSettingsBadge() + } + .store(in: &cancellables) termsManager.termsAcceptedObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in - self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onNext: { [weak self] _ in + self?.syncSettingsBadge() + }) + .disposed(by: disposeBag) walletConnectSessionManager.activePendingRequestsObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in - self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onNext: { [weak self] _ in + self?.syncSettingsBadge() + }) + .disposed(by: disposeBag) contactBookManager.iCloudErrorObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in - self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) - + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onNext: { [weak self] _ in + self?.syncSettingsBadge() + }) + .disposed(by: disposeBag) syncSettingsBadge() } @@ -82,8 +80,7 @@ class MainBadgeService { private func syncSettingsBadge() { let count = walletConnectSessionManager.activePendingRequests.count let cloudError = contactBookManager.iCloudError != nil && contactBookManager.remoteSync - let visible = accountRestoreWarningManager.hasNonStandard || !backupManager.allBackedUp || !pinKit.isPinSet || !termsManager.termsAccepted || cloudError || count != 0 + let visible = accountRestoreWarningManager.hasNonStandard || !backupManager.allBackedUp || !passcodeManager.isPasscodeSet || !termsManager.termsAccepted || cloudError || count != 0 settingsBadgeRelay.accept((visible, count)) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift index 7198c2162c..421f59f35a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift @@ -15,13 +15,14 @@ struct MainModule { accountManager: App.shared.accountManager, walletManager: App.shared.walletManager, appManager: App.shared.appManager, - pinKit: App.shared.pinKit, + passcodeManager: App.shared.passcodeManager, + lockManager: App.shared.lockManager, presetTab: presetTab ) let badgeService = MainBadgeService( backupManager: App.shared.backupManager, accountRestoreWarningManager: App.shared.accountRestoreWarningManager, - pinKit: App.shared.pinKit, + passcodeManager: App.shared.passcodeManager, termsManager: App.shared.termsManager, walletConnectSessionManager: App.shared.walletConnectSessionManager, contactBookManager: App.shared.contactManager @@ -50,7 +51,7 @@ struct MainModule { let deepLinkHandler = WalletConnectAppShowModule.handler(parentViewController: viewController) eventHandler.append(handler: deepLinkHandler) - App.shared.pinKitDelegate.viewController = viewController + App.shared.lockDelegate.viewController = viewController return viewController } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainService.swift index f874eb4628..65c87ad114 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainService.swift @@ -1,8 +1,7 @@ import Foundation -import RxSwift import RxRelay +import RxSwift import StorageKit -import PinKit class MainService { private let keyTabIndex = "main-tab-index" @@ -11,7 +10,8 @@ class MainService { private let storage: StorageKit.ILocalStorage private let launchScreenManager: LaunchScreenManager private let accountManager: AccountManager - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager + private let lockManager: LockManager private let presetTab: MainModule.Tab? private let disposeBag = DisposeBag() @@ -33,17 +33,18 @@ class MainService { } } - private let handleAlertsRelay = PublishRelay<()>() + private let handleAlertsRelay = PublishRelay() private let showMarketRelay = PublishRelay() private var isColdStart: Bool = true - init(localStorage: LocalStorage, storage: StorageKit.ILocalStorage, launchScreenManager: LaunchScreenManager, accountManager: AccountManager, walletManager: WalletManager, appManager: IAppManager, pinKit: PinKit.Kit, presetTab: MainModule.Tab?) { + init(localStorage: LocalStorage, storage: StorageKit.ILocalStorage, launchScreenManager: LaunchScreenManager, accountManager: AccountManager, walletManager: WalletManager, appManager: IAppManager, passcodeManager: PasscodeManager, lockManager: LockManager, presetTab: MainModule.Tab?) { self.localStorage = localStorage self.storage = storage self.launchScreenManager = launchScreenManager self.accountManager = accountManager - self.pinKit = pinKit + self.passcodeManager = passcodeManager + self.lockManager = lockManager self.presetTab = presetTab subscribe(disposeBag, accountManager.accountsObservable) { [weak self] in self?.sync(accounts: $0) } @@ -68,20 +69,18 @@ class MainService { } private func didBecomeActive() { - if !pinKit.isPinSet, isColdStart { // If pin not set, in first time we don't need to handleAlerts. (ViewController handle it from didAppear) + if !passcodeManager.isPasscodeSet, isColdStart { // If passcode not set, in first time we don't need to handleAlerts. (ViewController handle it from didAppear) isColdStart = false return } - if !pinKit.isLocked { // If pin locked, after input it ViewController will handle alerts form didAppear + if !lockManager.isLocked { // If passcode locked, after input it ViewController will handle alerts form didAppear handleAlertsRelay.accept(()) } } - } extension MainService { - var hasAccountsObservable: Observable { hasAccountsRelay.asObservable() } @@ -94,7 +93,7 @@ extension MainService { launchScreenManager.showMarket } - var handleAlertsObservable: Observable<()> { + var handleAlertsObservable: Observable { handleAlertsRelay.asObservable() } @@ -137,5 +136,4 @@ extension MainService { var activeAccount: Account? { accountManager.activeAccount } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift index e110217c4a..d1037349c1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountModule.swift @@ -1,31 +1,29 @@ -import UIKit -import ThemeKit -import StorageKit import LanguageKit +import StorageKit +import ThemeKit +import UIKit struct ManageAccountModule { - static func viewController(accountId: String, sourceViewController: ManageAccountsViewController) -> UIViewController? { guard let service = ManageAccountService( - accountId: accountId, - accountManager: App.shared.accountManager, - cloudBackupManager: App.shared.cloudBackupManager, - pinKit: App.shared.pinKit + accountId: accountId, + accountManager: App.shared.accountManager, + cloudBackupManager: App.shared.cloudBackupManager, + passcodeManager: App.shared.passcodeManager ) else { return nil } let accountRestoreWarningFactory = AccountRestoreWarningFactory( - localStorage: StorageKit.LocalStorage.default, - languageManager: LanguageManager.shared + localStorage: StorageKit.LocalStorage.default, + languageManager: LanguageManager.shared ) let viewModel = ManageAccountViewModel( - service: service, - accountRestoreWarningFactory: accountRestoreWarningFactory + service: service, + accountRestoreWarningFactory: accountRestoreWarningFactory ) let viewController = ManageAccountViewController(viewModel: viewModel, sourceViewController: sourceViewController) return ThemeNavigationController(rootViewController: viewController) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift index 2fd125acc1..1b3e84fd3e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountService.swift @@ -1,8 +1,7 @@ -import RxSwift -import RxRelay -import MarketKit -import PinKit import Combine +import MarketKit +import RxRelay +import RxSwift class ManageAccountService { private let accountRelay = PublishRelay() @@ -14,7 +13,7 @@ class ManageAccountService { private let accountManager: AccountManager private let cloudBackupManager: CloudBackupManager - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager private let disposeBag = DisposeBag() private var cancellables = Set() @@ -25,12 +24,12 @@ class ManageAccountService { } } - private let accountDeletedRelay = PublishRelay<()>() - private let cloudBackedUpRelay = PublishRelay<()>() + private let accountDeletedRelay = PublishRelay() + private let cloudBackedUpRelay = PublishRelay() private var newName: String - init?(accountId: String, accountManager: AccountManager, cloudBackupManager: CloudBackupManager, pinKit: PinKit.Kit) { + init?(accountId: String, accountManager: AccountManager, cloudBackupManager: CloudBackupManager, passcodeManager: PasscodeManager) { guard let account = accountManager.account(id: accountId) else { return nil } @@ -38,8 +37,7 @@ class ManageAccountService { self.account = account self.accountManager = accountManager self.cloudBackupManager = cloudBackupManager - - self.pinKit = pinKit + self.passcodeManager = passcodeManager newName = account.name @@ -47,16 +45,16 @@ class ManageAccountService { subscribe(disposeBag, accountManager.accountDeletedObservable) { [weak self] in self?.handleDeleted(account: $0) } cloudBackupManager.$oneWalletItems - .sink { [weak self] _ in - self?.cloudBackedUpRelay.accept(()) - } - .store(in: &cancellables) + .sink { [weak self] _ in + self?.cloudBackedUpRelay.accept(()) + } + .store(in: &cancellables) syncState() } private func syncState() { - if !newName.isEmpty && account.name != newName { + if !newName.isEmpty, account.name != newName { state = .canSave } else { state = .cannotSave @@ -74,11 +72,9 @@ class ManageAccountService { accountDeletedRelay.accept(()) } } - } extension ManageAccountService { - var stateObservable: Observable { stateRelay.asObservable() } @@ -87,11 +83,11 @@ extension ManageAccountService { accountRelay.asObservable() } - var accountDeletedObservable: Observable<()> { + var accountDeletedObservable: Observable { accountDeletedRelay.asObservable() } - var cloudBackedUpObservable: Observable<()> { + var cloudBackedUpObservable: Observable { cloudBackedUpRelay.asObservable() } @@ -99,8 +95,8 @@ extension ManageAccountService { cloudBackupManager.backedUp(uniqueId: account.type.uniqueId()) } - var isPinSet: Bool { - pinKit.isPinSet + var isPasscodeSet: Bool { + passcodeManager.isPasscodeSet } func set(name: String) { @@ -116,14 +112,11 @@ extension ManageAccountService { func deleteCloudBackup() throws { try cloudBackupManager.delete(uniqueId: account.type.uniqueId()) } - } extension ManageAccountService { - enum State { case cannotSave case canSave } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewController.swift index 62a737623f..9a8a124b5d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewController.swift @@ -1,11 +1,10 @@ -import UIKit -import ThemeKit +import ComponentKit +import RxCocoa +import RxSwift import SectionsTableView import SnapKit -import RxSwift -import RxCocoa -import ComponentKit -import PinKit +import ThemeKit +import UIKit class ManageAccountViewController: KeyboardAwareViewController { private let viewModel: ManageAccountViewModel @@ -30,7 +29,8 @@ class ManageAccountViewController: KeyboardAwareViewController { hidesBottomBarWhenPushed = true } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -101,16 +101,10 @@ class ManageAccountViewController: KeyboardAwareViewController { } private func openUnlock() { - let insets = UIEdgeInsets(top: 0, left: 0, bottom: .margin48, right: 0) - let viewController = App.shared.pinKit.unlockPinModule( - biometryUnlockMode: .auto, - insets: insets, - cancellable: true, - autoDismiss: true, - onUnlock: { [weak self] in - self?.viewModel.onUnlock() - } - ) + let viewController = UnlockModule.moduleUnlockView { [weak self] in + self?.viewModel.onUnlock() + }.toNavigationViewController() + present(viewController, animated: true) } @@ -132,7 +126,7 @@ class ManageAccountViewController: KeyboardAwareViewController { navigationController?.pushViewController(viewController, animated: true) } - private func openBackup(account: Account, onComplete: (() -> ())? = nil) { + private func openBackup(account: Account, onComplete: (() -> Void)? = nil) { guard let viewController = BackupModule.manualViewController(account: account, onComplete: onComplete) else { return } @@ -203,88 +197,86 @@ class ManageAccountViewController: KeyboardAwareViewController { return self.present(controller, animated: true) } } - } extension ManageAccountViewController: SectionsDataSource { - private func row(keyAction: ManageAccountViewModel.KeyAction, isFirst: Bool, isLast: Bool) -> RowProtocol { switch keyAction { case .recoveryPhrase: return tableView.universalRow48( - id: "recovery-phrase", - image: .local(UIImage(named: "paper_contract_24")), - title: .body("manage_account.recovery_phrase".localized), - accessoryType: .disclosure, - autoDeselect: true, - isFirst: isFirst, - isLast: isLast + id: "recovery-phrase", + image: .local(UIImage(named: "paper_contract_24")), + title: .body("manage_account.recovery_phrase".localized), + accessoryType: .disclosure, + autoDeselect: true, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.viewModel.onTapRecoveryPhrase() } case .privateKeys: return tableView.universalRow48( - id: "private-keys", - image: .local(UIImage(named: "key_24")), - title: .body("manage_account.private_keys".localized), - accessoryType: .disclosure, - isFirst: isFirst, - isLast: isLast + id: "private-keys", + image: .local(UIImage(named: "key_24")), + title: .body("manage_account.private_keys".localized), + accessoryType: .disclosure, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.openPrivateKeys() } case .publicKeys: return tableView.universalRow48( - id: "public-keys", - image: .local(UIImage(named: "binocule_24")), - title: .body("manage_account.public_keys".localized), - accessoryType: .disclosure, - isFirst: isFirst, - isLast: isLast + id: "public-keys", + image: .local(UIImage(named: "binocule_24")), + title: .body("manage_account.public_keys".localized), + accessoryType: .disclosure, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.openPublicKeys() } case let .manualBackup(isManualBackedUp): let accessory: CellBuilderNew.CellElement.AccessoryType = isManualBackedUp ? - CellBuilderNew.CellElement.ImageAccessoryType(image: UIImage(named: "check_1_20")?.withTintColor(.themeRemus)) : - CellBuilderNew.CellElement.ImageAccessoryType(image: UIImage(named: "warning_2_24")?.withTintColor(.themeLucian)) + CellBuilderNew.CellElement.ImageAccessoryType(image: UIImage(named: "check_1_20")?.withTintColor(.themeRemus)) : + CellBuilderNew.CellElement.ImageAccessoryType(image: UIImage(named: "warning_2_24")?.withTintColor(.themeLucian)) return tableView.universalRow48( - id: "backup-recovery-phrase", - image: .local(UIImage(named: "edit_24")?.withTintColor(.themeJacob)), - title: .body("manage_account.backup_recovery_phrase".localized, color: .themeJacob), - accessoryType: accessory, - autoDeselect: true, - isFirst: isFirst, - isLast: isLast + id: "backup-recovery-phrase", + image: .local(UIImage(named: "edit_24")?.withTintColor(.themeJacob)), + title: .body("manage_account.backup_recovery_phrase".localized, color: .themeJacob), + accessoryType: accessory, + autoDeselect: true, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.viewModel.onTapBackup() } case let .cloudBackedUp(isCloudBackedUp, isManualBackedUp): if isCloudBackedUp { return tableView.universalRow48( - id: "cloud-backup-recovery", - image: .local(UIImage(named: "no_internet_24")?.withTintColor(.themeLucian)), - title: .body("manage_account.cloud_delete_backup_recovery_phrase".localized, color: .themeLucian), - autoDeselect: true, - isFirst: isFirst, - isLast: isLast + id: "cloud-backup-recovery", + image: .local(UIImage(named: "no_internet_24")?.withTintColor(.themeLucian)), + title: .body("manage_account.cloud_delete_backup_recovery_phrase".localized, color: .themeLucian), + autoDeselect: true, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.viewModel.onTapDeleteCloudBackup() } } return tableView.universalRow48( - id: "cloud-backup-recovery", - image: .local(UIImage(named: "icloud_24")?.withTintColor(.themeJacob)), - title: .body("manage_account.cloud_backup_recovery_phrase".localized, color: .themeJacob), - accessoryType: CellBuilderNew.CellElement.ImageAccessoryType( - image: UIImage(named: "warning_2_24")?.withTintColor(.themeLucian), - visible: !isManualBackedUp - ), - autoDeselect: true, - isFirst: isFirst, - isLast: isLast + id: "cloud-backup-recovery", + image: .local(UIImage(named: "icloud_24")?.withTintColor(.themeJacob)), + title: .body("manage_account.cloud_backup_recovery_phrase".localized, color: .themeJacob), + accessoryType: CellBuilderNew.CellElement.ImageAccessoryType( + image: UIImage(named: "warning_2_24")?.withTintColor(.themeLucian), + visible: !isManualBackedUp + ), + autoDeselect: true, + isFirst: isFirst, + isLast: isLast ) { [weak self] in self?.viewModel.onTapCloudBackup() } @@ -294,78 +286,77 @@ extension ManageAccountViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { var sections: [SectionProtocol] = [ Section( - id: "margin", - headerState: .margin(height: .margin12) + id: "margin", + headerState: .margin(height: .margin12) ), Section( - id: "name", - headerState: tableView.sectionHeader(text: "manage_account.name".localized), - footerState: .margin(height: .margin32), - rows: [ - StaticRow( - cell: nameCell, - id: "name", - height: .heightSingleLineCell - ) - ] - ) + id: "name", + headerState: tableView.sectionHeader(text: "manage_account.name".localized), + footerState: .margin(height: .margin32), + rows: [ + StaticRow( + cell: nameCell, + id: "name", + height: .heightSingleLineCell + ), + ] + ), ] if let warningViewItem = warningViewItem { sections.append( - Section( - id: "migration-warning", - footerState: .margin(height: .margin32), - rows: [ - Row( - id: "migration-cell", - dynamicHeight: { [weak self] containerWidth in - let text = self?.warningViewItem?.text ?? "" - return TitledHighlightedDescriptionCell.height(containerWidth: containerWidth, text: text) - }, - bind: { [weak self] cell, _ in - cell.set(backgroundStyle: .transparent, isFirst: true) - cell.bind(caution: warningViewItem) - cell.onBackgroundButton = { self?.onOpenWarning() } - } - ) - ] - ) + Section( + id: "migration-warning", + footerState: .margin(height: .margin32), + rows: [ + Row( + id: "migration-cell", + dynamicHeight: { [weak self] containerWidth in + let text = self?.warningViewItem?.text ?? "" + return TitledHighlightedDescriptionCell.height(containerWidth: containerWidth, text: text) + }, + bind: { [weak self] cell, _ in + cell.set(backgroundStyle: .transparent, isFirst: true) + cell.bind(caution: warningViewItem) + cell.onBackgroundButton = { self?.onOpenWarning() } + } + ), + ] + ) ) } sections.append(contentsOf: - keyActions.enumerated().map { (index, section) in + keyActions.enumerated().map { index, section in Section( - id: "actions-\(index)", - footerState: section.footerText.isEmpty ? .margin(height: .margin32) : tableView.sectionFooter(text: section.footerText), - rows: section.keyActions.enumerated().map { index, keyAction in - row(keyAction: keyAction, isFirst: index == 0, isLast: index == section.keyActions.count - 1) - } + id: "actions-\(index)", + footerState: section.footerText.isEmpty ? .margin(height: .margin32) : tableView.sectionFooter(text: section.footerText), + rows: section.keyActions.enumerated().map { index, keyAction in + row(keyAction: keyAction, isFirst: index == 0, isLast: index == section.keyActions.count - 1) + } ) } ) sections.append( - Section( + Section( + id: "unlink", + footerState: .margin(height: .margin32), + rows: [ + tableView.universalRow48( id: "unlink", - footerState: .margin(height: .margin32), - rows: [ - tableView.universalRow48( - id: "unlink", - image: .local(UIImage(named: "trash_24")?.withTintColor(.themeLucian)), - title: .body("manage_account.unlink".localized, color: .themeLucian), - autoDeselect: true, - isFirst: true, - isLast: true - ) { [weak self] in - self?.onTapUnlink() - } - ] - ) + image: .local(UIImage(named: "trash_24")?.withTintColor(.themeLucian)), + title: .body("manage_account.unlink".localized, color: .themeLucian), + autoDeselect: true, + isFirst: true, + isLast: true + ) { [weak self] in + self?.onTapUnlink() + }, + ] + ) ) return sections } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewModel.swift index 914224dde5..f19e78a41a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/ManageAccountViewModel.swift @@ -1,7 +1,7 @@ import Foundation -import RxSwift -import RxRelay import RxCocoa +import RxRelay +import RxSwift class ManageAccountViewModel { private let service: ManageAccountService @@ -11,7 +11,7 @@ class ManageAccountViewModel { private let keyActionsRelay = BehaviorRelay<[KeyActionSection]>(value: []) private let showWarningRelay = BehaviorRelay(value: nil) private let saveEnabledRelay = BehaviorRelay(value: false) - private let openUnlockRelay = PublishRelay<()>() + private let openUnlockRelay = PublishRelay() private let openRecoveryPhraseRelay = PublishRelay() private let openBackupRelay = PublishRelay() private let openBackupAndDeleteCloudRelay = PublishRelay() @@ -19,7 +19,7 @@ class ManageAccountViewModel { private let confirmDeleteCloudBackupRelay = PublishRelay() private let cloudBackupDeletedRelay = PublishRelay() private let openUnlinkRelay = PublishRelay() - private let finishRelay = PublishRelay<()>() + private let finishRelay = PublishRelay() private var unlockRequest: UnlockRequest = .recoveryPhrase @@ -46,13 +46,13 @@ class ManageAccountViewModel { private func keyActions(account: Account, isCloudBackedUp: Bool) -> [KeyActionSection] { var backupActions = [KeyAction]() - var footerText: String = "" + var footerText = "" if account.canBeBackedUp { backupActions.append(.manualBackup(account.backedUp)) footerText = !(account.backedUp || isCloudBackedUp) ? - "manage_account.backup.no_backup_yet_description".localized : - "manage_account.backup.has_backup_description".localized + "manage_account.backup.no_backup_yet_description".localized : + "manage_account.backup.has_backup_description".localized } if !account.watchAccount { @@ -69,7 +69,7 @@ class ManageAccountViewModel { case .mnemonic: keyActions.append(contentsOf: [.recoveryPhrase, .privateKeys, .publicKeys]) case .evmPrivateKey: keyActions.append(contentsOf: [.privateKeys, .publicKeys]) case .evmAddress, .tronAddress: () - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch key { case .private: keyActions.append(contentsOf: [.privateKeys, .publicKeys]) case .public: keyActions.append(contentsOf: [.publicKeys]) @@ -95,11 +95,9 @@ class ManageAccountViewModel { showWarningRelay.accept(accountRestoreWarningFactory.caution(account: account, canIgnoreActiveAccountWarning: false)) keyActionsRelay.accept(keyActions(account: account, isCloudBackedUp: service.isCloudBackedUp)) } - } extension ManageAccountViewModel { - var saveEnabledDriver: Driver { saveEnabledRelay.asDriver() } @@ -116,7 +114,7 @@ extension ManageAccountViewModel { accountRestoreWarningFactory.warningUrl(account: service.account) } - var openUnlockSignal: Signal<()> { + var openUnlockSignal: Signal { openUnlockRelay.asSignal() } @@ -148,7 +146,7 @@ extension ManageAccountViewModel { openUnlinkRelay.asSignal() } - var finishSignal: Signal<()> { + var finishSignal: Signal { finishRelay.asSignal() } @@ -178,7 +176,7 @@ extension ManageAccountViewModel { } func onTapRecoveryPhrase() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .recoveryPhrase openUnlockRelay.accept(()) } else { @@ -200,22 +198,20 @@ extension ManageAccountViewModel { } func deleteCloudBackupAfterManualBackup() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .backupAndDeleteCloud openUnlockRelay.accept(()) } else { openBackupAndDeleteCloudRelay.accept(service.account) } - } - func onTapCloudBackup() { openCloudBackupRelay.accept(service.account) } func onTapBackup() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .backup openUnlockRelay.accept(()) } else { @@ -226,11 +222,9 @@ extension ManageAccountViewModel { func onTapUnlink() { openUnlinkRelay.accept(service.account) } - } extension ManageAccountViewModel { - enum UnlockRequest { case recoveryPhrase case backup @@ -253,7 +247,5 @@ extension ManageAccountViewModel { self.keyActions = keyActions self.footerText = footerText } - } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysModule.swift index 79b6bd2085..6c103862ec 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysModule.swift @@ -1,11 +1,9 @@ import UIKit struct PrivateKeysModule { - static func viewController(account: Account) -> UIViewController { - let service = PrivateKeysService(account: account, pinKit: App.shared.pinKit) + let service = PrivateKeysService(account: account, passcodeManager: App.shared.passcodeManager) let viewModel = PrivateKeysViewModel(service: service) return PrivateKeysViewController(viewModel: viewModel) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysService.swift index 94cd06383d..64e86a7c77 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysService.swift @@ -1,20 +1,16 @@ -import PinKit - class PrivateKeysService { private let account: Account - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager - init(account: Account, pinKit: PinKit.Kit) { + init(account: Account, passcodeManager: PasscodeManager) { self.account = account - self.pinKit = pinKit + self.passcodeManager = passcodeManager } - } extension PrivateKeysService { - - var isPinSet: Bool { - pinKit.isPinSet + var isPasscodeSet: Bool { + passcodeManager.isPasscodeSet } var accountType: AccountType { @@ -31,7 +27,7 @@ extension PrivateKeysService { var bip32RootKeySupported: Bool { switch account.type { case .mnemonic: return true - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch key { case .private: switch key.derivedType { @@ -47,7 +43,7 @@ extension PrivateKeysService { var accountExtendedPrivateKeySupported: Bool { switch account.type { case .mnemonic: return true - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch key { case .private: return true default: return false @@ -55,5 +51,4 @@ extension PrivateKeysService { default: return false } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewController.swift index dff3c88742..274797229b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewController.swift @@ -1,11 +1,10 @@ -import UIKit -import RxSwift +import ComponentKit import RxCocoa +import RxSwift +import SectionsTableView import SnapKit import ThemeKit -import SectionsTableView -import ComponentKit -import PinKit +import UIKit class PrivateKeysViewController: ThemeViewController { private let viewModel: PrivateKeysViewModel @@ -19,7 +18,8 @@ class PrivateKeysViewController: ThemeViewController { super.init() } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -58,16 +58,10 @@ class PrivateKeysViewController: ThemeViewController { } private func openUnlock() { - let insets = UIEdgeInsets(top: 0, left: 0, bottom: .margin48, right: 0) - let viewController = App.shared.pinKit.unlockPinModule( - biometryUnlockMode: .auto, - insets: insets, - cancellable: true, - autoDismiss: true, - onUnlock: { [weak self] in - self?.viewModel.onUnlock() - } - ) + let viewController = UnlockModule.moduleUnlockView { [weak self] in + self?.viewModel.onUnlock() + }.toNavigationViewController() + present(viewController, animated: true) } @@ -88,80 +82,77 @@ class PrivateKeysViewController: ThemeViewController { let viewController = ExtendedKeyModule.viewController(mode: .accountExtendedPrivateKey, accountType: accountType) navigationController?.pushViewController(viewController, animated: true) } - } extension PrivateKeysViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { var sections: [SectionProtocol] = [ Section( - id: "margin", - headerState: .margin(height: .margin12) - ) + id: "margin", + headerState: .margin(height: .margin12) + ), ] if viewModel.showEvmPrivateKey { sections.append( - Section( + Section( + id: "evm-private-key", + footerState: tableView.sectionFooter(text: "private_keys.evm_private_key.description".localized), + rows: [ + tableView.universalRow48( id: "evm-private-key", - footerState: tableView.sectionFooter(text: "private_keys.evm_private_key.description".localized), - rows: [ - tableView.universalRow48( - id: "evm-private-key", - title: .body("private_keys.evm_private_key".localized), - accessoryType: .disclosure, - isFirst: true, - isLast: true - ) { [weak self] in - self?.viewModel.onTapEvmPrivateKey() - } - ] - ) + title: .body("private_keys.evm_private_key".localized), + accessoryType: .disclosure, + isFirst: true, + isLast: true + ) { [weak self] in + self?.viewModel.onTapEvmPrivateKey() + }, + ] + ) ) } if viewModel.showBip32RootKey { sections.append( - Section( + Section( + id: "bip32-root-key", + footerState: tableView.sectionFooter(text: "private_keys.bip32_root_key.description".localized), + rows: [ + tableView.universalRow48( id: "bip32-root-key", - footerState: tableView.sectionFooter(text: "private_keys.bip32_root_key.description".localized), - rows: [ - tableView.universalRow48( - id: "bip32-root-key", - title: .body("private_keys.bip32_root_key".localized), - accessoryType: .disclosure, - isFirst: true, - isLast: true - ) { [weak self] in - self?.viewModel.onTapBip32RootKey() - } - ] - ) + title: .body("private_keys.bip32_root_key".localized), + accessoryType: .disclosure, + isFirst: true, + isLast: true + ) { [weak self] in + self?.viewModel.onTapBip32RootKey() + }, + ] + ) ) } if viewModel.showAccountExtendedPrivateKey { sections.append( - Section( + Section( + id: "account-extended-private-key", + footerState: tableView.sectionFooter(text: "private_keys.account_extended_private_key.description".localized), + rows: [ + tableView.universalRow48( id: "account-extended-private-key", - footerState: tableView.sectionFooter(text: "private_keys.account_extended_private_key.description".localized), - rows: [ - tableView.universalRow48( - id: "account-extended-private-key", - title: .body("private_keys.account_extended_private_key".localized), - accessoryType: .disclosure, - isFirst: true, - isLast: true - ) { [weak self] in - self?.viewModel.onTapAccountExtendedPrivateKey() - } - ] - ) + title: .body("private_keys.account_extended_private_key".localized), + accessoryType: .disclosure, + isFirst: true, + isLast: true + ) { [weak self] in + self?.viewModel.onTapAccountExtendedPrivateKey() + }, + ] + ) ) } return sections } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewModel.swift index e34a1cb727..3d2fc3a4c7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccount/PrivateKeys/PrivateKeysViewModel.swift @@ -58,7 +58,7 @@ extension PrivateKeysViewModel { } func onTapEvmPrivateKey() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .evmPrivateKey openUnlockRelay.accept(()) } else { @@ -67,7 +67,7 @@ extension PrivateKeysViewModel { } func onTapBip32RootKey() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .bip32RootKey openUnlockRelay.accept(()) } else { @@ -76,7 +76,7 @@ extension PrivateKeysViewModel { } func onTapAccountExtendedPrivateKey() { - if service.isPinSet { + if service.isPasscodeSet { unlockRequest = .accountExtendedPrivateKey openUnlockRelay.accept(()) } else { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift new file mode 100644 index 0000000000..536d22992b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift @@ -0,0 +1,24 @@ +import Combine + +class CreateDuressPasscodeViewModel: SetPasscodeViewModel { + override var title: String { + "create_duress_passcode.title".localized + } + + override var passcodeDescription: String { + "create_duress_passcode.description".localized + } + + override var confirmDescription: String { + "create_duress_passcode.confirm_passcode".localized + } + + override func onEnter(passcode: String) { + do { + try passcodeManager.set(duressPasscode: passcode) + finishSubject.send() + } catch { + print("Create Duress Passcode Error: \(error)") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift new file mode 100644 index 0000000000..17948c52f9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct CreatePasscodeModule { + static func createPasscodeView(reason: CreatePasscodeReason, onCreate: @escaping () -> Void, onCancel: @escaping () -> Void) -> some View { + let viewModel = CreatePasscodeViewModel( + passcodeManager: App.shared.passcodeManager, + reason: reason, + onCreate: onCreate, + onCancel: onCancel + ) + + return SetPasscodeView(viewModel: viewModel) + } + + static func createDuressPasscodeView() -> some View { + let viewModel = CreateDuressPasscodeViewModel( + passcodeManager: App.shared.passcodeManager + ) + + return SetPasscodeView(viewModel: viewModel) + } + + enum CreatePasscodeReason: Hashable, Identifiable { + case regular + case biometry(type: BiometryType) + case duress + + var description: String { + switch self { + case .regular: return "create_passcode.description".localized + case .biometry(let type): return "create_passcode.description.biometry".localized(type.title) + case .duress: return "create_passcode.description.duress_mode".localized + } + } + + var id: Self { + self + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeViewModel.swift new file mode 100644 index 0000000000..ea27d34e9a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeViewModel.swift @@ -0,0 +1,41 @@ +import Combine + +class CreatePasscodeViewModel: SetPasscodeViewModel { + private let reason: CreatePasscodeModule.CreatePasscodeReason + private let onCreate: () -> Void + private let _onCancel: () -> Void + + init(passcodeManager: PasscodeManager, reason: CreatePasscodeModule.CreatePasscodeReason, onCreate: @escaping () -> Void, onCancel: @escaping () -> Void) { + self.reason = reason + self.onCreate = onCreate + _onCancel = onCancel + + super.init(passcodeManager: passcodeManager) + } + + override var title: String { + "create_passcode.title".localized + } + + override var passcodeDescription: String { + reason.description + } + + override var confirmDescription: String { + "create_passcode.confirm_passcode".localized + } + + override func onEnter(passcode: String) { + do { + try passcodeManager.set(passcode: passcode) + finishSubject.send() + onCreate() + } catch { + print("Create Passcode Error: \(error)") + } + } + + override func onCancel() { + _onCancel() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift new file mode 100644 index 0000000000..46aa676d85 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift @@ -0,0 +1,24 @@ +import Combine + +class EditDuressPasscodeViewModel: SetPasscodeViewModel { + override var title: String { + "edit_duress_passcode.title".localized + } + + override var passcodeDescription: String { + "edit_duress_passcode.enter_new_passcode".localized + } + + override var confirmDescription: String { + "edit_duress_passcode.confirm_new_passcode".localized + } + + override func onEnter(passcode: String) { + do { + try passcodeManager.set(duressPasscode: passcode) + finishSubject.send() + } catch { + print("Edit Duress Passcode Error: \(error)") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift new file mode 100644 index 0000000000..cb3c46c339 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct EditPasscodeModule { + static func editPasscodeView() -> some View { + let viewModel = EditPasscodeViewModel(passcodeManager: App.shared.passcodeManager) + return SetPasscodeView(viewModel: viewModel) + } + + static func editDuressPasscodeView() -> some View { + let viewModel = EditDuressPasscodeViewModel(passcodeManager: App.shared.passcodeManager) + return SetPasscodeView(viewModel: viewModel) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift new file mode 100644 index 0000000000..574d55e77a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift @@ -0,0 +1,24 @@ +import Combine + +class EditPasscodeViewModel: SetPasscodeViewModel { + override var title: String { + "edit_passcode.title".localized + } + + override var passcodeDescription: String { + "edit_passcode.enter_new_passcode".localized + } + + override var confirmDescription: String { + "edit_passcode.confirm_new_passcode".localized + } + + override func onEnter(passcode: String) { + do { + try passcodeManager.set(passcode: passcode) + finishSubject.send() + } catch { + print("Edit Passcode Error: \(error)") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift new file mode 100644 index 0000000000..29234649b7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct SetPasscodeView: View { + @ObservedObject var viewModel: SetPasscodeViewModel + + @Environment(\.presentationMode) private var presentationMode + + var body: some View { + ThemeNavigationView { + ThemeView { + PasscodeView( + maxDigits: viewModel.passcodeLength, + description: $viewModel.description, + errorText: $viewModel.errorText, + passcode: $viewModel.passcode, + biometryType: Binding(get: { nil }, set: { _ in }), + lockoutState: Binding(get: { .unlocked(attemptsLeft: Int.max, maxAttempts: Int.max) }, set: { _ in }), + randomEnabled: false + ) + } + .navigationTitle(viewModel.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + viewModel.onCancel() + presentationMode.wrappedValue.dismiss() + } + } + .onReceive(viewModel.finishSubject) { + presentationMode.wrappedValue.dismiss() + } + } + .interactiveDismiss(canDismissSheet: false) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift new file mode 100644 index 0000000000..b4bbbd5ab2 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift @@ -0,0 +1,59 @@ +import Combine + +class SetPasscodeViewModel: ObservableObject { + let passcodeLength = 6 + + @Published var description: String = "" + @Published var errorText: String = "" + @Published var passcode: String = "" { + didSet { + if passcode.count == passcodeLength { + Task { + try await Task.sleep(nanoseconds: 200_000_000) + await handlePasscodeChanged() + } + } else if passcode.count != 0 { + errorText = "" + } + } + } + + let passcodeManager: PasscodeManager + + var finishSubject = PassthroughSubject() + + private var enteredPasscode: String? + + init(passcodeManager: PasscodeManager) { + self.passcodeManager = passcodeManager + syncDescription() + } + + var title: String { "" } + var passcodeDescription: String { "" } + var confirmDescription: String { "" } + func onEnter(passcode _: String) {} + func onCancel() {} + + @MainActor + private func handlePasscodeChanged() { + if let enteredPasscode { + if enteredPasscode == passcode { + onEnter(passcode: passcode) + } else { + self.enteredPasscode = nil + passcode = "" + syncDescription() + errorText = "set_passcode.invalid_confirmation".localized + } + } else { + enteredPasscode = passcode + passcode = "" + syncDescription() + } + } + + private func syncDescription() { + description = enteredPasscode == nil ? passcodeDescription : confirmDescription + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift new file mode 100644 index 0000000000..74635ef27a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import ThemeKit + +struct NumPadView: View { + @Binding var digits: [Int] + @Binding var biometryType: BiometryType? + + let onTapDigit: (Int) -> Void + let onTapBackspace: () -> Void + var onTapBiometry: (() -> Void)? = nil + + var body: some View { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: .margin16) { + ForEach(Array(digits.prefix(9).enumerated()), id: \.offset) { _, digit in + NumberView(digit: digit) { onTapDigit(digit) } + } + + if let biometryType { + Button(action: { + onTapBiometry?() + }) { + Image(biometryType.iconName).themeIcon() + } + } else { + Text("") + } + + if let digit = digits.last { + NumberView(digit: digit) { onTapDigit(digit) } + } + + Button(action: { + onTapBackspace() + }) { + Image("backspace_24").themeIcon() + } + } + .frame(width: 280) + } + + struct NumberView: View { + let digit: Int + let onTap: () -> Void + + var body: some View { + Button(action: { + onTap() + }) { + Text(String(digit)) + .font(.themeTitle2R) + .foregroundColor(.themeLeah) + .frame(width: 72, height: 72) + } + .buttonStyle(NumPadButtonStyle()) + .animation(.easeOut, value: digit) + } + } + + struct NumPadButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background(configuration.isPressed ? Color.themeSteel20 : Color.clear) + .clipShape(Circle()) + .overlay(Circle().stroke(Color.themeSteel20, lineWidth: .heightOneDp)) + .animation(.easeOut, value: configuration.isPressed) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift new file mode 100644 index 0000000000..4b3151e5b6 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift @@ -0,0 +1,110 @@ +import SwiftUI + +struct PasscodeView: View { + let maxDigits: Int + + @Binding var description: String + @Binding var errorText: String + @Binding var passcode: String { + didSet { + backspaceVisible = !passcode.isEmpty + } + } + + @Binding var biometryType: BiometryType? + @Binding var lockoutState: LockoutState + let randomEnabled: Bool + var onTapBiometry: (() -> Void)? = nil + + @State var digits: [Int] = (1 ... 9) + [0] + @State var backspaceVisible: Bool = false + @State var randomized: Bool = false { + didSet { + if randomized { + digits = (0 ... 9).shuffled() + } else { + digits = (1 ... 9) + [0] + } + } + } + + var body: some View { + VStack { + VStack { + switch lockoutState { + case let .unlocked(attemptsLeft, _): + Text(description) + .font(.themeSubhead2) + .foregroundColor(.themeGray) + .padding(.horizontal, .margin48) + .multilineTextAlignment(.center) + .frame(maxHeight: .infinity, alignment: .bottom) + .transition(.opacity.animation(.easeOut)) + .id(description) + + HStack(spacing: .margin12) { + ForEach(0 ..< maxDigits, id: \.self) { index in + Circle() + .fill(index < passcode.count ? Color.themeJacob : Color.themeSteel20) + .frame(width: .margin12, height: .margin12) + } + } + .modifier(Shake(animatableData: CGFloat(attemptsLeft))) + .padding(.vertical, .margin16) + .animation(.linear(duration: 0.3), value: attemptsLeft) + .animation(.easeOut(duration: 0.1), value: passcode) + + Text(errorText) + .font(.themeCaption) + .foregroundColor(.themeLucian) + .padding(.horizontal, .margin48) + .multilineTextAlignment(.center) + .frame(maxHeight: .infinity, alignment: .top) + .transition(.opacity.animation(.easeOut)) + .id(errorText) + case let .locked(unlockDate): + Text("unlock.disabled_until".localized("\(unlockDate)")) + } + } + .frame(maxHeight: .infinity) + + VStack(spacing: .margin24) { + NumPadView( + digits: $digits, + biometryType: $biometryType, + onTapDigit: { digit in + guard passcode.count < maxDigits else { + return + } + + passcode = passcode + "\(digit)" + }, + onTapBackspace: { + passcode = String(passcode.dropLast()) + }, + onTapBiometry: onTapBiometry + ) + + if randomEnabled { + Button(action: { + randomized.toggle() + }) { + Text(randomized ? "unlock.regular_mode".localized : "unlock.random_mode".localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default)) + } + } + .padding(.bottom, .margin32) + } + } +} + +struct Shake: GeometryEffect { + var amount: CGFloat = 8 + var shakesPerUnit = 4 + var animatableData: CGFloat + + func effectValue(size _: CGSize) -> ProjectionTransform { + ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift new file mode 100644 index 0000000000..3d738fdb56 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift @@ -0,0 +1,42 @@ +import Combine + +class AppUnlockViewModel: BaseUnlockViewModel { + private let autoDismiss: Bool + private let onUnlock: (() -> Void)? + private let lockManager: LockManager + + init(autoDismiss: Bool, onUnlock: (() -> Void)?, passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockoutManager: LockoutManager, lockManager: LockManager, blurManager: BlurManager) { + self.autoDismiss = autoDismiss + self.onUnlock = onUnlock + self.lockManager = lockManager + + super.init(passcodeManager: passcodeManager, biometryManager: biometryManager, lockoutManager: lockoutManager, blurManager: blurManager, biometryAllowed: true) + } + + override func isValid(passcode: String) -> Bool { + passcodeManager.has(passcode: passcode) + } + + override func onEnterValid(passcode: String) { + super.onEnterValid(passcode: passcode) + + passcodeManager.set(currentPasscode: passcode) + handleUnlock() + } + + override func onBiometryUnlock() { + super.onBiometryUnlock() + + passcodeManager.setLastPasscode() + handleUnlock() + } + + private func handleUnlock() { + lockManager.onUnlock() + onUnlock?() + + if autoDismiss { + finishSubject.send() + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift new file mode 100644 index 0000000000..5df91617e4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -0,0 +1,111 @@ +import Combine +import HsExtensions +import LocalAuthentication + +class BaseUnlockViewModel: ObservableObject { + let passcodeLength = 6 + + @Published var description: String = "unlock.passcode".localized + @Published var errorText: String = "" + @Published var passcode: String = "" { + didSet { + if passcode.count == passcodeLength { + Task { + try await Task.sleep(nanoseconds: 200_000_000) + await handlePasscodeChanged() + } + } + } + } + + @Published var resolvedBiometryType: BiometryType? + var biometryType: BiometryType? + var biometryEnabled: Bool + @Published var lockoutState: LockoutState { + didSet { + syncErrorText() + } + } + + let finishSubject = PassthroughSubject() + let unlockWithBiometrySubject = PassthroughSubject() + + let passcodeManager: PasscodeManager + private let biometryManager: BiometryManager + private let lockoutManager: LockoutManager + private let blurManager: BlurManager + private let biometryAllowed: Bool + private var cancellables = Set() + private var tasks = Set() + + init(passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockoutManager: LockoutManager, blurManager: BlurManager, biometryAllowed: Bool) { + self.passcodeManager = passcodeManager + self.biometryManager = biometryManager + self.lockoutManager = lockoutManager + self.blurManager = blurManager + self.biometryAllowed = biometryAllowed + + biometryType = biometryManager.biometryType + biometryEnabled = biometryManager.biometryEnabled + lockoutState = lockoutManager.lockoutState + + biometryManager.$biometryType + .sink { [weak self] in + self?.biometryType = $0 + self?.syncBiometryType() + } + .store(in: &cancellables) + biometryManager.$biometryEnabled + .sink { [weak self] in + self?.biometryEnabled = $0 + self?.syncBiometryType() + } + .store(in: &cancellables) + lockoutManager.$lockoutState + .sink { [weak self] in self?.lockoutState = $0 } + .store(in: &cancellables) + + syncErrorText() + syncBiometryType() + } + + private func syncBiometryType() { + resolvedBiometryType = biometryEnabled && biometryAllowed ? biometryType : nil + } + + func isValid(passcode _: String) -> Bool { false } + func onEnterValid(passcode _: String) {} + func onBiometryUnlock() {} + + @MainActor + private func handlePasscodeChanged() { + if isValid(passcode: passcode) { + onEnterValid(passcode: passcode) + lockoutManager.didUnlock() + } else { + passcode = "" + lockoutManager.didFailUnlock() + } + } + + private func syncErrorText() { + switch lockoutState { + case let .unlocked(attemptsLeft, maxAttempts): + errorText = attemptsLeft == maxAttempts ? "" : "unlock.attempts_left".localized(String(attemptsLeft)) + default: + errorText = "" + } + } + + func onAppear() { + blurManager.isEnabled = false + + if resolvedBiometryType != nil { + unlockWithBiometrySubject.send() + } + } + + func onDisappear() { + blurManager.isEnabled = true + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift new file mode 100644 index 0000000000..1d9b58dd31 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct ModuleUnlockView: View { + @ObservedObject var viewModel: ModuleUnlockViewModel + + @Environment(\.presentationMode) private var presentationMode + + var body: some View { + UnlockView(viewModel: viewModel, autoDismiss: true) + .navigationTitle("unlock.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + presentationMode.wrappedValue.dismiss() + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift new file mode 100644 index 0000000000..75a3aec03b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift @@ -0,0 +1,29 @@ +import Combine +import Foundation + +class ModuleUnlockViewModel: BaseUnlockViewModel { + private let onUnlock: () -> Void + + init(passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockoutManager: LockoutManager, blurManager: BlurManager, biometryAllowed: Bool, onUnlock: @escaping () -> Void) { + self.onUnlock = onUnlock + + super.init(passcodeManager: passcodeManager, biometryManager: biometryManager, lockoutManager: lockoutManager, blurManager: blurManager, biometryAllowed: biometryAllowed) + } + + override func isValid(passcode: String) -> Bool { + passcodeManager.isValid(passcode: passcode) + } + + override func onEnterValid(passcode: String) { + super.onEnterValid(passcode: passcode) + + onUnlock() + finishSubject.send() + } + + override func onBiometryUnlock() { + super.onBiometryUnlock() + + onUnlock() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift new file mode 100644 index 0000000000..e01bae2153 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct UnlockModule { + static func appUnlockView(autoDismiss: Bool = false, onUnlock: (() -> Void)? = nil) -> some View { + let viewModel = AppUnlockViewModel( + autoDismiss: autoDismiss, + onUnlock: onUnlock, + passcodeManager: App.shared.passcodeManager, + biometryManager: App.shared.biometryManager, + lockoutManager: App.shared.lockoutManager, + lockManager: App.shared.lockManager, + blurManager: App.shared.blurManager + ) + + return UnlockView(viewModel: viewModel, autoDismiss: autoDismiss) + } + + static func moduleUnlockView(biometryAllowed: Bool = true, onUnlock: @escaping () -> Void) -> some View { + let viewModel = ModuleUnlockViewModel( + passcodeManager: App.shared.passcodeManager, + biometryManager: App.shared.biometryManager, + lockoutManager: App.shared.lockoutManager, + blurManager: App.shared.blurManager, + biometryAllowed: biometryAllowed, + onUnlock: onUnlock + ) + + return ModuleUnlockView(viewModel: viewModel) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift new file mode 100644 index 0000000000..a407a427fe --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift @@ -0,0 +1,53 @@ +import LocalAuthentication +import SwiftUI + +struct UnlockView: View { + @ObservedObject var viewModel: BaseUnlockViewModel + let autoDismiss: Bool + + @Environment(\.presentationMode) private var presentationMode + + var body: some View { + ThemeView { + PasscodeView( + maxDigits: viewModel.passcodeLength, + description: $viewModel.description, + errorText: $viewModel.errorText, + passcode: $viewModel.passcode, + biometryType: $viewModel.resolvedBiometryType, + lockoutState: $viewModel.lockoutState, + randomEnabled: true, + onTapBiometry: { + unlockWithBiometry() + } + ) + } + .onAppear { + viewModel.onAppear() + } + .onDisappear { + viewModel.onDisappear() + } + .onReceive(viewModel.finishSubject) { + presentationMode.wrappedValue.dismiss() + } + .onReceive(viewModel.unlockWithBiometrySubject) { + unlockWithBiometry() + } + } + + private func unlockWithBiometry() { + let localAuthenticationContext = LAContext() + localAuthenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "unlock.biometry_reason".localized) { success, _ in + if success { + DispatchQueue.main.async { + viewModel.onBiometryUnlock() + + if autoDismiss { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift index 26a69bc867..8acf2796b6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift @@ -1,25 +1,23 @@ import UIKit struct MainSettingsModule { - static func viewController() -> UIViewController { let service = MainSettingsService( - backupManager: App.shared.backupManager, - cloudAccountBackupManager: App.shared.cloudBackupManager, - accountRestoreWarningManager: App.shared.accountRestoreWarningManager, - accountManager: App.shared.accountManager, - contactBookManager: App.shared.contactManager, - pinKit: App.shared.pinKit, - termsManager: App.shared.termsManager, - systemInfoManager: App.shared.systemInfoManager, - currencyKit: App.shared.currencyKit, - walletConnectSessionManager: App.shared.walletConnectSessionManager, - subscriptionManager: App.shared.subscriptionManager + backupManager: App.shared.backupManager, + cloudAccountBackupManager: App.shared.cloudBackupManager, + accountRestoreWarningManager: App.shared.accountRestoreWarningManager, + accountManager: App.shared.accountManager, + contactBookManager: App.shared.contactManager, + passcodeManager: App.shared.passcodeManager, + termsManager: App.shared.termsManager, + systemInfoManager: App.shared.systemInfoManager, + currencyKit: App.shared.currencyKit, + walletConnectSessionManager: App.shared.walletConnectSessionManager, + subscriptionManager: App.shared.subscriptionManager ) let viewModel = MainSettingsViewModel(service: service) return MainSettingsViewController(viewModel: viewModel, urlManager: UrlManager(inApp: true)) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift index 7314d41c78..e4e43ec873 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift @@ -1,12 +1,10 @@ import Combine -import RxSwift -import RxRelay +import CurrencyKit import LanguageKit +import RxRelay +import RxSwift import ThemeKit -import CurrencyKit -import PinKit import WalletConnectV1 -import ThemeKit class MainSettingsService { private let disposeBag = DisposeBag() @@ -16,7 +14,7 @@ class MainSettingsService { private let accountRestoreWarningManager: AccountRestoreWarningManager private let accountManager: AccountManager private let contactBookManager: ContactBookManager - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager private let termsManager: TermsManager private let systemInfoManager: SystemInfoManager private let currencyKit: CurrencyKit.Kit @@ -26,14 +24,15 @@ class MainSettingsService { private let iCloudAvailableErrorRelay = BehaviorRelay(value: false) private let noWalletRequiredActionsRelay = BehaviorRelay(value: false) - init(backupManager: BackupManager, cloudAccountBackupManager: CloudBackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, accountManager: AccountManager, contactBookManager: ContactBookManager, pinKit: PinKit.Kit, termsManager: TermsManager, - systemInfoManager: SystemInfoManager, currencyKit: CurrencyKit.Kit, walletConnectSessionManager: WalletConnectSessionManager, subscriptionManager: SubscriptionManager) { + init(backupManager: BackupManager, cloudAccountBackupManager: CloudBackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, accountManager: AccountManager, contactBookManager: ContactBookManager, passcodeManager: PasscodeManager, termsManager: TermsManager, + systemInfoManager: SystemInfoManager, currencyKit: CurrencyKit.Kit, walletConnectSessionManager: WalletConnectSessionManager, subscriptionManager: SubscriptionManager) + { self.cloudAccountBackupManager = cloudAccountBackupManager self.backupManager = backupManager self.accountRestoreWarningManager = accountRestoreWarningManager self.accountManager = accountManager self.contactBookManager = contactBookManager - self.pinKit = pinKit + self.passcodeManager = passcodeManager self.termsManager = termsManager self.systemInfoManager = systemInfoManager self.currencyKit = currencyKit @@ -41,7 +40,7 @@ class MainSettingsService { self.subscriptionManager = subscriptionManager subscribe(disposeBag, contactBookManager.iCloudErrorObservable) { [weak self] error in - if error != nil, (self?.contactBookManager.remoteSync ?? false) { + if error != nil, self?.contactBookManager.remoteSync ?? false { self?.iCloudAvailableErrorRelay.accept(true) } else { self?.iCloudAvailableErrorRelay.accept(false) @@ -57,11 +56,9 @@ class MainSettingsService { private func syncWalletRequiredActions() { noWalletRequiredActionsRelay.accept(backupManager.allBackedUp && !accountRestoreWarningManager.hasNonStandard) } - } extension MainSettingsService { - var noWalletRequiredActions: Bool { backupManager.allBackedUp && !accountRestoreWarningManager.hasNonStandard } @@ -70,12 +67,12 @@ extension MainSettingsService { noWalletRequiredActionsRelay.asObservable() } - var isPinSet: Bool { - pinKit.isPinSet + var isPasscodeSet: Bool { + passcodeManager.isPasscodeSet } - var isPinSetPublisher: AnyPublisher { - pinKit.isPinSetPublisher + var isPasscodeSetPublisher: AnyPublisher { + passcodeManager.$isPasscodeSet } var isCloudAvailableError: Bool { @@ -110,7 +107,6 @@ extension MainSettingsService { walletConnectSessionManager.activePendingRequestsObservable.map { $0.count } } - var currentLanguageDisplayName: String? { LanguageManager.shared.currentLanguageDisplayName } @@ -153,16 +149,13 @@ extension MainSettingsService { var analyticsLink: String { AppConfig.analyticsLink } - } extension MainSettingsService { - enum WalletConnectState { case noAccount case backedUp case nonSupportedAccountType(accountType: AccountType) case unBackedUpAccount(account: Account) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift index aa03bd9565..5afa0c8089 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift @@ -23,7 +23,7 @@ class MainSettingsViewModel { self.service = service manageWalletsAlertRelay = BehaviorRelay(value: !service.noWalletRequiredActions) - securityCenterAlertRelay = BehaviorRelay(value: !service.isPinSet) + securityCenterAlertRelay = BehaviorRelay(value: !service.isPasscodeSet) iCloudSyncAlertRelay = BehaviorRelay(value: service.isCloudAvailableError) walletConnectCountRelay = BehaviorRelay(value: Self.convert(walletConnectSessionCount: service.walletConnectSessionCount, walletConnectPendingRequestCount: service.walletConnectPendingRequestCount)) baseCurrencyRelay = BehaviorRelay(value: service.baseCurrency.code) @@ -36,7 +36,7 @@ class MainSettingsViewModel { }) .disposed(by: disposeBag) - service.isPinSetPublisher + service.isPasscodeSetPublisher .sink { [weak self] isPinSet in self?.securityCenterAlertRelay.accept(!isPinSet) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift index afa088dfc9..6e2b66aacd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift @@ -2,7 +2,11 @@ import SwiftUI struct SecuritySettingsModule { static func view() -> some View { - let viewModel = SecuritySettingsViewModel(pinKit: App.shared.pinKit) + let viewModel = SecuritySettingsViewModel( + passcodeManager: App.shared.passcodeManager, + biometryManager: App.shared.biometryManager, + lockManager: App.shared.lockManager + ) return SecuritySettingsView(viewModel: viewModel) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index 770f1676f8..b49b8a65d2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -1,109 +1,128 @@ -import PinKit import SwiftUI struct SecuritySettingsView: View { @ObservedObject var viewModel: SecuritySettingsViewModel - @State var editPasscodePresented: Bool = false + + @State var createPasscodeReason: CreatePasscodeModule.CreatePasscodeReason? + @State var unlockReason: UnlockReason? + + @State var editPasscodePresented = false + @State var createDuressPasscodePresented = false + @State var editDuressPasscodePresented = false var body: some View { ScrollableThemeView { VStack(spacing: .margin32) { ListSection { - ListRow { - Image("dialpad_alt_2_24") - Text("settings_security.passcode".localized).themeBody() - Spacer() - - if !viewModel.passcodeEnabled { - Image("warning_2_20") - .renderingMode(.template) - .foregroundColor(.themeLucian) + if viewModel.isPasscodeSet { + ClickableRow(action: { + unlockReason = .changePasscode + }) { + Image("dialpad_alt_2_24").themeIcon(color: .themeJacob) + Text("settings_security.edit_passcode".localized).themeBody(color: .themeJacob) } - Toggle(isOn: $viewModel.passcodeSwitchOn) {} - .labelsHidden() - } - .sheet(isPresented: $viewModel.setPasscodePresented, onDismiss: { viewModel.cancelSetPasscode() }) { - SetPinView( - cancelAction: { viewModel.cancelSetPasscode() } - ).edgesIgnoringSafeArea(.all) - } - .sheet(isPresented: $viewModel.unlockPasscodePresented, onDismiss: { viewModel.cancelUnlock() }) { - UnlockPinView( - unlockAction: { viewModel.onUnlock() }, - cancelAction: { viewModel.cancelUnlock() } - ).edgesIgnoringSafeArea(.all) - } - - if viewModel.passcodeEnabled { - ClickableRow(action: { editPasscodePresented = true }) { - Text("settings_security.change_pin".localized).themeBody() - Image.disclosureIcon + ClickableRow(action: { + unlockReason = .disablePasscode + }) { + Image("trash_24").themeIcon(color: .themeLucian) + Text("settings_security.disable_passcode".localized).themeBody(color: .themeLucian) } - .sheet(isPresented: $editPasscodePresented) { - EditPinView().edgesIgnoringSafeArea(.all) + } else { + ClickableRow(action: { + createPasscodeReason = .regular + }) { + Image("dialpad_alt_2_24").themeIcon(color: .themeJacob) + Text("settings_security.enable_passcode".localized).themeBody(color: .themeJacob) + Image("warning_2_20").themeIcon(color: .themeLucian) } } } - if viewModel.passcodeEnabled && viewModel.biometryAvailable { + if let biometryType = viewModel.biometryType { ListSection { ListRow { - Image(viewModel.biometryIconName) - Toggle(isOn: $viewModel.biometryEnabled) { - Text(viewModel.biometryTitle).themeBody() + Image(biometryType.iconName) + Toggle(isOn: $viewModel.isBiometryToggleOn) { + Text(biometryType.title).themeBody() + } + .onChange(of: viewModel.isBiometryToggleOn) { isOn in + if !viewModel.isPasscodeSet, isOn { + createPasscodeReason = .biometry(type: biometryType) + } + } + } + } + } + } + .sheet(item: $createPasscodeReason) { reason in + CreatePasscodeModule.createPasscodeView( + reason: reason, + onCreate: { + switch reason { + case .biometry: + viewModel.set(biometryEnabled: true) + case .duress: + DispatchQueue.main.async { + createDuressPasscodePresented = true + } + default: () + } + }, + onCancel: { + switch reason { + case .biometry: viewModel.isBiometryToggleOn = false + default: () + } + } + ) + } + .sheet(item: $unlockReason) { reason in + ThemeNavigationView { + UnlockModule.moduleUnlockView(biometryAllowed: false) { + switch reason { + case .changePasscode: + DispatchQueue.main.async { + editPasscodePresented = true + } + case .disablePasscode: + viewModel.removePasscode() + case .enableDuressMode: + DispatchQueue.main.async { + createDuressPasscodePresented = true + } + case .changeDuressPasscode: + DispatchQueue.main.async { + editDuressPasscodePresented = true } + case .disableDuressMode: + viewModel.removeDuressPasscode() } } } } + .sheet(isPresented: $editPasscodePresented) { + EditPasscodeModule.editPasscodeView() + } + .sheet(isPresented: $createDuressPasscodePresented) { + CreatePasscodeModule.createDuressPasscodeView() + } + .sheet(isPresented: $editDuressPasscodePresented) { + EditPasscodeModule.editDuressPasscodeView() + } .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } } -} - -struct SetPinView: UIViewControllerRepresentable, ISetPinDelegate { - typealias UIViewControllerType = UIViewController - - let cancelAction: () -> () - - func makeUIViewController(context: Context) -> UIViewController { - App.shared.pinKit.setPinModule(delegate: self) - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} - - func didCancelSetPin() { - cancelAction() - } -} - -struct EditPinView: UIViewControllerRepresentable { - typealias UIViewControllerType = UIViewController - - func makeUIViewController(context: Context) -> UIViewController { - return App.shared.pinKit.editPinModule - } - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} -} - -struct UnlockPinView: UIViewControllerRepresentable { - typealias UIViewControllerType = UIViewController - - let unlockAction: () -> () - let cancelAction: () -> () + enum UnlockReason: Identifiable { + case changePasscode + case disablePasscode + case enableDuressMode + case changeDuressPasscode + case disableDuressMode - func makeUIViewController(context: Context) -> UIViewController { - return App.shared.pinKit.unlockPinModule( - biometryUnlockMode: .disabled, - insets: .zero, - cancellable: true, - autoDismiss: true, - onUnlock: unlockAction, - onCancelUnlock: cancelAction - ) + var id: Self { + self + } } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift index 33eb0e247b..6f5b427951 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift @@ -1,105 +1,70 @@ import Combine -import ComponentKit -import PinKit -import SwiftUI class SecuritySettingsViewModel: ObservableObject { - private let pinKit: PinKit.Kit + private let passcodeManager: PasscodeManager + private let biometryManager: BiometryManager + private let lockManager: LockManager private var cancellables = Set() - @Published var passcodeEnabled: Bool = false { - didSet { - passcodeSwitchOn = passcodeEnabled - } - } - - @Published var passcodeSwitchOn: Bool = false { - didSet { - guard oldValue != passcodeSwitchOn else { - return - } + @Published var currentPasscodeLevel: Int + @Published var isPasscodeSet: Bool + @Published var isDuressPasscodeSet: Bool + @Published var biometryType: BiometryType? - if passcodeSwitchOn { - if !passcodeEnabled { - setPasscodePresented = true - } - } else { - if passcodeEnabled { - unlockPasscodePresented = true - } - } - } - } - - @Published var setPasscodePresented: Bool = false - @Published var unlockPasscodePresented: Bool = false - - @Published var biometryEnabled: Bool = false { + @Published var isBiometryToggleOn: Bool { didSet { - if pinKit.biometryEnabled != biometryEnabled { - pinKit.biometryEnabled = biometryEnabled + if isBiometryToggleOn != biometryManager.biometryEnabled, isPasscodeSet { + set(biometryEnabled: isBiometryToggleOn) } } } - @Published var biometryAvailable: Bool = true + init(passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockManager: LockManager) { + self.passcodeManager = passcodeManager + self.biometryManager = biometryManager + self.lockManager = lockManager - var biometryTitle: String = "" - var biometryIconName: String = "" + currentPasscodeLevel = passcodeManager.currentPasscodeLevel + isPasscodeSet = passcodeManager.isPasscodeSet + isDuressPasscodeSet = passcodeManager.isDuressPasscodeSet + biometryType = biometryManager.biometryType - init(pinKit: PinKit.Kit) { - self.pinKit = pinKit + isBiometryToggleOn = biometryManager.biometryEnabled - pinKit.isPinSetPublisher - .sink { [weak self] _ in self?.sync() } + passcodeManager.$currentPasscodeLevel + .sink { [weak self] in self?.currentPasscodeLevel = $0 } .store(in: &cancellables) - - pinKit.biometryTypePublisher - .sink { [weak self] _ in self?.sync() } + passcodeManager.$isPasscodeSet + .sink { [weak self] in self?.isPasscodeSet = $0 } + .store(in: &cancellables) + passcodeManager.$isDuressPasscodeSet + .sink { [weak self] in self?.isDuressPasscodeSet = $0 } + .store(in: &cancellables) + biometryManager.$biometryType + .sink { [weak self] in self?.biometryType = $0 } + .store(in: &cancellables) + biometryManager.$biometryEnabled + .sink { [weak self] in self?.isBiometryToggleOn = $0 } .store(in: &cancellables) - - sync() - } - - private func sync() { - passcodeEnabled = pinKit.isPinSet - biometryEnabled = pinKit.biometryEnabled - - switch pinKit.biometryType { - case .faceId: - biometryAvailable = true - biometryTitle = "settings_security.face_id".localized - biometryIconName = "face_id_24" - case .touchId: - biometryAvailable = true - biometryTitle = "settings_security.touch_id".localized - biometryIconName = "touch_id_2_24" - default: - biometryAvailable = false - biometryTitle = "" - biometryIconName = "" - } } -} -extension SecuritySettingsViewModel { - func onUnlock() { + func removePasscode() { do { - try pinKit.clear() + try passcodeManager.removePasscode() } catch { - HudHelper.instance.show(banner: .error(string: error.smartDescription)) + print("Remove Passcode Error: \(error)") } } - func cancelSetPasscode() { - if !passcodeEnabled { - passcodeSwitchOn = false + func removeDuressPasscode() { + do { + try passcodeManager.removeDuressPasscode() + } catch { + print("Remove Duress Passcode Error: \(error)") } } - func cancelUnlock() { - if passcodeEnabled { - passcodeSwitchOn = true - } + func set(biometryEnabled: Bool) { + biometryManager.biometryEnabled = biometryEnabled } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift index 6a8f45983d..6c8aa76af9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowModule.swift @@ -6,7 +6,7 @@ class WalletConnectAppShowModule { walletConnectManager: App.shared.walletConnectSessionManager, cloudAccountBackupManager: App.shared.cloudBackupManager, accountManager: App.shared.accountManager, - pinKit: App.shared.pinKit + lockManager: App.shared.lockManager ) let walletConnectWorkerViewModel = WalletConnectAppShowViewModel(service: walletConnectWorkerService) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift index c789afa373..736054d58d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowService.swift @@ -1,6 +1,5 @@ import Combine import Foundation -import PinKit import RxSwift import WalletConnectSign @@ -10,16 +9,16 @@ class WalletConnectAppShowService { private let walletConnectManager: WalletConnectSessionManager private let cloudAccountBackupManager: CloudBackupManager private let accountManager: AccountManager - private let pinKit: PinKit.Kit + private let lockManager: LockManager private let showSessionProposalSubject = PassthroughSubject() private let showSessionRequestSubject = PassthroughSubject() - init(walletConnectManager: WalletConnectSessionManager, cloudAccountBackupManager: CloudBackupManager, accountManager: AccountManager, pinKit: PinKit.Kit) { + init(walletConnectManager: WalletConnectSessionManager, cloudAccountBackupManager: CloudBackupManager, accountManager: AccountManager, lockManager: LockManager) { self.walletConnectManager = walletConnectManager self.cloudAccountBackupManager = cloudAccountBackupManager self.accountManager = accountManager - self.pinKit = pinKit + self.lockManager = lockManager subscribe(disposeBag, walletConnectManager.service.receiveProposalObservable) { [weak self] in self?.receive(proposal: $0) } subscribe(disposeBag, walletConnectManager.sessionRequestReceivedObservable) { [weak self] in self?.receive(request: $0) } @@ -30,7 +29,7 @@ class WalletConnectAppShowService { } private func receive(request: WalletConnectRequest) { - if !pinKit.isLocked { + if !lockManager.isLocked { showSessionRequestSubject.send(request) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift new file mode 100644 index 0000000000..7c3f267eb1 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift @@ -0,0 +1,11 @@ +import UIKit + +class LockDelegate { + var viewController: UIViewController? + + func onLock() { + let module = UnlockModule.appUnlockView(autoDismiss: true).toViewController() + module.modalPresentationStyle = .fullScreen + viewController?.visibleController.present(module, animated: false) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/PinKitDelegate.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/PinKitDelegate.swift deleted file mode 100644 index 60eb87a6c1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/PinKitDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -import UIKit -import PinKit - -class PinKitDelegate { - var viewController: UIViewController? -} - -extension PinKitDelegate: IPinKitDelegate { - - func onLock() { - viewController?.visibleController.present(LockScreenModule.viewController(pinKit: App.shared.pinKit, appStart: false), animated: false) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InteractiveDismiss.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InteractiveDismiss.swift new file mode 100644 index 0000000000..8337479820 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InteractiveDismiss.swift @@ -0,0 +1,52 @@ +import SwiftUI + +class ModalHostingController: UIHostingController, UIAdaptivePresentationControllerDelegate { + var canDismissSheet = true + var onDismissalAttempt: (() -> Void)? + + override func willMove(toParent parent: UIViewController?) { + super.willMove(toParent: parent) + + parent?.presentationController?.delegate = self + } + + func presentationControllerShouldDismiss(_: UIPresentationController) -> Bool { + canDismissSheet + } + + func presentationControllerDidAttemptToDismiss(_: UIPresentationController) { + onDismissalAttempt?() + } +} + +struct ModalView: UIViewControllerRepresentable { + let view: T + let canDismissSheet: Bool + let onDismissalAttempt: (() -> Void)? + + func makeUIViewController(context _: Context) -> ModalHostingController { + let controller = ModalHostingController(rootView: view) + + controller.canDismissSheet = canDismissSheet + controller.onDismissalAttempt = onDismissalAttempt + + return controller + } + + func updateUIViewController(_ uiViewController: ModalHostingController, context _: Context) { + uiViewController.rootView = view + + uiViewController.canDismissSheet = canDismissSheet + uiViewController.onDismissalAttempt = onDismissalAttempt + } +} + +extension View { + func interactiveDismiss(canDismissSheet: Bool, onDismissalAttempt: (() -> Void)? = nil) -> some View { + ModalView( + view: self, + canDismissSheet: canDismissSheet, + onDismissalAttempt: onDismissalAttempt + ).edgesIgnoringSafeArea(.all) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift new file mode 100644 index 0000000000..2ed3a87667 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct SecondaryButtonStyle: ButtonStyle { + let style: Style + + @Environment(\.isEnabled) var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(EdgeInsets(top: 5.5, leading: .margin16, bottom: 5.5, trailing: .margin16)) + .font(.themeSubhead1) + .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .clipShape(Capsule(style: .continuous)) + .animation(.easeOut(duration: 0.2), value: configuration.isPressed) + } + + enum Style { + case `default` + case transparent + + func foregroundColor(isEnabled: Bool, isPressed: Bool) -> Color { + switch self { + case .default, .transparent: return isEnabled ? (isPressed ? .themeGray : .themeLeah) : .themeGray50 + } + } + + func backgroundColor(isEnabled _: Bool, isPressed: Bool) -> Color { + switch self { + case .default: return isPressed ? .themeSteel10 : .themeSteel20 + case .transparent: return .clear + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index efb7dbaa66..c56d19fcb8 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -94,6 +94,9 @@ "selector.any" = "Any"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + // Access Camera "access_camera.message" = "%@ needs access to your camera to scan the QR code. @@ -1072,11 +1075,50 @@ Go to Settings - > %@ and allow access to the camera."; // Settings -> Security "settings_security.title" = "Security"; -"settings_security.passcode" = "Passcode"; -"settings_security.change_pin" = "Edit Passcode"; -"settings_security.touch_id" = "Touch ID"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Blockchain Settings"; +"settings_security.enable_passcode" = "Enable Passcode"; +"settings_security.edit_passcode" = "Edit Passcode"; +"settings_security.disable_passcode" = "Disable Passcode"; + +// Create Passcode + +"create_passcode.title" = "Create Passcode"; +"create_passcode.description" = "Your passcode will be used to unlock your wallet"; +"create_passcode.description.biometry" = "Set a passcode to enable %@"; +"create_passcode.description.duress_mode" = "Set a passcode to enable Duress Mode"; +"create_passcode.confirm_passcode" = "Confirm passcode"; + +// Create Duress Passcode + +"create_duress_passcode.title" = "Duress Passcode"; +"create_duress_passcode.description" = "Set a passcode for Duress Mode"; +"create_duress_passcode.confirm_passcode" = "Confirm passcode"; + +// Edit Passcode + +"edit_passcode.title" = "Edit Passcode"; +"edit_passcode.enter_new_passcode" = "Enter new passcode"; +"edit_passcode.confirm_new_passcode" = "Confirm new passcode"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Edit Duress Passcode"; +"edit_duress_passcode.enter_new_passcode" = "Enter new passcode for Duress Mode"; +"edit_duress_passcode.confirm_new_passcode" = "Confirm new passcode"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Invalid Confirmation"; + +// Unlock + +"unlock.title" = "Unlock"; +"unlock.passcode" = "Passcode"; +"unlock.biometry_reason" = "Unlock wallet"; +"unlock.attempts_left" = "Attempts left: %@"; +"unlock.disabled_until" = "Disabled until: %@"; +"unlock.regular_mode" = "Regular Mode"; +"unlock.random_mode" = "Random Mode"; + "security_settings.delete_alert_button" = "Delete from Phone"; "btc_blockchain_settings.restore_source" = "Restore Source"; @@ -1225,25 +1267,6 @@ Go to Settings - > %@ and allow access to the camera."; "contacts.settings.alert_error.title" = "iCloud Error"; -// Set PIN - -"set_pin.title" = "Passcode"; -"set_pin.info" = "Your passcode will be used to unlock your wallet"; -"set_pin.wrong_confirmation" = "Passcode did not match. Try again"; - -// Edit PIN - -"edit_pin.title" = "Edit Passcode"; -"edit_pin.unlock_info" = "Current Passcode"; -"edit_pin.new_pin_info" = "New Passcode"; - -// Unlock PIN - -"unlock_pin.info" = "Passcode"; -"unlock_pin.cant_save_pin" = "Ouch! We cannot save your passcode, please contact us asap!"; -"unlock_pin.blocked_until" = "Disabled until: %@"; - - // Key Types "chart.time_duration.day" = "24H"; From 9e7944e74f40a560b180e832b24a4588b1b2c92b Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 27 Sep 2023 14:40:39 +0600 Subject: [PATCH 15/63] Add design for lockout state in Unlock module --- .../project.pbxproj | 224 +++++++++--------- .../Core/Managers/LockoutManager.swift | 24 +- .../Modules/Passcode/NumPadView.swift | 19 +- .../Modules/Passcode/PasscodeView.swift | 13 +- .../Passcode/Unlock/BaseUnlockViewModel.swift | 7 +- .../SwiftUI/SecondaryCircleButtonStyle.swift | 37 +++ 6 files changed, 205 insertions(+), 119 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 589cee4219..5275a9ed65 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -193,12 +193,14 @@ 11B352184FCE4B2B3E68E459 /* CurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */; }; 11B3521C81ACF7BFC8875A61 /* TransactionsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DA1121283E64C139183 /* TransactionsHeaderView.swift */; }; 11B352210BEEE91481291D4C /* FormCautionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3568F6FAF721301DEC188 /* FormCautionView.swift */; }; + 11B35224D7A5A864C1C6F167 /* SecondaryCircleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */; }; 11B35228CD314AB9DC68DDAA /* EnabledWalletCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566B18FBFBA85D98D824 /* EnabledWalletCacheManager.swift */; }; 11B352309B81355B88BF6B66 /* OneInchKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3514BC3FF2FAC76ADF9F7 /* OneInchKit.swift */; }; 11B3523397D1FA961BFE9967 /* ClickableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D179817528224E926D1 /* ClickableRow.swift */; }; 11B352339E8052E6EE9CA3CF /* EnabledWalletCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D2FC6A2DABFE73D1025 /* EnabledWalletCache.swift */; }; 11B352346D3565C7D6395D21 /* PasteInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3575530EE722514F89A61 /* PasteInputView.swift */; }; 11B3523E47942D2118DBC290 /* ManageAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DC48EEBE1160676B269 /* ManageAccountService.swift */; }; + 11B3523E8B466F259DB32E37 /* SecondaryCircleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */; }; 11B352407989CB29F849C0BA /* CexAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357EC69F650DCA696F48D /* CexAsset.swift */; }; 11B3524401E294D8A919186E /* EnabledWallet_v_0_20.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35577CFC2384E3A454329 /* EnabledWallet_v_0_20.swift */; }; 11B35245BBF4B5F9F07676F4 /* CexWithdrawConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3517F84E9913C9030E749 /* CexWithdrawConfirmViewController.swift */; }; @@ -3051,6 +3053,7 @@ 11B3583528958D290AD3CE0C /* BackupMnemonicWordsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupMnemonicWordsCell.swift; sourceTree = ""; }; 11B3583932F270503C1DF3F0 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = ""; }; 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryViewController.swift; sourceTree = ""; }; + 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryCircleButtonStyle.swift; sourceTree = ""; }; 11B35850DF16D11D45C44A60 /* CexDepositNetworkSelectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectService.swift; sourceTree = ""; }; 11B358556C8FC5368E14D81E /* AccountRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecordStorage.swift; sourceTree = ""; }; 11B3585EF1DA625D906AF9B5 /* BalanceButtonsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceButtonsView.swift; sourceTree = ""; }; @@ -4603,6 +4606,7 @@ 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */, 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */, 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */, + 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */, ); path = SwiftUI; sourceTree = ""; @@ -7861,43 +7865,43 @@ D3BF1E61274CBBCE00229A00 /* XCRemoteSwiftPackageReference "DeepDiff" */, 500F1D0F27AA87BC002AA419 /* XCRemoteSwiftPackageReference "AlignedCollectionViewFlowLayout" */, D36E0C2828D084AB00B622B9 /* XCRemoteSwiftPackageReference "CollectionViewCenteredFlowLayout" */, - D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */, - D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */, - D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */, - D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */, - D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */, - D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */, - D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */, - D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */, - D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */, - D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */, - D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */, - D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */, - D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */, - D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */, - D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */, - D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */, + D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */, + D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */, + D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */, + D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */, + D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */, + D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */, + D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */, + D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */, + D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */, + D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */, + D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */, + D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */, + D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */, + D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */, + D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */, + D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */, D3993D9C28F41F5C008720FB /* XCRemoteSwiftPackageReference "wallet-connect-swift" */, D3993DA328F4229F008720FB /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */, D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */, - D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */, + D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */, D3993DC028F42992008720FB /* XCRemoteSwiftPackageReference "resolution-swift" */, - D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */, + D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */, D3C187B82907CFAB00FE1900 /* XCRemoteSwiftPackageReference "Checkpoints" */, - D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */, - D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */, - D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */, - D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */, - D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */, - D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */, - D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */, - 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */, + D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */, + D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */, + D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */, + D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */, + D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */, + D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */, + D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */, + 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */, 6BDA29A929D6EA9B003847ED /* XCRemoteSwiftPackageReference "ECashKit.Swift" */, 6BDA29AE29D6F934003847ED /* XCRemoteSwiftPackageReference "HsToolKit.Swift" */, D3AF5A8729FFD85800C1399E /* XCRemoteSwiftPackageReference "RxSwift" */, D023D2612A249E59004F65B0 /* XCRemoteSwiftPackageReference "TronKit.Swift" */, D0EC34D92A4450B100BB308B /* XCRemoteSwiftPackageReference "HCaptcha-ios-sdk" */, - 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */, + 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */, D3E6756C2AA9A21300F2BF60 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, ); productRefGroup = D3285F4320BD158E00644076 /* Products */; @@ -9238,6 +9242,7 @@ 11B353C7553F40CEEA28678B /* PasscodeManager.swift in Sources */, 11B35353A5C1E254839CD61B /* InteractiveDismiss.swift in Sources */, 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */, + 11B3523E8B466F259DB32E37 /* SecondaryCircleButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10539,6 +10544,7 @@ 11B35951600F986F1C424E24 /* PasscodeManager.swift in Sources */, 11B350A27335B798701EE7B3 /* InteractiveDismiss.swift in Sources */, 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */, + 11B35224D7A5A864C1C6F167 /* SecondaryCircleButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10841,7 +10847,7 @@ kind = branch; }; }; - 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */ = { + 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinCore.Swift"; requirement = { @@ -10849,7 +10855,7 @@ version = 2.0.2; }; }; - 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */ = { + 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/UIExtensions.Swift"; requirement = { @@ -10897,7 +10903,7 @@ version = 1.0.0; }; }; - D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */ = { + D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HsCryptoKit.Swift"; requirement = { @@ -10905,7 +10911,7 @@ version = 1.2.1; }; }; - D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */ = { + D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/EvmKit.Swift"; requirement = { @@ -10913,7 +10919,7 @@ version = 2.0.7; }; }; - D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */ = { + D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Eip20Kit.Swift"; requirement = { @@ -10921,7 +10927,7 @@ version = 2.0.0; }; }; - D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */ = { + D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/NftKit.Swift"; requirement = { @@ -10929,7 +10935,7 @@ version = 2.0.0; }; }; - D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */ = { + D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/UniswapKit.Swift"; requirement = { @@ -10937,7 +10943,7 @@ version = 2.0.5; }; }; - D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */ = { + D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/OneInchKit.Swift"; requirement = { @@ -10945,7 +10951,7 @@ version = 2.0.1; }; }; - D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */ = { + D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinKit.Swift"; requirement = { @@ -10953,7 +10959,7 @@ version = 2.0.0; }; }; - D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */ = { + D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BitcoinCashKit.Swift"; requirement = { @@ -10961,7 +10967,7 @@ version = 2.0.0; }; }; - D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */ = { + D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/LitecoinKit.Swift"; requirement = { @@ -10969,7 +10975,7 @@ version = 2.0.0; }; }; - D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */ = { + D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/MarketKit.Swift"; requirement = { @@ -10977,7 +10983,7 @@ version = 2.2.4; }; }; - D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */ = { + D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ScanQrKit.Swift"; requirement = { @@ -10985,7 +10991,7 @@ version = 2.0.0; }; }; - D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */ = { + D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ModuleKit.Swift"; requirement = { @@ -10993,7 +10999,7 @@ version = 2.0.0; }; }; - D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */ = { + D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/CurrencyKit.Swift"; requirement = { @@ -11001,7 +11007,7 @@ version = 2.0.1; }; }; - D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */ = { + D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Chart.Swift"; requirement = { @@ -11009,7 +11015,7 @@ version = 2.1.3; }; }; - D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */ = { + D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/FeeRateKit.Swift"; requirement = { @@ -11017,7 +11023,7 @@ version = 2.1.0; }; }; - D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */ = { + D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/BinanceChainKit.Swift"; requirement = { @@ -11025,7 +11031,7 @@ version = 2.0.0; }; }; - D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */ = { + D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/DashKit.Swift"; requirement = { @@ -11065,7 +11071,7 @@ version = 1.6.10; }; }; - D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */ = { + D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ActionSheet.Swift"; requirement = { @@ -11105,7 +11111,7 @@ minimumVersion = 2.3.3; }; }; - D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */ = { + D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HdWalletKit.Swift"; requirement = { @@ -11121,7 +11127,7 @@ version = 1.0.13; }; }; - D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */ = { + D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ThemeKit.Swift"; requirement = { @@ -11129,7 +11135,7 @@ version = 2.0.2; }; }; - D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */ = { + D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/ComponentKit.Swift"; requirement = { @@ -11137,7 +11143,7 @@ version = 2.0.11; }; }; - D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */ = { + D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/HUD.Swift"; requirement = { @@ -11145,7 +11151,7 @@ version = 2.0.0; }; }; - D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */ = { + D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/LanguageKit.Swift"; requirement = { @@ -11153,7 +11159,7 @@ version = 1.0.0; }; }; - D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */ = { + D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/SectionsTableView.Swift"; requirement = { @@ -11161,7 +11167,7 @@ version = 1.0.0; }; }; - D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */ = { + D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/StorageKit.Swift"; requirement = { @@ -11169,7 +11175,7 @@ version = 2.0.0; }; }; - D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */ = { + D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/horizontalsystems/Hodler.Swift"; requirement = { @@ -11200,12 +11206,12 @@ }; 6B423FD32913785800EE5E70 /* BitcoinCore */ = { isa = XCSwiftPackageProductDependency; - package = 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore" */; + package = 6B423FD22913785800EE5E70 /* XCRemoteSwiftPackageReference "BitcoinCore.Swift" */; productName = BitcoinCore; }; 6B5546202A6E73190054B524 /* UIExtensions */ = { isa = XCSwiftPackageProductDependency; - package = 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */; + package = 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions.Swift" */; productName = UIExtensions; }; 6BDA29AA29D6F37C003847ED /* ECashKit */ = { @@ -11265,172 +11271,172 @@ }; D339A93C29126D0F00B895BE /* HsCryptoKit */ = { isa = XCSwiftPackageProductDependency; - package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */; + package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */; productName = HsCryptoKit; }; D339A93E29126D2A00B895BE /* HsCryptoKit */ = { isa = XCSwiftPackageProductDependency; - package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit" */; + package = D339A93B29126D0E00B895BE /* XCRemoteSwiftPackageReference "HsCryptoKit.Swift" */; productName = HsCryptoKit; }; D3604E4228F02A020066C366 /* EvmKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */; + package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */; productName = EvmKit; }; D3604E4428F02A260066C366 /* EvmKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit" */; + package = D3604E4128F02A020066C366 /* XCRemoteSwiftPackageReference "EvmKit.Swift" */; productName = EvmKit; }; D3604E4928F02A8C0066C366 /* Eip20Kit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */; + package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */; productName = Eip20Kit; }; D3604E4C28F02AB40066C366 /* NftKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */; + package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */; productName = NftKit; }; D3604E4F28F02AE70066C366 /* UniswapKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */; + package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */; productName = UniswapKit; }; D3604E5228F02B150066C366 /* OneInchKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */; + package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */; productName = OneInchKit; }; D3604E5428F02B280066C366 /* Eip20Kit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */; + package = D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit.Swift" */; productName = Eip20Kit; }; D3604E5628F02B280066C366 /* NftKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit" */; + package = D3604E4B28F02AB40066C366 /* XCRemoteSwiftPackageReference "NftKit.Swift" */; productName = NftKit; }; D3604E5828F02B280066C366 /* UniswapKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit" */; + package = D3604E4E28F02AE60066C366 /* XCRemoteSwiftPackageReference "UniswapKit.Swift" */; productName = UniswapKit; }; D3604E5A28F02B280066C366 /* OneInchKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit" */; + package = D3604E5128F02B150066C366 /* XCRemoteSwiftPackageReference "OneInchKit.Swift" */; productName = OneInchKit; }; D3604E6528F02D9A0066C366 /* BitcoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */; + package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */; productName = BitcoinKit; }; D3604E6828F02DF30066C366 /* BitcoinCashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */; + package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */; productName = BitcoinCashKit; }; D3604E6B28F02E3F0066C366 /* LitecoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */; + package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */; productName = LitecoinKit; }; D3604E6F28F03AC80066C366 /* MarketKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */; + package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */; productName = MarketKit; }; D3604E7228F03B0A0066C366 /* ScanQrKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */; + package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */; productName = ScanQrKit; }; D3604E7828F03B9F0066C366 /* ModuleKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */; + package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */; productName = ModuleKit; }; D3604E7B28F03BD20066C366 /* CurrencyKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */; + package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */; productName = CurrencyKit; }; D3604E7E28F03C1D0066C366 /* Chart */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */; + package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */; productName = Chart; }; D3604E8128F03C6B0066C366 /* FeeRateKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */; + package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */; productName = FeeRateKit; }; D3604E8428F03CDC0066C366 /* BinanceChainKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */; + package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */; productName = BinanceChainKit; }; D3604E8728F03D9E0066C366 /* DashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */; + package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */; productName = DashKit; }; D3604E8928F03DBF0066C366 /* BitcoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */; + package = D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit.Swift" */; productName = BitcoinKit; }; D3604E8B28F03DBF0066C366 /* BitcoinCashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */; + package = D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit.Swift" */; productName = BitcoinCashKit; }; D3604E8D28F03DBF0066C366 /* LitecoinKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */; + package = D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit.Swift" */; productName = LitecoinKit; }; D3604E8F28F03DC00066C366 /* MarketKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */; + package = D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit.Swift" */; productName = MarketKit; }; D3604E9128F03DC00066C366 /* ScanQrKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit" */; + package = D3604E7128F03B0A0066C366 /* XCRemoteSwiftPackageReference "ScanQrKit.Swift" */; productName = ScanQrKit; }; D3604E9528F03DC00066C366 /* ModuleKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit" */; + package = D3604E7728F03B9F0066C366 /* XCRemoteSwiftPackageReference "ModuleKit.Swift" */; productName = ModuleKit; }; D3604E9728F03DC00066C366 /* CurrencyKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit" */; + package = D3604E7A28F03BD20066C366 /* XCRemoteSwiftPackageReference "CurrencyKit.Swift" */; productName = CurrencyKit; }; D3604E9928F03DC00066C366 /* Chart */ = { isa = XCSwiftPackageProductDependency; - package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */; + package = D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart.Swift" */; productName = Chart; }; D3604E9B28F03DC00066C366 /* FeeRateKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */; + package = D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */; productName = FeeRateKit; }; D3604E9D28F03DC00066C366 /* BinanceChainKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit" */; + package = D3604E8328F03CDC0066C366 /* XCRemoteSwiftPackageReference "BinanceChainKit.Swift" */; productName = BinanceChainKit; }; D3604E9F28F03DC00066C366 /* DashKit */ = { isa = XCSwiftPackageProductDependency; - package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */; + package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit.Swift" */; productName = DashKit; }; D36E0C2928D084AB00B622B9 /* CollectionViewCenteredFlowLayout */ = { @@ -11475,12 +11481,12 @@ }; D3993DBA28F4277E008720FB /* ActionSheet */ = { isa = XCSwiftPackageProductDependency; - package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */; + package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */; productName = ActionSheet; }; D3993DBC28F4278F008720FB /* ActionSheet */ = { isa = XCSwiftPackageProductDependency; - package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet" */; + package = D3993DB928F4277E008720FB /* XCRemoteSwiftPackageReference "ActionSheet.Swift" */; productName = ActionSheet; }; D3993DC128F42992008720FB /* UnstoppableDomainsResolution */ = { @@ -11545,12 +11551,12 @@ }; D3C187B22907A60800FE1900 /* HdWalletKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */; + package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */; productName = HdWalletKit; }; D3C187B42907A63600FE1900 /* HdWalletKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit" */; + package = D3C187B12907A60700FE1900 /* XCRemoteSwiftPackageReference "HdWalletKit.Swift" */; productName = HdWalletKit; }; D3C187B92907CFAB00FE1900 /* Checkpoints */ = { @@ -11565,72 +11571,72 @@ }; D3C187CE290FCF2D00FE1900 /* ThemeKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */; + package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */; productName = ThemeKit; }; D3C187D1290FCF3D00FE1900 /* ComponentKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */; + package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */; productName = ComponentKit; }; D3C187D4290FCF7D00FE1900 /* HUD */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */; + package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */; productName = HUD; }; D3C187D7290FCF9C00FE1900 /* LanguageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */; + package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */; productName = LanguageKit; }; D3C187DA290FCFBC00FE1900 /* SectionsTableView */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */; + package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */; productName = SectionsTableView; }; D3C187DD290FCFE400FE1900 /* StorageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */; + package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */; productName = StorageKit; }; D3C187DF290FD00E00FE1900 /* ThemeKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */; + package = D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */; productName = ThemeKit; }; D3C187E1290FD00E00FE1900 /* ComponentKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit" */; + package = D3C187D0290FCF3D00FE1900 /* XCRemoteSwiftPackageReference "ComponentKit.Swift" */; productName = ComponentKit; }; D3C187E3290FD00E00FE1900 /* HUD */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */; + package = D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD.Swift" */; productName = HUD; }; D3C187E5290FD00E00FE1900 /* LanguageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit" */; + package = D3C187D6290FCF9C00FE1900 /* XCRemoteSwiftPackageReference "LanguageKit.Swift" */; productName = LanguageKit; }; D3C187E7290FD00E00FE1900 /* SectionsTableView */ = { isa = XCSwiftPackageProductDependency; - package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView" */; + package = D3C187D9290FCFBC00FE1900 /* XCRemoteSwiftPackageReference "SectionsTableView.Swift" */; productName = SectionsTableView; }; D3C187E9290FD00E00FE1900 /* StorageKit */ = { isa = XCSwiftPackageProductDependency; - package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit" */; + package = D3C187DC290FCFE400FE1900 /* XCRemoteSwiftPackageReference "StorageKit.Swift" */; productName = StorageKit; }; D3E1D00A2990D9BE00C68F00 /* Hodler */ = { isa = XCSwiftPackageProductDependency; - package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */; + package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */; productName = Hodler; }; D3E1D00C2990DA0400C68F00 /* Hodler */ = { isa = XCSwiftPackageProductDependency; - package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler" */; + package = D3E1D0092990D9BE00C68F00 /* XCRemoteSwiftPackageReference "Hodler.Swift" */; productName = Hodler; }; D3E6756D2AA9A21300F2BF60 /* SDWebImageSwiftUI */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift index 0c740ac7e8..8eefba8bf7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockoutManager.swift @@ -8,6 +8,7 @@ class LockoutManager { private let maxAttempts = 5 private var secureStorage: ISecureStorage + private var timer: Timer? @PostPublished private(set) var lockoutState: LockoutState = .unlocked(attemptsLeft: 0, maxAttempts: 0) @@ -52,8 +53,9 @@ class LockoutManager { } extension LockoutManager { - func syncState() { + timer?.invalidate() + if unlockAttempts < maxAttempts { lockoutState = .unlocked(attemptsLeft: maxAttempts - unlockAttempts, maxAttempts: maxAttempts) } else { @@ -63,7 +65,11 @@ extension LockoutManager { if timePast > lockoutInterval { lockoutState = .unlocked(attemptsLeft: 1, maxAttempts: maxAttempts) } else { - lockoutState = .locked(unlockDate: Date().addingTimeInterval(lockoutInterval - timePast)) + let timeInterval = lockoutInterval - timePast + lockoutState = .locked(unlockDate: Date().addingTimeInterval(timeInterval)) + timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in + self?.syncState() + } } } } @@ -83,4 +89,18 @@ extension LockoutManager { enum LockoutState { case unlocked(attemptsLeft: Int, maxAttempts: Int) case locked(unlockDate: Date) + + var isLocked: Bool { + switch self { + case .unlocked: return false + case .locked: return true + } + } + + var isAttempted: Bool { + switch self { + case let .unlocked(attemptsLeft, maxAttempts): return attemptsLeft != maxAttempts + case .locked: return true + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift index 74635ef27a..a2873b585c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift @@ -4,6 +4,7 @@ import ThemeKit struct NumPadView: View { @Binding var digits: [Int] @Binding var biometryType: BiometryType? + @Binding var disabled: Bool let onTapDigit: (Int) -> Void let onTapBackspace: () -> Void @@ -12,34 +13,39 @@ struct NumPadView: View { var body: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: .margin16) { ForEach(Array(digits.prefix(9).enumerated()), id: \.offset) { _, digit in - NumberView(digit: digit) { onTapDigit(digit) } + NumberView(digit: digit, disabled: disabled) { onTapDigit(digit) } } if let biometryType { Button(action: { onTapBiometry?() }) { - Image(biometryType.iconName).themeIcon() + Image(biometryType.iconName).renderingMode(.template) } + .buttonStyle(SecondaryCircleButtonStyle(style: .transparent)) + .disabled(disabled) } else { Text("") } if let digit = digits.last { - NumberView(digit: digit) { onTapDigit(digit) } + NumberView(digit: digit, disabled: disabled) { onTapDigit(digit) } } Button(action: { onTapBackspace() }) { - Image("backspace_24").themeIcon() + Image("backspace_24").renderingMode(.template) } + .buttonStyle(SecondaryCircleButtonStyle(style: .transparent)) + .disabled(disabled) } .frame(width: 280) } struct NumberView: View { let digit: Int + let disabled: Bool let onTap: () -> Void var body: some View { @@ -48,17 +54,20 @@ struct NumPadView: View { }) { Text(String(digit)) .font(.themeTitle2R) - .foregroundColor(.themeLeah) .frame(width: 72, height: 72) } .buttonStyle(NumPadButtonStyle()) + .disabled(disabled) .animation(.easeOut, value: digit) } } struct NumPadButtonStyle: ButtonStyle { + @Environment(\.isEnabled) var isEnabled + func makeBody(configuration: Configuration) -> some View { configuration.label + .foregroundColor(isEnabled ? .themeLeah : .themeSteel20) .background(configuration.isPressed ? Color.themeSteel20 : Color.clear) .clipShape(Circle()) .overlay(Circle().stroke(Color.themeSteel20, lineWidth: .heightOneDp)) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift index 4b3151e5b6..379edf35bc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift @@ -1,3 +1,4 @@ +import LanguageKit import SwiftUI struct PasscodeView: View { @@ -63,7 +64,15 @@ struct PasscodeView: View { .transition(.opacity.animation(.easeOut)) .id(errorText) case let .locked(unlockDate): - Text("unlock.disabled_until".localized("\(unlockDate)")) + VStack(spacing: .margin16) { + Image("lock_48") + .foregroundColor(.themeGray) + Text("unlock.disabled_until".localized(DateFormatter.cachedFormatter(format: "\(LanguageHourFormatter.hourFormat):mm:ss").string(from: unlockDate))) + .foregroundColor(.themeGray) + .font(.themeSubhead2) + .padding(.horizontal, .margin48) + .multilineTextAlignment(.center) + } } } .frame(maxHeight: .infinity) @@ -72,6 +81,7 @@ struct PasscodeView: View { NumPadView( digits: $digits, biometryType: $biometryType, + disabled: Binding(get: { lockoutState.isLocked }, set: { _ in }), onTapDigit: { digit in guard passcode.count < maxDigits else { return @@ -92,6 +102,7 @@ struct PasscodeView: View { Text(randomized ? "unlock.regular_mode".localized : "unlock.random_mode".localized) } .buttonStyle(SecondaryButtonStyle(style: .default)) + .disabled(lockoutState.isLocked) } } .padding(.bottom, .margin32) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift index 5df91617e4..513020f467 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -62,7 +62,10 @@ class BaseUnlockViewModel: ObservableObject { } .store(in: &cancellables) lockoutManager.$lockoutState - .sink { [weak self] in self?.lockoutState = $0 } + .sink { [weak self] in + self?.lockoutState = $0 + self?.syncBiometryType() + } .store(in: &cancellables) syncErrorText() @@ -70,7 +73,7 @@ class BaseUnlockViewModel: ObservableObject { } private func syncBiometryType() { - resolvedBiometryType = biometryEnabled && biometryAllowed ? biometryType : nil + resolvedBiometryType = biometryEnabled && biometryAllowed && !lockoutState.isAttempted ? biometryType : nil } func isValid(passcode _: String) -> Bool { false } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift new file mode 100644 index 0000000000..8442269d98 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct SecondaryCircleButtonStyle: ButtonStyle { + let style: Style + + @Environment(\.isEnabled) var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(.margin4) + .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .clipShape(Circle()) + .animation(.easeOut(duration: 0.2), value: configuration.isPressed) + } + + enum Style { + case `default` + case transparent + case red + + func foregroundColor(isEnabled: Bool, isPressed: Bool) -> Color { + switch self { + case .default: return isEnabled ? (isPressed ? .themeGray : .themeLeah) : .themeGray50 + case .transparent: return isEnabled ? (isPressed ? .themeGray50 : .themeGray) : .themeGray50 + case .red: return isEnabled ? (isPressed ? .themeRed50 : .themeLucian) : .themeGray50 + } + } + + func backgroundColor(isEnabled _: Bool, isPressed: Bool) -> Color { + switch self { + case .default, .red: return isPressed ? .themeSteel10 : .themeSteel20 + case .transparent: return .clear + } + } + } +} From 5786097b6a9355c45d80672139124f9aac19ffbe Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 27 Sep 2023 14:44:02 +0600 Subject: [PATCH 16/63] Disable biometry unlock for module unlocks --- .../Modules/Passcode/Unlock/UnlockModule.swift | 2 +- .../Modules/Settings/Security/SecuritySettingsView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift index e01bae2153..838082fb47 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift @@ -15,7 +15,7 @@ struct UnlockModule { return UnlockView(viewModel: viewModel, autoDismiss: autoDismiss) } - static func moduleUnlockView(biometryAllowed: Bool = true, onUnlock: @escaping () -> Void) -> some View { + static func moduleUnlockView(biometryAllowed: Bool = false, onUnlock: @escaping () -> Void) -> some View { let viewModel = ModuleUnlockViewModel( passcodeManager: App.shared.passcodeManager, biometryManager: App.shared.biometryManager, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index b49b8a65d2..d8a095dbc3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -79,7 +79,7 @@ struct SecuritySettingsView: View { } .sheet(item: $unlockReason) { reason in ThemeNavigationView { - UnlockModule.moduleUnlockView(biometryAllowed: false) { + UnlockModule.moduleUnlockView { switch reason { case .changePasscode: DispatchQueue.main.async { From 0bb4d2551585624cbf0dac3e85bbc27e4f3da893 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 27 Sep 2023 14:54:56 +0600 Subject: [PATCH 17/63] Move `Balance Auto Hide` from Appearance module to SecuritySettings --- .../Settings/Appearance/AppearanceModule.swift | 3 +-- .../Settings/Appearance/AppearanceView.swift | 10 ---------- .../Settings/Appearance/AppearanceViewModel.swift | 11 +---------- .../Settings/Security/SecuritySettingsModule.swift | 3 ++- .../Settings/Security/SecuritySettingsView.swift | 13 +++++++++++++ .../Security/SecuritySettingsViewModel.swift | 11 ++++++++++- .../UnstoppableWallet/en.lproj/Localizable.strings | 4 ++-- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceModule.swift index 177189bedd..253fa24eac 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceModule.swift @@ -7,8 +7,7 @@ struct AppearanceModule { launchScreenManager: App.shared.launchScreenManager, appIconManager: App.shared.appIconManager, balancePrimaryValueManager: App.shared.balancePrimaryValueManager, - balanceConversionManager: App.shared.balanceConversionManager, - balanceHiddenManager: App.shared.balanceHiddenManager + balanceConversionManager: App.shared.balanceConversionManager ) return AppearanceView(viewModel: viewModel) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift index f878477fd9..99ed6a32b8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift @@ -100,16 +100,6 @@ struct AppearanceView: View { } } - ListSection { - ListRow { - Image("eye_off_24").themeIcon() - Toggle(isOn: $viewModel.balanceAutoHide) { - Text("appearance.balance_auto_hide".localized).themeBody() - } - } - } - .padding(.top, .margin8) - VStack(spacing: 0) { ListSectionHeader(text: "appearance.app_icon".localized) ListSection { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift index b718ee099b..5891575597 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift @@ -8,7 +8,6 @@ class AppearanceViewModel: ObservableObject { private let appIconManager: AppIconManager private let balancePrimaryValueManager: BalancePrimaryValueManager private let balanceConversionManager: BalanceConversionManager - private let balanceHiddenManager: BalanceHiddenManager let themeModes: [ThemeMode] = [.system, .dark, .light] let conversionTokens: [Token] @@ -43,25 +42,18 @@ class AppearanceViewModel: ObservableObject { } } - @Published var balanceAutoHide: Bool { - didSet { - balanceHiddenManager.set(balanceAutoHide: balanceAutoHide) - } - } - @Published var appIcon: AppIcon { didSet { appIconManager.appIcon = appIcon } } - init(themeManager: ThemeManager, launchScreenManager: LaunchScreenManager, appIconManager: AppIconManager, balancePrimaryValueManager: BalancePrimaryValueManager, balanceConversionManager: BalanceConversionManager, balanceHiddenManager: BalanceHiddenManager) { + init(themeManager: ThemeManager, launchScreenManager: LaunchScreenManager, appIconManager: AppIconManager, balancePrimaryValueManager: BalancePrimaryValueManager, balanceConversionManager: BalanceConversionManager) { self.themeManager = themeManager self.launchScreenManager = launchScreenManager self.appIconManager = appIconManager self.balancePrimaryValueManager = balancePrimaryValueManager self.balanceConversionManager = balanceConversionManager - self.balanceHiddenManager = balanceHiddenManager conversionTokens = balanceConversionManager.conversionTokens @@ -70,7 +62,6 @@ class AppearanceViewModel: ObservableObject { launchScreen = launchScreenManager.launchScreen conversionToken = balanceConversionManager.conversionToken balancePrimaryValue = balancePrimaryValueManager.balancePrimaryValue - balanceAutoHide = balanceHiddenManager.balanceAutoHide appIcon = appIconManager.appIcon } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift index 6e2b66aacd..3d019112cc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsModule.swift @@ -5,7 +5,8 @@ struct SecuritySettingsModule { let viewModel = SecuritySettingsViewModel( passcodeManager: App.shared.passcodeManager, biometryManager: App.shared.biometryManager, - lockManager: App.shared.lockManager + lockManager: App.shared.lockManager, + balanceHiddenManager: App.shared.balanceHiddenManager ) return SecuritySettingsView(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index d8a095dbc3..39b0357e1f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -54,6 +54,19 @@ struct SecuritySettingsView: View { } } } + + VStack(spacing: 0) { + ListSection { + ListRow { + Image("eye_off_24").themeIcon() + Toggle(isOn: $viewModel.balanceAutoHide) { + Text("settings_security.balance_auto_hide".localized).themeBody() + } + } + } + + ListSectionFooter(text: "settings_security.balance_auto_hide.description".localized) + } } .sheet(item: $createPasscodeReason) { reason in CreatePasscodeModule.createPasscodeView( diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift index 6f5b427951..570d4d36eb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift @@ -4,6 +4,7 @@ class SecuritySettingsViewModel: ObservableObject { private let passcodeManager: PasscodeManager private let biometryManager: BiometryManager private let lockManager: LockManager + private let balanceHiddenManager: BalanceHiddenManager private var cancellables = Set() @Published var currentPasscodeLevel: Int @@ -19,10 +20,17 @@ class SecuritySettingsViewModel: ObservableObject { } } - init(passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockManager: LockManager) { + @Published var balanceAutoHide: Bool { + didSet { + balanceHiddenManager.set(balanceAutoHide: balanceAutoHide) + } + } + + init(passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockManager: LockManager, balanceHiddenManager: BalanceHiddenManager) { self.passcodeManager = passcodeManager self.biometryManager = biometryManager self.lockManager = lockManager + self.balanceHiddenManager = balanceHiddenManager currentPasscodeLevel = passcodeManager.currentPasscodeLevel isPasscodeSet = passcodeManager.isPasscodeSet @@ -30,6 +38,7 @@ class SecuritySettingsViewModel: ObservableObject { biometryType = biometryManager.biometryType isBiometryToggleOn = biometryManager.biometryEnabled + balanceAutoHide = balanceHiddenManager.balanceAutoHide passcodeManager.$currentPasscodeLevel .sink { [weak self] in self?.currentPasscodeLevel = $0 } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index c56d19fcb8..007468ec22 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1078,6 +1078,8 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.enable_passcode" = "Enable Passcode"; "settings_security.edit_passcode" = "Edit Passcode"; "settings_security.disable_passcode" = "Disable Passcode"; +"settings_security.balance_auto_hide" = "Balance Auto Hide"; +"settings_security.balance_auto_hide.description" = "Automatically hides balance each time the app is opened, regardless of previous preferences."; // Create Passcode @@ -1206,8 +1208,6 @@ Go to Settings - > %@ and allow access to the camera."; "appearance.balance_value.coin_value" = "Coin Value"; "appearance.balance_value.fiat_value" = "Fiat Value"; -"appearance.balance_auto_hide" = "Balance Auto Hide"; - // Settings -> Contacts "contacts.title" = "Contacts"; From 688021aee2fdae0f6b158f99f87cb3a1522ff6bf Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 27 Sep 2023 15:16:37 +0600 Subject: [PATCH 18/63] Add haptic feedback on invalid passcode --- .../Modules/Passcode/Unlock/BaseUnlockViewModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift index 513020f467..b2d98ba940 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -1,6 +1,7 @@ import Combine import HsExtensions import LocalAuthentication +import UIKit class BaseUnlockViewModel: ObservableObject { let passcodeLength = 6 @@ -88,6 +89,7 @@ class BaseUnlockViewModel: ObservableObject { } else { passcode = "" lockoutManager.didFailUnlock() + UINotificationFeedbackGenerator().notificationOccurred(.error) } } From e256a5e3981b9a9d489078194dbba71d98744153 Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 27 Sep 2023 15:23:15 +0400 Subject: [PATCH 19/63] Update zcash library - Implement spend-before-sync - Refactor balanceData. separate custom balanceData types - Fix zcash transactions memos and recipients --- .../project.pbxproj | 6 + .../Core/Adapters/BinanceAdapter.swift | 4 +- .../Core/Adapters/BitcoinBaseAdapter.swift | 6 +- .../Core/Adapters/Evm/BaseEvmAdapter.swift | 2 +- .../Core/Adapters/Evm/Eip20Adapter.swift | 2 +- .../Core/Adapters/Evm/EvmAdapter.swift | 2 +- .../Core/Adapters/Tron/BaseTronAdapter.swift | 2 +- .../Core/Adapters/Tron/Trc20Adapter.swift | 2 +- .../Core/Adapters/Tron/TronAdapter.swift | 2 +- .../Core/Adapters/ZcashAdapter.swift | 80 ++++---- .../Core/Adapters/ZcashTransactionPool.swift | 27 ++- .../Adapters/ZcashTransactionWrapper.swift | 4 +- .../Core/Managers/RateAppManager.swift | 2 +- .../Core/Storage/StorageMigrator.swift | 39 ++-- .../Models/BalanceData.swift | 174 ++++++++++++++++-- .../Models/EnabledWalletCache.swift | 21 ++- .../Models/EnabledWalletCache_v_0_36.swift | 51 +++++ .../CoinSelect/CoinSelectService.swift | 2 +- .../Modules/SendEvm/SendEvmService.swift | 10 +- .../Modules/SendTron/SendTronService.swift | 10 +- .../Adapters/OneInch/OneInchService.swift | 2 +- .../Adapters/Uniswap/UniswapService.swift | 2 +- .../Adapters/UniswapV3/UniswapV3Service.swift | 2 +- .../WalletTokenBalanceService.swift | 2 +- .../WalletTokenBalanceViewItemFactory.swift | 109 ++++------- .../TokenList/WalletTokenListService.swift | 2 +- .../Wallet/WalletCexElementService.swift | 2 +- .../Modules/Wallet/WalletService.swift | 2 +- .../Modules/Wallet/WalletSorter.swift | 4 +- .../UserInterface/Extensions/HudHelper.swift | 2 +- .../en.lproj/Localizable.strings | 3 + 31 files changed, 378 insertions(+), 202 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache_v_0_36.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 5275a9ed65..a4dbc85627 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2238,6 +2238,7 @@ ABC9AB0F8FE5808DB889C081 /* WalletConnectScanQrViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3DBB89D0AE0C127742B /* WalletConnectScanQrViewModel.swift */; }; ABC9AB11FDD018A96BB86557 /* BottomGradientHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */; }; ABC9AB1E703AE57DF856ECD9 /* SendAmountCautionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A07A33870908ED1BA338 /* SendAmountCautionViewModel.swift */; }; + ABC9AB2E235EA006E2DAD8DD /* EnabledWalletCache_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */; }; ABC9AB308727D81FBB8EBCDD /* BackupCloudPassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF6AA02DA39787C053F0 /* BackupCloudPassphraseService.swift */; }; ABC9AB3DAD30AA400DEB719C /* SendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */; }; ABC9AB401FD98F99EF6B07C6 /* RestoreTypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A939DD222D4A2BD3D71C /* RestoreTypeViewController.swift */; }; @@ -2302,6 +2303,7 @@ ABC9AD3001AAA0570B503876 /* ManageBarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6CFDF38D413679D2088 /* ManageBarButtonView.swift */; }; ABC9AD3276132B33F6045AFF /* MarketCategoryMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */; }; ABC9AD41E7C88963F6512905 /* ChartIndicatorsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */; }; + ABC9AD46006A85E907826E2B /* EnabledWalletCache_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */; }; ABC9AD46AE6B5F432E0D2085 /* WalletTokenBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A52822CE6B8830CF5EF4 /* WalletTokenBalanceViewModel.swift */; }; ABC9AD49CCD14F97CD912454 /* SendBitcoinAdapterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1BD3B1B53C72DDF923A /* SendBitcoinAdapterService.swift */; }; ABC9AD565E3BAB7074D02D40 /* ProFeaturesAuthorizationAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D3446ABE98F4D1C0CC /* ProFeaturesAuthorizationAdapter.swift */; }; @@ -3800,6 +3802,7 @@ ABC9A6363DB5DAE5B58AFDC0 /* UniswapV3Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3Module.swift; sourceTree = ""; }; ABC9A64A66778C137FA9642C /* WalletTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenViewController.swift; sourceTree = ""; }; ABC9A6663522498A53CF4174 /* KdfParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KdfParams.swift; sourceTree = ""; }; + ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWalletCache_v_0_36.swift; sourceTree = ""; }; ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinService.swift; sourceTree = ""; }; ABC9A6B2EF46FF7EDA4728D3 /* CheckboxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = ""; }; ABC9A6CFDF38D413679D2088 /* ManageBarButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageBarButtonView.swift; sourceTree = ""; }; @@ -5048,6 +5051,7 @@ 11B35799B0DCCF655F0766BF /* CexDepositNetwork.swift */, 11B35B617A9CE668EEF4978B /* AmountData.swift */, 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */, + ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */, ); path = Models; sourceTree = ""; @@ -9243,6 +9247,7 @@ 11B35353A5C1E254839CD61B /* InteractiveDismiss.swift in Sources */, 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */, 11B3523E8B466F259DB32E37 /* SecondaryCircleButtonStyle.swift in Sources */, + ABC9AD46006A85E907826E2B /* EnabledWalletCache_v_0_36.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10545,6 +10550,7 @@ 11B350A27335B798701EE7B3 /* InteractiveDismiss.swift in Sources */, 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */, 11B35224D7A5A864C1C6F167 /* SecondaryCircleButtonStyle.swift in Sources */, + ABC9AB2E235EA006E2DAD8DD /* EnabledWalletCache_v_0_36.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift index 3953e7ba3f..ddfa4bce3b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift @@ -51,7 +51,7 @@ class BinanceAdapter { } private func balanceInfo(balance: Decimal) -> BalanceData { - BalanceData(balance: balance) + BalanceData(available: balance) } } @@ -114,7 +114,7 @@ extension BinanceAdapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { asset.balanceObservable.map { [weak self] in - self?.balanceInfo(balance: $0) ?? BalanceData(balance: 0) + self?.balanceInfo(balance: $0) ?? BalanceData(available: 0) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift index 95fdcafee4..2bd2f90cbf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift @@ -140,8 +140,8 @@ class BitcoinBaseAdapter { } private func balanceData(balanceInfo: BalanceInfo) -> BalanceData { - BalanceData( - balance: Decimal(balanceInfo.spendable) / coinRate, + LockedBalanceData( + available: Decimal(balanceInfo.spendable) / coinRate, locked: Decimal(balanceInfo.unspendable) / coinRate ) } @@ -389,6 +389,6 @@ class DepositAddress { let address: String init(_ receiveAddress: String) { - self.address = receiveAddress + address = receiveAddress } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/BaseEvmAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/BaseEvmAdapter.swift index 1b142e564b..43e8ffe048 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/BaseEvmAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/BaseEvmAdapter.swift @@ -44,7 +44,7 @@ class BaseEvmAdapter { } func balanceData(balance: BigUInt?) -> BalanceData { - BalanceData(balance: balanceDecimal(kitBalance: balance, decimals: decimals)) + BalanceData(available: balanceDecimal(kitBalance: balance, decimals: decimals)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift index 0a06b0523d..de0bcfe791 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift @@ -59,7 +59,7 @@ extension Eip20Adapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { eip20Kit.balanceObservable.map { [weak self] in - self?.balanceData(balance: $0) ?? BalanceData(balance: 0) + self?.balanceData(balance: $0) ?? BalanceData(available: 0) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmAdapter.swift index d4f4faaa91..c11aa0f631 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmAdapter.swift @@ -57,7 +57,7 @@ extension EvmAdapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { evmKit.accountStateObservable.map { [weak self] in - self?.balanceData(balance: $0.balance) ?? BalanceData(balance: 0) + self?.balanceData(balance: $0.balance) ?? BalanceData(available: 0) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/BaseTronAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/BaseTronAdapter.swift index 801ed7cd92..106883f427 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/BaseTronAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/BaseTronAdapter.swift @@ -44,7 +44,7 @@ class BaseTronAdapter { } func balanceData(balance: BigUInt?) -> BalanceData { - BalanceData(balance: balanceDecimal(kitBalance: balance, decimals: decimals)) + BalanceData(available: balanceDecimal(kitBalance: balance, decimals: decimals)) } func accountActive(address: TronKit.Address) async -> Bool { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/Trc20Adapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/Trc20Adapter.swift index 231d07f1af..4510a3dc2f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/Trc20Adapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/Trc20Adapter.swift @@ -51,7 +51,7 @@ extension Trc20Adapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { tronKit.trc20BalancePublisher(contractAddress: contractAddress).asObservable().map { [weak self] in - self?.balanceData(balance: $0) ?? BalanceData(balance: 0) + self?.balanceData(balance: $0) ?? BalanceData(available: 0) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronAdapter.swift index 2e0977baf2..c177f8d359 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Tron/TronAdapter.swift @@ -55,7 +55,7 @@ extension TronAdapter: IBalanceAdapter { var balanceDataUpdatedObservable: Observable { tronKit.trxBalancePublisher.asObservable().map { [weak self] in - self?.balanceData(balance: $0) ?? BalanceData(balance: 0) + self?.balanceData(balance: $0) ?? BalanceData(available: 0) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index 0b7482ce23..155fc46c74 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -45,15 +45,6 @@ class ZcashAdapter { private var started = false private var lastBlockHeight: Int = 0 - private var waitForStart: Bool = false { - didSet { - print("Change waitForStart to \(waitForStart) : zAddress : \(zAddress != nil)") - if waitForStart, zAddress != nil { // already prepared and has address - syncMain() - } - } - } - private var synchronizerState: SynchronizerState? { didSet { lastBlockUpdatedSubject.onNext(()) @@ -89,7 +80,7 @@ class ZcashAdapter { } init(wallet: Wallet, restoreSettings: RestoreSettings) throws { - logger = HsToolKit.Logger(minLogLevel: .debug) // App.shared.logger.scoped(with: "ZCashKit") // + logger = App.shared.logger.scoped(with: "ZCashKit") // HsToolKit.Logger(minLogLevel: .debug) // guard let seed = wallet.account.type.mnemonicSeed else { throw AdapterError.unsupportedAccount @@ -141,7 +132,6 @@ class ZcashAdapter { // subscribe on background and events from sapling downloader NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - prepare(seedData: seedData, walletBirthday: birthday, for: initMode) } private func prepare(seedData: [UInt8], walletBirthday: BlockHeight, for initMode: WalletInitMode) { @@ -195,9 +185,9 @@ class ZcashAdapter { let shielded = await (try? synchronizer.getShieldedBalance(accountIndex: 0).decimalValue.decimalValue) ?? 0 let shieldedVerified = await (try? synchronizer.getShieldedVerifiedBalance(accountIndex: 0).decimalValue.decimalValue) ?? 0 self?.balanceSubject.onNext( - BalanceData( - balance: shieldedVerified, - locked: shielded - shieldedVerified + VerifiedBalanceData( + fullBalance: shielded, + available: shieldedVerified ) ) self?.lastBlockHeight = try await synchronizer.latestHeight() @@ -218,9 +208,26 @@ class ZcashAdapter { private func finishPrepare() { state = .idle - if waitForStart { - logger?.log(level: .debug, message: "Start kit after finish preparing!") - start() + logger?.log(level: .debug, message: "Start kit after finish preparing!") + startSynchronizer() + } + + private func startSynchronizer() { + guard !state.isPrepairing else { // postpone start library until preparing will finish + logger?.log(level: .debug, message: "Can't start because preparing!") + return + } + + if zAddress == nil { // else we need to try prepare library again + logger?.log(level: .debug, message: "No address, try to prepare kit again!") + prepare(seedData: seedData, walletBirthday: birthday, for: initMode) + + return + } + + if saplingDataExist() { + logger?.log(level: .debug, message: "Start syncing kit!") + syncMain() } } @@ -298,7 +305,7 @@ class ZcashAdapter { transactions.forEach { overview in logger?.log(level: .debug, message: "tx: v =\(overview.value.decimalValue.decimalString) : fee = \(overview.fee?.decimalString() ?? "N/A") : height = \(overview.minedHeight?.description ?? "N/A")") } - let lastBlockHeight = inRange.upperBound + let lastBlockHeight = max(inRange.upperBound, lastBlockHeight) Task { let newTxs = await transactionPool?.sync(transactions: transactions, lastBlockHeight: lastBlockHeight) ?? [] transactionRecordsSubject.onNext(newTxs.map { @@ -474,16 +481,12 @@ class ZcashAdapter { private var _balanceData: BalanceData { guard let synchronizerState = synchronizerState else { - return BalanceData(balance: 0) + return BalanceData(available: 0) } - let verifiedBalance: Zatoshi = synchronizerState.shieldedBalance.verified - let balance: Zatoshi = synchronizerState.shieldedBalance.total - let diff = balance - verifiedBalance - - return BalanceData( - balance: verifiedBalance.decimalValue.decimalValue, - locked: diff.decimalValue.decimalValue + return VerifiedBalanceData( + fullBalance: synchronizerState.shieldedBalance.total.decimalValue.decimalValue, + available: synchronizerState.shieldedBalance.verified.decimalValue.decimalValue ) } @@ -571,24 +574,7 @@ extension ZcashAdapter: IAdapter { } func start() { - guard !state.isPrepairing else { // postpone start library until preparing will finish - logger?.log(level: .debug, message: "Can't start because preparing!") - waitForStart = true - return - } - - if zAddress == nil { // else we need to try prepare library again - logger?.log(level: .debug, message: "No address, try to prepare kit again!") - prepare(seedData: seedData, walletBirthday: birthday, for: initMode) - - return - } - - waitForStart = false // if we has address just start syncing library or downloading sapling data - if saplingDataExist() { - logger?.log(level: .debug, message: "Start syncing kit!") - syncMain() - } + prepare(seedData: seedData, walletBirthday: birthday, for: initMode) } func stop() { @@ -597,7 +583,7 @@ extension ZcashAdapter: IAdapter { } func refresh() { - start() + startSynchronizer() } private func syncMain() { @@ -632,7 +618,7 @@ extension ZcashAdapter: IAdapter { balanceState = """ shielded balance total: \(balanceData.balanceTotal.description) - verified: \(balanceData.balance) + verified: \(balanceData.available) transparent balance total: \(String(describing: status.transparentBalance.total)) verified: \(String(describing: status.transparentBalance.verified)) @@ -726,7 +712,7 @@ extension ZcashAdapter: ISendZcashAdapter { } var availableBalance: Decimal { - max(0, balanceData.balance - fee) // TODO: check + max(0, balanceData.available - fee) } func validate(address: String) throws -> AddressType { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift index f3e77497de..7fccd05526 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionPool.swift @@ -1,6 +1,6 @@ import Foundation -import ZcashLightClientKit import RxSwift +import ZcashLightClientKit class ZcashTransactionPool { private var confirmedTransactions = Set() @@ -8,7 +8,6 @@ class ZcashTransactionPool { private let synchronizer: Synchronizer private let receiveAddress: SaplingAddress - init(receiveAddress: SaplingAddress, synchronizer: Synchronizer) { self.receiveAddress = receiveAddress self.synchronizer = synchronizer @@ -45,8 +44,16 @@ class ZcashTransactionPool { private func transactionWithAdditional(tx: ZcashTransaction.Overview, lastBlockHeight: Int) async throws -> ZcashTransactionWrapper? { let memos: [Memo] = (try? await synchronizer.getMemos(for: tx)) ?? [] + let firstMemo = memos + .compactMap { $0.toString() } + .first + let recipients = await synchronizer.getRecipients(for: tx) - return ZcashTransactionWrapper(tx: tx, memo: memos.first, recipient: recipients.first, lastBlockHeight: lastBlockHeight) + let firstAddress = recipients + .filter { $0.hasAddress } + .first + + return ZcashTransactionWrapper(tx: tx, memo: firstMemo, recipient: firstAddress, lastBlockHeight: lastBlockHeight) } private func sync(own: inout Set, incoming: [ZcashTransactionWrapper]) { @@ -63,7 +70,7 @@ class ZcashTransactionPool { func sync(transactions: [ZcashTransaction.Overview], lastBlockHeight: Int) async -> [ZcashTransactionWrapper] { let txs = await zcashTransactions(transactions, lastBlockHeight: lastBlockHeight) - // todo: sync pending and confirmed but How? + // TODO: sync pending and confirmed but How? sync(own: &confirmedTransactions, incoming: txs) return txs } @@ -71,11 +78,9 @@ class ZcashTransactionPool { func transaction(by hash: String) -> ZcashTransactionWrapper? { transactions(filter: .all).first { $0.transactionHash == hash } } - } extension ZcashTransactionPool { - var all: [ZcashTransactionWrapper] { transactions(filter: .all) } @@ -88,9 +93,17 @@ extension ZcashTransactionPool { } if let index = transactions.firstIndex(where: { $0.transactionHash == transaction.transactionHash }) { - return Single.just((Array(transactions.suffix(from: index + 1).prefix(limit)))) + return Single.just(Array(transactions.suffix(from: index + 1).prefix(limit))) } return Single.just([]) } +} +extension TransactionRecipient { + var hasAddress: Bool { + switch self { + case .address: return true + case .internalAccount: return false + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift index 5c334a9c63..ea287a82ac 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashTransactionWrapper.swift @@ -16,7 +16,7 @@ class ZcashTransactionWrapper { let memo: String? let failed: Bool - init?(tx: ZcashTransaction.Overview, memo: Memo?, recipient: TransactionRecipient?, lastBlockHeight: Int) { + init?(tx: ZcashTransaction.Overview, memo: String?, recipient: TransactionRecipient?, lastBlockHeight: Int) { raw = tx.raw transactionHash = tx.rawID.hs.reversedHex transactionIndex = tx.index ?? 0 @@ -32,7 +32,7 @@ class ZcashTransactionWrapper { timestamp = failed ? 0 : (tx.blockTime ?? Date().timeIntervalSince1970) // need this to update pending transactions and shows on transaction tab value = tx.value fee = tx.fee - self.memo = memo.flatMap { $0.toString() } + self.memo = memo } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift index 0431b5eb51..3c54adbe2e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift @@ -29,7 +29,7 @@ class RateAppManager { return false } - return adapter.balanceData.balance > 0 + return adapter.balanceData.available > 0 } guard hasBalance else { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift index 521395a3c1..d9c1e3eb91 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift @@ -669,20 +669,20 @@ class StorageMigrator { try record.insert(db) } - // EnabledWalletCache + // EnabledWalletCache_v_0_36 if try db.tableExists("enabled_wallet_caches") { try db.drop(table: "enabled_wallet_caches") } - try db.create(table: EnabledWalletCache.databaseTableName) { t in - t.column(EnabledWalletCache.Columns.tokenQueryId.name, .text).notNull() + try db.create(table: EnabledWalletCache_v_0_36.databaseTableName) { t in + t.column(EnabledWalletCache_v_0_36.Columns.tokenQueryId.name, .text).notNull() t.column("coinSettingsId", .text).notNull() - t.column(EnabledWalletCache.Columns.accountId.name, .text).notNull() - t.column(EnabledWalletCache.Columns.balance.name, .text).notNull() - t.column(EnabledWalletCache.Columns.balanceLocked.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.accountId.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.balance.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.balanceLocked.name, .text).notNull() - t.primaryKey([EnabledWalletCache.Columns.tokenQueryId.name, EnabledWalletCache.Columns.accountId.name], onConflict: .replace) + t.primaryKey([EnabledWalletCache_v_0_36.Columns.tokenQueryId.name, EnabledWalletCache_v_0_36.Columns.accountId.name], onConflict: .replace) } } @@ -730,14 +730,14 @@ class StorageMigrator { } migrator.registerMigration("Update EnabledWallet entities") { db in - try db.drop(table: EnabledWalletCache.databaseTableName) - try db.create(table: EnabledWalletCache.databaseTableName) { t in - t.column(EnabledWalletCache.Columns.tokenQueryId.name, .text).notNull() - t.column(EnabledWalletCache.Columns.accountId.name, .text).notNull() - t.column(EnabledWalletCache.Columns.balance.name, .text).notNull() - t.column(EnabledWalletCache.Columns.balanceLocked.name, .text).notNull() + try db.drop(table: EnabledWalletCache_v_0_36.databaseTableName) + try db.create(table: EnabledWalletCache_v_0_36.databaseTableName) { t in + t.column(EnabledWalletCache_v_0_36.Columns.tokenQueryId.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.accountId.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.balance.name, .text).notNull() + t.column(EnabledWalletCache_v_0_36.Columns.balanceLocked.name, .text).notNull() - t.primaryKey([EnabledWalletCache.Columns.tokenQueryId.name, EnabledWalletCache.Columns.accountId.name], onConflict: .replace) + t.primaryKey([EnabledWalletCache_v_0_36.Columns.tokenQueryId.name, EnabledWalletCache_v_0_36.Columns.accountId.name], onConflict: .replace) } var enabledWallets: [EnabledWallet] = [] @@ -773,6 +773,17 @@ class StorageMigrator { } } + migrator.registerMigration("Update EnabledWalletCache fields") { db in + try db.drop(table: EnabledWalletCache_v_0_36.databaseTableName) + try db.create(table: EnabledWalletCache.databaseTableName) { t in + t.column(EnabledWalletCache.Columns.tokenQueryId.name, .text).notNull() + t.column(EnabledWalletCache.Columns.accountId.name, .text).notNull() + t.column(EnabledWalletCache.Columns.balances.name, .text).notNull() + + t.primaryKey([EnabledWalletCache.Columns.tokenQueryId.name, EnabledWalletCache_v_0_36.Columns.accountId.name], onConflict: .replace) + } + } + try migrator.migrate(dbPool) } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift b/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift index a897bb2a69..3a07913ada 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift @@ -1,26 +1,170 @@ import Foundation -struct BalanceData: Equatable { - let balance: Decimal +class BalanceData: Codable, Equatable { + let available: Decimal + + enum CodingKeys: String, CodingKey { + case available + } + + init(available: Decimal) { + self.available = available + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(available, forKey: .available) + } + + var balanceTotal: Decimal { + available + } + + var sendBeforeSync: Bool { + false + } + + var customStates: [CustomState] { + [] + } + + static func == (lhs: BalanceData, rhs: BalanceData) -> Bool { + lhs.available == rhs.available + } +} + +extension BalanceData { + private static var types: [Decodable.Type] { [VerifiedBalanceData.self, LockedBalanceData.self] } + + static func instance(data: Data) throws -> BalanceData { + let decoder = JSONDecoder() + for type in types { + if let decoded = try? decoder.decode(type, from: data), + let instance = decoded as? BalanceData + { + return instance + } + } + return try decoder.decode(BalanceData.self, from: data) + } + + struct CustomState { + let title: String + let value: Decimal + let infoTitle: String + let infoDescription: String + } +} + +class LockedBalanceData: BalanceData { let locked: Decimal - let staked: Decimal - let frozen: Decimal - init(balance: Decimal, locked: Decimal = 0, staked: Decimal = 0, frozen: Decimal = 0) { - self.balance = balance + init(available: Decimal, locked: Decimal = 0) { self.locked = locked - self.staked = staked - self.frozen = frozen + super.init(available: available) } - var balanceTotal: Decimal { - balance + locked + staked + frozen + enum CodingKeys: String, CodingKey { + case locked + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + locked = try container.decode(Decimal.self, forKey: .locked) + + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(locked, forKey: .locked) + } + + override var balanceTotal: Decimal { + super.balanceTotal + locked } - static func ==(lhs: BalanceData, rhs: BalanceData) -> Bool { - lhs.balance == rhs.balance && - lhs.locked == rhs.locked && - lhs.staked == rhs.staked && - lhs.frozen == rhs.frozen + override var customStates: [CustomState] { + var states = super.customStates + if !locked.isZero { + states.append( + CustomState( + title: "balance.token.locked".localized, + value: locked, + infoTitle: "balance.token.locked.info.title".localized, + infoDescription: "balance.token.locked.info.description".localized + ) + ) + } + return states + } + + static func == (lhs: LockedBalanceData, rhs: LockedBalanceData) -> Bool { + lhs.available == rhs.available && + lhs.locked == rhs.locked } } + +class VerifiedBalanceData: BalanceData { + let fullBalance: Decimal + + override var balanceTotal: Decimal { super.balanceTotal } + override var sendBeforeSync: Bool { true } + + init(fullBalance: Decimal, available: Decimal) { + self.fullBalance = fullBalance + super.init(available: available) + } + + enum CodingKeys: String, CodingKey { + case full + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + fullBalance = try container.decode(Decimal.self, forKey: .full) + + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fullBalance, forKey: .full) + } + + override var customStates: [CustomState] { + var states = super.customStates + let processingBalance = fullBalance - available + if !processingBalance.isZero { + states.append( + CustomState( + title: "balance.token.processing".localized, + value: processingBalance, + infoTitle: "balance.token.processing.info.title".localized, + infoDescription: "balance.token.processing.info.description".localized + ) + ) + } + return states + } +} + +// TODO: implement when will be needed +// let staked: Decimal +// let frozen: Decimal +// CustomState( +// title: "balance.token.staked".localized, +// value: item.balanceData.staked, +// infoTitle: "balance.token.staked.info.title".localized, +// infoDescription: "balance.token.staked.info.description".localized +// ), +// CustomState( +// title: "balance.token.frozen".localized, +// value: item.balanceData.frozen, +// infoTitle: "balance.token.frozen.info.title".localized, +// infoDescription: "balance.token.frozen.info.description".localized +// ), diff --git a/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache.swift b/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache.swift index 88ce2f1547..0dfbbc6823 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache.swift @@ -4,20 +4,23 @@ import GRDB class EnabledWalletCache: Record { let tokenQueryId: String let accountId: String - let balance: Decimal - let balanceLocked: Decimal + let balances: Data init(wallet: Wallet, balanceData: BalanceData) { tokenQueryId = wallet.token.tokenQuery.id accountId = wallet.account.id - balance = balanceData.balance - balanceLocked = balanceData.locked + balances = balanceData.encoded super.init() } var balanceData: BalanceData { - BalanceData(balance: balance, locked: balanceLocked) + do { + let balanceData = try BalanceData.instance(data: balances) + return balanceData + } catch { + return BalanceData(available: 0) + } } override class var databaseTableName: String { @@ -25,14 +28,13 @@ class EnabledWalletCache: Record { } enum Columns: String, ColumnExpression { - case tokenQueryId, accountId, balance, balanceLocked // todo: migration - remove coinSettingsId + case tokenQueryId, accountId, balances } required init(row: Row) { tokenQueryId = row[Columns.tokenQueryId] accountId = row[Columns.accountId] - balance = row[Columns.balance] - balanceLocked = row[Columns.balanceLocked] + balances = row[Columns.balances] super.init(row: row) } @@ -40,8 +42,7 @@ class EnabledWalletCache: Record { override func encode(to container: inout PersistenceContainer) { container[Columns.tokenQueryId] = tokenQueryId container[Columns.accountId] = accountId - container[Columns.balance] = balance - container[Columns.balanceLocked] = balanceLocked + container[Columns.balances] = balances } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache_v_0_36.swift b/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache_v_0_36.swift new file mode 100644 index 0000000000..c57622703f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/EnabledWalletCache_v_0_36.swift @@ -0,0 +1,51 @@ +import Foundation +import GRDB + +class EnabledWalletCache_v_0_36: Record { + let tokenQueryId: String + let accountId: String + let balance: Decimal + let balanceLocked: Decimal + let balances: Data + + init(wallet: Wallet, balanceData: BalanceData) { + tokenQueryId = wallet.token.tokenQuery.id + accountId = wallet.account.id + balance = balanceData.available + balanceLocked = 0 + + balances = Data() + + super.init() + } + + var balanceData: BalanceData { + BalanceData(available: balance) + } + + override class var databaseTableName: String { + "enabled_wallet_caches" + } + + enum Columns: String, ColumnExpression { + case tokenQueryId, accountId, balance, balanceLocked // todo: migration - remove coinSettingsId + } + + required init(row: Row) { + tokenQueryId = row[Columns.tokenQueryId] + accountId = row[Columns.accountId] + balance = row[Columns.balance] + balanceLocked = row[Columns.balanceLocked] + balances = Data() + + super.init(row: row) + } + + override func encode(to container: inout PersistenceContainer) { + container[Columns.tokenQueryId] = tokenQueryId + container[Columns.accountId] = accountId + container[Columns.balance] = balance + container[Columns.balanceLocked] = balanceLocked + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/CoinSelect/CoinSelectService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/CoinSelect/CoinSelectService.swift index 2ca95f91c9..7584112392 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/CoinSelect/CoinSelectService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/CoinSelect/CoinSelectService.swift @@ -51,7 +51,7 @@ class CoinSelectService { return nil } - return (token: wallet.token, balance: adapter.balanceData.balance) + return (token: wallet.token, balance: adapter.balanceData.available) } return balanceCoins.map { token, balance -> Item in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift index 49d8044544..8cd8211bbf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift @@ -76,7 +76,7 @@ class SendEvmService { throw AmountError.invalidDecimal } - guard amount <= adapter.balanceData.balance else { + guard amount <= adapter.balanceData.available else { throw AmountError.insufficientBalance } @@ -100,7 +100,7 @@ extension SendEvmService { extension SendEvmService: IAvailableBalanceService { var availableBalance: DataStatus { - .completed(adapter.balanceData.balance) + .completed(adapter.balanceData.available) } var availableBalanceObservable: Observable> { @@ -120,7 +120,7 @@ extension SendEvmService: IAmountInputService { } var balance: Decimal? { - adapter.balanceData.balance + adapter.balanceData.available } var amountObservable: Observable { @@ -132,7 +132,7 @@ extension SendEvmService: IAmountInputService { } var balanceObservable: Observable { - .just(adapter.balanceData.balance) + .just(adapter.balanceData.available) } func onChange(amount: Decimal) { @@ -141,7 +141,7 @@ extension SendEvmService: IAmountInputService { evmAmount = try validEvmAmount(amount: amount) var amountWarning: AmountWarning? = nil - if amount.isEqual(to: adapter.balanceData.balance) { + if amount.isEqual(to: adapter.balanceData.available) { switch sendToken.type { case .native: amountWarning = AmountWarning.coinNeededForFee default: () diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift index 2c065365cf..dc2103693e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift @@ -82,7 +82,7 @@ class SendTronService { throw AmountError.invalidDecimal } - guard amount <= adapter.balanceData.balance else { + guard amount <= adapter.balanceData.available else { throw AmountError.insufficientBalance } @@ -114,7 +114,7 @@ extension SendTronService { extension SendTronService: IAvailableBalanceService { var availableBalance: DataStatus { - .completed(adapter.balanceData.balance) + .completed(adapter.balanceData.available) } var availableBalanceObservable: Observable> { @@ -134,7 +134,7 @@ extension SendTronService: IAmountInputService { } var balance: Decimal? { - adapter.balanceData.balance + adapter.balanceData.available } var amountObservable: Observable { @@ -146,7 +146,7 @@ extension SendTronService: IAmountInputService { } var balanceObservable: Observable { - .just(adapter.balanceData.balance) + .just(adapter.balanceData.available) } func onChange(amount: Decimal) { @@ -155,7 +155,7 @@ extension SendTronService: IAmountInputService { tronAmount = try validTronAmount(amount: amount) var amountWarning: AmountWarning? = nil - if amount.isEqual(to: adapter.balanceData.balance) { + if amount.isEqual(to: adapter.balanceData.available) { switch sendToken.type { case .native: amountWarning = AmountWarning.coinNeededForFee default: () diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/OneInch/OneInchService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/OneInch/OneInchService.swift index 3a0a7d2bbc..ef136c7943 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/OneInch/OneInchService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/OneInch/OneInchService.swift @@ -173,7 +173,7 @@ class OneInchService { } private func balance(token: MarketKit.Token) -> Decimal? { - (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.balance + (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.available } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/Uniswap/UniswapService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/Uniswap/UniswapService.swift index af13819c1a..24e4777909 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/Uniswap/UniswapService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/Uniswap/UniswapService.swift @@ -173,7 +173,7 @@ class UniswapService { } private func balance(token: MarketKit.Token) -> Decimal? { - (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.balance + (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.available } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/UniswapV3/UniswapV3Service.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/UniswapV3/UniswapV3Service.swift index 2d8ac16b1d..99349555b1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/UniswapV3/UniswapV3Service.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/Adapters/UniswapV3/UniswapV3Service.swift @@ -173,7 +173,7 @@ class UniswapV3Service { } private func balance(token: MarketKit.Token) -> Decimal? { - (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.balance + (adapterManager.adapter(for: token) as? IBalanceAdapter)?.balanceData.available } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift index fe816e2892..bcbc6d358f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift @@ -78,7 +78,7 @@ class WalletTokenBalanceService { } private var fallbackBalanceData: BalanceData { - BalanceData(balance: 0) + BalanceData(available: 0) } private var fallbackAdapterState: AdapterState { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index e996178c80..a7b25752d8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -1,5 +1,5 @@ -import Foundation import CurrencyKit +import Foundation import MarketKit class WalletTokenBalanceViewItemFactory { @@ -14,11 +14,16 @@ class WalletTokenBalanceViewItemFactory { var buttons = [WalletModule.Button: ButtonState]() switch item.element { - case .wallet(let wallet): + case let .wallet(wallet): if item.watchAccount { buttons[.address] = .enabled } else { - let sendButtonState: ButtonState = item.state == .synced ? .enabled : .disabled + let sendButtonState: ButtonState + switch item.state { + case .synced: sendButtonState = .enabled + case .syncing, .customSyncing: sendButtonState = item.balanceData.sendBeforeSync ? .enabled : .disabled + case .stopped, .notSynced: sendButtonState = .disabled + } buttons[.send] = sendButtonState buttons[.receive] = .enabled @@ -27,7 +32,7 @@ class WalletTokenBalanceViewItemFactory { buttons[.swap] = sendButtonState } } - case .cexAsset(let cexAsset): + case let .cexAsset(cexAsset): buttons[.withdraw] = cexAsset.withdrawEnabled ? .enabled : .disabled buttons[.deposit] = cexAsset.depositEnabled ? .enabled : .disabled } @@ -41,21 +46,21 @@ class WalletTokenBalanceViewItemFactory { let state = item.state return WalletTokenBalanceViewModel.ViewItem( - isMainNet: item.isMainNet, - iconUrlString: iconUrlString(coin: item.element.coin, state: state), - placeholderIconName: item.element.wallet?.token.placeholderImageName ?? "placeholder_circle_32", - syncSpinnerProgress: syncSpinnerProgress(state: state), - indefiniteSearchCircle: indefiniteSearchCircle(state: state), - failedImageViewVisible: failedImageViewVisible(state: state), - balanceValue: balanceValue(item: item, balanceHidden: balanceHidden), - descriptionValue: descriptionValue(item: item, balanceHidden: balanceHidden), - customStates: customStates(item: item, balanceHidden: balanceHidden) + isMainNet: item.isMainNet, + iconUrlString: iconUrlString(coin: item.element.coin, state: state), + placeholderIconName: item.element.wallet?.token.placeholderImageName ?? "placeholder_circle_32", + syncSpinnerProgress: syncSpinnerProgress(state: state), + indefiniteSearchCircle: indefiniteSearchCircle(state: state), + failedImageViewVisible: failedImageViewVisible(state: state), + balanceValue: balanceValue(item: item, balanceHidden: balanceHidden), + descriptionValue: descriptionValue(item: item, balanceHidden: balanceHidden), + customStates: customStates(item: item, balanceHidden: balanceHidden) ) } private func descriptionValue(item: WalletTokenBalanceService.BalanceItem, balanceHidden: Bool) -> (text: String?, dimmed: Bool) { if case let .syncing(progress, lastBlockDate) = item.state { - var text: String = "" + var text = "" if let progress = progress { text = "balance.syncing_percent".localized("\(progress)%") } else { @@ -86,7 +91,7 @@ class WalletTokenBalanceViewItemFactory { private func syncSpinnerProgress(state: AdapterState) -> Int? { switch state { - case let .syncing(progress, _), .customSyncing(_, _, let progress): + case let .syncing(progress, _), let .customSyncing(_, _, progress): return progress.map { max(minimumProgress, $0) } ?? infiniteProgress default: return nil } @@ -116,8 +121,8 @@ class WalletTokenBalanceViewItemFactory { private func coinValue(value: Decimal, decimalCount: Int, symbol: String? = nil, balanceHidden: Bool, state: AdapterState) -> (text: String?, dimmed: Bool) { ( - text: balanceHidden ? "*****" : ValueFormatter.instance.formatFull(value: value, decimalCount: decimalCount, symbol: symbol), - dimmed: state != .synced + text: balanceHidden ? "*****" : ValueFormatter.instance.formatFull(value: value, decimalCount: decimalCount, symbol: symbol), + dimmed: state != .synced ) } @@ -130,66 +135,22 @@ class WalletTokenBalanceViewItemFactory { let currencyValue = CurrencyValue(currency: price.currency, value: value * price.value) return ( - text: balanceHidden ? "*****" : ValueFormatter.instance.formatFull(currencyValue: currencyValue), - dimmed: state != .synced || priceItem.expired + text: balanceHidden ? "*****" : ValueFormatter.instance.formatFull(currencyValue: currencyValue), + dimmed: state != .synced || priceItem.expired ) } private func customStates(item: WalletTokenBalanceService.BalanceItem, balanceHidden: Bool) -> [WalletTokenBalanceViewModel.BalanceCustomStateViewItem] { - let stateItems = [ - CustomStateItem( - title: "balance.token.locked".localized, - amount: item.balanceData.locked, - infoTitle: "balance.token.locked.info.title".localized, - infoDescription: "balance.token.locked.info.description".localized - ), - CustomStateItem( - title: "balance.token.staked".localized, - amount: item.balanceData.staked, - infoTitle: "balance.token.staked.info.title".localized, - infoDescription: "balance.token.staked.info.description".localized - ), - CustomStateItem( - title: "balance.token.frozen".localized, - amount: item.balanceData.frozen, - infoTitle: "balance.token.frozen.info.title".localized, - infoDescription: "balance.token.frozen.info.description".localized - ), - ] - - return stateItems - .compactMap { - lockedAmountViewItem( - customStateItem: $0, - item: item, - balanceHidden: balanceHidden - ) - } - } - - private func lockedAmountViewItem(customStateItem: CustomStateItem, item: WalletTokenBalanceService.BalanceItem, balanceHidden: Bool) -> WalletTokenBalanceViewModel.BalanceCustomStateViewItem? { - guard customStateItem.amount > 0 else { - return nil - } - - let value = coinValue(value: customStateItem.amount, decimalCount: item.element.decimals, symbol: item.element.coin?.code, balanceHidden: balanceHidden, state: item.state) - return .init( - title: customStateItem.title, - amountValue: value, - infoTitle: customStateItem.infoTitle, - infoDescription: customStateItem.infoDescription - ) - } - -} - -extension WalletTokenBalanceViewItemFactory { - - private struct CustomStateItem { - let title: String - let amount: Decimal - let infoTitle: String - let infoDescription: String + item.balanceData + .customStates + .map { + let value = coinValue(value: $0.value, decimalCount: item.element.decimals, symbol: item.element.coin?.code, balanceHidden: balanceHidden, state: item.state) + return .init( + title: $0.title, + amountValue: value, + infoTitle: $0.infoTitle, + infoDescription: $0.infoDescription + ) + } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift index bfc40d0135..4a53d0034b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift @@ -158,7 +158,7 @@ class WalletTokenListService: IWalletTokenListService { } private var fallbackBalanceData: BalanceData { - BalanceData(balance: 0) + BalanceData(available: 0) } private var fallbackAdapterState: AdapterState { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCexElementService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCexElementService.swift index 093ef2c326..39b1ae553c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCexElementService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCexElementService.swift @@ -90,7 +90,7 @@ extension WalletCexElementService: IWalletElementService { return nil } - return BalanceData(balance: cexAsset.freeBalance, locked: cexAsset.lockedBalance) + return VerifiedBalanceData(fullBalance: cexAsset.freeBalance + cexAsset.lockedBalance, available: cexAsset.freeBalance) } func state(element: WalletModule.Element) -> AdapterState? { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift index bd4a20827a..86f034259e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift @@ -285,7 +285,7 @@ class WalletService { } private var fallbackBalanceData: BalanceData { - BalanceData(balance: 0) + BalanceData(available: 0) } private var fallbackAdapterState: AdapterState { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletSorter.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletSorter.swift index 132ffbd6b2..d03be88f48 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletSorter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletSorter.swift @@ -52,7 +52,7 @@ protocol ISortableWalletItem { extension WalletService.Item: ISortableWalletItem { var balance: Decimal { - balanceData.balance + balanceData.available } var name: String { @@ -68,7 +68,7 @@ extension WalletService.Item: ISortableWalletItem { extension WalletTokenListService.Item: ISortableWalletItem { var balance: Decimal { - balanceData.balance + balanceData.available } var name: String { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift index 1bf5328fbc..56df90caf6 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/HudHelper.swift @@ -117,7 +117,7 @@ extension HudHelper { var showingTime: TimeInterval? { switch self { - case .waitingForSession, .disconnectingWalletConnect, .enabling: return nil + case .waitingForSession, .disconnectingWalletConnect, .sending, .enabling: return nil default: return 2 } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 007468ec22..230b8dddd7 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -322,6 +322,9 @@ Go to Settings - > %@ and allow access to the camera."; "balance.token.locked" = "Locked"; "balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "The sender sent these funds with a spending lock that will expire on the shown date. \n\nNo worries, the received Bitcoins are already yours, but until the lock period expires you cannot spend them on the Bitcoin network."; +"balance.token.processing" = "Processing"; +"balance.token.processing.info.title" = "Processing amount"; +"balance.token.processing.info.description" = "Transactions with this amount still syncing. And when they will be confirmed, this tokens will be available for spending"; "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; From d53a9f92c2679a8db2679a2bf352da0736622271 Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 27 Sep 2023 19:07:33 +0400 Subject: [PATCH 20/63] Fix send/swap token list for zcash --- .../Core/Adapters/ZcashAdapter.swift | 21 +++++++++---------- .../Models/AdapterState.swift | 8 +++++++ .../Modules/Wallet/BalanceViewItem.swift | 1 + .../WalletTokenBalanceViewItemFactory.swift | 9 +++----- .../TokenList/WalletTokenListDataSource.swift | 2 +- .../WalletTokenListViewItemFactory.swift | 2 ++ .../TokenList/WalletTokenListViewModel.swift | 9 ++++---- .../Wallet/WalletViewItemFactory.swift | 4 +++- .../en.lproj/Localizable.strings | 1 + 9 files changed, 34 insertions(+), 23 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index 155fc46c74..20221182e5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -66,17 +66,12 @@ class ZcashAdapter { private(set) var syncing: Bool = true private func defaultFee(network: ZcashNetwork, height: Int? = nil) -> Zatoshi { - let fee: Zatoshi - if let lastBlockHeight = height { - fee = network.constants.defaultFee(for: lastBlockHeight) - } else { - fee = network.constants.defaultFee() - } - return fee + network.constants.defaultFee(for: height ?? BlockHeight.max) } private func defaultFeeDecimal(network: ZcashNetwork, height: Int? = nil) -> Decimal { - defaultFee(network: network, height: height).decimalValue.decimalValue + // todo update fee settings + fee// defaultFee(network: network, height: height).decimalValue.decimalValue } init(wallet: Wallet, restoreSettings: RestoreSettings) throws { @@ -87,7 +82,9 @@ class ZcashAdapter { } network = ZcashNetworkBuilder.network(for: .mainnet) - fee = network.constants.defaultFee().decimalValue.decimalValue + + // todo: update fee settings + fee = 10_000//network.constants.defaultFee().decimalValue.decimalValue token = wallet.token transactionSource = wallet.transactionSource @@ -190,7 +187,9 @@ class ZcashAdapter { available: shieldedVerified ) ) - self?.lastBlockHeight = try await synchronizer.latestHeight() + let height = try await synchronizer.latestHeight() + self?.lastBlockHeight = height + self?.lastBlockUpdatedSubject.onNext(()) self?.finishPrepare() @@ -828,7 +827,7 @@ enum ZCashAdapterState: Equatable { case let .downloadingSapling(progress): return .customSyncing(main: "balance.downloading_sapling".localized(progress), secondary: nil, progress: progress) case let .downloadingBlocks(progress, _): - let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress)), showSign: false) + let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), showSign: false) return .customSyncing(main: "balance.downloading_blocks".localized, secondary: percentValue, progress: Int(progress * 100)) case let .scanningBlocks(number, lastBlock): return .customSyncing(main: "Scanning Blocks", secondary: "\(number)/\(lastBlock)", progress: nil) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift index 8e96f93e52..f05ae385c0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift @@ -21,6 +21,14 @@ enum AdapterState { } } + func spendAllowed(beforeSync: Bool) -> Bool { + switch self { + case .synced: return true + case .syncing, .customSyncing: return beforeSync ? true : false + case .stopped, .notSynced: return false + } + } + } extension AdapterState: Equatable { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift index 6057997ecc..cc629f96da 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift @@ -17,6 +17,7 @@ struct BalanceTopViewItem { let syncSpinnerProgress: Int? let indefiniteSearchCircle: Bool let failedImageViewVisible: Bool + let sendEnabled: Bool let primaryValue: (text: String?, dimmed: Bool)? let secondaryInfo: BalanceSecondaryInfoViewItem diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index a7b25752d8..577304d5f2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -18,12 +18,9 @@ class WalletTokenBalanceViewItemFactory { if item.watchAccount { buttons[.address] = .enabled } else { - let sendButtonState: ButtonState - switch item.state { - case .synced: sendButtonState = .enabled - case .syncing, .customSyncing: sendButtonState = item.balanceData.sendBeforeSync ? .enabled : .disabled - case .stopped, .notSynced: sendButtonState = .disabled - } + let sendButtonState: ButtonState = item + .state + .spendAllowed(beforeSync: item.balanceData.sendBeforeSync) ? .enabled : .disabled buttons[.send] = sendButtonState buttons[.receive] = .enabled diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift index 381254ab34..81a02882c0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift @@ -166,7 +166,7 @@ extension WalletTokenListDataSource: ISectionDataSource { } subscribe(disposeBag, viewModel.noConnectionErrorSignal) { HudHelper.instance.show(banner: .noInternet) } - subscribe(disposeBag, viewModel.showSyncingSignal) { HudHelper.instance.show(banner: .attention(string: "Wait for synchronization")) } + subscribe(disposeBag, viewModel.showSyncingSignal) { HudHelper.instance.show(banner: .attention(string: "wait_for_synchronization".localized)) } subscribe(disposeBag, viewModel.selectWalletSignal) { [weak self] in self?.onSelect(wallet: $0) } subscribe(disposeBag, viewModel.openSyncErrorSignal) { [weak self] in self?.openSyncError(wallet: $0, error: $1) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift index ce6b087df8..4a503e2ade 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift @@ -9,6 +9,7 @@ class WalletTokenListViewItemFactory { private func topViewItem(item: WalletTokenListService.Item, balancePrimaryValue: BalancePrimaryValue) -> BalanceTopViewItem { let state = item.state + let sendEnabled = state.spendAllowed(beforeSync: item.balanceData.sendBeforeSync) return BalanceTopViewItem( isMainNet: item.isMainNet, @@ -19,6 +20,7 @@ class WalletTokenListViewItemFactory { syncSpinnerProgress: syncSpinnerProgress(state: state), indefiniteSearchCircle: indefiniteSearchCircle(state: state), failedImageViewVisible: failedImageViewVisible(state: state), + sendEnabled: sendEnabled, primaryValue: primaryValue(item: item, balancePrimaryValue: balancePrimaryValue), secondaryInfo: secondaryInfo(item: item, balancePrimaryValue: balancePrimaryValue) ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift index f5145bfa7e..1fd3303de4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift @@ -164,13 +164,14 @@ extension WalletTokenListViewModel { } func didSelect(item: BalanceViewItem) { - if item.topViewItem.indefiniteSearchCircle || item.topViewItem.syncSpinnerProgress != nil { + if item.topViewItem.failedImageViewVisible { + onTapFailedIcon(element: item.element) + return + } + if !item.topViewItem.sendEnabled { showSyncingRelay.accept(()) return } - if item.topViewItem.failedImageViewVisible { - onTapFailedIcon(element: item.element) - } else if let wallet = item.element.wallet { selectWalletRelay.accept(wallet) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index bcddb0eb6c..e656e0ab3e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -9,6 +9,7 @@ class WalletViewItemFactory { private func topViewItem(item: WalletService.Item, balancePrimaryValue: BalancePrimaryValue, balanceHidden: Bool) -> BalanceTopViewItem { let state = item.state + let sendEnabled = state.spendAllowed(beforeSync: item.balanceData.sendBeforeSync) return BalanceTopViewItem( isMainNet: item.isMainNet, @@ -19,6 +20,7 @@ class WalletViewItemFactory { syncSpinnerProgress: syncSpinnerProgress(state: state), indefiniteSearchCircle: indefiniteSearchCircle(state: state), failedImageViewVisible: failedImageViewVisible(state: state), + sendEnabled: sendEnabled, primaryValue: balanceHidden ? nil : primaryValue(item: item, balancePrimaryValue: balancePrimaryValue), secondaryInfo: secondaryInfo(item: item, balancePrimaryValue: balancePrimaryValue, balanceHidden: balanceHidden) ) @@ -67,7 +69,7 @@ class WalletViewItemFactory { private func indefiniteSearchCircle(state: AdapterState) -> Bool { switch state { - case .customSyncing: return true + case .customSyncing(_, _, let progress): return progress == nil default: return false } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 230b8dddd7..4e1d9975fa 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -298,6 +298,7 @@ Go to Settings - > %@ and allow access to the camera."; "balance.downloading_blocks" = "Downloading Blocks"; "balance.scanning_blocks" = "Scanning Blocks"; "balance.enhancing_transactions" = "Enhancing Transactions"; +"wait_for_synchronization" = "Wait for synchronization"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Syncing... %@"; From 6c8cc9e40189201c8096896d33cdc8d37667a636 Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 28 Sep 2023 14:23:46 +0400 Subject: [PATCH 21/63] Fix send to self error for zcash - Remove logs - Change fee to 10k zatoshi --- .../UnstoppableWallet/Core/Adapters/ZcashAdapter.swift | 10 +++++----- .../Core/Address/ZcashAddressParserItem.swift | 9 ++++----- .../UnstoppableWallet/Core/Protocols.swift | 2 +- .../Send/Platforms/Zcash/SendZcashService.swift | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index 20221182e5..bc591b6309 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -84,7 +84,7 @@ class ZcashAdapter { network = ZcashNetworkBuilder.network(for: .mainnet) // todo: update fee settings - fee = 10_000//network.constants.defaultFee().decimalValue.decimalValue + fee = Zatoshi(10_000).decimalValue.decimalValue//network.constants.defaultFee().decimalValue.decimalValue token = wallet.token transactionSource = wallet.transactionSource @@ -515,7 +515,7 @@ extension ZcashAdapter { outputParamsURL: outputParamsURL(uniqueId: uniqueId), saplingParamsSourceURL: SaplingParamsSourceURL.default, alias: .custom(uniqueId), - loggingPolicy: .default(.debug) + loggingPolicy: .default(.error) ) } @@ -714,9 +714,9 @@ extension ZcashAdapter: ISendZcashAdapter { max(0, balanceData.available - fee) } - func validate(address: String) throws -> AddressType { - guard address != receiveAddress.address else { - throw AppError.addressInvalid + func validate(address: String, checkSendToSelf: Bool = true) throws -> AddressType { + if checkSendToSelf, address == receiveAddress.address { + throw AppError.zcash(reason: .sendToSelf) } do { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/ZcashAddressParserItem.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/ZcashAddressParserItem.swift index 4d794f4ffa..e65fb3b75f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Address/ZcashAddressParserItem.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/ZcashAddressParserItem.swift @@ -8,11 +8,11 @@ class ZcashAddressParserItem { self.parserType = parserType } - private func validate(address: String) -> Single
{ + private func validate(address: String, checkSendToSelf: Bool) -> Single
{ do { switch parserType { case .adapter(let adapter): - _ = try adapter.validate(address: address) + _ = try adapter.validate(address: address, checkSendToSelf: checkSendToSelf) return Single.just(Address(raw: address, domain: nil)) case .validator(let validator): try validator.validate(address: address) @@ -29,13 +29,12 @@ class ZcashAddressParserItem { extension ZcashAddressParserItem: IAddressParserItem { func handle(address: String) -> Single
{ - validate(address: address) + validate(address: address, checkSendToSelf: true) } func isValid(address: String) -> Single { - validate(address: address) + validate(address: address, checkSendToSelf: false) .map { _ in true } - .catchErrorJustReturn(false) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift index b8e2d1b995..06abfcd3fc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift @@ -110,7 +110,7 @@ protocol ISendBinanceAdapter { protocol ISendZcashAdapter { var availableBalance: Decimal { get } - func validate(address: String) throws -> ZcashAdapter.AddressType + func validate(address: String, checkSendToSelf: Bool) throws -> ZcashAdapter.AddressType var fee: Decimal { get } func sendSingle(amount: Decimal, address: Recipient, memo: Memo?) -> Single func recipient(from stringEncodedAddress: String) -> Recipient? diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift index 50cff08b7a..a12957aed9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift @@ -59,7 +59,7 @@ class SendZcashService { private func syncState() { let address = addressService.state.address?.raw - let addressType = address.map { try? adapter.validate(address: $0) } + let addressType = address.map { try? adapter.validate(address: $0, checkSendToSelf: true) } isMemoAvailable = addressType.map { $0 == .shielded } ?? false guard amountCautionService.amountCaution == nil, From 3fb0123593c27c6b0c4bc303884a91bc9722fbe0 Mon Sep 17 00:00:00 2001 From: Ermat Date: Thu, 28 Sep 2023 18:33:14 +0600 Subject: [PATCH 22/63] Implement Duress Mode --- .../project.pbxproj | 68 ++++++++++- .../UnstoppableWallet/Core/App.swift | 17 ++- .../Core/Factories/AccountFactory.swift | 29 +++-- .../Core/Managers/AccountManager.swift | 107 +++++++++++++++--- .../Core/Managers/PasscodeManager.swift | 6 +- .../Core/Storage/AccountStorage.swift | 2 + .../Core/Storage/ActiveAccountStorage.swift | 23 ++-- .../Core/Storage/StorageMigrator.swift | 61 ++++++---- .../UnstoppableWallet/Models/Account.swift | 17 ++- .../Models/AccountRecord.swift | 8 +- .../Models/ActiveAccount.swift | 10 +- .../Deprecated/AccountRecord_v_0_36.swift | 62 ++++++++++ .../Deprecated/ActiveAccount_v_0_36.swift | 32 ++++++ .../DuressMode/DuressModeIntroView.swift | 79 +++++++++++++ .../DuressMode/DuressModeModule.swift | 11 ++ .../DuressMode/DuressModeSelectView.swift | 85 ++++++++++++++ .../DuressMode/DuressModeViewModel.swift | 22 ++++ .../CreateDuressPasscodeViewModel.swift | 21 +++- .../Manage/CreatePasscodeModule.swift | 12 +- .../Passcode/Manage/EditPasscodeModule.swift | 8 +- .../Passcode/Manage/SetPasscodeView.swift | 46 ++++---- .../Security/SecuritySettingsView.swift | 80 +++++++++---- .../UserInterface/SwiftUI/ClickableRow.swift | 4 +- .../SwiftUI/HorizontalDivider.swift | 3 +- .../UserInterface/SwiftUI/ListRow.swift | 4 +- .../UserInterface/SwiftUI/ListSection.swift | 12 +- .../UserInterface/SwiftUI/ListStyle.swift | 37 ++++++ .../UserInterface/SwiftUI/NavigationRow.swift | 2 +- .../SwiftUI/PageDescription.swift | 11 ++ .../UserInterface/SwiftUI/RowButton.swift | 10 -- .../SwiftUI/RowButtonStyle.swift | 10 ++ .../en.lproj/Localizable.strings | 28 ++++- 32 files changed, 750 insertions(+), 177 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Models/Deprecated/AccountRecord_v_0_36.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Models/Deprecated/ActiveAccount_v_0_36.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListStyle.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PageDescription.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButton.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index a4dbc85627..babd88d801 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ 11B350CB4E7C006C26AE5FB3 /* EnabledWalletStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35763ED14419B9EE4C6F9 /* EnabledWalletStorage.swift */; }; 11B350D00FA0A18EF540C945 /* BottomSingleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EF3688D60C8E6823267 /* BottomSingleSelectorViewController.swift */; }; 11B350D6CBB602F510882F1E /* WalletConnectRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CD5EBBB403D46BDEF0B /* WalletConnectRequest.swift */; }; + 11B350D931616C0C296B6082 /* DuressModeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */; }; 11B350DBF23645FE8641A193 /* FiatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350EE043CD96E484F9524 /* FiatService.swift */; }; 11B350DCAD95F45727869A56 /* EvmMethodLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E3F01D5A5CFE5A4E94B /* EvmMethodLabel.swift */; }; 11B350DD9B9E483A88C064D2 /* SimpleActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A3B86D99FBB036C74C7 /* SimpleActivateView.swift */; }; @@ -140,6 +141,7 @@ 11B35183F103B22537F9F9DF /* ManageWalletsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3550ED151B4C6824B9779 /* ManageWalletsViewModel.swift */; }; 11B351856787DD75A41861B6 /* CoinInvestorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A0F912218FEC2A196C0 /* CoinInvestorsViewController.swift */; }; 11B3518578A4531274D73A21 /* UnlinkWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35592753D3F2A9CCA5809 /* UnlinkWatchViewController.swift */; }; + 11B35189844EFD9E4B58269D /* PageDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FDDBEF227E161F6A0E /* PageDescription.swift */; }; 11B3518B594ECB199242C5CB /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B3518BEA8865CADA5DA684 /* LaunchScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEC0AB0B09C7E4209A /* LaunchScreenManager.swift */; }; 11B3518C9B837CB6C740AABB /* CreatePasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */; }; @@ -270,6 +272,7 @@ 11B3530088E70831A648EC63 /* CexDepositNetworkRaw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502198C667A95C21DCF3 /* CexDepositNetworkRaw.swift */; }; 11B35307AE70D7996F483DAE /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */; }; 11B353096900F82EDF084F3B /* SetPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */; }; + 11B35309CE9FBDA200067C4F /* ActiveAccount_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */; }; 11B3530E7755A4882F7E0C0A /* SelectorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353C09FE554834C760777 /* SelectorModule.swift */; }; 11B35311CEEC40EA3089293D /* SubscriptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351CD91AE01747F66E746 /* SubscriptionInfoViewController.swift */; }; 11B35313AC2978EE7DBC3EA9 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354712C102B954BCEE258 /* FilterView.swift */; }; @@ -301,6 +304,7 @@ 11B35355FF6481B50773C868 /* NftCollectionOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4F17D4CC8E89F7DC3B /* NftCollectionOverviewService.swift */; }; 11B35356AA508971CA689290 /* CoinAnalyticsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3540B41309A446C1DDB83 /* CoinAnalyticsViewController.swift */; }; 11B35357032B368120BA1C06 /* TestNetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EE072CE5471B0DFF841 /* TestNetManager.swift */; }; + 11B353577381981235B90A82 /* ListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356EF92FFD23F4385A991 /* ListStyle.swift */; }; 11B3535ABA61D7BD84EE500C /* NftEventMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BACB38FE566F6F575B /* NftEventMetadata.swift */; }; 11B3535C10D649F8CD1BCDAF /* HsLabelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EDE31BA3EF80F78859A /* HsLabelProvider.swift */; }; 11B3535EF39FCD22171AC21C /* FaqService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352B4E116BEC01B972A39 /* FaqService.swift */; }; @@ -320,6 +324,7 @@ 11B3539B3634BF7B3B1B9061 /* DescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3515BDAF15B6F7EEAB609 /* DescriptionCell.swift */; }; 11B3539E833ABB2D6F696916 /* BlockchainSettingRecord_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */; }; 11B353A07F9259765D90F3BA /* NftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355129D9F61172FCAB8C0 /* NftService.swift */; }; + 11B353A8B524526D20195D37 /* DuressModeIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35420841B4F9B886A6507 /* DuressModeIntroView.swift */; }; 11B353AA4AFFB020A68E09B6 /* AccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D6F474C2EB3687EB4 /* AccountFactory.swift */; }; 11B353AD1FE351B86CA538EA /* RestoreMnemonicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598A8D7D1A8D5E17BE15 /* RestoreMnemonicViewModel.swift */; }; 11B353AE1D1D9A8E5CF8E7A2 /* BaseTransactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35935EF1B2237E0289669 /* BaseTransactionsViewModel.swift */; }; @@ -365,6 +370,7 @@ 11B3542D112D915738AB1045 /* SimpleActivateModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35178643181B9CE1D6C8B /* SimpleActivateModule.swift */; }; 11B35434C09F1E3818DC857B /* ReceiveDerivationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357EEC98939F9C7AA3271 /* ReceiveDerivationViewModel.swift */; }; 11B3543A420A23064B056925 /* ReceiveAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532A1DC90E3D0E3403F8 /* ReceiveAddressViewModel.swift */; }; + 11B3543A7A9EB1E0E0E8753D /* DuressModeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */; }; 11B35440714FF3AAF24542D4 /* WalletCexElementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7CCC41913AA8D36CBC /* WalletCexElementService.swift */; }; 11B35444DADD43277D30DFE6 /* AmountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B617A9CE668EEF4978B /* AmountData.swift */; }; 11B354460024FA6EDB8B27DC /* BackupVerifyWordsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FA70D9570CB2708E1CA /* BackupVerifyWordsService.swift */; }; @@ -408,6 +414,7 @@ 11B354D754D2E2312223F9C0 /* ReceiveSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3580D6EDF1BB135965CC5 /* ReceiveSelectorViewController.swift */; }; 11B354D8DCBDAA82A6C51205 /* ManageAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355949F6D268EF1977DC9 /* ManageAccountViewModel.swift */; }; 11B354DB9BD0F91CFF4EB9C6 /* TransactionsViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D89546EBA13B01A1ED /* TransactionsViewItemFactory.swift */; }; + 11B354DC983042AD922339A6 /* DuressModeSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */; }; 11B354DEFBE83147106A5FFE /* CexAssetRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35195509787CD52A6873A /* CexAssetRecord.swift */; }; 11B354E72D9BDF04E75C8748 /* WalletHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3597A2B0B529BE97F85C8 /* WalletHeaderCell.swift */; }; 11B354E85FD7EE82D34FD1C4 /* BalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB7206DA0EDBB43C814 /* BalanceCell.swift */; }; @@ -450,6 +457,7 @@ 11B3555CA9B2F01358E055BE /* UnlinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35419B0C846238DDC50F3 /* UnlinkViewModel.swift */; }; 11B3555F968EFA0AF7D1DF46 /* WalletElementServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F4B9522FCCD91582AAF /* WalletElementServiceFactory.swift */; }; 11B35567A098667C9955F1F9 /* RecoveryPhraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3517B0E763E2C217654A7 /* RecoveryPhraseModule.swift */; }; + 11B355696714B5570748EF03 /* AccountRecord_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */; }; 11B3556B3FAAA6B1FA63C8B1 /* TransactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351167EBAE5FE1AA45882 /* TransactionsViewModel.swift */; }; 11B3556B4E9B6E54C93205D6 /* CexCoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352F071CE0EF1505A8380 /* CexCoinSelectViewController.swift */; }; 11B3556C12B91FD86A72A193 /* LitecoinAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356861F703A5A5C6630B6 /* LitecoinAdapter.swift */; }; @@ -504,7 +512,7 @@ 11B3562466F0ADD109244158 /* NftCollectionAssetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35100DD6E2DBF905FD19B /* NftCollectionAssetsModule.swift */; }; 11B3562D78E70F5F14B81B3A /* CexWithdrawNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572F134D41A670EE9244 /* CexWithdrawNetwork.swift */; }; 11B3562EE896D758066FEECB /* CexDepositNetworkSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35850DF16D11D45C44A60 /* CexDepositNetworkSelectService.swift */; }; - 11B35631BD5C6570C9359BEC /* RowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButton.swift */; }; + 11B35631BD5C6570C9359BEC /* RowButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */; }; 11B35631E5455A54854A2A6F /* RestoreMnemonicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55DCC92BED4FA87CA0 /* RestoreMnemonicService.swift */; }; 11B356330572A72E56DC2FEA /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */; }; 11B35633B952154A098532A4 /* NftCollectionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35708A630D70385F34A8B /* NftCollectionModule.swift */; }; @@ -536,6 +544,7 @@ 11B356A19A721D3557D7213C /* CoinReportsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E767DA0B5D7C0DAF203 /* CoinReportsViewModel.swift */; }; 11B356A2666F52C272B4E465 /* WalletTokenListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35136653741E9703E61DE /* WalletTokenListViewModel.swift */; }; 11B356A300ED689602ACD35D /* BalanceCoinIconHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3560C1FC3F73833FA4439 /* BalanceCoinIconHolder.swift */; }; + 11B356A35A5981DD231E580C /* ListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356EF92FFD23F4385A991 /* ListStyle.swift */; }; 11B356A4B22FA16BE27AFAB1 /* LogRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35822E26E7298100CD69D /* LogRecordStorage.swift */; }; 11B356A5B50D4E6EF2282398 /* EditDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */; }; 11B356A8E75C3F3C9FC4E530 /* ShortcutInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1C200EC15159154E3F /* ShortcutInputCell.swift */; }; @@ -616,6 +625,7 @@ 11B357BD9D9681D0D79DDEBE /* UITabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350369A891BEA3A525E5B /* UITabBarItem.swift */; }; 11B357BF378060E7E35F7052 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; 11B357BF7588CB317EA62167 /* MarketOverviewCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */; }; + 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */; }; 11B357C5FC1B7FDE86244DA5 /* SingleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CAE2327342F9CEC6AC9 /* SingleSelectorViewController.swift */; }; 11B357CD9544E312865CE36F /* WalletConnectInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A9F154EC84CEFA909B9 /* WalletConnectInteractor.swift */; }; 11B357D1A2BD673DAB7B4C61 /* SecondaryButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3587A6A05EFF1036F6C4B /* SecondaryButtonCell.swift */; }; @@ -634,6 +644,7 @@ 11B357F4C63379217B25AA75 /* RestoreSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358C7DF6F82875527031E /* RestoreSelectModule.swift */; }; 11B357FDC1C6BD6C39FE6853 /* MarketAdvancedSearchResultModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */; }; 11B357FE4C2E1EC8E26ED68F /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */; }; + 11B357FF80E87451A99BEE4A /* AccountRecord_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */; }; 11B357FF94D326846E12B940 /* WalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D547F1BB38D2AD6AD5 /* WalletManager.swift */; }; 11B358006AEB85BBE0BF47A7 /* EditPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */; }; 11B358033DAB0FF23CF0E309 /* NftActivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E034126F57DB7B4263 /* NftActivityService.swift */; }; @@ -678,6 +689,7 @@ 11B358623111DC1A8ED499DC /* EvmAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35962622F74F89FD32D2B /* EvmAddressViewModel.swift */; }; 11B358657FCC50C9B3A10294 /* ManageWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D645B4468F84EADB7 /* ManageWalletsViewController.swift */; }; 11B3586BF6AC0538272E71A4 /* NftCollectionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35708A630D70385F34A8B /* NftCollectionModule.swift */; }; + 11B3586F6BFCA16BDFD5921D /* DuressModeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A81FB3D4C06BBFEE7E7 /* DuressModeModule.swift */; }; 11B35871BA700133050E9241 /* CexWithdrawViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2465CB748311AF03D5 /* CexWithdrawViewModel.swift */; }; 11B3587D9E89A97F63CD0C5A /* EditPasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */; }; 11B3587DEC9342190880D3C3 /* TransactionsCoinSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF9B3B86F74961FADE1 /* TransactionsCoinSelectModule.swift */; }; @@ -755,6 +767,7 @@ 11B35953182487E864EB4946 /* ActivateSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E1107158B6A2BF2149 /* ActivateSubscriptionService.swift */; }; 11B35953404F5C8903DDA70D /* RecipientAddressCautionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358B22BAF021E8FA028BF /* RecipientAddressCautionCell.swift */; }; 11B35959AAF414186CE39698 /* AddTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E8892971578502EF33 /* AddTokenViewModel.swift */; }; + 11B3595AD0AA7108CAC814CC /* DuressModeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A81FB3D4C06BBFEE7E7 /* DuressModeModule.swift */; }; 11B3595BD960FE1B998ADF6F /* BinanceCexProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E9CE0702287077F975 /* BinanceCexProvider.swift */; }; 11B3595CF65B69B3B04635E0 /* TermsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EB9BA551F2F1AF7739D /* TermsManager.swift */; }; 11B3595D3E150BF50856A746 /* PoolSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC9E0E936067225C787 /* PoolSource.swift */; }; @@ -835,6 +848,7 @@ 11B35A426FD3D729DEB89DEA /* MarketTopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */; }; 11B35A42BF19B93C6005FBD9 /* AddTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356FFA77A8F6918B13FCA /* AddTokenService.swift */; }; 11B35A42D28B8BC4CDA57D8E /* AccountRecord_v_0_19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F6C5F6ABC288511AF0 /* AccountRecord_v_0_19.swift */; }; + 11B35A48CF68A2A45E1A429E /* PageDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FDDBEF227E161F6A0E /* PageDescription.swift */; }; 11B35A4CBD60780E0870E77C /* NftAssetBriefMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E32AEEE37347E255C4 /* NftAssetBriefMetadata.swift */; }; 11B35A4D9BD4B8C29FBAFACF /* AboutModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E80D544DAF20B12B56 /* AboutModule.swift */; }; 11B35A4E8657330B03FB2BCF /* SwitchAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D884F1698E70F2536E /* SwitchAccountService.swift */; }; @@ -878,6 +892,7 @@ 11B35AB06F713851D58C60E3 /* ChooseBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35249BB89CF45176701EA /* ChooseBlockchainService.swift */; }; 11B35AB0C3F757E23D249330 /* TransactionTypeFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3592E4DA65E72C0BC6BEB /* TransactionTypeFilter.swift */; }; 11B35AB1A8FB2E49C98FCBEB /* NftCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35396831B92AAC156DF1D /* NftCollectionViewModel.swift */; }; + 11B35AB1D397D409EA179917 /* ActiveAccount_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */; }; 11B35AB1DB6398B5C0ADF32A /* CexWithdrawConfirmModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F24FEE8233477BCDA18 /* CexWithdrawConfirmModule.swift */; }; 11B35AB6026D794BAFEC094E /* NftCollectionAssetsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CD0F79715E1A5EE8BF /* NftCollectionAssetsService.swift */; }; 11B35AB97CD3E6C07C2D008C /* ManageAccountsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359F01A63378AFAAEE113 /* ManageAccountsModule.swift */; }; @@ -1131,6 +1146,7 @@ 11B35DE80BDECA16EF0C74EA /* SimpleActivateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3564CFEC257DB52301CFC /* SimpleActivateViewModel.swift */; }; 11B35DF1D8B5125CF13A1812 /* RestoreMnemonicHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB288AF5A54B99A51E4 /* RestoreMnemonicHintView.swift */; }; 11B35DF3813AEB74E254A05A /* NftAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E1584E954D281FA87D /* NftAssetView.swift */; }; + 11B35DF625EA2A1412C2D984 /* DuressModeIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35420841B4F9B886A6507 /* DuressModeIntroView.swift */; }; 11B35DFCD3AD44FF72A38BBA /* CoinMarketsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F0A7192BA590254A16E /* CoinMarketsViewController.swift */; }; 11B35DFCEC1D363B160479EE /* MarketTopService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */; }; 11B35DFF8F15AA74356061A0 /* ReceiveSelectCoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35615F3ECB5D6E467B49A /* ReceiveSelectCoinService.swift */; }; @@ -1282,7 +1298,7 @@ 11B35F9F489F4B358FCCE893 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543968337A40168D3EB0 /* MarkdownParser.swift */; }; 11B35FA3A00690573A482BAC /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35FA6F9EE876BD65E9AD6 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; - 11B35FA70EB07440E1576A56 /* RowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButton.swift */; }; + 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */; }; 11B35FAB3263E489CB9017FC /* AddTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */; }; 11B35FB1B7B34756830942DC /* LaunchErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4096D259C9B1540D10 /* LaunchErrorViewController.swift */; }; 11B35FB28152F8881369DD9D /* AdapterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4E49ED2D2BF8E60863 /* AdapterManager.swift */; }; @@ -2799,6 +2815,7 @@ 11B351F1248EDA20F7141AB8 /* ExtendedKeyModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyModule.swift; sourceTree = ""; }; 11B351F33517C6DDA1E7AF59 /* AddEvmSyncSourceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEvmSyncSourceViewController.swift; sourceTree = ""; }; 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUnlockViewModel.swift; sourceTree = ""; }; + 11B351FDDBEF227E161F6A0E /* PageDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageDescription.swift; sourceTree = ""; }; 11B352034B036C9CB7A52724 /* BaseCurrencySettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsViewController.swift; sourceTree = ""; }; 11B352044BCE494491257933 /* LocalStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = ""; }; 11B35216E1F4300730E08C5D /* CheckboxCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxCell.swift; sourceTree = ""; }; @@ -2820,6 +2837,7 @@ 11B3527F1528AA697AAA6E61 /* TopPlatformViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformViewModel.swift; sourceTree = ""; }; 11B3528090862B6792A76DA4 /* FaqCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqCell.swift; sourceTree = ""; }; 11B352884D47E0B23DCF2C2C /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; + 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveAccount_v_0_36.swift; sourceTree = ""; }; 11B3529499CD211CC5A21CA2 /* NftCollectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionService.swift; sourceTree = ""; }; 11B352951AD68524C33022C0 /* CreatePasscodeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePasscodeModule.swift; sourceTree = ""; }; 11B352970EA9924258E5BB75 /* ListSectionFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionFooter.swift; sourceTree = ""; }; @@ -2899,6 +2917,7 @@ 11B35410733A35D1558E55B2 /* EvmCoinServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmCoinServiceFactory.swift; sourceTree = ""; }; 11B35419084A6CB11230E3C6 /* NftViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftViewController.swift; sourceTree = ""; }; 11B35419B0C846238DDC50F3 /* UnlinkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkViewModel.swift; sourceTree = ""; }; + 11B35420841B4F9B886A6507 /* DuressModeIntroView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuressModeIntroView.swift; sourceTree = ""; }; 11B35420B8191814543CBFA8 /* AddressInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressInputCell.swift; sourceTree = ""; }; 11B3543968337A40168D3EB0 /* MarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = ""; }; 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketHeaderCell.swift; sourceTree = ""; }; @@ -2940,6 +2959,7 @@ 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewService.swift; sourceTree = ""; }; 11B35542A7D7FE1BDC2E73E2 /* AccountType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountType.swift; sourceTree = ""; }; 11B355436F62829DBE3C92B4 /* CellComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellComponent.swift; sourceTree = ""; }; + 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuressModeSelectView.swift; sourceTree = ""; }; 11B3555A19D9E41785D88A5E /* KeychainKitDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainKitDelegate.swift; sourceTree = ""; }; 11B35564351D59D37278C723 /* ExtendedKeyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyService.swift; sourceTree = ""; }; 11B35577CFC2384E3A454329 /* EnabledWallet_v_0_20.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_20.swift; sourceTree = ""; }; @@ -2985,6 +3005,7 @@ 11B35690912F374FEE910193 /* NftCollectionMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionMetadata.swift; sourceTree = ""; }; 11B356940B04C8486835FDAA /* SwapApproveConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveConfirmationViewController.swift; sourceTree = ""; }; 11B3569F2E6BD5E9CBCFCA1F /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_36.swift; sourceTree = ""; }; 11B356B9F833E1AEE0D6D589 /* CexDepositService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositService.swift; sourceTree = ""; }; 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchViewModel.swift; sourceTree = ""; }; 11B356C2E5AF8ED41E2B545D /* WalletConnectRequestModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectRequestModule.swift; sourceTree = ""; }; @@ -2995,6 +3016,7 @@ 11B356E0F2BC23304E545B13 /* NftModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftModule.swift; sourceTree = ""; }; 11B356E4E27F5C12FC3859D1 /* CustomToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomToken.swift; sourceTree = ""; }; 11B356E71050EDF5C82FEFD9 /* BalanceTopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceTopView.swift; sourceTree = ""; }; + 11B356EF92FFD23F4385A991 /* ListStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListStyle.swift; sourceTree = ""; }; 11B356F4578E266268264021 /* QrCodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QrCodeCell.swift; sourceTree = ""; }; 11B356F9C155F16A441EC3A0 /* PoolGroupFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolGroupFactory.swift; sourceTree = ""; }; 11B356FFA77A8F6918B13FCA /* AddTokenService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTokenService.swift; sourceTree = ""; }; @@ -3166,6 +3188,7 @@ 11B35A6DE18A1E6E837DFB21 /* ContactBookManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookManager.swift; sourceTree = ""; }; 11B35A774105F0F012935845 /* ExtendedKeyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyViewController.swift; sourceTree = ""; }; 11B35A81AD46F48B63E59ED3 /* ReceiveBitcoinCashCoinTypeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveBitcoinCashCoinTypeViewModel.swift; sourceTree = ""; }; + 11B35A81FB3D4C06BBFEE7E7 /* DuressModeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuressModeModule.swift; sourceTree = ""; }; 11B35A8342513D5834B2145A /* ManageAccountsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountsViewModel.swift; sourceTree = ""; }; 11B35A8370C726989F4F456E /* WatchEvmAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchEvmAddressViewModel.swift; sourceTree = ""; }; 11B35A9DB4112F41D7FCAC12 /* PrivateKeysViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateKeysViewModel.swift; sourceTree = ""; }; @@ -3210,7 +3233,7 @@ 11B35B7D66631DD5D91D0773 /* CoinMarketsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMarketsModule.swift; sourceTree = ""; }; 11B35B968B299A67FC7FEAE3 /* WalletConnectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectManager.swift; sourceTree = ""; }; 11B35B96D2BC5994AC8EC794 /* MainModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainModule.swift; sourceTree = ""; }; - 11B35BAA4EA85B4A3A173498 /* RowButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowButton.swift; sourceTree = ""; }; + 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowButtonStyle.swift; sourceTree = ""; }; 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionOverviewViewController.swift; sourceTree = ""; }; 11B35BB370AE2C896BB9F877 /* TopPlatformViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformViewController.swift; sourceTree = ""; }; 11B35BB3B8928864A742C83E /* ReceiveAddressModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressModule.swift; sourceTree = ""; }; @@ -3356,6 +3379,7 @@ 11B35F4B9522FCCD91582AAF /* WalletElementServiceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletElementServiceFactory.swift; sourceTree = ""; }; 11B35F57D462E2C9E9AEF67C /* LockManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockManager.swift; sourceTree = ""; }; 11B35F5A3CC8C229C0849756 /* PublicKeysViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicKeysViewController.swift; sourceTree = ""; }; + 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuressModeViewModel.swift; sourceTree = ""; }; 11B35F60AFA103D0CD2369C3 /* BlockchainTokensView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainTokensView.swift; sourceTree = ""; }; 11B35F6B511DA5E0C60ED156 /* SendEvmViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmViewModel.swift; sourceTree = ""; }; 11B35F7D3814B59092D32FF9 /* FeeCoinProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeCoinProvider.swift; sourceTree = ""; }; @@ -4397,6 +4421,8 @@ 11B35EBD933DD3C9E72F1CA8 /* EnabledWallet_v_0_25.swift */, 11B357B2D07C69579BAEC997 /* CoinType.swift */, 11B3509AC90AEDF72F5989C6 /* EnabledWallet_v_0_34.swift */, + 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */, + 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */, ); path = Deprecated; sourceTree = ""; @@ -4600,7 +4626,7 @@ 11B35E7E7A5DBB09A2A5197D /* ThemeView.swift */, 11B35AC2D01DF06DC50EAC6A /* HighlightedTextView.swift */, 11B3578FB80AA013BD351A26 /* NavigationRow.swift */, - 11B35BAA4EA85B4A3A173498 /* RowButton.swift */, + 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */, 11B35D179817528224E926D1 /* ClickableRow.swift */, 11B352970EA9924258E5BB75 /* ListSectionFooter.swift */, 11B350C0CB7083E2738D356C /* ListSectionHeader.swift */, @@ -4610,6 +4636,8 @@ 11B3586B2387D758371A07AB /* InteractiveDismiss.swift */, 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */, 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */, + 11B351FDDBEF227E161F6A0E /* PageDescription.swift */, + 11B356EF92FFD23F4385A991 /* ListStyle.swift */, ); path = SwiftUI; sourceTree = ""; @@ -5095,6 +5123,7 @@ 11B3566C587E62F8E154C9BC /* Unlock */, 11B353E1284B381BE56AC663 /* NumPadView.swift */, 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */, + 11B3580CFABA60F3840C093E /* DuressMode */, ); path = Passcode; sourceTree = ""; @@ -5225,6 +5254,17 @@ path = ManageWallets; sourceTree = ""; }; + 11B3580CFABA60F3840C093E /* DuressMode */ = { + isa = PBXGroup; + children = ( + 11B35420841B4F9B886A6507 /* DuressModeIntroView.swift */, + 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */, + 11B35A81FB3D4C06BBFEE7E7 /* DuressModeModule.swift */, + 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */, + ); + path = DuressMode; + sourceTree = ""; + }; 11B3581839DBB7AA34EFEF90 /* Assets */ = { isa = PBXGroup; children = ( @@ -9198,7 +9238,7 @@ 11B35AFE3ECB8A5EE7649F2D /* ExperimentalFeaturesView.swift in Sources */, 11B35ACD13702502B1ED3362 /* HighlightedTextView.swift in Sources */, 11B35F134E5EF8572BF330CB /* NavigationRow.swift in Sources */, - 11B35FA70EB07440E1576A56 /* RowButton.swift in Sources */, + 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */, 11B35CA92AA402BE72B4F5D6 /* Image.swift in Sources */, ABC9AD2688A8DF327A3F92FC /* NoAccountWalletTokenListService.swift in Sources */, ABC9A3FCFC46EC73A7E57EA3 /* WalletConnectPairingModule.swift in Sources */, @@ -9248,6 +9288,14 @@ 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */, 11B3523E8B466F259DB32E37 /* SecondaryCircleButtonStyle.swift in Sources */, ABC9AD46006A85E907826E2B /* EnabledWalletCache_v_0_36.swift in Sources */, + 11B35309CE9FBDA200067C4F /* ActiveAccount_v_0_36.swift in Sources */, + 11B357FF80E87451A99BEE4A /* AccountRecord_v_0_36.swift in Sources */, + 11B35DF625EA2A1412C2D984 /* DuressModeIntroView.swift in Sources */, + 11B35A48CF68A2A45E1A429E /* PageDescription.swift in Sources */, + 11B350D931616C0C296B6082 /* DuressModeViewModel.swift in Sources */, + 11B3595AD0AA7108CAC814CC /* DuressModeModule.swift in Sources */, + 11B356A35A5981DD231E580C /* ListStyle.swift in Sources */, + 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10501,7 +10549,7 @@ 11B35DDA6B6FB48499F6E0D3 /* ExperimentalFeaturesView.swift in Sources */, 11B353FD73E7731A9BC50C4E /* HighlightedTextView.swift in Sources */, 11B3574287AAA5FC16E3E3DA /* NavigationRow.swift in Sources */, - 11B35631BD5C6570C9359BEC /* RowButton.swift in Sources */, + 11B35631BD5C6570C9359BEC /* RowButtonStyle.swift in Sources */, 11B3541ED37746BAFF1832BA /* Image.swift in Sources */, ABC9AC5671A5EA9BF5ACBC5D /* NoAccountWalletTokenListService.swift in Sources */, ABC9A9CDDC14BA6259450ECA /* WalletConnectPairingModule.swift in Sources */, @@ -10551,6 +10599,14 @@ 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */, 11B35224D7A5A864C1C6F167 /* SecondaryCircleButtonStyle.swift in Sources */, ABC9AB2E235EA006E2DAD8DD /* EnabledWalletCache_v_0_36.swift in Sources */, + 11B35AB1D397D409EA179917 /* ActiveAccount_v_0_36.swift in Sources */, + 11B355696714B5570748EF03 /* AccountRecord_v_0_36.swift in Sources */, + 11B353A8B524526D20195D37 /* DuressModeIntroView.swift in Sources */, + 11B35189844EFD9E4B58269D /* PageDescription.swift in Sources */, + 11B3543A7A9EB1E0E0E8753D /* DuressModeViewModel.swift in Sources */, + 11B3586F6BFCA16BDFD5921D /* DuressModeModule.swift in Sources */, + 11B353577381981235B90A82 /* ListStyle.swift in Sources */, + 11B354DC983042AD922339A6 /* DuressModeSelectView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 25bcffc310..88ae3837cb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -157,11 +157,17 @@ class App { pasteboardManager = PasteboardManager() reachabilityManager = ReachabilityManager() + biometryManager = BiometryManager(localStorage: StorageKit.LocalStorage.default) + passcodeManager = PasscodeManager(biometryManager: biometryManager, secureStorage: keychainKit.secureStorage) + lockManager = LockManager(passcodeManager: passcodeManager, localStorage: StorageKit.LocalStorage.default, delegate: lockDelegate) + lockoutManager = LockoutManager(secureStorage: keychainKit.secureStorage) + + blurManager = BlurManager(lockManager: lockManager) + let accountRecordStorage = AccountRecordStorage(dbPool: dbPool) let accountStorage = AccountStorage(secureStorage: keychainKit.secureStorage, storage: accountRecordStorage) let activeAccountStorage = ActiveAccountStorage(dbPool: dbPool) - let accountCachedStorage = AccountCachedStorage(accountStorage: accountStorage, activeAccountStorage: activeAccountStorage) - accountManager = AccountManager(storage: accountCachedStorage) + accountManager = AccountManager(passcodeManager: passcodeManager, accountStorage: accountStorage, activeAccountStorage: activeAccountStorage) accountRestoreWarningManager = AccountRestoreWarningManager(accountManager: accountManager, localStorage: StorageKit.LocalStorage.default) accountFactory = AccountFactory(accountManager: accountManager) @@ -244,13 +250,6 @@ class App { let favoriteCoinRecordStorage = FavoriteCoinRecordStorage(dbPool: dbPool) favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage) - biometryManager = BiometryManager(localStorage: StorageKit.LocalStorage.default) - passcodeManager = PasscodeManager(biometryManager: biometryManager, secureStorage: keychainKit.secureStorage) - lockManager = LockManager(passcodeManager: passcodeManager, localStorage: StorageKit.LocalStorage.default, delegate: lockDelegate) - lockoutManager = LockoutManager(secureStorage: keychainKit.secureStorage) - - blurManager = BlurManager(lockManager: lockManager) - let appVersionRecordStorage = AppVersionRecordStorage(dbPool: dbPool) let appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift index 42dd93cf5a..778d6af7c7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift @@ -1,5 +1,5 @@ -import Foundation import EvmKit +import Foundation class AccountFactory { private let accountManager: AccountManager @@ -7,11 +7,9 @@ class AccountFactory { init(accountManager: AccountManager) { self.accountManager = accountManager } - } extension AccountFactory { - var nextAccountName: String { let nonWatchAccounts = accountManager.accounts.filter { !$0.watchAccount } let order = nonWatchAccounts.count + 1 @@ -22,7 +20,7 @@ extension AccountFactory { func nextAccountName(cex: Cex) -> String { let cexAccounts = accountManager.accounts.filter { account in switch account.type { - case .cex(let cexAccount): return cexAccount.cex == cex + case let .cex(cexAccount): return cexAccount.cex == cex default: return false } } @@ -40,22 +38,23 @@ extension AccountFactory { func account(type: AccountType, origin: AccountOrigin, backedUp: Bool, name: String) -> Account { Account( - id: UUID().uuidString, - name: name, - type: type, - origin: origin, - backedUp: backedUp + id: UUID().uuidString, + level: accountManager.currentLevel, + name: name, + type: type, + origin: origin, + backedUp: backedUp ) } func watchAccount(type: AccountType, name: String) -> Account { Account( - id: UUID().uuidString, - name: name, - type: type, - origin: .restored, - backedUp: true + id: UUID().uuidString, + level: accountManager.currentLevel, + name: name, + type: type, + origin: .restored, + backedUp: true ) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift index 27f9915ea4..9d5043b69b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift @@ -1,8 +1,11 @@ -import RxSwift +import Combine import RxRelay +import RxSwift class AccountManager { + private let passcodeManager: PasscodeManager private let storage: AccountCachedStorage + private var cancellables = Set() private let activeAccountRelay = PublishRelay() private let accountsRelay = PublishRelay<[Account]>() @@ -12,8 +15,44 @@ class AccountManager { private var lastCreatedAccount: Account? - init(storage: AccountCachedStorage) { - self.storage = storage + init(passcodeManager: PasscodeManager, accountStorage: AccountStorage, activeAccountStorage: ActiveAccountStorage) { + self.passcodeManager = passcodeManager + + storage = AccountCachedStorage(level: passcodeManager.currentPasscodeLevel, accountStorage: accountStorage, activeAccountStorage: activeAccountStorage) + + passcodeManager.$currentPasscodeLevel + .sink { [weak self] level in + self?.handle(level: level) + } + .store(in: &cancellables) + + passcodeManager.$isDuressPasscodeSet + .sink { [weak self] isSet in + if !isSet { + self?.handleDisableDuress() + } + } + .store(in: &cancellables) + } + + private func handle(level: Int) { + storage.set(level: level) + + accountsRelay.accept(storage.accounts) + activeAccountRelay.accept(storage.activeAccount) + } + + private func handleDisableDuress() { + let currentLevel = passcodeManager.currentPasscodeLevel + + for account in storage.accounts { + if account.level > currentLevel { + account.level = currentLevel + storage.save(account: account) + } + } + + accountsRelay.accept(storage.accounts) } private func clearAccounts(ids: [String]) { @@ -21,7 +60,7 @@ class AccountManager { storage.delete(accountId: $0) } - if storage.accounts.isEmpty { + if storage.allAccounts.isEmpty { accountsLostRelay.accept(true) } } @@ -29,7 +68,6 @@ class AccountManager { } extension AccountManager { - var activeAccountObservable: Observable { activeAccountRelay.asObservable() } @@ -50,6 +88,10 @@ extension AccountManager { accountsLostRelay.asObservable() } + var currentLevel: Int { + passcodeManager.currentPasscodeLevel + } + var activeAccount: Account? { storage.activeAccount } @@ -145,21 +187,47 @@ extension AccountManager { return account } + func setDuress(accountIds: [String]) { + let currentLevel = passcodeManager.currentPasscodeLevel + + for account in storage.accounts { + if accountIds.contains(account.id) { + account.level = currentLevel + 1 + storage.save(account: account) + } + } + + accountsRelay.accept(storage.accounts) + } } class AccountCachedStorage { private let accountStorage: AccountStorage private let activeAccountStorage: ActiveAccountStorage - private var _accounts: [String: Account] + private var _allAccounts: [String: Account] + + private var level: Int + private var _accounts = [String: Account]() private var _activeAccount: Account? - init(accountStorage: AccountStorage, activeAccountStorage: ActiveAccountStorage) { + init(level: Int, accountStorage: AccountStorage, activeAccountStorage: ActiveAccountStorage) { + self.level = level self.accountStorage = accountStorage self.activeAccountStorage = activeAccountStorage - _accounts = accountStorage.allAccounts.reduce(into: [String: Account]()) { $0[$1.id] = $1 } - _activeAccount = activeAccountStorage.activeAccountId.flatMap { _accounts[$0] } + _allAccounts = accountStorage.allAccounts.reduce(into: [String: Account]()) { $0[$1.id] = $1 } + + syncAccounts() + } + + private func syncAccounts() { + _accounts = _allAccounts.filter { _, account in account.level >= level } + _activeAccount = activeAccountStorage.activeAccountId(level: level).flatMap { _accounts[$0] } + } + + var allAccounts: [Account] { + Array(_allAccounts.values) } var accounts: [Account] { @@ -174,33 +242,46 @@ class AccountCachedStorage { accountStorage.lostAccountIds } + func set(level: Int) { + self.level = level + syncAccounts() + } + func account(id: String) -> Account? { - _accounts[id] + _allAccounts[id] } func set(activeAccountId: String?) { - activeAccountStorage.activeAccountId = activeAccountId + activeAccountStorage.save(activeAccountId: activeAccountId, level: level) _activeAccount = activeAccountId.flatMap { _accounts[$0] } } func save(account: Account) { accountStorage.save(account: account) - _accounts[account.id] = account + _allAccounts[account.id] = account + + if account.level >= level { + _accounts[account.id] = account + } else { + _accounts.removeValue(forKey: account.id) + } } func delete(account: Account) { accountStorage.delete(account: account) + _allAccounts.removeValue(forKey: account.id) _accounts.removeValue(forKey: account.id) } func delete(accountId: String) { accountStorage.delete(accountId: accountId) + _allAccounts.removeValue(forKey: accountId) _accounts.removeValue(forKey: accountId) } func clear() { accountStorage.clear() + _allAccounts = [:] _accounts = [:] } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift index 18ce8b5025..dacbd33672 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift @@ -11,9 +11,9 @@ class PasscodeManager { private var passcodes = [String]() - @PostPublished private(set) var currentPasscodeLevel: Int - @PostPublished private(set) var isPasscodeSet = false - @PostPublished private(set) var isDuressPasscodeSet = false + @DistinctPublished private(set) var currentPasscodeLevel: Int + @DistinctPublished private(set) var isPasscodeSet = false + @DistinctPublished private(set) var isDuressPasscodeSet = false init(biometryManager: BiometryManager, secureStorage: ISecureStorage) { self.biometryManager = biometryManager diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift index 15f804af2a..a8f63fab4a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift @@ -83,6 +83,7 @@ class AccountStorage { return Account( id: id, + level: record.level, name: record.name, type: type, origin: origin, @@ -126,6 +127,7 @@ class AccountStorage { return AccountRecord( id: id, + level: account.level, name: account.name, type: typeName.rawValue, origin: account.origin.rawValue, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ActiveAccountStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ActiveAccountStorage.swift index 4b9c934007..5469d59b53 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ActiveAccountStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ActiveAccountStorage.swift @@ -11,19 +11,18 @@ class ActiveAccountStorage { extension ActiveAccountStorage { - var activeAccountId: String? { - get { - try! dbPool.read { db in - try ActiveAccount.fetchOne(db)?.accountId - } + func activeAccountId(level: Int) -> String? { + try? dbPool.read { db in + try ActiveAccount.filter(ActiveAccount.Columns.level == level).fetchOne(db)?.accountId } - set { - _ = try! dbPool.write { db in - if let accountId = newValue { - try ActiveAccount(accountId: accountId).insert(db) - } else { - try ActiveAccount.deleteAll(db) - } + } + + func save(activeAccountId: String?, level: Int) { + _ = try? dbPool.write { db in + if let activeAccountId { + try ActiveAccount(level: level, accountId: activeAccountId).insert(db) + } else { + try ActiveAccount.filter(ActiveAccount.Columns.level == level).deleteAll(db) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift index d9c1e3eb91..03edb45cdd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift @@ -343,11 +343,11 @@ class StorageMigrator { } migrator.registerMigration("createActiveAccount") { db in - try db.create(table: ActiveAccount.databaseTableName) { t in - t.column(ActiveAccount.Columns.uniqueId.name, .text).notNull() - t.column(ActiveAccount.Columns.accountId.name, .text).notNull() + try db.create(table: ActiveAccount_v_0_36.databaseTableName) { t in + t.column(ActiveAccount_v_0_36.Columns.uniqueId.name, .text).notNull() + t.column(ActiveAccount_v_0_36.Columns.accountId.name, .text).notNull() - t.primaryKey([ActiveAccount.Columns.uniqueId.name], onConflict: .replace) + t.primaryKey([ActiveAccount_v_0_36.Columns.uniqueId.name], onConflict: .replace) } } @@ -367,17 +367,17 @@ class StorageMigrator { try db.drop(table: AccountRecord_v_0_20.databaseTableName) - try db.create(table: AccountRecord.databaseTableName) { t in - t.column(AccountRecord.Columns.id.name, .text).notNull() - t.column(AccountRecord.Columns.name.name, .text).notNull() - t.column(AccountRecord.Columns.type.name, .text).notNull() - t.column(AccountRecord.Columns.origin.name, .text).notNull() - t.column(AccountRecord.Columns.backedUp.name, .boolean).notNull() - t.column(AccountRecord.Columns.wordsKey.name, .text) - t.column(AccountRecord.Columns.saltKey.name, .text) - t.column(AccountRecord.Columns.dataKey.name, .text) + try db.create(table: AccountRecord_v_0_36.databaseTableName) { t in + t.column(AccountRecord_v_0_36.Columns.id.name, .text).notNull() + t.column(AccountRecord_v_0_36.Columns.name.name, .text).notNull() + t.column(AccountRecord_v_0_36.Columns.type.name, .text).notNull() + t.column(AccountRecord_v_0_36.Columns.origin.name, .text).notNull() + t.column(AccountRecord_v_0_36.Columns.backedUp.name, .boolean).notNull() + t.column(AccountRecord_v_0_36.Columns.wordsKey.name, .text) + t.column(AccountRecord_v_0_36.Columns.saltKey.name, .text) + t.column(AccountRecord_v_0_36.Columns.dataKey.name, .text) - t.primaryKey([AccountRecord.Columns.id.name], onConflict: .replace) + t.primaryKey([AccountRecord_v_0_36.Columns.id.name], onConflict: .replace) } for (index, oldAccount) in oldAccounts.enumerated() { @@ -402,7 +402,7 @@ class StorageMigrator { accountType = "mnemonic" } - let newAccount = AccountRecord( + let newAccount = AccountRecord_v_0_36( id: oldAccount.id, name: "Wallet \(index + 1)", type: accountType, @@ -417,7 +417,7 @@ class StorageMigrator { try newAccount.insert(db) if index == 0 { - let activeAccount = ActiveAccount(accountId: oldAccount.id) + let activeAccount = ActiveAccount_v_0_36(accountId: oldAccount.id) try activeAccount.insert(db) } } @@ -495,7 +495,7 @@ class StorageMigrator { migrator.registerMigration("fillSaltToAccountsKeychain") { db in let keychain = Keychain(service: "io.horizontalsystems.bank.dev") - let records = try AccountRecord.fetchAll(db) + let records = try AccountRecord_v_0_36.fetchAll(db) for record in records { try keychain.set("", key: "mnemonic_\(record.id)_salt") @@ -697,8 +697,8 @@ class StorageMigrator { } migrator.registerMigration("checkBIP39Compliance") { db in - try db.alter(table: AccountRecord.databaseTableName) { t in - t.add(column: AccountRecord.Columns.bip39Compliant.name, .boolean) + try db.alter(table: AccountRecord_v_0_36.databaseTableName) { t in + t.add(column: AccountRecord_v_0_36.Columns.bip39Compliant.name, .boolean) } } @@ -784,6 +784,29 @@ class StorageMigrator { } } + migrator.registerMigration("Add level to AccountRecord") { db in + try db.alter(table: AccountRecord.databaseTableName) { t in + t.add(column: AccountRecord.Columns.level.name, .integer).defaults(to: 0) + } + } + + migrator.registerMigration("Update ActiveAccount table") { db in + let activeAccountId = try ActiveAccount_v_0_36.fetchOne(db)?.accountId + + try db.drop(table: ActiveAccount_v_0_36.databaseTableName) + + try db.create(table: ActiveAccount.databaseTableName) { t in + t.column(ActiveAccount.Columns.level.name, .integer).notNull() + t.column(ActiveAccount.Columns.accountId.name, .text).notNull() + + t.primaryKey([ActiveAccount.Columns.level.name], onConflict: .replace) + } + + if let activeAccountId { + try ActiveAccount(level: 0, accountId: activeAccountId).insert(db) + } + } + try migrator.migrate(dbPool) } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Account.swift b/UnstoppableWallet/UnstoppableWallet/Models/Account.swift index b90f0a9026..3852a1ff9d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Account.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Account.swift @@ -1,14 +1,16 @@ import HdWalletKit -class Account { +class Account: Identifiable { let id: String + var level: Int var name: String let type: AccountType let origin: AccountOrigin var backedUp: Bool - init(id: String, name: String, type: AccountType, origin: AccountOrigin, backedUp: Bool) { + init(id: String, level: Int, name: String, type: AccountType, origin: AccountOrigin, backedUp: Bool) { self.id = id + self.level = level self.name = name self.type = type self.origin = origin @@ -19,7 +21,7 @@ class Account { switch type { case .evmAddress, .tronAddress: return true - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch key { case .public: return true default: return false @@ -37,7 +39,7 @@ class Account { } var nonStandard: Bool { - guard case .mnemonic(_, _, let bip39Compliant) = type else { + guard case let .mnemonic(_, _, bip39Compliant) = type else { return false } @@ -45,7 +47,7 @@ class Account { } var nonRecommended: Bool { - guard case .mnemonic(let words, let salt, let bip39Compliant) = type, bip39Compliant else { + guard case let .mnemonic(words, salt, bip39Compliant) = type, bip39Compliant else { return false } @@ -58,19 +60,16 @@ class Account { case .hdExtendedKey, .evmAddress, .tronAddress, .evmPrivateKey, .cex: return false } } - } extension Account: Hashable { - - public static func ==(lhs: Account, rhs: Account) -> Bool { + public static func == (lhs: Account, rhs: Account) -> Bool { lhs.id == rhs.id } public func hash(into hasher: inout Hasher) { hasher.combine(id) } - } enum AccountOrigin: String { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift index 9c7aa1687f..87027f01b5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift @@ -2,6 +2,7 @@ import GRDB class AccountRecord: Record { let id: String + let level: Int let name: String let type: String let origin: String @@ -11,8 +12,9 @@ class AccountRecord: Record { var dataKey: String? var bip39Compliant: Bool? - init(id: String, name: String, type: String, origin: String, backedUp: Bool, wordsKey: String?, saltKey: String?, dataKey: String?, bip39Compliant: Bool?) { + init(id: String, level: Int, name: String, type: String, origin: String, backedUp: Bool, wordsKey: String?, saltKey: String?, dataKey: String?, bip39Compliant: Bool?) { self.id = id + self.level = level self.name = name self.type = type self.origin = origin @@ -30,11 +32,12 @@ class AccountRecord: Record { } enum Columns: String, ColumnExpression { - case id, name, type, origin, backedUp, wordsKey, saltKey, dataKey, bip39Compliant + case id, level, name, type, origin, backedUp, wordsKey, saltKey, dataKey, bip39Compliant } required init(row: Row) { id = row[Columns.id] + level = row[Columns.level] name = row[Columns.name] type = row[Columns.type] origin = row[Columns.origin] @@ -49,6 +52,7 @@ class AccountRecord: Record { override func encode(to container: inout PersistenceContainer) { container[Columns.id] = id + container[Columns.level] = level container[Columns.name] = name container[Columns.type] = type container[Columns.origin] = origin diff --git a/UnstoppableWallet/UnstoppableWallet/Models/ActiveAccount.swift b/UnstoppableWallet/UnstoppableWallet/Models/ActiveAccount.swift index ab4daff5b1..cb88e26b4d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/ActiveAccount.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/ActiveAccount.swift @@ -1,10 +1,11 @@ import GRDB class ActiveAccount: Record { - let uniqueId: String = "active_account" + let level: Int let accountId: String - init(accountId: String) { + init(level: Int, accountId: String) { + self.level = level self.accountId = accountId super.init() @@ -15,17 +16,18 @@ class ActiveAccount: Record { } enum Columns: String, ColumnExpression { - case uniqueId, accountId + case level, accountId } required init(row: Row) { + level = row[Columns.level] accountId = row[Columns.accountId] super.init(row: row) } override func encode(to container: inout PersistenceContainer) { - container[Columns.uniqueId] = uniqueId + container[Columns.level] = level container[Columns.accountId] = accountId } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/AccountRecord_v_0_36.swift b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/AccountRecord_v_0_36.swift new file mode 100644 index 0000000000..3a970e1f8e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/AccountRecord_v_0_36.swift @@ -0,0 +1,62 @@ +import GRDB + +class AccountRecord_v_0_36: Record { + let id: String + let name: String + let type: String + let origin: String + let backedUp: Bool + var wordsKey: String? + var saltKey: String? + var dataKey: String? + var bip39Compliant: Bool? + + init(id: String, name: String, type: String, origin: String, backedUp: Bool, wordsKey: String?, saltKey: String?, dataKey: String?, bip39Compliant: Bool?) { + self.id = id + self.name = name + self.type = type + self.origin = origin + self.backedUp = backedUp + self.wordsKey = wordsKey + self.saltKey = saltKey + self.dataKey = dataKey + self.bip39Compliant = bip39Compliant + + super.init() + } + + override class var databaseTableName: String { + "account_records" + } + + enum Columns: String, ColumnExpression { + case id, name, type, origin, backedUp, wordsKey, saltKey, dataKey, bip39Compliant + } + + required init(row: Row) { + id = row[Columns.id] + name = row[Columns.name] + type = row[Columns.type] + origin = row[Columns.origin] + backedUp = row[Columns.backedUp] + wordsKey = row[Columns.wordsKey] + saltKey = row[Columns.saltKey] + dataKey = row[Columns.dataKey] + bip39Compliant = row[Columns.bip39Compliant] + + super.init(row: row) + } + + override func encode(to container: inout PersistenceContainer) { + container[Columns.id] = id + container[Columns.name] = name + container[Columns.type] = type + container[Columns.origin] = origin + container[Columns.backedUp] = backedUp + container[Columns.wordsKey] = wordsKey + container[Columns.saltKey] = saltKey + container[Columns.dataKey] = dataKey + container[Columns.bip39Compliant] = bip39Compliant + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/ActiveAccount_v_0_36.swift b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/ActiveAccount_v_0_36.swift new file mode 100644 index 0000000000..a625c52efe --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/ActiveAccount_v_0_36.swift @@ -0,0 +1,32 @@ +import GRDB + +class ActiveAccount_v_0_36: Record { + let uniqueId: String = "active_account" + let accountId: String + + init(accountId: String) { + self.accountId = accountId + + super.init() + } + + override class var databaseTableName: String { + "active_account" + } + + enum Columns: String, ColumnExpression { + case uniqueId, accountId + } + + required init(row: Row) { + accountId = row[Columns.accountId] + + super.init(row: row) + } + + override func encode(to container: inout PersistenceContainer) { + container[Columns.uniqueId] = uniqueId + container[Columns.accountId] = accountId + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift new file mode 100644 index 0000000000..9d66405c1d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift @@ -0,0 +1,79 @@ +import LocalAuthentication +import SwiftUI + +struct DuressModeIntroView: View { + let viewModel: DuressModeViewModel + @Binding var showParentSheet: Bool + + var body: some View { + ThemeView { + BottomGradientWrapper { + VStack(spacing: 0) { + PageDescription(text: "enable_duress_mode.intro.description".localized) + + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.intro.notes".localized) + ListSection { + if let biometryType = viewModel.biometryType { + InfoRow( + icon: Image(biometryType.iconName), + title: biometryType.title, + description: "enable_duress_mode.intro.biometrics.description".localized(biometryType.title, biometryType.title) + ) + } + + InfoRow( + icon: Image("dialpad_alt_2_24"), + title: "enable_duress_mode.intro.passcode_disabling".localized, + description: "enable_duress_mode.intro.passcode_disabling.description".localized + ) + + InfoRow( + icon: Image("edit_24"), + title: "enable_duress_mode.intro.passcode_change".localized, + description: "enable_duress_mode.intro.passcode_change.description".localized + ) + } + .listStyle(.bordered) + } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + NavigationLink(destination: { + if (viewModel.regularAccounts + viewModel.watchAccounts).isEmpty { + CreatePasscodeModule.createDuressPasscodeView(accountIds: [], showParentSheet: $showParentSheet) + } else { + DuressModeSelectView(viewModel: viewModel, showParentSheet: $showParentSheet) + } + }) { + Text("button.continue".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .navigationTitle("enable_duress_mode.intro.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + showParentSheet = false + } + } + } + + private struct InfoRow: View { + let icon: Image + let title: String + let description: String + + var body: some View { + ListRow { + icon.themeIcon(color: .themeJacob) + + VStack(spacing: .margin4) { + Text(title).themeBody() + Text(description).themeSubhead2() + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeModule.swift new file mode 100644 index 0000000000..2377f8ad41 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeModule.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct DuressModeModule { + static func view(showParentSheet: Binding) -> some View { + let viewModel = DuressModeViewModel( + biometryManager: App.shared.biometryManager, + accountManager: App.shared.accountManager + ) + return DuressModeIntroView(viewModel: viewModel, showParentSheet: showParentSheet) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift new file mode 100644 index 0000000000..a08173a7ee --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct DuressModeSelectView: View { + @ObservedObject var viewModel: DuressModeViewModel + @Binding var showParentSheet: Bool + + var body: some View { + ThemeView { + BottomGradientWrapper { + VStack(spacing: 0) { + PageDescription(text: "enable_duress_mode.select.description".localized) + + VStack(spacing: .margin24) { + if !viewModel.regularAccounts.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.select.wallets".localized) + ListSection { + ForEach(viewModel.regularAccounts) { account in + AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + } + } + } + } + + if !viewModel.watchAccounts.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.select.watch_wallets".localized) + ListSection { + ForEach(viewModel.watchAccounts) { account in + AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + } + } + } + } + } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + NavigationLink(destination: { + CreatePasscodeModule.createDuressPasscodeView(accountIds: Array(viewModel.selectedAccountIds), showParentSheet: $showParentSheet) + }) { + Text("button.next".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .navigationTitle("enable_duress_mode.select.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + showParentSheet = false + } + } + } + + private struct AccountRow: View { + let account: Account + @Binding var selectedAccountIds: Set + + var body: some View { + ClickableRow(action: { + if selectedAccountIds.contains(account.id) { + selectedAccountIds.remove(account.id) + } else { + selectedAccountIds.insert(account.id) + } + }) { + VStack(spacing: 1) { + Text(account.name).themeBody() + Text(account.type.detailedDescription).themeSubhead2() + } + + ZStack { + RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) + .stroke(Color.themeGray, lineWidth: 1.5) + .frame(width: .margin24, height: .margin24) + + if selectedAccountIds.contains(account.id) { + Image("check_2_20").themeIcon(color: .themeJacob) + } + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeViewModel.swift new file mode 100644 index 0000000000..026e29ca52 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeViewModel.swift @@ -0,0 +1,22 @@ +import Combine + +class DuressModeViewModel: ObservableObject { + private let biometryManager: BiometryManager + + let regularAccounts: [Account] + let watchAccounts: [Account] + + @Published var selectedAccountIds = Set() + + init(biometryManager: BiometryManager, accountManager: AccountManager) { + self.biometryManager = biometryManager + + let sortedAccounts = accountManager.accounts.sorted { $0.name.lowercased() < $1.name.lowercased() } + regularAccounts = sortedAccounts.filter { !$0.watchAccount } + watchAccounts = sortedAccounts.filter { $0.watchAccount } + } + + var biometryType: BiometryType? { + biometryManager.biometryType + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift index 536d22992b..35753bf0ce 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift @@ -1,21 +1,36 @@ import Combine class CreateDuressPasscodeViewModel: SetPasscodeViewModel { + private let accountIds: [String] + private let accountManager: AccountManager + + init(accountIds: [String], accountManager: AccountManager, passcodeManager: PasscodeManager) { + self.accountIds = accountIds + self.accountManager = accountManager + + super.init(passcodeManager: passcodeManager) + } + override var title: String { - "create_duress_passcode.title".localized + "enable_duress_mode.passcode.title".localized } override var passcodeDescription: String { - "create_duress_passcode.description".localized + "enable_duress_mode.passcode.description".localized } override var confirmDescription: String { - "create_duress_passcode.confirm_passcode".localized + "enable_duress_mode.passcode.confirm".localized } override func onEnter(passcode: String) { do { try passcodeManager.set(duressPasscode: passcode) + + if !accountIds.isEmpty { + accountManager.setDuress(accountIds: accountIds) + } + finishSubject.send() } catch { print("Create Duress Passcode Error: \(error)") diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift index 17948c52f9..351bd2e536 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreatePasscodeModule.swift @@ -1,7 +1,7 @@ import SwiftUI struct CreatePasscodeModule { - static func createPasscodeView(reason: CreatePasscodeReason, onCreate: @escaping () -> Void, onCancel: @escaping () -> Void) -> some View { + static func createPasscodeView(reason: CreatePasscodeReason, showParentSheet: Binding, onCreate: @escaping () -> Void, onCancel: @escaping () -> Void) -> some View { let viewModel = CreatePasscodeViewModel( passcodeManager: App.shared.passcodeManager, reason: reason, @@ -9,15 +9,17 @@ struct CreatePasscodeModule { onCancel: onCancel ) - return SetPasscodeView(viewModel: viewModel) + return SetPasscodeView(viewModel: viewModel, showParentSheet: showParentSheet) } - static func createDuressPasscodeView() -> some View { + static func createDuressPasscodeView(accountIds: [String], showParentSheet: Binding) -> some View { let viewModel = CreateDuressPasscodeViewModel( + accountIds: accountIds, + accountManager: App.shared.accountManager, passcodeManager: App.shared.passcodeManager ) - return SetPasscodeView(viewModel: viewModel) + return SetPasscodeView(viewModel: viewModel, showParentSheet: showParentSheet) } enum CreatePasscodeReason: Hashable, Identifiable { @@ -28,7 +30,7 @@ struct CreatePasscodeModule { var description: String { switch self { case .regular: return "create_passcode.description".localized - case .biometry(let type): return "create_passcode.description.biometry".localized(type.title) + case let .biometry(type): return "create_passcode.description.biometry".localized(type.title) case .duress: return "create_passcode.description.duress_mode".localized } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift index cb3c46c339..8a5db69e06 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeModule.swift @@ -1,13 +1,13 @@ import SwiftUI struct EditPasscodeModule { - static func editPasscodeView() -> some View { + static func editPasscodeView(showParentSheet: Binding) -> some View { let viewModel = EditPasscodeViewModel(passcodeManager: App.shared.passcodeManager) - return SetPasscodeView(viewModel: viewModel) + return SetPasscodeView(viewModel: viewModel, showParentSheet: showParentSheet) } - static func editDuressPasscodeView() -> some View { + static func editDuressPasscodeView(showParentSheet: Binding) -> some View { let viewModel = EditDuressPasscodeViewModel(passcodeManager: App.shared.passcodeManager) - return SetPasscodeView(viewModel: viewModel) + return SetPasscodeView(viewModel: viewModel, showParentSheet: showParentSheet) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift index 29234649b7..7aaeb2c3b6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift @@ -2,34 +2,30 @@ import SwiftUI struct SetPasscodeView: View { @ObservedObject var viewModel: SetPasscodeViewModel - - @Environment(\.presentationMode) private var presentationMode + @Binding var showParentSheet: Bool var body: some View { - ThemeNavigationView { - ThemeView { - PasscodeView( - maxDigits: viewModel.passcodeLength, - description: $viewModel.description, - errorText: $viewModel.errorText, - passcode: $viewModel.passcode, - biometryType: Binding(get: { nil }, set: { _ in }), - lockoutState: Binding(get: { .unlocked(attemptsLeft: Int.max, maxAttempts: Int.max) }, set: { _ in }), - randomEnabled: false - ) - } - .navigationTitle(viewModel.title) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - Button("button.cancel".localized) { - viewModel.onCancel() - presentationMode.wrappedValue.dismiss() - } - } - .onReceive(viewModel.finishSubject) { - presentationMode.wrappedValue.dismiss() + ThemeView { + PasscodeView( + maxDigits: viewModel.passcodeLength, + description: $viewModel.description, + errorText: $viewModel.errorText, + passcode: $viewModel.passcode, + biometryType: Binding(get: { nil }, set: { _ in }), + lockoutState: Binding(get: { .unlocked(attemptsLeft: Int.max, maxAttempts: Int.max) }, set: { _ in }), + randomEnabled: false + ) + } + .navigationTitle(viewModel.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + viewModel.onCancel() + showParentSheet = false } } - .interactiveDismiss(canDismissSheet: false) + .onReceive(viewModel.finishSubject) { + showParentSheet = false + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index 39b0357e1f..2ada24f12f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -67,28 +67,65 @@ struct SecuritySettingsView: View { ListSectionFooter(text: "settings_security.balance_auto_hide.description".localized) } + + VStack(spacing: 0) { + ListSection { + if viewModel.isDuressPasscodeSet { + ClickableRow(action: { + unlockReason = .changeDuressPasscode + }) { + Image("switch_wallet_24").themeIcon(color: .themeJacob) + Text("settings_security.edit_duress_passcode".localized).themeBody(color: .themeJacob) + } + + ClickableRow(action: { + unlockReason = .disableDuressMode + }) { + Image("trash_24").themeIcon(color: .themeLucian) + Text("settings_security.disable_duress_mode".localized).themeBody(color: .themeLucian) + } + } else { + ClickableRow(action: { + if viewModel.isPasscodeSet { + unlockReason = .enableDuressMode + } else { + createPasscodeReason = .duress + } + }) { + Image("switch_wallet_24").themeIcon(color: .themeJacob) + Text("settings_security.enable_duress_mode".localized).themeBody(color: .themeJacob) + } + } + } + + ListSectionFooter(text: "settings_security.duress_mode.description".localized) + } } .sheet(item: $createPasscodeReason) { reason in - CreatePasscodeModule.createPasscodeView( - reason: reason, - onCreate: { - switch reason { - case .biometry: - viewModel.set(biometryEnabled: true) - case .duress: - DispatchQueue.main.async { - createDuressPasscodePresented = true + ThemeNavigationView { + CreatePasscodeModule.createPasscodeView( + reason: reason, + showParentSheet: Binding(get: { createPasscodeReason != nil }, set: { if !$0 { createPasscodeReason = nil } }), + onCreate: { + switch reason { + case .biometry: + viewModel.set(biometryEnabled: true) + case .duress: + DispatchQueue.main.async { + createDuressPasscodePresented = true + } + default: () + } + }, + onCancel: { + switch reason { + case .biometry: viewModel.isBiometryToggleOn = false + default: () } - default: () - } - }, - onCancel: { - switch reason { - case .biometry: viewModel.isBiometryToggleOn = false - default: () } - } - ) + ) + } + .interactiveDismiss(canDismissSheet: false) } .sheet(item: $unlockReason) { reason in ThemeNavigationView { @@ -115,16 +152,17 @@ struct SecuritySettingsView: View { } } .sheet(isPresented: $editPasscodePresented) { - EditPasscodeModule.editPasscodeView() + ThemeNavigationView { EditPasscodeModule.editPasscodeView(showParentSheet: $editPasscodePresented) } } .sheet(isPresented: $createDuressPasscodePresented) { - CreatePasscodeModule.createDuressPasscodeView() + ThemeNavigationView { DuressModeModule.view(showParentSheet: $createDuressPasscodePresented) } } .sheet(isPresented: $editDuressPasscodePresented) { - EditPasscodeModule.editDuressPasscodeView() + ThemeNavigationView { EditPasscodeModule.editDuressPasscodeView(showParentSheet: $editDuressPasscodePresented) } } .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } + .navigationTitle("settings_security.title".localized) } enum UnlockReason: Identifiable { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift index 827a58792a..cb37355e66 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift @@ -10,7 +10,7 @@ struct ClickableRow: View { content } }) - .buttonStyle(RowButton()) - .contentShape(Rectangle()) + .buttonStyle(RowButtonStyle()) + .contentShape(Rectangle()) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/HorizontalDivider.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/HorizontalDivider.swift index c9d27d86a4..71abc234f2 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/HorizontalDivider.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/HorizontalDivider.swift @@ -4,7 +4,7 @@ struct HorizontalDivider: View { private let color: Color private let height: CGFloat - init(color: Color = .themeSteel10, height: CGFloat = 1) { + init(color: Color = .themeSteel10, height: CGFloat = .heightOneDp) { self.color = color self.height = height } @@ -12,5 +12,4 @@ struct HorizontalDivider: View { var body: some View { color.frame(height: height) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListRow.swift index 19afb98790..0110886f3e 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListRow.swift @@ -7,7 +7,7 @@ struct ListRow: View { HStack(spacing: .margin16) { content } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16)) - .frame(minHeight: .heightCell48) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16)) + .frame(minHeight: .heightCell48) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListSection.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListSection.swift index a2116d81f9..e83bd90300 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListSection.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListSection.swift @@ -1,15 +1,18 @@ import SwiftUI struct ListSection: View { + @Environment(\.listStyle) var listStyle + @ViewBuilder let content: Content var body: some View { VStack(spacing: 0) { _VariadicView.Tree(Layout()) { - content - } - .background(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).fill(Color.themeLawrence)) - .clipShape(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous)) + content + } + .background(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).fill(listStyle.backgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: .cornerRadius12).stroke(listStyle.borderColor, lineWidth: .heightOneDp)) } } @@ -29,5 +32,4 @@ struct ListSection: View { } } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListStyle.swift new file mode 100644 index 0000000000..113a9ecab3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ListStyle.swift @@ -0,0 +1,37 @@ +import SwiftUI + +enum ListStyle { + case lawrence + case bordered + + var backgroundColor: Color { + switch self { + case .lawrence: return .themeLawrence + case .bordered: return .clear + } + } + + var borderColor: Color { + switch self { + case .lawrence: return .clear + case .bordered: return .themeSteel20 + } + } +} + +struct ListStyleKey: EnvironmentKey { + static let defaultValue = ListStyle.lawrence +} + +extension EnvironmentValues { + var listStyle: ListStyle { + get { self[ListStyleKey.self] } + set { self[ListStyleKey.self] = newValue } + } +} + +extension View { + func listStyle(_ listStyle: ListStyle) -> some View { + environment(\.listStyle, listStyle) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift index fa9efa3195..deb5523386 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift @@ -10,6 +10,6 @@ struct NavigationRow: View { content } } - .buttonStyle(RowButton()) + .buttonStyle(RowButtonStyle()) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PageDescription.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PageDescription.swift new file mode 100644 index 0000000000..a4e343fe8a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PageDescription.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct PageDescription: View { + let text: String + + var body: some View { + Text(text) + .themeSubhead2() + .padding(EdgeInsets(top: .margin12, leading: .margin32, bottom: .margin32, trailing: .margin32)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButton.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButton.swift deleted file mode 100644 index 82d26eaec6..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButton.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -struct RowButton: ButtonStyle { - - func makeBody(configuration: Self.Configuration) -> some View { - configuration.label - .background(configuration.isPressed ? Color.themeLawrencePressed : Color.themeLawrence) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift new file mode 100644 index 0000000000..7e83f83c71 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift @@ -0,0 +1,10 @@ +import SwiftUI + +struct RowButtonStyle: ButtonStyle { + @Environment(\.listStyle) var listStyle + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .background(configuration.isPressed ? Color.themeLawrencePressed : listStyle.backgroundColor) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 4e1d9975fa..7e8f5f63f3 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1084,6 +1084,10 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.disable_passcode" = "Disable Passcode"; "settings_security.balance_auto_hide" = "Balance Auto Hide"; "settings_security.balance_auto_hide.description" = "Automatically hides balance each time the app is opened, regardless of previous preferences."; +"settings_security.enable_duress_mode" = "Enable Duress Mode"; +"settings_security.edit_duress_passcode" = "Edit Duress Passcode"; +"settings_security.disable_duress_mode" = "Disable Duress Mode"; +"settings_security.duress_mode.description" = "A specialized mode designed to keep selected wallets safe under coercion."; // Create Passcode @@ -1093,11 +1097,25 @@ Go to Settings - > %@ and allow access to the camera."; "create_passcode.description.duress_mode" = "Set a passcode to enable Duress Mode"; "create_passcode.confirm_passcode" = "Confirm passcode"; -// Create Duress Passcode - -"create_duress_passcode.title" = "Duress Passcode"; -"create_duress_passcode.description" = "Set a passcode for Duress Mode"; -"create_duress_passcode.confirm_passcode" = "Confirm passcode"; +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Duress Passcode"; +"enable_duress_mode.intro.description" = "This mode allows user to setup multiple unlock app passcodes where a desired passcode shows only specified wallets. Designed to keep selected wallets safe under coercion or threats."; +"enable_duress_mode.intro.notes" = "Notes"; +"enable_duress_mode.intro.biometrics.description" = "The %@ feature will work to unlock the Duress Mode. You can disable %@ for convenience."; +"enable_duress_mode.intro.passcode_disabling" = "Passcode Disabling"; +"enable_duress_mode.intro.passcode_disabling.description" = "Disabling the passcode in the main mode will automatically reset the Duress Mode."; +"enable_duress_mode.intro.passcode_change" = "Passcode Change"; +"enable_duress_mode.intro.passcode_change.description" = "Changing the passcode in the Duress Mode will also change the current passcode code for that mode."; + +"enable_duress_mode.select.title" = "Select Wallets"; +"enable_duress_mode.select.description" = "Select the wallets that will be displayed in Duress Mode."; +"enable_duress_mode.select.wallets" = "Wallets"; +"enable_duress_mode.select.watch_wallets" = "Watch Wallets"; + +"enable_duress_mode.passcode.title" = "Duress Passcode"; +"enable_duress_mode.passcode.description" = "Set a passcode for Duress Mode"; +"enable_duress_mode.passcode.confirm" = "Confirm passcode"; // Edit Passcode From efa9d0061b1e8382a09fed32785654c8d7f95737 Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 15:23:30 +0600 Subject: [PATCH 23/63] Fix touch behavior in NumPad view --- .../UnstoppableWallet/Modules/Passcode/NumPadView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift index a2873b585c..ce4f4e4452 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/NumPadView.swift @@ -58,7 +58,7 @@ struct NumPadView: View { } .buttonStyle(NumPadButtonStyle()) .disabled(disabled) - .animation(.easeOut, value: digit) + .animation(.easeOut(duration: 0.2), value: digit) } } @@ -68,10 +68,10 @@ struct NumPadView: View { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(isEnabled ? .themeLeah : .themeSteel20) - .background(configuration.isPressed ? Color.themeSteel20 : Color.clear) + .background(configuration.isPressed ? Color.themeSteel20 : Color.themeTyler) .clipShape(Circle()) .overlay(Circle().stroke(Color.themeSteel20, lineWidth: .heightOneDp)) - .animation(.easeOut, value: configuration.isPressed) + .animation(.easeOut(duration: 0.1), value: configuration.isPressed) } } } From cb5dedc4b7f43478bb549298f66ccd5a4cce547d Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 15:23:56 +0600 Subject: [PATCH 24/63] Fix auto-selecting account when switching to Duress Mode --- .../UnstoppableWallet/Core/Managers/AccountManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift index 9d5043b69b..e397b42a91 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AccountManager.swift @@ -223,7 +223,7 @@ class AccountCachedStorage { private func syncAccounts() { _accounts = _allAccounts.filter { _, account in account.level >= level } - _activeAccount = activeAccountStorage.activeAccountId(level: level).flatMap { _accounts[$0] } + _activeAccount = activeAccountStorage.activeAccountId(level: level).flatMap { _accounts[$0] } ?? _accounts.first?.value } var allAccounts: [Account] { From 4f873bc3c85b4e320277e5be0295d9994eb08841 Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 15:42:27 +0600 Subject: [PATCH 25/63] Fix shake passcode view animation --- .../Passcode/Manage/SetPasscodeView.swift | 1 + .../Manage/SetPasscodeViewModel.swift | 1 + .../Modules/Passcode/PasscodeView.swift | 21 ++++++++++--------- .../Passcode/Unlock/BaseUnlockViewModel.swift | 3 +++ .../Modules/Passcode/Unlock/UnlockView.swift | 1 + 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift index 7aaeb2c3b6..26275a3bf1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeView.swift @@ -13,6 +13,7 @@ struct SetPasscodeView: View { passcode: $viewModel.passcode, biometryType: Binding(get: { nil }, set: { _ in }), lockoutState: Binding(get: { .unlocked(attemptsLeft: Int.max, maxAttempts: Int.max) }, set: { _ in }), + shakeTrigger: $viewModel.shakeTrigger, randomEnabled: false ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift index b4bbbd5ab2..5f7ea5d919 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift @@ -17,6 +17,7 @@ class SetPasscodeViewModel: ObservableObject { } } } + @Published var shakeTrigger: Int = 0 let passcodeManager: PasscodeManager diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift index 379edf35bc..6ebbf4667c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift @@ -14,6 +14,7 @@ struct PasscodeView: View { @Binding var biometryType: BiometryType? @Binding var lockoutState: LockoutState + @Binding var shakeTrigger: Int let randomEnabled: Bool var onTapBiometry: (() -> Void)? = nil @@ -33,7 +34,7 @@ struct PasscodeView: View { VStack { VStack { switch lockoutState { - case let .unlocked(attemptsLeft, _): + case .unlocked: Text(description) .font(.themeSubhead2) .foregroundColor(.themeGray) @@ -50,9 +51,9 @@ struct PasscodeView: View { .frame(width: .margin12, height: .margin12) } } - .modifier(Shake(animatableData: CGFloat(attemptsLeft))) + .modifier(Shake(animatableData: CGFloat(shakeTrigger))) .padding(.vertical, .margin16) - .animation(.linear(duration: 0.3), value: attemptsLeft) + .animation(.linear(duration: 0.3), value: shakeTrigger) .animation(.easeOut(duration: 0.1), value: passcode) Text(errorText) @@ -108,14 +109,14 @@ struct PasscodeView: View { .padding(.bottom, .margin32) } } -} -struct Shake: GeometryEffect { - var amount: CGFloat = 8 - var shakesPerUnit = 4 - var animatableData: CGFloat + private struct Shake: GeometryEffect { + var amount: CGFloat = 8 + var shakesPerUnit = 4 + var animatableData: CGFloat - func effectValue(size _: CGSize) -> ProjectionTransform { - ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0)) + func effectValue(size _: CGSize) -> ProjectionTransform { + ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0)) + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift index b2d98ba940..53f9246a63 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -27,6 +27,7 @@ class BaseUnlockViewModel: ObservableObject { syncErrorText() } } + @Published var shakeTrigger: Int = 0 let finishSubject = PassthroughSubject() let unlockWithBiometrySubject = PassthroughSubject() @@ -89,6 +90,8 @@ class BaseUnlockViewModel: ObservableObject { } else { passcode = "" lockoutManager.didFailUnlock() + + shakeTrigger += 1 UINotificationFeedbackGenerator().notificationOccurred(.error) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift index a407a427fe..90851e523d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift @@ -16,6 +16,7 @@ struct UnlockView: View { passcode: $viewModel.passcode, biometryType: $viewModel.resolvedBiometryType, lockoutState: $viewModel.lockoutState, + shakeTrigger: $viewModel.shakeTrigger, randomEnabled: true, onTapBiometry: { unlockWithBiometry() From 1d8ada1fc032bc83f54d10dc782486269d91563e Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 15:51:31 +0600 Subject: [PATCH 26/63] Validate if new passcode is already used --- .../Modules/Passcode/Manage/SetPasscodeViewModel.swift | 8 ++++++++ .../UnstoppableWallet/en.lproj/Localizable.strings | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift index 5f7ea5d919..45b85b8ed9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift @@ -1,4 +1,5 @@ import Combine +import UIKit class SetPasscodeViewModel: ObservableObject { let passcodeLength = 6 @@ -17,6 +18,7 @@ class SetPasscodeViewModel: ObservableObject { } } } + @Published var shakeTrigger: Int = 0 let passcodeManager: PasscodeManager @@ -47,6 +49,12 @@ class SetPasscodeViewModel: ObservableObject { syncDescription() errorText = "set_passcode.invalid_confirmation".localized } + } else if passcodeManager.has(passcode: passcode) { + passcode = "" + errorText = "set_passcode.already_used".localized + + shakeTrigger += 1 + UINotificationFeedbackGenerator().notificationOccurred(.error) } else { enteredPasscode = passcode passcode = "" diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 7e8f5f63f3..92c631324b 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1131,7 +1131,8 @@ Go to Settings - > %@ and allow access to the camera."; // Set Passcode -"set_passcode.invalid_confirmation" = "Invalid Confirmation"; +"set_passcode.invalid_confirmation" = "Invalid confirmation"; +"set_passcode.already_used" = "This passcode is already being used"; // Unlock From 6631236dca4d52cb8fda877ed716f4c60c4b3e14 Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 17:39:40 +0600 Subject: [PATCH 27/63] Reload app interface when passcode level changes --- .../Core/Managers/PasscodeManager.swift | 22 ++++++++++---- .../Modules/Launch/LaunchModule.swift | 5 +--- .../Passcode/Unlock/AppUnlockViewModel.swift | 30 +++++++------------ .../Passcode/Unlock/BaseUnlockViewModel.swift | 4 +-- .../Passcode/Unlock/ModuleUnlockView.swift | 2 +- .../Unlock/ModuleUnlockViewModel.swift | 9 ++---- .../Passcode/Unlock/UnlockModule.swift | 7 ++--- .../Modules/Passcode/Unlock/UnlockView.swift | 13 ++++---- .../UserInterface/LockDelegate.swift | 2 +- 9 files changed, 46 insertions(+), 48 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift index dacbd33672..7dffaf4da5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PasscodeManager.swift @@ -63,26 +63,36 @@ extension PasscodeManager { passcodes.contains(passcode) } - func setLastPasscode() { + func setLastPasscode() -> Bool { guard !passcodes.isEmpty else { - return + return false } - currentPasscodeLevel = passcodes.count - 1 + let level = passcodes.count - 1 + + guard currentPasscodeLevel != level else { + return false + } + + currentPasscodeLevel = level syncState() + + return true } - func set(currentPasscode: String) { + func set(currentPasscode: String) -> Bool { guard let level = passcodes.firstIndex(of: currentPasscode) else { - return + return false } guard currentPasscodeLevel != level else { - return + return false } currentPasscodeLevel = level syncState() + + return true } func set(passcode: String) throws { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift index dec28b9251..f6d1e28eee 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Launch/LaunchModule.swift @@ -14,10 +14,7 @@ class LaunchModule { case .passcodeNotSet: return NoPasscodeViewController(mode: .noPasscode) case .cannotCheckPasscode: return NoPasscodeViewController(mode: .cannotCheckPasscode) case .intro: return WelcomeScreenViewController() - case .unlock: return UnlockModule.appUnlockView { - UIApplication.shared.windows.first { $0.isKeyWindow }?.set(newRootController: MainModule.instance()) - } - .toViewController() + case .unlock: return UnlockModule.appUnlockView(appStart: true).toViewController() case .main: return MainModule.instance() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift index 3d738fdb56..2612111c26 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/AppUnlockViewModel.swift @@ -1,13 +1,11 @@ import Combine class AppUnlockViewModel: BaseUnlockViewModel { - private let autoDismiss: Bool - private let onUnlock: (() -> Void)? + private let appStart: Bool private let lockManager: LockManager - init(autoDismiss: Bool, onUnlock: (() -> Void)?, passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockoutManager: LockoutManager, lockManager: LockManager, blurManager: BlurManager) { - self.autoDismiss = autoDismiss - self.onUnlock = onUnlock + init(appStart: Bool, passcodeManager: PasscodeManager, biometryManager: BiometryManager, lockoutManager: LockoutManager, lockManager: LockManager, blurManager: BlurManager) { + self.appStart = appStart self.lockManager = lockManager super.init(passcodeManager: passcodeManager, biometryManager: biometryManager, lockoutManager: lockoutManager, blurManager: blurManager, biometryAllowed: true) @@ -18,25 +16,19 @@ class AppUnlockViewModel: BaseUnlockViewModel { } override func onEnterValid(passcode: String) { - super.onEnterValid(passcode: passcode) - - passcodeManager.set(currentPasscode: passcode) - handleUnlock() + let levelChanged = passcodeManager.set(currentPasscode: passcode) + handleUnlock(levelChanged: levelChanged) } - override func onBiometryUnlock() { - super.onBiometryUnlock() + override func onBiometryUnlock() -> Bool { + let levelChanged = passcodeManager.setLastPasscode() + handleUnlock(levelChanged: levelChanged) - passcodeManager.setLastPasscode() - handleUnlock() + return !levelChanged } - private func handleUnlock() { + private func handleUnlock(levelChanged: Bool) { lockManager.onUnlock() - onUnlock?() - - if autoDismiss { - finishSubject.send() - } + finishSubject.send(appStart || levelChanged) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift index 53f9246a63..3370b8ec76 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -29,7 +29,7 @@ class BaseUnlockViewModel: ObservableObject { } @Published var shakeTrigger: Int = 0 - let finishSubject = PassthroughSubject() + let finishSubject = PassthroughSubject() let unlockWithBiometrySubject = PassthroughSubject() let passcodeManager: PasscodeManager @@ -80,7 +80,7 @@ class BaseUnlockViewModel: ObservableObject { func isValid(passcode _: String) -> Bool { false } func onEnterValid(passcode _: String) {} - func onBiometryUnlock() {} + func onBiometryUnlock() -> Bool { false } @MainActor private func handlePasscodeChanged() { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift index 1d9b58dd31..58408e837a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockView.swift @@ -6,7 +6,7 @@ struct ModuleUnlockView: View { @Environment(\.presentationMode) private var presentationMode var body: some View { - UnlockView(viewModel: viewModel, autoDismiss: true) + UnlockView(viewModel: viewModel) .navigationTitle("unlock.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift index 75a3aec03b..76af1a33dd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/ModuleUnlockViewModel.swift @@ -15,15 +15,12 @@ class ModuleUnlockViewModel: BaseUnlockViewModel { } override func onEnterValid(passcode: String) { - super.onEnterValid(passcode: passcode) - onUnlock() - finishSubject.send() + finishSubject.send(false) } - override func onBiometryUnlock() { - super.onBiometryUnlock() - + override func onBiometryUnlock() -> Bool { onUnlock() + return true } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift index 838082fb47..514b96e690 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockModule.swift @@ -1,10 +1,9 @@ import SwiftUI struct UnlockModule { - static func appUnlockView(autoDismiss: Bool = false, onUnlock: (() -> Void)? = nil) -> some View { + static func appUnlockView(appStart: Bool) -> some View { let viewModel = AppUnlockViewModel( - autoDismiss: autoDismiss, - onUnlock: onUnlock, + appStart: appStart, passcodeManager: App.shared.passcodeManager, biometryManager: App.shared.biometryManager, lockoutManager: App.shared.lockoutManager, @@ -12,7 +11,7 @@ struct UnlockModule { blurManager: App.shared.blurManager ) - return UnlockView(viewModel: viewModel, autoDismiss: autoDismiss) + return UnlockView(viewModel: viewModel) } static func moduleUnlockView(biometryAllowed: Bool = false, onUnlock: @escaping () -> Void) -> some View { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift index 90851e523d..1e21b28106 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/UnlockView.swift @@ -3,7 +3,6 @@ import SwiftUI struct UnlockView: View { @ObservedObject var viewModel: BaseUnlockViewModel - let autoDismiss: Bool @Environment(\.presentationMode) private var presentationMode @@ -29,8 +28,12 @@ struct UnlockView: View { .onDisappear { viewModel.onDisappear() } - .onReceive(viewModel.finishSubject) { - presentationMode.wrappedValue.dismiss() + .onReceive(viewModel.finishSubject) { reloadApp in + if reloadApp { + UIApplication.shared.windows.first { $0.isKeyWindow }?.set(newRootController: MainModule.instance()) + } else { + presentationMode.wrappedValue.dismiss() + } } .onReceive(viewModel.unlockWithBiometrySubject) { unlockWithBiometry() @@ -42,9 +45,9 @@ struct UnlockView: View { localAuthenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "unlock.biometry_reason".localized) { success, _ in if success { DispatchQueue.main.async { - viewModel.onBiometryUnlock() + let shouldDismiss = viewModel.onBiometryUnlock() - if autoDismiss { + if shouldDismiss { presentationMode.wrappedValue.dismiss() } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift index 7c3f267eb1..9eaf5b55ec 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/LockDelegate.swift @@ -4,7 +4,7 @@ class LockDelegate { var viewController: UIViewController? func onLock() { - let module = UnlockModule.appUnlockView(autoDismiss: true).toViewController() + let module = UnlockModule.appUnlockView(appStart: false).toViewController() module.modalPresentationStyle = .fullScreen viewController?.visibleController.present(module, animated: false) } From ad5bebd971ad4cdf4a3e59878e601698ee552d4f Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 29 Sep 2023 18:02:13 +0600 Subject: [PATCH 28/63] Change behavior of Random button in Passcode view --- .../Modules/Passcode/PasscodeView.swift | 4 ++-- .../SwiftUI/SecondaryButtonStyle.swift | 20 ++++++++++++------- .../en.lproj/Localizable.strings | 3 +-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift index 6ebbf4667c..7d8ba2ac57 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/PasscodeView.swift @@ -100,9 +100,9 @@ struct PasscodeView: View { Button(action: { randomized.toggle() }) { - Text(randomized ? "unlock.regular_mode".localized : "unlock.random_mode".localized) + Text("unlock.random".localized) } - .buttonStyle(SecondaryButtonStyle(style: .default)) + .buttonStyle(SecondaryButtonStyle(isActive: randomized)) .disabled(lockoutState.isLocked) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift index 2ed3a87667..d6dedc1dba 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift @@ -2,6 +2,12 @@ import SwiftUI struct SecondaryButtonStyle: ButtonStyle { let style: Style + let isActive: Bool + + init(style: Style = .default, isActive: Bool = false) { + self.style = style + self.isActive = isActive + } @Environment(\.isEnabled) var isEnabled @@ -9,8 +15,8 @@ struct SecondaryButtonStyle: ButtonStyle { configuration.label .padding(EdgeInsets(top: 5.5, leading: .margin16, bottom: 5.5, trailing: .margin16)) .font(.themeSubhead1) - .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) - .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isActive: isActive, isPressed: configuration.isPressed)) + .background(style.backgroundColor(isEnabled: isEnabled, isActive: isActive, isPressed: configuration.isPressed)) .clipShape(Capsule(style: .continuous)) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) } @@ -19,16 +25,16 @@ struct SecondaryButtonStyle: ButtonStyle { case `default` case transparent - func foregroundColor(isEnabled: Bool, isPressed: Bool) -> Color { + func foregroundColor(isEnabled: Bool, isActive: Bool, isPressed: Bool) -> Color { switch self { - case .default, .transparent: return isEnabled ? (isPressed ? .themeGray : .themeLeah) : .themeGray50 + case .default, .transparent: return isEnabled ? (isActive ? .themeDark : (isPressed ? .themeGray : .themeLeah)) : .themeGray50 } } - func backgroundColor(isEnabled _: Bool, isPressed: Bool) -> Color { + func backgroundColor(isEnabled: Bool, isActive: Bool, isPressed: Bool) -> Color { switch self { - case .default: return isPressed ? .themeSteel10 : .themeSteel20 - case .transparent: return .clear + case .default: return isEnabled ? (isActive ? (isPressed ? .themeYellow50 : .themeYellow) : (isPressed ? .themeSteel10 : .themeSteel20)) : .themeSteel20 + case .transparent: return isEnabled && isActive ? (isPressed ? .themeYellow50 : .themeYellow) : .clear } } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 92c631324b..1d64fd6b25 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1141,8 +1141,7 @@ Go to Settings - > %@ and allow access to the camera."; "unlock.biometry_reason" = "Unlock wallet"; "unlock.attempts_left" = "Attempts left: %@"; "unlock.disabled_until" = "Disabled until: %@"; -"unlock.regular_mode" = "Regular Mode"; -"unlock.random_mode" = "Random Mode"; +"unlock.random" = "Random"; "security_settings.delete_alert_button" = "Delete from Phone"; From cf727b750e09d8f063b9a0da322b762da4aca8e9 Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 2 Oct 2023 12:08:35 +0600 Subject: [PATCH 29/63] Update bundle and minor fix for Matchfile --- Gemfile | 2 +- Gemfile.lock | 49 +++++++++---------- .../xcshareddata/IDEWorkspaceChecks.plist | 8 --- fastlane/Matchfile | 2 +- 4 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 UnstoppableWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Gemfile b/Gemfile index e64c7905f2..81ebdce9ef 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source 'https://rubygems.org' -ruby '~> 3.0' +ruby '3.0.0' group :fastlane do gem 'fastlane' diff --git a/Gemfile.lock b/Gemfile.lock index 1ea7cbb84a..c313a201ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,22 +3,22 @@ GEM specs: CFPropertyList (3.0.6) rexml - addressable (2.8.4) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.785.0) - aws-sdk-core (3.177.0) + aws-partitions (1.830.0) + aws-sdk-core (3.184.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.70.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.128.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-s3 (1.136.0) + aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) aws-sigv4 (1.6.0) @@ -36,7 +36,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.100.0) + excon (0.104.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.7) - fastlane (2.213.0) + fastlane (2.216.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -87,6 +87,7 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) @@ -98,19 +99,19 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-appcenter (2.1.0) + fastlane-plugin-appcenter (2.1.1) fastlane-plugin-xcconfig (2.0.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.45.0) + google-apis-androidpublisher_v3 (0.50.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.0) + google-apis-core (0.11.1) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -139,10 +140,9 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.6.0) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) @@ -153,9 +153,8 @@ GEM jmespath (1.6.2) json (2.6.3) jwt (2.7.1) - memoist (0.16.2) mini_magick (4.12.0) - mini_mime (1.1.2) + mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.3.0) nanaimo (0.3.0) @@ -163,19 +162,19 @@ GEM optparse (0.1.1) os (1.1.4) plist (3.7.0) - public_suffix (5.0.1) + public_suffix (5.0.3) rake (13.0.6) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.17.0) + signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -184,8 +183,8 @@ GEM CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) @@ -195,13 +194,13 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (1.8.0) + unicode-display_width (2.4.2) webrick (1.8.1) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.22.0) + xcodeproj (1.23.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -224,7 +223,7 @@ DEPENDENCIES xcodeproj RUBY VERSION - ruby 3.0.2p107 + ruby 3.0.0p0 BUNDLED WITH 2.2.22 diff --git a/UnstoppableWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/UnstoppableWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/UnstoppableWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 461c05b80b..770d047936 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -1,2 +1,2 @@ -git_url("https://github.com/horizontalsystems/certificates") +git_url("git@github.com:horizontalsystems/certificates.git") storage_mode("git") From a66729228f60657f8ba12b5704b9a572cdab054f Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 2 Oct 2023 12:21:36 +0600 Subject: [PATCH 30/63] Revert ruby version constraint in Bundle --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 81ebdce9ef..e64c7905f2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source 'https://rubygems.org' -ruby '3.0.0' +ruby '~> 3.0' group :fastlane do gem 'fastlane' diff --git a/Gemfile.lock b/Gemfile.lock index c313a201ea..d65eb0f064 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -194,7 +194,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.4.2) + unicode-display_width (2.5.0) webrick (1.8.1) word_wrap (1.0.0) xcode-install (2.8.1) From 1e17ac4efb97a9dc7a81c2785d4f5e930e473a03 Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 2 Oct 2023 13:01:43 +0600 Subject: [PATCH 31/63] Revert Matchfile --- fastlane/Matchfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 770d047936..461c05b80b 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -1,2 +1,2 @@ -git_url("git@github.com:horizontalsystems/certificates.git") +git_url("https://github.com/horizontalsystems/certificates") storage_mode("git") From 92405c4a67172ba2c946ff0b42d3ffd41cb0d6f0 Mon Sep 17 00:00:00 2001 From: _imadia Date: Mon, 2 Oct 2023 15:37:00 +0600 Subject: [PATCH 32/63] Revert RU translations, change text for duress mode --- .../en.lproj/Localizable.strings | 12 +- .../ru.lproj/Localizable.strings | 2698 +++++++++-------- 2 files changed, 1357 insertions(+), 1353 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 1d64fd6b25..9e90d6bf72 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1084,7 +1084,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.disable_passcode" = "Disable Passcode"; "settings_security.balance_auto_hide" = "Balance Auto Hide"; "settings_security.balance_auto_hide.description" = "Automatically hides balance each time the app is opened, regardless of previous preferences."; -"settings_security.enable_duress_mode" = "Enable Duress Mode"; +"settings_security.enable_duress_mode" = "Set Duress Mode"; "settings_security.edit_duress_passcode" = "Edit Duress Passcode"; "settings_security.disable_duress_mode" = "Disable Duress Mode"; "settings_security.duress_mode.description" = "A specialized mode designed to keep selected wallets safe under coercion."; @@ -1095,7 +1095,7 @@ Go to Settings - > %@ and allow access to the camera."; "create_passcode.description" = "Your passcode will be used to unlock your wallet"; "create_passcode.description.biometry" = "Set a passcode to enable %@"; "create_passcode.description.duress_mode" = "Set a passcode to enable Duress Mode"; -"create_passcode.confirm_passcode" = "Confirm passcode"; +"create_passcode.confirm_passcode" = "Confirm"; // Enable Duress Mode @@ -1115,19 +1115,19 @@ Go to Settings - > %@ and allow access to the camera."; "enable_duress_mode.passcode.title" = "Duress Passcode"; "enable_duress_mode.passcode.description" = "Set a passcode for Duress Mode"; -"enable_duress_mode.passcode.confirm" = "Confirm passcode"; +"enable_duress_mode.passcode.confirm" = "Confirm"; // Edit Passcode "edit_passcode.title" = "Edit Passcode"; "edit_passcode.enter_new_passcode" = "Enter new passcode"; -"edit_passcode.confirm_new_passcode" = "Confirm new passcode"; +"edit_passcode.confirm_new_passcode" = "Confirm"; // Edit Duress Passcode "edit_duress_passcode.title" = "Edit Duress Passcode"; "edit_duress_passcode.enter_new_passcode" = "Enter new passcode for Duress Mode"; -"edit_duress_passcode.confirm_new_passcode" = "Confirm new passcode"; +"edit_duress_passcode.confirm_new_passcode" = "Confirm"; // Set Passcode @@ -1137,7 +1137,7 @@ Go to Settings - > %@ and allow access to the camera."; // Unlock "unlock.title" = "Unlock"; -"unlock.passcode" = "Passcode"; +"unlock.passcode" = "Enter Passcode"; "unlock.biometry_reason" = "Unlock wallet"; "unlock.attempts_left" = "Attempts left: %@"; "unlock.disabled_until" = "Disabled until: %@"; diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index 7ce43f5aac..d5ece57cf3 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -1,1074 +1,1071 @@ -"button.ok" = "OK"; -"button.apply" = "Apply"; -"button.cancel" = "Cancel"; -"button.close" = "Close"; -"button.continue" = "Continue"; -"button.done" = "Done"; -"button.reset" = "Reset"; -"button.import" = "Import"; -"button.next" = "Next"; -"button.delete" = "Delete"; -"button.share" = "Share"; -"button.paste" = "Paste"; -"button.resend" = "Resend"; -"button.backup" = "Backup"; -"button.copy" = "Copy"; -"button.retry" = "Retry"; -"button.report" = "Report"; -"button.add" = "Add"; -"button.approve" = "Approve"; -"button.revoke" = "Revoke"; -"button.reject" = "Reject"; -"button.connect" = "Connect"; -"button.save" = "Save"; -"button.send" = "Send"; -"button.sign" = "Sign"; -"button.more" = "Show More"; -"button.i_understand" = "I Understand"; -"button.learn_more" = "Learn More"; - -"alert" = "Alert"; -"alert.copied" = "Copied"; -"alert.saved" = "Saved"; -"alert.saved_to_icloud" = "Saved to iCloud"; -"alert.no_internet" = "No Internet"; -"alert.wrong_amount" = "Wrong Amount"; -"alert.no_fee" = "Wrong Fee"; -"alert.warning" = "Warning"; -"alert.error" = "Error"; -"alert.unknown_error" = "Unknown Error"; -"alert.success_action" = "Done"; -"alert.success" = "Success"; - -"alert.added_to_watchlist" = "Added to Watchlist"; -"alert.removed_from_watchlist" = "Removed from Watchlist"; -"alert.added_to_wallet" = "Added to Wallet"; -"alert.removed_from_wallet" = "Removed from Wallet"; -"alert.already_added_to_wallet" = "Already added to Wallet"; -"alert.not_supported_yet" = "Not Supported Yet"; -"alert.created" = "Created"; -"alert.imported" = "Imported"; -"alert.wallet_added" = "Wallet Added"; -"alert.deleted" = "Deleted"; -"alert.waiting_for_session" = "Waiting for Session"; -"alert.try_again" = "Try Again"; -"alert.disconnecting" = "Disconnecting"; -"alert.disconnected" = "Disconnected"; -"alert.enabling" = "Enabling"; -"alert.enabled_coins" = "Enabled %@ more coins"; -"alert.sending" = "Sending"; -"alert.sent" = "Sent"; -"alert.swapping" = "Swapping"; -"alert.swapped" = "Swapped"; -"alert.approving" = "Approving"; -"alert.approved" = "Approved"; -"alert.revoking" = "Revoking"; -"alert.revoked" = "Revoked"; -"alert.cant_recognize" = "Can't Recognize"; - -"no_results_found" = "No results found"; -"action.loading" = "loading..."; -"placeholder.search" = "Search"; - -"status" = "Status"; -"connecting" = "Connecting..."; -"online" = "Online"; -"offline" = "Offline"; -"confirm" = "Confirm"; -"connect" = "Connect"; -"price" = "Price"; -"value" = "Value"; -"note" = "Note"; - -"version" = "Version %@"; - -"n/a" = "N/A"; - -"sync_error" = "Sync error. Try again."; - -"number.thousand" = "%@K"; -"number.million" = "%@M"; -"number.billion" = "%@B"; -"number.trillion" = "%@T"; -"number.quadrillion" = "%@Q"; - -"selector.any" = "Any"; +"button.ok" = "ОК"; +"button.apply" = "Применить"; +"button.cancel" = "Отменить"; +"button.close" = "Закрыть"; +"button.continue" = "Продолжить"; +"button.done" = "Готово"; +"button.reset" = "Сбросить"; +"button.import" = "Импорт"; +"button.next" = "Далее"; +"button.delete" = "Удалить"; +"button.share" = "Поделиться"; +"button.paste" = "Вставить"; +"button.resend" = "Отправить повторно"; +"button.backup" = "Резервная копия"; +"button.copy" = "Копировать"; +"button.retry" = "Повторить"; +"button.report" = "Пожаловаться"; +"button.add" = "Добавить"; +"button.approve" = "Разрешить"; +"button.revoke" = "Отменить"; +"button.reject" = "Отклонить"; +"button.connect" = "Подключиться"; +"button.save" = "Сохранить"; +"button.send" = "Отправить"; +"button.sign" = "Подписать"; +"button.more" = "Показать больше"; +"button.i_understand" = "Я понимаю"; +"button.learn_more" = "Подробнее"; + +"alert" = "Предупреждение"; +"alert.copied" = "Скопировано"; +"alert.saved" = "Сохранено"; +"alert.saved_to_icloud" = "Сохранено в iCloud"; +"alert.no_internet" = "Нет интернета"; +"alert.wrong_amount" = "Неправильная сумма"; +"alert.no_fee" = "Неправильная комиссия"; +"alert.warning" = "Внимание"; +"alert.error" = "Ошибка"; +"alert.unknown_error" = "Неизвестная ошибка"; +"alert.success_action" = "Готово"; +"alert.success" = "Успешно"; + +"alert.added_to_watchlist" = "Добавлено в избранное"; +"alert.removed_from_watchlist" = "Удалить из избранного"; +"alert.added_to_wallet" = "Добавлено в кошелек"; +"alert.removed_from_wallet" = "Удалено из кошелька"; +"alert.already_added_to_wallet" = "Уже добавлено в кошелек"; +"alert.not_supported_yet" = "Ещё не поддерживается"; +"alert.copied" = "Скопировано"; +"alert.created" = "Создан"; +"alert.imported" = "Импортировано"; +"alert.wallet_added" = "Кошелек добавлен"; +"alert.deleted" = "Удалено"; +"alert.waiting_for_session" = "Ожидание cессии"; +"alert.try_again" = "Попробовать ещё раз"; +"alert.disconnecting" = "Отсоединение"; +"alert.disconnected" = "Отсоединено"; +"alert.enabling" = "Идет активация"; +"alert.enabled_coins" = "Включено ещё %@ монет"; +"alert.sending" = "Отправка"; +"alert.sent" = "Отправлено"; +"alert.swapping" = "Обмен"; +"alert.swapped" = "Обмен выполнен"; +"alert.approving" = "Подтверждение"; +"alert.approved" = "Разрешено"; +"alert.revoking" = "Отмена"; +"alert.revoked" = "Отменен"; +"alert.cant_recognize" = "Не удалось распознать"; + +"no_results_found" = "Ничего не найдено"; +"action.loading" = "загрузка..."; +"placeholder.search" = "Поиск"; + +"status" = "Статус"; +"connecting" = "Подключение..."; +"online" = "Доступен"; +"offline" = "Не доступен"; +"confirm" = "Подтвердить"; +"connect" = "Подключиться"; +"price" = "Цена"; +"value" = "Значение"; +"note" = "Примечание"; + +"version" = "Версия %@"; + +"n/a" = "Нет данных"; + +"sync_error" = "Ошибка синхронизации. Повторите попытку."; + +"number.thousand" = "%@ тыс."; +"number.million" = "%@ млн"; +"number.billion" = "%@ млрд"; +"number.trillion" = "%@ трл"; +"number.quadrillion" = "%@ квадрлн"; + +"selector.any" = "Любое"; // Access Camera -"access_camera.message" = "Listen folks, %@ needs to see through your camera, okay. -To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; +"access_camera.message" = "%@ требуется доступ к вашей камере для сканирования QR-кода. -"access_camera.settings" = "Settings"; +Перейдите в \"Настройки\" - > %@ и разрешите доступ к камере."; +"access_camera.settings" = "Настройки"; // Restore -"restore.title" = "Import Wallet"; -"restore.advanced" = "Advanced"; -"restore.import_by" = "Import By"; -"restore.restore_type.mnemonic" = "Recovery Phrase"; -"restore.restore_type.private_key" = "Private Key."; -"restore.mnemonic.placeholder" = "Give me those magic words! Recovery Phrase, come on!"; -"restore.private_key.placeholder" = "Enter EVM Private Key, BIP32 Root Key or Account Extended Private Key"; -"restore.private_key.invalid_key" = "Invalid Key"; -"restore_error.mnemonic_word_count" = "Listen, we need 12 to 24 words. Not more, not less. Perfect balance. You gave me: %@"; -"restore.checksum_error" = "Invalid checksum"; -"restore.passphrase" = "Passphrase"; -"restore.input.passphrase" = "Passphrase"; -"restore.passphrase_description" = "You know, we have this tremendous password. It's for your wallet backup, not your Apple thing. It's so good, if you lose it, even I can't get it back for you!"; -"restore.error.empty_passphrase" = "Come on folks, you've got to give me something here! Can't have an empty passphrase."; -"restore.non_standard_import" = "Something a little different, a bit out of the box, folks."; -"restore.non_standard_import.description" = "This page, it's special, believe me. For all you with those non-standard wallets. Maybe you've used some fancy foreign words or characters. You know which version I'm talking about, the %@ one. Very specific."; -"restore.warning.non_recommended.description" = "Looks like this wallet's been to some exotic places with its characters! If your balance looks funny, it's not fake news. Dive in below for the real story."; -"restore.error.non_standard.description" = "This wallet? It's not your everyday kind of thing. Tap below, and I'll spill the beans."; +"restore.title" = "Импорт кошелька"; +"restore.advanced" = "Доп. настройки"; +"restore.import_by" = "Через"; +"restore.restore_type.mnemonic" = "Фраза восстановления"; +"restore.restore_type.private_key" = "Приватный ключ"; +"restore.mnemonic.placeholder" = "Введите фразу восстановления"; +"restore.private_key.placeholder" = "Введите приватный ключ EVM, BIP32 Root Key или Account Extended Private Key"; +"restore.private_key.invalid_key" = "Неверный ключ"; +"restore_error.mnemonic_word_count" = "Неправильное количество слов. Должно быть 12-24 слова. Вы ввели: %@"; +"restore.checksum_error" = "Неверная контрольная сумма"; +"restore.passphrase" = "Кодовая фраза"; +"restore.input.passphrase" = "Кодовая фраза"; +"restore.passphrase_description" = "Этот пароль используется для шифрования файлов резервной копии вашего кошелька. Он не связан с вашим паролем Apple iCloud. Если вы потеряете или забудете этот пароль, его нельзя будет восстановить или сбросить."; +"restore.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; +"restore.non_standard_import" = "Нестандартный импорт"; +"restore.non_standard_import.description" = "На этой странице представлен специальный механизм восстановления кошелька для пользователей %@ с нестандартным кошельком. Как правило, такие кошельки могли быть созданы в версиях %@ 0.27–0.28 с использованием неанглоязычного списка мнемонических слов и/или специального символа в парольной фразе кошелька (например, диакритического знака). +\n\nЕсли вы являетесь затронутым пользователем, то баланс вашего кошелька будет отображаться как 0 после восстановления такого кошелька в версии 0.29 или выше.Эта страница позволит вам восстановить доступ к вашему нестандартному кошельку. После восстановления рекомендуется создать новый кошелек (который будет соответствовать стандарту) и перевести туда средства."; +"restore.warning.non_recommended.description" = "Похоже, этот кошелек использует нестандартный символ в списке мнемонических слов и/или парольной фразе. Если вы не видите баланс или транзакции, пожалуйста, прочтите подробности ниже. +\n\nПОЖАЛУЙСТА НАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ."; +"restore.error.non_standard.description" = "Это нестандартный кошелек.\n\nНАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; // Restore Type -"restore_type.title" = "Import Wallet"; +"restore_type.title" = "Импорт кошелька"; -"restore_type.recovery.title" = "from Recovery Phrase"; -"restore_type.cloud.title" = "from iCloud"; -"restore_type.cex.title" = "from Exchange Wallet"; +"restore_type.recovery.title" = "Фраза восстановления"; +"restore_type.cloud.title" = "iCloud"; +"restore_type.cex.title" = "с кошелька биржи"; -"restore_type.recovery.description" = "Importing with the mightiest recovery phrase or a super private key!"; -"restore_type.cloud.description" = "Importing from the backup – straight outta your keychain!"; -"restore_type.cex.description" = "Connecting to the wallet on the grand centralized exchange!"; +"restore_type.recovery.description" = "Импорт с помощью фразы восстановления или приватного ключа."; +"restore_type.cloud.description" = "Импорт из файла резервной копии в вашем keychain."; +"restore_type.cex.description" = "Подключиться к кошельку на централизованной бирже."; // Restore Cloud -"restore.cloud.title" = "Select Backup"; -"restore.cloud.description" = "Pick your wallet's backup, the one you really wanna bring back!"; -"restore.cloud.empty" = "No backups found"; -"restore.cloud.imported" = "Imported wallets"; +"restore.cloud.title" = "Резерв. копия"; +"restore.cloud.description" = "Выберите резервную копию кошелька, который вы хотите восстановить."; +"restore.cloud.empty" = "Резервные копии не найдены."; +"restore.cloud.imported" = "Импортированные кошельки"; -"restore.cloud.password.title" = "Enter Password"; -"restore.cloud.password.placeholder" = "Backup Password"; -"restore.cloud.password.description" = "Key in your majestic backup password to get that wallet out of iCloud!"; +"restore.cloud.password.title" = "Введите пароль"; +"restore.cloud.password.placeholder" = "Пароль резервной копии"; +"restore.cloud.password.description" = "Введите пароль резервной копии для импорта вашего кошелька из iCloud."; // Restore Cex -"restore.cex.title" = "Select CEX"; -"restore.cex.description" = "Choose your centralized exchange, the greatest of them all, believe me!"; +"restore.cex.title" = "Выберите CEX"; +"restore.cex.description" = "Выберите централизованный обмен, к которому вы хотите подключиться."; // Restore Binance -"restore.binance.description" = "Hand over those API Keys and Secret! Only the best for our exchange!"; -"restore.binance.api_key" = "API Key"; -"restore.binance.secret_key" = "Secret Key"; -"restore.binance.connect" = "Connect"; -"restore.binance.connecting" = "Connecting..."; -"restore.binance.get_api_keys" = "Get API Keys"; -"restore.binance.failed_to_connect" = "Failed to connect your API key"; -"restore.binance.invalid_qr_code" = "Invalid QR Code"; +"restore.binance.description" = "Пожалуйста, предоставьте ключ API и секрет API, чтобы связать вашу биржу."; +"restore.binance.api_key" = "API ключ"; +"restore.binance.secret_key" = "Секретный ключ"; +"restore.binance.connect" = "Подключиться"; +"restore.binance.connecting" = "Подключение..."; +"restore.binance.get_api_keys" = "Получить API ключ"; +"restore.binance.failed_to_connect" = "Не удалось подключиться к твоему API ключу"; +"restore.binance.invalid_qr_code" = "Недопустимый QR-код"; // Coin Settings -"coin_settings.title" = "Blockchain Settings"; +"coin_settings.title" = "Настройки блокчейна"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; -"sync_mode.from_blockchain" = "From Blockchain"; -"blockchain_settings.description" = "Choose your address format. Let's make your payments great, just like when you pick the right tie! Always be spot on."; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (рекомендованный)"; +"sync_mode.from_blockchain" = "Из блокчейна"; +"blockchain_settings.description" = "Выберите формат адреса для получения средств. При восстановлении существующего кошелька должен быть выбран правильный формат."; // Coin Platforms -"coin_platforms.native" = "Native"; +"coin_platforms.native" = "Нативный"; // Copy Warning -"copy_warning.title" = "Risk of Copying"; -"copy_warning.description" = "For safety, folks, I'd say don't copy this! I have the best safety tips."; -"copy_warning.dont_copy" = "Don't Copy"; -"copy_warning.i_will_risk_it" = "I'm feeling brave today!"; +"copy_warning.title" = "Риск копирования"; +"copy_warning.description" = "В качестве меры безопасности мы рекомендуем не использовать действие копирования."; +"copy_warning.dont_copy" = "Не копировать"; +"copy_warning.i_will_risk_it" = "Я рискую"; // Recovery Phrase -"recovery_phrase.title" = "Recovery Phrase"; -"recovery_phrase.warning" = "Never ever share! Remember, the %@ team won't ask. It's like your personal hair secret."; -"recovery_phrase.tap_to_show" = "Give it a tap! Unveil the magic words."; -"recovery_phrase.passphrase" = "Passphrase"; -"recovery_phrase.copy_warning.title" = "Risk of Recovery Phrase copy"; -"recovery_phrase.copy_warning.description" = "Stay safe! I wouldn't copy that if I were you. Best recommendation."; +"recovery_phrase.title" = "Фраза восстановления"; +"recovery_phrase.warning" = "Никогда ни с кем не делитесь вашей фразой восстановления. Команда %@ никогда не запросит вашу фразу восстановления."; +"recovery_phrase.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; +"recovery_phrase.passphrase" = "Кодовая фраза"; +"recovery_phrase.copy_warning.title" = "Риск копирования фразы восстановления"; +"recovery_phrase.copy_warning.description" = "В целях безопасности мы не рекомендуем использовать действие копирования фразы восстановления."; // EVM Private Key -"evm_private_key.title" = "EVM Private Key"; -"evm_private_key.tap_to_show" = "Tap to show private key"; +"evm_private_key.title" = "Приватный Ключ EVM"; +"evm_private_key.tap_to_show" = "Нажмите, чтобы показать приватный ключ"; // Extended Key - "extended_key.bip32_root_key" = "BIP32 Root Key"; - "extended_key.account_extended_private_key" = "Account Extended Private Key"; - "extended_key.account_extended_public_key" = "Account Extended Public Key"; - "extended_key.purpose" = "Purpose"; - "extended_key.blockchain" = "Blockchain"; - "extended_key.account" = "Account"; - "extended_key.tap_to_show" = "Tap to show extended private key"; +"extended_key.bip32_root_key" = "BIP32 Root Key"; +"extended_key.account_extended_private_key" = "Account Extended Private Key"; +"extended_key.account_extended_public_key" = "Account Extended Public Key"; +"extended_key.purpose" = "Purpose"; +"extended_key.blockchain" = "Blockchain"; +"extended_key.account" = "Account"; +"extended_key.tap_to_show" = "Нажмите, чтобы показать extended private key"; // Backup -"backup.title" = "Recovery Phrase"; -"backup.description" = "Jot these words down, in the right order, just like my best speeches. Keep them safe, maybe next to your tax returns!"; -"backup.tap_to_show" = "Give it a tap to reveal the best recovery phrase!"; -"backup.passphrase" = "Passphrase"; -"backup.verify" = "Verify"; -"backup.verified" = "Verified"; +"backup.title" = "Фраза восстановления"; +"backup.description" = "Напишите эти слова в правильном порядке и храните их в безопасном месте"; +"backup.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; +"backup.passphrase" = "Кодовая фраза"; +"backup.verify" = "Подтвердить"; +"backup.verified" = "Подтверждено"; // Backup Verify Words -"backup_verify_words.title" = "Verify"; -"backup_verify_words.description" = "Pick the two words I'm asking for, from your top-notch recovery phrase"; -"backup_verify_words.incorrect_word" = "Incorrect Word"; +"backup_verify_words.title" = "Подтвердить"; +"backup_verify_words.description" = "Выберите два запрошенные слова из вашей фразы восстановления"; +"backup_verify_words.incorrect_word" = "Неверное слово"; // Backup Verify Passphrase -"backup_verify_passphrase.title" = "Verify"; -"backup_verify_passphrase.description" = "Type in the passphrase. Make sure it's terrific!"; -"backup_verify_passphrase.incorrect_passphrase" = "Incorrect passphrase"; +"backup_verify_passphrase.title" = "Подтвердить"; +"backup_verify_passphrase.description" = "Введите кодовую фразу"; +"backup_verify_passphrase.incorrect_passphrase" = "Неверная кодовая фраза"; // Backup Required -"backup_required.title" = "Backup Required"; - -// **Backup Prompt** - -"backup_prompt.title" = "Manual Backup"; -"backup_prompt.warning" = "You want to make a yuuuge backup of this recovery phrase, trust me. Don't lose out if your phone goes missing, or breaks!"; -"backup_prompt.backup" = "Backup"; -"backup_prompt.backup_manual" = "Manual Backup"; -"backup_prompt.backup_cloud" = "Backup to iCloud!"; -"backup_prompt.later" = "Later"; - -// **Backup to iCloud** - -"backup.cloud.title" = "Backup to iCloud"; -"backup.cloud.description" = "So, iCloud's an Apple thing. They'll keep your stuff, not on your gadgets. You're trusting them, not us. Just so we're clear!"; -"backup.cloud.terms.item.1" = "Listen up! Lose access to iCloud, and you say goodbye to this backup."; - -"backup.cloud.name.title" = "Backup Name"; -"backup.cloud.name.description" = "Name your backup, maybe after one of my hotels?"; -"backup.cloud.name.empty" = "Give it a name, c'mon!"; -"backup.cloud.name.error.empty" = "A name's what we need!"; -"backup.cloud.name.error.already_exist" = "Been there, named that. Pick another!"; -"backup.cloud.name.placeholder" = "Name"; - -"backup.cloud.password.title" = "Set Password"; -"backup.cloud.password.description" = "Craft a password that's tremendous! Needs 8 characters and a mix of the alphabet, numbers, and those little symbols."; -"backup.cloud.password.highlighted_description" = "Remember this one! It's not your Apple password, and it's irreplaceable!"; -"backup.cloud.password.placeholder" = "Password"; -"backup.cloud.password.confirm.placeholder" = "Confirm"; -"backup.cloud.password.save" = "Save and Backup"; - -"backup.cloud.password.error.empty_passphrase" = "Hey, we need that passphrase!"; -"backup.cloud.password.error.forbidden_symbols" = "Stick to the basics, folks: A-Z, a-z, 0-9, and those symbols!"; -"backup.cloud.password.error.minimum_requirement" = "Needs 8 characters, at least! Upper, lower, number, and symbol. The total package!"; -"backup.cloud.password.error.invalid_password" = "Not the right password, but keep trying!"; -"backup.cloud.password.error.invalid_backup" = "Whoops! Something's fishy with the backup."; -"backup.cloud.password.confirm.error.doesnt_match" = "Those passwords aren't buddies yet. Try again!"; -"backup.cloud.not_available" = "iCloud's taking a break!"; -"backup.cloud.cant_create_file" = "Can't ship that file to iCloud!"; -"backup.cloud.cant_delete_file" = "Can't kick it out of iCloud!"; -"backup.cloud.no_access.title" = "Access iCloud"; -"backup.cloud.no_access.description" = "Need your green light for iCloud storage!"; +"backup_required.title" = "Требуется резервная копия"; + +// Backup Prompt + +"backup_prompt.title" = "Ручное резервное копирование"; +"backup_prompt.warning" = "Создайте резервную копию вашей фразы восстановления и пароля, которые позволят вам восстановить ваш кошелек, если телефон потерян, украден, сломал и т.д."; +"backup_prompt.backup" = "Резервная копия"; +"backup_prompt.backup_manual" = "Ручное резервное копирование"; +"backup_prompt.backup_cloud" = "Резерв. копирование в iCloud"; +"backup_prompt.later" = "Позже"; + +// Backup to iCloud + +"backup.cloud.title" = "Резерв. копирование в iCloud"; +"backup.cloud.description" = "Хранилище iCloud является облачным хранилищем, предоставляемым сторонней компанией и доступным через Apple. Важно знать, что ваши данные будут храниться на серверах Apple, а не на ваших личных устройствах. Это означает, что вы доверяете свои данные и передаете безопасность своей информации стороннему сервису."; + +"backup.cloud.terms.item.1" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; + +"backup.cloud.name.title" = "Имя резервной копии"; +"backup.cloud.name.description" = "Введите имя файла резервной копии."; +"backup.cloud.name.empty" = "Имя резервной копии не может быть пустым!"; +"backup.cloud.name.error.empty" = "Имя резервной копии не должно быть пустым"; +"backup.cloud.name.error.already_exist" = "Имя резервной копии уже существует!"; +"backup.cloud.name.placeholder" = "Название"; + +"backup.cloud.password.title" = "Установить пароль"; +"backup.cloud.password.description" = "Установите пароль разблокировки для своей резервной копии. Пароль должен содержать не менее 8 символов и включать как минимум одну строчную букву, заглавную букву, цифру и специальный символ."; +"backup.cloud.password.highlighted_description" = "Не забудьте этот пароль! Он отличается от вашего пароля для Apple iCloud и не может быть восстановлен или сброшен."; +"backup.cloud.password.placeholder" = "Пароль"; +"backup.cloud.password.confirm.placeholder" = "Подтвердить"; +"backup.cloud.password.save" = "Сохранить и создать резервную копию"; + +"backup.cloud.password.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; +"backup.cloud.password.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"backup.cloud.password.error.minimum_requirement" = "Не менее 8 символов, включая одну заглавную букву, одну строчную букву, одну цифру и один символ"; +"backup.cloud.password.error.invalid_password" = "Неверный пароль"; +"backup.cloud.password.error.invalid_backup" = "Резервная копия повреждена"; +"backup.cloud.password.confirm.error.doesnt_match" = "Пароли не совпадают"; +"backup.cloud.not_available" = "iCloud недоступен"; +"backup.cloud.cant_create_file" = "Не удается сохранить файл в iCloud"; +"backup.cloud.cant_delete_file" = "Не удается удалить из iCloud"; +"backup.cloud.no_access.title" = "Доступ к iCloud"; +"backup.cloud.no_access.title" = "Доступ к iCloud"; +"backup.cloud.no_access.description" = "Для создания резервной копии необходимо предоставить доступ к iCloud памяти."; + // Errors -"error.send.self_transfer" = "C'mon now, sending to yourself? We don't do that here!"; -"error.send_binance.memo_required" = "The receiver's like 'I need a memo!' Don't leave it empty!"; -"error.send_binance.only_digits_allowed" = "Memo's gotta have digits only! Numbers, always winning!"; -"error.send_z_cash.transparent_address" = "%@ says 'No way!' to payments to a transparent address."; +"error.send.self_transfer" = "Отправка самому себе невозможна"; +"error.send_binance.memo_required" = "При отправке транзакции получателю необходимо заполнить поле мемо"; +"error.send_binance.only_digits_allowed" = "Поле мемо должно содержать только цифры"; +"error.send_z_cash.transparent_address" = "%@ не поддерживает платежи на публичный адрес"; // Balance -"balance.title" = "Balance"; -"balance.tab_bar_item" = "Balance"; -"balance.send" = "Send"; -"balance.withdraw" = "Withdraw"; -"balance.swap" = "Swap"; -"balance.receive" = "Receive"; -"balance.deposit" = "Deposit"; -"balance.address" = "Address"; -"balance.rate_per_coin" = "%@ per %@"; -"balance.syncing" = "Syncing..."; -"balance.searching" = "Searching transactions..."; -"balance.downloading_sapling" = "Downloading Sapling... %d%%"; -"balance.downloading_blocks" = "Downloading Blocks"; -"balance.scanning_blocks" = "Scanning Blocks"; -"balance.enhancing_transactions" = "Enhancing Transactions"; +"balance.title" = "Баланс"; +"balance.tab_bar_item" = "Баланс"; +"balance.send" = "Отправить"; +"balance.withdraw" = "Вывод средств"; +"balance.swap" = "Обменять"; +"balance.receive" = "Получить"; +"balance.deposit" = "Получить"; +"balance.address" = "Адрес"; +"balance.rate_per_coin" = "%@ за %@"; +"balance.syncing" = "Синхронизация..."; +"balance.searching" = "Поиск транзакций..."; +"balance.downloading_sapling" = "Загрузка sapling... %d%%"; +"balance.downloading_blocks" = "Загрузка блоков"; +"balance.scanning_blocks" = "Сканирование блоков"; +"balance.enhancing_transactions" = "Улучшение транзакций"; "balance.searching.count" = "%@ tx"; -"balance.syncing_percent" = "Syncing... %@"; -"balance.synced_through" = "until %@"; -"balance.add_coin" = "Add Coin"; -"balance.invalid_api_key" = "Invalid API Key"; -"balance.empty.add_coins" = "Add Coins!"; -"balance.empty.description" = "No coins in this wallet yet. Sad!"; -"balance.watch_empty.description" = "This wallet address is feeling a bit light!"; -"balance.sort_by" = "Sort By"; -"balance.sort.header" = "Sort by"; -"balance.sort.valueHighToLow" = "Balance"; -"balance.sort.az" = "Name"; -"balance.sort.price_change" = "Price Change"; - -"balance_error.change_source" = "Change Source"; -"balance_error.sync_error" = "Sync Error"; -"lost_accounts.warning_title" = "iOS Keychain Error!"; -"lost_accounts.warning_message" = "The encrypted treasure holding your wallet got messed up 'cause you changed your iOS lock screen."; +"balance.syncing_percent" = "Синхронизация... %@"; +"balance.synced_through" = "до %@"; +"balance.add_coin" = "Добавить токен"; +"balance.invalid_api_key" = "Недействительный ключ API"; +"balance.empty.add_coins" = "Добавить токены"; +"balance.empty.description" = "Вы еще не добавили токены в этот кошелек."; +"balance.watch_empty.description" = "У кошелька с этим адресом нет баланса"; +"balance.sort_by" = "Сортировать"; +"balance.sort.header" = "Сортировать"; +"balance.sort.valueHighToLow" = "Баланс"; +"balance.sort.az" = "Название"; +"balance.sort.price_change" = "По изменению цены (%)"; + +"balance_error.change_source" = "Изменить источник"; +"balance_error.sync_error" = "Ошибка синхронизации"; +"lost_accounts.warning_title" = "Ошибка связки ключей iOS"; +"lost_accounts.warning_message" = "Зашифрованные данные вашего кошелька были недавно аннулированы, поскольку ваш экран блокировки iOS был изменен"; // Token Balance Page -"balance.token.locked" = "On Lockdown"; +"balance.token.locked" = "Заблокировано"; "balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "The sender's put a timer on this. The Bitcoins are yours, but you gotta wait a tad to spend them on the Bitcoin network."; +"balance.token.locked.info.description" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; -"balance.token.staked.info.description" = "Let's talk staking!"; +"balance.token.staked.info.description" = "Staked Description Text"; "balance.token.frozen" = "Frozen"; -"balance.token.frozen.info.title" = "Cool Off"; -"balance.token.frozen.info.description" = "Let's chat about why it's so cool."; +"balance.token.frozen.info.title" = "Frozen title"; +"balance.token.frozen.info.description" = "Frozen Description Text"; -// **Account switcher** +// Account switcher -"switch_account.title" = "Switch Wallets"; -"switch_account.wallets" = "Wallets"; -"switch_account.watch_wallets" = "Watch Wallets"; +"switch_account.title" = "Переключить кошелек"; +"switch_account.wallets" = "Кошельки"; +"switch_account.watch_wallets" = "Просмотр кошелька"; -// **Release notes** +// Release notes -"release_notes.title" = "What's New"; -"release_notes.follow_us" = "Follow Us"; +"release_notes.title" = "Что нового"; +"release_notes.follow_us" = "Мы в соцсетях"; // Deposit -"deposit.receive_coin" = "Receive %@!"; -"deposit.address" = "Address!"; -"deposit.your_address" = "Your Address!"; -"receive_alert.not_backed_up_description" = "Hold on! You've got to back up %@ before we're winning with %@."; -"receive_alert.any_coins.not_backed_up_description" = "You've gotta backup %@ before making any yuge coin deals!"; -"deposit.no_adapter.error" = "Can't provide address"; - -"deposit.address_format" = "Format"; -"deposit.address_network" = "Network"; -"deposit.qr_code_description" = "Your VIP ticket to deposit %@"; -"deposit.qr_code_description.watch" = "Keep an eye on this %@ address!"; +"deposit.receive_coin" = "Получить %@"; +"deposit.address" = "Адрес"; +"deposit.your_address" = "Ваш адрес"; +"receive_alert.not_backed_up_description" = "Вам нужно сделать резервную копию %@ перед получением %@."; +"receive_alert.any_coins.not_backed_up_description" = "Вам нужно сделать резервную копию %@ перед тем, как вы сможете получить любые монеты."; +"deposit.no_adapter.error" = "Не удается предоставить адрес"; + +"deposit.address_format" = "Формат"; +"deposit.address_network" = "Сеть"; +"deposit.qr_code_description" = "Ваш адрес для депозита %@"; +"deposit.qr_code_description.watch" = "Отслеживаемый адрес %@"; "deposit.account" = "Account"; -"deposit.not_active" = "Not active"; -"deposit.not_active.title" = "Not Active Address"; -"deposit.not_active.tron_description" = "Just so you know, new accounts on TRON aren't awake yet. To get them going, send over some TRX or TRC-10 tokens. It'll cost you just 1 TRX."; +"deposit.not_active" = "не активен"; +"deposit.not_active.title" = "Неактивный адрес"; +"deposit.not_active.tron_description" = "Недавно созданные учетные записи в блокчейне TRON неактивны и не могут быть запрошены или изучены. Они должны быть активированы.\n\nАктивация новой учетной записи в цепочке Tron требует комиссию в размере 1 TRX. Для активации достаточно просто перевести токены TRX или TRC-10 на неактивный адрес аккаунта"; -"deposit.zcash.restore.description" = "Ever had a little ZEC action before?"; -"deposit.zcash.restore.already_own" = "Of course, I've got some!"; -"deposit.zcash.restore.dont_have" = "Nope, fresh start!"; +"deposit.zcash.restore.description" = "Вы уже владели какими-либо монетами ZEC?"; +"deposit.zcash.restore.already_own" = "Да, у меня уже есть"; +"deposit.zcash.restore.dont_have" = "Нет, у меня нет"; -"deposit.warning" = "Only send %@ here, or you'll be losing bigly!"; +"deposit.warning" = "Отправка только %@ на этот адрес. Отправка других типов токенов на этот адрес приведет к их окончательной потере."; -"receive_network_select.title" = "Network"; -"receive_network_select.description" = "Choose the best network and grab an address!"; +"receive_network_select.title" = "Сеть"; +"receive_network_select.description" = "Выберите сеть и получите адрес для получения."; -"receive_address_format_select.title" = "Address Format"; -"receive_address_format_select.description" = "Pick a network, get the finest address!"; -"receive_address_format_select.bitcoin.bottom_description" = "For the best deals in Bitcoin, Native SegWit is the way. Any address will do – Taproot, SegWit, Legacy. It's all great!"; -"receive_address_format_select.bitcoin_cash.bottom_description" = "Want the smoothest experience for Bitcoin Cash? Go with the Cash Address."; +"receive_address_format_select.title" = "Формат адреса"; +"receive_address_format_select.description" = "Выберите формат адреса для получения вашего адреса."; +"receive_address_format_select.bitcoin.bottom_description" = "Формат Native SegWit предпочтителен в Биткойне для повышения пропускной способности и безопасности. Все форматы адресов (Taproot, SegWit, Legacy) могут использоваться взаимозаменяемо для получения BTC независимо от формата адреса отправителя, что обеспечивает бесперебойные транзакции между различными типами монет."; +"receive_address_format_select.bitcoin_cash.bottom_description" = "Формат Cash Address предпочтителен для получения Bitcoin Cash (BCH) из-за улучшенного пользовательского опыта и совместимости. Однако, оба формата адреса могут использоваться взаимозаменяемо для получения BCH, независимо от формата адреса отправителя."; -"blockchain_type.recommended" = " (recommended)"; +"blockchain_type.recommended" = " (рекомендовано)"; // Send -"send.title" = "Send %@!"; -"send.send" = "Send"; -"send.no_assets" = "Looks like your vault's empty, sad!"; -"send.amount_placeholder" = "Amount"; -"send.address_placeholder" = "Address"; -"send.address_or_domain_placeholder" = "Address or Domain"; -"send.fee" = "Fee"; -"send.network_fee" = "Network Fee"; -"send.estimated_fee" = "Estimated Fee"; -"send.max_fee" = "Max Fee"; -"send.duration.hours" = "%d h"; -"send.duration.minutes" = "%d min."; -"send.available_balance" = "Available Balance"; -"send.max_button" = "Max"; -"send.next_button" = "Next"; -"send.error.invalid" = "Invalid"; -"send.error.address" = "Address!"; +"send.title" = "Отправить %@"; +"send.send" = "Отправить"; +"send.no_assets" = "У вас нет активов для отправки."; +"send.amount_placeholder" = "Сумма"; +"send.address_placeholder" = "Адрес"; +"send.address_or_domain_placeholder" = "Адрес или домен"; +"send.fee" = "Комиссия"; +"send.network_fee" = "Комиссия сети"; +"send.estimated_fee" = "Предполагаемая комиссия"; +"send.max_fee" = "Макс. комиссия"; +"send.duration.hours" = "%d ч."; +"send.duration.minutes" = "%d мин."; +"send.available_balance" = "Доступный баланс"; +"send.max_button" = "Макс."; +"send.next_button" = "Далее"; +"send.error.invalid" = "Неверно"; +"send.error.address" = "Адрес"; "send.hodler_locktime" = "TimeLock"; -"send.hodler_locktime_hour" = "1 hour"; -"send.hodler_locktime_month" = "1 month"; -"send.hodler_locktime_half_year" = "6 months"; -"send.hodler_locktime_year" = "1 year"; -"send.hodler_locktime_off" = "Off"; -"send.hodler_error.unsupported_address" = "Time vaulting? Only for top-tier addresses starting with 1...!"; -"send.fee_info.title" = "Fee Rate"; -"send.fee_info.description" = "Everybody pays a bit to keep things moving on the blockchain. Busy days mean higher fees. But hey, we'll suggest the best deal for you!"; -"send.transaction_inputs_outputs_info.title" = "In's and Out's of Transactions"; -"send.transaction_inputs_outputs_info.description" = "Most transactions have two parts: what you send and what you get back. We're smart, making it tricky for anyone trying to snoop."; -"send.transaction_inputs_outputs_info.shuffle.title" = "1. The Trump Card Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "We mix things up, so it's all unpredictable. Trust me, it's the best!"; -"send.transaction_inputs_outputs_info.deterministic.title" = "2. Classic Play"; -"send.transaction_inputs_outputs_info.deterministic.description" = "We've got standards, like the BIP69. It's a new thing, so not everyone's on board yet."; - -"send.confirmation.you_send" = "You Send"; -"send.confirmation.to" = "To"; -"send.confirmation.contact_name" = "Contact Name"; -"send.confirmation.domain" = "Domain"; -"send.confirmation.address" = "Address"; +"send.hodler_locktime_hour" = "1 час"; +"send.hodler_locktime_month" = "1 месяц"; +"send.hodler_locktime_half_year" = "6 месяцев"; +"send.hodler_locktime_year" = "1 год"; +"send.hodler_locktime_off" = "Выкл."; +"send.hodler_error.unsupported_address" = "TimeLock работает только при отправке на платёжные адреса, начинающиеся с 1... (также известных как BIP44 адреса)"; +"send.fee_info.title" = "Комиссия"; +"send.fee_info.description" = "Блокчейн требует от пользователей оплаты сетевых сборов при отправке транзакций. Комиссия выше, когда в сети проходит много транзакций.\n\nКошелек %@ оценивает комиссию на основе текущей активности блокчейна и рекомендует оптимальное значение для того, чтобы транзакция была обработана в разумные сроки.\n\nРекомендованная ставка комиссии показана как количество сатоши, которое пользователь должен заплатить за один байт транзакции. Таким образом, общая сумма комиссии зависит от общего размера транзакции, который измеряется в байтах.\n\n\nПользователи могут использовать предусмотренные элементы управления, чтобы увеличить или уменьшить значение ставки комиссии. Изменение ставки комиссии изменяет общую плату за транзакцию, которую заплатит пользователь.\n\n\nУстановка комиссии ниже рекомендуемого значения может привести к тому, что транзакция будет находиться в ожидании в течение нескольких часов или будет отклонена. Чем ниже значение, тем больше времени потребуется для подтверждения транзакции. Для транзакций, где важен приоритет, мы рекомендуем установить более высокую ставку комиссии."; +"send.transaction_inputs_outputs_info.title" = "Вводы / выводы транзакций"; +"send.transaction_inputs_outputs_info.description" = "Большинство транзакций с биткоином, а также транзакций с подобными криптовалютами, включая Bitcoin Cash, Dash и Litecoin, генерируют два выхода. Один выход - это сумма, которая поступает получателю, а другой - это изменение, которое возвращается отправителю. То, как большинство кошельков строят транзакции, позволяет третьей стороне легко понять, какой из выходов достался получающей стороне, а какой - сумма сдачи, возвращенная отправителю. Поскольку сумма, возвращенная отправителю, впоследствии используется в будущих транзакциях, связь между этими двумя транзакциями становится очевидной.\n\nВ кошельке %@ реализованы меры, чтобы затруднить кому-либо выяснение того, какой вывод куда идет.\n\nДля пользователей %@ доступны два варианта:"; +"send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; +"send.transaction_inputs_outputs_info.shuffle.description" = "Порядок выхода транзакций меняется случайным образом в каждой транзакции. Иногда изменение может быть первым выводом, иногда - вторым. Если пользователь доверяет разработчику приложения, то рекомендуем использовать этот вариант."; +"send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; +"send.transaction_inputs_outputs_info.deterministic.description" = "Существует общепринятый стандарт упорядочивания выходов транзакций (известный как BIP69). В кошельках с открытым исходным кодом этот стандарт гарантирует, что пользователям кошелька не нужно доверять тому, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт является новым, не многие кошельки его еще внедрили. В результате на блокчейне можно увидеть, была ли транзакция отправлена из кошелька, использующего этот стандарт, или нет."; + +"send.confirmation.you_send" = "Вы отправляете"; +"send.confirmation.to" = "Кому"; +"send.confirmation.contact_name" = "Имя контакта"; +"send.confirmation.domain" = "Домен"; +"send.confirmation.address" = "Адрес"; "send.confirmation.account" = "Account"; "send.confirmation.memo" = "Memo"; "send.confirmation.memo_placeholder" = "Memo"; -"send.confirmation.total" = "Total"; -"send.confirmation.fee" = "Fee"; +"send.confirmation.total" = "Всего"; +"send.confirmation.fee" = "Комиссия"; "send.confirmation.time_lock" = "TimeLock"; -"send.confirmation.slide_to_send" = "Slide to Send"; -"send.confirmation.sending" = "Sending"; -"send.confirmation.resend_description" = "Let's try ousting that old transaction with a bigger, better fee. Only the best one stays!"; -"send.confirmation.resend" = "Resend"; -"send.confirmation.cancel_description" = "We'll try sneaking a new zero-amount transaction past the old one. Only the top dog will make it!"; -"send.confirmation.cancel" = "Cancel Transaction"; -"send.confirmation.nonce" = "Nonce"; -"send.confirmation.method" = "Method"; -"send.amount_error.balance" = "Not Enough Balance"; -"send.address_error.own_address" = "Why send TRX to yourself? That's just showing off!"; -"send.amount_error.maximum_amount" = "Maximum amount %@"; -"send.amount_error.minimum_amount" = "Minimum amount %@"; -"send.amount_error.min_required_balance" = "Gotta leave some %@ behind for the party!"; -"send.amount_warning.coin_needed_for_fee" = "Keep some %@ around, it's always good for the bill!"; -"send.token.insufficient_fee_alert" = "Heads up! You'll need %@ to cover the %@ transfer on %@."; - -"send.fee_settings.amount_error.balance.title" = "Insufficient Balance"; -"send.fee_settings.amount_error.balance" = "Your %@ is a bit low for this grand move."; - -"send.fee_settings.stuck_warning.title" = "Could get sticky!"; -"send.fee_settings.stuck_warning" = "Might get tangled up or just flop."; - -"send.fee_settings.fee_error.title" = "Fee error"; -"send.fee_settings.too_low" = "Fee Rate is too low"; -"send.fee_settings.fee_rate_unavailable" = "Listen folks, our fee rate's taking a little vacation. Tremendous idea to check them manually. Believe me, I've done it!"; - -"send.stuck_warning" = "Warning! Risk of getting stuck"; +"send.confirmation.slide_to_send" = "Проведите для отправки"; +"send.confirmation.sending" = "Отправка"; +"send.confirmation.resend_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее с более высокой комиссией. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; +"send.confirmation.resend" = "Отправить повторно"; +"send.confirmation.cancel_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее себе как транзакцию с нулевой суммой. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; +"send.confirmation.cancel" = "Отменить транзакцию"; +"send.confirmation.nonce" = "Memo"; +"send.confirmation.method" = "Метод"; +"send.amount_error.balance" = "Недостаточно средств"; +"send.address_error.own_address" = "Невозможно отправить TRX самому себе"; +"send.amount_error.maximum_amount" = "Макс. сумма %@"; +"send.amount_error.minimum_amount" = "Мин. сумма %@"; +"send.amount_error.min_required_balance" = "Мин. обязательный остаток %@"; +"send.amount_warning.coin_needed_for_fee" = "Вы можете оставить некоторую сумму в размере %@ на балансе, чтобы оплачивать будущие транзакции."; +"send.token.insufficient_fee_alert" = "Комиссии за транзакцию %@ (%@) взимаются в %@. Вам нужно %@."; + +"send.fee_settings.amount_error.balance.title" = "Недостаточный баланс"; +"send.fee_settings.amount_error.balance" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; + +"send.fee_settings.stuck_warning.title" = "Внимание! Транзакция может застрять в сети"; +"send.fee_settings.stuck_warning" = "Транзакция может застрять в сети или будет отклонена."; +"send.fee_settings.fee_error.title" = "Ошибка комиссии"; +"send.fee_settings.too_low" = "Ставка комиссии слишком низкая."; +"send.fee_settings.fee_rate_unavailable" = "Ставка комиссии недоступна. Пожалуйста, проверьте ставки комиссии вручную"; + +"send.stuck_warning" = "Внимание! Транзакция может застрять в сети"; "send.lock_time" = "TimeLock"; -"approve.confirmation.you_approve" = "You Approve"; -"approve.confirmation.you_revoke" = "You Revoke"; -"approve.confirmation.spender" = "Spender"; +"approve.confirmation.you_approve" = "Вы разрешаете"; +"approve.confirmation.you_revoke" = "Вы отменяете"; +"approve.confirmation.spender" = "Покупатель"; // Donate -"donate.list.title" = "Donate with"; -"donate.list.get_address" = "Get Address"; -"donate.list.get_address.title" = "Address"; -"donate.title" = "Donate %@!"; -"donate.no_assets" = "Looks like you're a bit short, buddy."; -"donate.support.description" = "Let's work together to make this app HUGE! The best app ever."; +"donate.list.title" = "Поддержи нас"; +"donate.list.get_address" = "Получить адрес"; +"donate.list.get_address.title" = "Адреса"; +"donate.title" = "Пожертвовать %@"; +"donate.no_assets" = "У вас нет активов для пожертвования."; +"donate.support.description" = "С вашей поддержкой мы вместе сможем сделать это приложение еще лучше!"; // CoinSelector -"choose_coin.title" = "Choose Coins"; +"choose_coin.title" = "Выберите токены"; // Swap -"swap.title" = "Swap!"; -"swap.no_assets" = "You're empty! Fill it up."; -"swap.you_pay" = "You Pay"; -"swap.estimated" = "estimated"; -"swap.balance" = "Balance"; -"swap.allowance" = "Allowance"; -"swap.you_get" = "You Get"; -"swap.token" = "Select"; -"swap.advanced_settings" = "Swap Settings"; -"swap.proceed_button" = "Next"; -"swap.approve.title" = "Swap Approve"; -"swap.approve.description" = "Time to grant permission! But don't worry, it doesn't touch your stash. Just a little fee, and you're golden!"; -"swap.approve.amount_error.already_approved" = "Been there, done that! You're set."; -"swap.approving_button" = "Approving..."; -"swap.revoke_warning" = "Trade or revoke and try again!"; -"swap.revoking_button" = "Revoking..."; -"swap.not_available_button" = "Balance N/A"; -"swap.trade_error.not_found" = "This swap's a no-go!"; -"swap.trade_error.wrap_unwrap_not_allowed" = "No wrapping or unwrapping here! 1Inch is the way to go!"; -"swap.button_error.insufficient_balance" = "Insufficient Balance"; -"swap.switch_provider.title" = "Swap Service"; -"swap.amount_type.coin" = "Coin"; - -"swap.price" = "Price"; -"swap.buy_price" = "Buy Price"; -"swap.sell_price" = "Sell Price!"; -"swap.price_impact" = "Price Impact"; -"swap.maximum_paid" = "Maximum Amount"; -"swap.minimum_got" = "Guaranteed Amount"; -"swap.estimate_short" = "(est)"; -"swap.minimum_short" = "(min)"; -"swap.maximum_short" = "(max)"; +"swap.title" = "Обменять"; +"swap.no_assets" = "У вас нет активов для обмена."; +"swap.you_pay" = "Платите"; +"swap.estimated" = "приблизительно"; +"swap.balance" = "Баланс"; +"swap.allowance" = "Разрешение"; +"swap.you_get" = "Получите"; +"swap.token" = "Выбрать"; +"swap.advanced_settings" = "Настройки обмена"; +"swap.proceed_button" = "Далее"; +"swap.approve.title" = "Разрешение обмена"; +"swap.approve.description" = "Вы должны предоставить разрешение на использование смарт-контракта для замены данного токена от вашего имени. Это разрешение устанавливает сумму, которая может быть использована смарт-контрактом. Это не повлияет на ваш баланс, но требует небольшой комиссии для выполнения утвержденной транзакции. \n\nХотя это может быть сделано по требованию перед каждой сделкой, предварительно одобрить более высокую сумму для будущих сделок."; +"swap.approve.amount_error.already_approved" = "У вас уже есть разрешение на эту сумму"; +"swap.approving_button" = "Разрешение..."; +"swap.revoke_warning" = "Вы можете обменять %@, или вы должны отменить и одобрить новую сумму"; +"swap.revoking_button" = "Отмена..."; +"swap.not_available_button" = "Баланс N/А"; +"swap.trade_error.not_found" = "Невозможно обменять эти токены"; +"swap.trade_error.wrap_unwrap_not_allowed" = "Эта служба не позволяет wrapping/unwrapping. Пожалуйста, попробуйте другой сервис обмена. Рекомендуется 1inch"; +"swap.button_error.insufficient_balance" = "Недостаточный баланс"; +"swap.switch_provider.title" = "Служба обмена"; +"swap.amount_type.coin" = "Монета"; + +"swap.price" = "Цена"; +"swap.buy_price" = "Цена покупки"; +"swap.sell_price" = "Цена продажи"; +"swap.price_impact" = "Отклонение от рын. цены"; +"swap.maximum_paid" = "Макс. сумма"; +"swap.minimum_got" = "Гарантированная сумма"; +"swap.estimate_short" = "(прим.)"; +"swap.minimum_short" = "(мин)"; +"swap.maximum_short" = "(макс)"; // Swap Advanced Settings -"swap.advanced_settings.slippage" = "Slippage Tolerance"; -"swap.advanced_settings.slippage.footer" = "If prices dip more than this, no deal!"; -"swap.advanced_settings.deadline" = "Transaction Deadline"; -"swap.advanced_settings.deadline.footer" = "No dilly-dallying! Time's ticking."; -"swap.advanced_settings.recipient.footer" = "Where's the money going after the swap?"; -"swap.advanced_settings.deadline_minute" = "%@ min"; -"swap.advanced_settings.recipient_address" = "Recipient Address"; -"swap.advanced_settings.warning.unusual_slippage" = "Watch out! Others might jump the line!"; -"swap.advanced_settings.service_fee_description" = "A small thank you fee for our service. Usually just a tiny 0.3% or 0.6%!"; -"swap.advanced_settings.error.lower_slippage" = "This might flop."; -"swap.advanced_settings.error.higher_slippage" = "Can't go over %@%%, buddy!"; -"swap.advanced_settings.error.invalid_address" = "Wonky address alert!"; -"swap.advanced_settings.error.invalid_slippage" = "Invalid Slippage"; -"swap.advanced_settings.error.invalid_deadline" = "Invalid Deadline"; - -"swap.one_inch.error.cannot_estimate" = "Estimation Error"; -"swap.one_inch.error.cannot_estimate.info" = "Check your stash. Maybe up the wiggle room and try again. Give it another go in 3..."; -"swap.one_inch.error.insufficient_liquidity" = "Insufficient Liquidity"; -"swap.one_inch.error.insufficient_liquidity.info" = "Not enough in the pot. Lower the ante."; - -"swap.service" = "Service"; -"swap.service.title" = "Service"; +"swap.advanced_settings.slippage" = "Допустимость отклонений"; +"swap.advanced_settings.slippage.footer" = "Ваша транзакция будет отменена, если цена изменится в неблагоприятную сторону более чем на этот процент"; +"swap.advanced_settings.deadline" = "Срок транзакции"; +"swap.advanced_settings.deadline.footer" = "Ваша транзакция будет отменена, если перевод займет больше указанного срока."; +"swap.advanced_settings.recipient.footer" = "После операции обмена сумма будет переведена на указанный адрес"; +"swap.advanced_settings.deadline_minute" = "%@ мин"; +"swap.advanced_settings.recipient_address" = "Адрес получателя"; +"swap.advanced_settings.warning.unusual_slippage" = "Ваша транзакция может быть подвержена фронтраннингу."; +"swap.advanced_settings.service_fee_description" = "Комиссия за услугу обмена на платформе обычно 0.3% или 0.6%"; +"swap.advanced_settings.error.lower_slippage" = "Возможно, ваша транзакция не удалась."; +"swap.advanced_settings.error.higher_slippage" = "Сопротивление скольжению не может превышать %@%%"; +"swap.advanced_settings.error.invalid_address" = "Неверный адрес"; +"swap.advanced_settings.error.invalid_slippage" = "Неверное отклонение"; +"swap.advanced_settings.error.invalid_deadline" = "Недопустимый срок"; + +"swap.one_inch.error.cannot_estimate" = "Ошибка оценки"; +"swap.one_inch.error.cannot_estimate.info" = "Проверьте баланс и убедитесь, что на нем достаточно %@ для покрытия комиссии. Или попробуйте увеличить предел скольжения цены и повторите попытку снова. Следующая попытка через 3 секунды..."; +"swap.one_inch.error.insufficient_liquidity" = "Недостаточно ликвидности"; +"swap.one_inch.error.insufficient_liquidity.info" = "Кажется, для этой сделки не хватает ликвидности. Попробуйте уменьшить сумму сделки."; + +"swap.service" = "Сервис"; +"swap.service.title" = "Сервис"; // Swap Approving -"swap.approve.subtitle" = "Swap"; +"swap.approve.subtitle" = "Обменять"; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Slide to Swap"; -"swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.impact_too_high" = "Hold up! %@ says this deal's a dud. Check out %@ instead!"; -"swap.confirmation.impact_warning" = "Watch out! It's a wild ride!"; - -"swap.confirmation.minimum_received" = "Minimum Received"; -"swap.confirmation.maximum_sent" = "Maximum Sent"; - -"swap.dex_info.description" = "This swanky service is by %@, the big shots in decentralized trading on the %@ chain. 100% automated, 100% reliable!"; -"swap.dex_info.header_dex_related" = "%@Related"; -"swap.dex_info.header_allowance" = "Allowance"; -"swap.dex_info.content_allowance" = "How much the exchange can use on your behalf. Gotta get this before the main event."; -"swap.dex_info.header_price_impact" = "Price Impact"; -"swap.dex_info.content_price_impact" = "How wild the price might get. Bigger swaps, bigger waves."; -"swap.dex_info.header_swap_fee" = "Swap Fee"; -"swap.dex_info.content_swap_fee" = "Our little thank you note. Usually 0.3% or 0.6%."; -"swap.dex_info.header_guaranteed_amount" = " Guaranteed Amount"; -"swap.dex_info.content_guaranteed_amount" = "The least you're gonna get after swapping."; -"swap.dex_info.header_maximum_spend" = "Maximum Spend"; -"swap.dex_info.content_maximum_spend" = "The most you're gonna use in the swap."; - -"swap.dex_info.header_other" = "Other"; -"swap.dex_info.header_transaction_fee" = "Transaction Fee"; -"swap.dex_info.content_transaction_fee" = "What you're paying to use the %@ chain. %@ stuff might be pricier."; -"swap.dex_info.header_transaction_speed" = "Transaction Speed"; -"swap.dex_info.content_transaction_speed" = "Pay more, go faster. It's that simple."; - -"swap.dex_info.link_button" = "%@ Site"; +"swap.confirmation.slide_to_swap" = "Проведите для обмена"; +"swap.confirmation.swapping" = "Обмен"; +"swap.confirmation.impact_too_high" = "%@ отключил действие \"swap\" для этой сделки, так как вы получаете чрезвычайно невыгодную цену. Это связано с крайне низкой ликвидностью. \nЕсли вы все еще хотите поменять валюту, используйте веб-сайт %@ вместо этого."; +"swap.confirmation.impact_warning" = "Важно! Вы получаете чрезвычайно неблагоприятную цену. Это связано с крайне низкой ликвидностью."; + +"swap.confirmation.minimum_received" = "Получено минимум"; +"swap.confirmation.maximum_sent" = "Максимальная трата"; + +"swap.dex_info.description" = "Этот обменный сервис работает при поддержке %@ - децентрализованного протокола обмена токенов, созданного в блокчейне %@.\n\n%@ полностью автоматизирован и управляется смарт-контрактами, которые надежным способом упрощают обмен токенами без осуществления каких-либо махинаций."; + +"swap.dex_info.header_dex_related" = "%@"; +"swap.dex_info.header_allowance" = "Разрешение"; +"swap.dex_info.content_allowance" = "Сумма, которую exchange может потратить от имени пользователя при выполнении обмена токенов. Действующая транзакция, устанавливающая достаточный уровень допустимости, необходима для того, чтобы транзакция обмена была осуществлена."; +"swap.dex_info.header_price_impact" = "Отклонение от рын. цены"; +"swap.dex_info.content_price_impact" = "Ожидаемое отклонение цены от указанной цены, обычно увеличивается при увеличении суммы обмена."; +"swap.dex_info.header_swap_fee" = "Комиссия за обмен"; +"swap.dex_info.content_swap_fee" = "Плата за услугу обмена на платформе, показана в валюте, в которой продает пользователь. Для большинства заказов составляет или 0,3% или 0,6%."; +"swap.dex_info.header_guaranteed_amount" = "Гарантированная сумма"; +"swap.dex_info.content_guaranteed_amount" = "Минимальная сумма, которую получит пользователь в результате обмена."; +"swap.dex_info.header_maximum_spend" = "Максимальная трата"; +"swap.dex_info.content_maximum_spend" = "Максимальная сумма, которую получит пользователь в результате обмена."; + +"swap.dex_info.header_other" = "Другое"; +"swap.dex_info.header_transaction_fee" = "Комиссия за транзакцию"; +"swap.dex_info.content_transaction_fee" = "Примерная стоимость услуги по обработке данной транзакции на блокчейне %@. Стоимость транзакций, связанных с %@, обычно выше, чем стоимость транзакций по передаче обычных токенов."; +"swap.dex_info.header_transaction_speed" = "Скорость транзакции"; +"swap.dex_info.content_transaction_speed" = "Обработка транзакций с более высокой комиссией будет ускорена. Также вено и обратное."; + +"swap.dex_info.link_button" = "%@ сайт"; // Market -"market.tab_bar_item" = "Markets!"; -"market.title" = "Best Markets Ever!"; -"market.category.overview" = "The Best View"; -"market.category.posts" = "Real News, Not Fake!"; -"market.category.watchlist" = "My Favorite List"; -"market.total_market_cap" = "The Big Money Cap"; -"market.24h_volume" = "24 Hours of Winning"; -"market.defi_cap" = "Big DeFi Bucks"; -"market.defi_tvl" = "The Biggest Money in DeFi"; - -"market.project_has_no_coin" = "No coin? Sad!"; - -"market.top.section.header.see_all" = "See All"; -"market.top.section.header.top_gainers" = "Top Gainers"; -"market.top.section.header.top_losers" = "Top Losers"; -"market.top.section.header.top_sectors" = "Top Sectors"; -"market.top.section.header.news" = "News"; -"market.top.volume.title" = "Vol"; -"market.top.market_cap.title" = "MCap"; -"market.top.diluted_market_cap.title" = "Dilluted MCap"; - -"market.market_field.mcap" = "MCap"; -"market.market_field.vol" = "Vol"; +"market.tab_bar_item" = "Рынки"; +"market.title" = "Рынки"; +"market.category.overview" = "Обзор"; +"market.category.posts" = "Новости"; +"market.category.watchlist" = "Избранное"; +"market.total_market_cap" = "Общая капитализация"; +"market.24h_volume" = "Объем торгов (24ч)"; +"market.defi_cap" = "Капитализация DeFi"; +"market.defi_tvl" = "TVL в DeFi"; + +"market.project_has_no_coin" = "У этого проекта нет токена"; + +"market.top.section.header.see_all" = "Посмотреть всё"; +"market.top.section.header.top_gainers" = "Взлеты"; +"market.top.section.header.top_losers" = "Падения"; +"market.top.section.header.top_sectors" = "Топ секторы"; +"market.top.section.header.news" = "Новости"; +"market.top.volume.title" = "Объём"; +"market.top.market_cap.title" = "Рын. кап."; +"market.top.diluted_market_cap.title" = "Разводненная рын.кап."; + +"market.market_field.mcap" = "Рын. кап."; +"market.market_field.vol" = "Объём"; "market.tvl.market_field.value" = "USD"; -"market.tvl.market_field.diff" = "Percent"; - -"market.tvl.platform_field.all" = "All"; - -"market.sort_by" = "Sort By"; - -"market.top.title" = "Top Coins"; -"market.top.description" = "Top Coins, because we only deal with the best!"; - -"market.top.highest_cap" = "Highest Cap"; -"market.top.lowest_cap" = "Lowest Cap"; -"market.top.highest_volume" = "Highest Volume"; -"market.top.lowest_volume" = "Lowest Volume"; -"market.top.top_gainers" = "Top Gainers"; -"market.top.top_losers" = "Top Losers"; -"market.top.top_collections" = "Top NFT Collections"; -"market.top.floor_price" = "Floor"; -"market.top.top_platforms" = "Top Platforms"; -"market.top.protocols" = "Protocols"; - -"top_platforms.title" = "Platform Ranks"; -"top_platforms.description" = "The best places to build greatness."; - -"top_platform.title" = "%@ Ecosystem"; -"top_platform.description" = "Where the money's at on the %@ stage"; - -"market_discovery.title" = "Discovery"; -"market_discovery.filters" = "Filters"; -"market_discovery.browse_categories" = "Browse Categories"; -"market_discovery.top_coins" = "TOP Coins"; -"market_discovery.not_found" = "No results found"; - -"market_watchlist.empty.caption" = "It's lonely here."; - -"market.search.title" = "Search"; -"market.search.empty_text" = "No results found"; - -"market.advanced_search.title" = "Filters"; -"market.advanced_search.show_results" = "Show Results"; -"market.advanced_search.empty_results" = "Empty Results"; -"market.advanced_search.dex_description" = "This is for the Ethereum (Uniswap) and Binance (Pancake) hotshots."; -"market.advanced_search.24h" = "24h"; - -"market.advanced_search.market_parameters" = "Market Parameters"; -"market.advanced_search.network_parameters" = "Network Parameters"; -"market.advanced_search.price_parameters" = "Price Parameters!"; -"market.advanced_search.choose_set" = "Choose Set"; -"market.advanced_search.market_cap" = "Market Cap"; -"market.advanced_search.volume" = "Trading Volume"; -"market.advanced_search.liquidity" = "DEX Liquidity"; -"market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.price_period" = "Price Period"; -"market.advanced_search.price_change" = "Price Change"; - -"market.advanced_search.outperformed_btc" = "Outperformed BTC"; -"market.advanced_search.outperformed_eth" = "Outperformed ETH!"; -"market.advanced_search.outperformed_bnb" = "Outperformed BNB!"; -"market.advanced_search.price_close_to_ath" = "Price Close To ATH"; -"market.advanced_search.price_close_to_atl" = "Price Close To ATL"; - -"market.advanced_search.top" = "Top %d"; -"market.advanced_search.reset_all" = "Reset"; - -"market.advanced_search.less_5_m" = "< 5M"; -"market.advanced_search.less_10_m" = "< 10M"; -"market.advanced_search.less_50_m" = "< 50M"; -"market.advanced_search.less_500_m" = "< 500M"; -"market.advanced_search.m_5_m_20" = "5M - 20M"; -"market.advanced_search.m_10_m_40" = "10M - 40M"; -"market.advanced_search.m_20_m_100" = "20M - 100M"; -"market.advanced_search.m_40_m_200" = "40M - 200M"; -"market.advanced_search.m_50_m_200" = "50M - 200M"; -"market.advanced_search.m_100_b_1" = "100M - 1B"; -"market.advanced_search.m_200_b_1" = "200M - 1B"; -"market.advanced_search.m_200_b_2" = "200M - 2B"; -"market.advanced_search.m_500_b_2" = "500M - 2B"; -"market.advanced_search.b_1_b_5" = "1B - 5B"; -"market.advanced_search.b_1_b_10" = "1B - 10B"; -"market.advanced_search.b_2_b_10" = "2B - 10B"; -"market.advanced_search.b_10_b_50" = "10B - 50B"; -"market.advanced_search.b_10_b_100" = "10B - 100B"; -"market.advanced_search.b_100_b_500" = "100B - 500B"; -"market.advanced_search.more_5_b" = "> 5B"; -"market.advanced_search.more_10_b" = "> 10B"; -"market.advanced_search.more_50_b" = "> 50B"; -"market.advanced_search.more_500_b" = "> 500B"; - -"market.advanced_search.day" = "1 Day"; -"market.advanced_search.week" = "1 Week"; -"market.advanced_search.week2" = "2 Weeks"; -"market.advanced_search.month" = "1 Month"; -"market.advanced_search.month6" = "6 Months"; -"market.advanced_search.year" = "1 Year"; - -"market.advanced_search_results.title" = "Results"; - -"market.global.total_market_cap.title" = "Total Market Cap"; -"market.global.total_market_cap.description" = "Every penny, because we're that good."; - -"market.global.volume_24h.title" = "24h Volume"; -"market.global.volume_24h.description" = "All the moves in 24 hours"; - -"market.global.defi_cap.title" = "DeFi Cap"; -"market.global.defi_cap.description" = "Where DeFi makes its mark."; - -"market.global.tvl_in_defi.title" = "TVL in DeFi"; -"market.global.tvl_in_defi.description" = "Where the real money sleeps."; -"market.global.tvl_in_defi.multi_chain" = "Multi-Chain"; -"market.global.tvl_in_defi.filter_by_chain" = "Filtered by Chain"; - +"market.tvl.market_field.diff" = "Процент"; + +"market.tvl.platform_field.all" = "Все"; + +"market.sort_by" = "Сортировать"; + +"market.top.title" = "Лучшие токены"; +"market.top.description" = "Топ токенов по рыночной капитализации"; + +"market.top.highest_cap" = "Наивысшая кап."; +"market.top.lowest_cap" = "Наименьшая кап."; +"market.top.highest_volume" = "Наивысший объем"; +"market.top.lowest_volume" = "Наименьший объем"; +"market.top.top_gainers" = "Взлеты"; +"market.top.top_losers" = "Падения"; +"market.top.top_collections" = "Топ NFT коллекции"; +"market.top.floor_price" = "Минимальная цена:"; +"market.top.top_platforms" = "Топ платформы"; +"market.top.protocols" = "Протоколы"; + +"top_platforms.title" = "Рейтинг платформ"; +"top_platforms.description" = "Лучшие ведущие блокчейн-платформы кумулятивного рынка проектов."; + +"top_platform.title" = "%@ Экосистема"; +"top_platform.description" = "Капитализация рынка всех протоколов на блокчейне %@"; + +"market_discovery.title" = "Токены"; +"market_discovery.filters" = "Фильтры"; +"market_discovery.browse_categories" = "Обзор категорий"; +"market_discovery.top_coins" = "Топ токены"; +"market_discovery.not_found" = "Ничего не найдено"; + +"market_watchlist.empty.caption" = "У вас нет токенов в избранном."; + +"market.search.title" = "Поиск"; +"market.search.empty_text" = "Ничего не найдено"; + +"market.advanced_search.title" = "Фильтры"; +"market.advanced_search.show_results" = "Показать результаты"; +"market.advanced_search.empty_results" = "Сбросить результаты"; +"market.advanced_search.dex_description" = "Эта настройка применяется к токенам, торгуемым на Ethereum (Uniswap DEX) и Binance Smart Chain (Pancake DEX)."; +"market.advanced_search.24h" = "24ч"; + +"market.advanced_search.market_parameters" = "Параметры рынка"; +"market.advanced_search.network_parameters" = "Параметры сети"; +"market.advanced_search.price_parameters" = "Параметры цены"; +"market.advanced_search.choose_set" = "Выбор набора"; +"market.advanced_search.market_cap" = "Рын. капитализация"; +"market.advanced_search.volume" = "Объем торговли"; +"market.advanced_search.liquidity" = "Ликвидность DEX"; +"market.advanced_search.blockchains" = "Блокчейны"; +"market.advanced_search.price_period" = "Ценовой период"; +"market.advanced_search.price_change" = "По изменению цены (%)"; + +"market.advanced_search.outperformed_btc" = "Обошел BTC"; +"market.advanced_search.outperformed_eth" = "Обошел ETH"; +"market.advanced_search.outperformed_bnb" = "Обошел BNB"; +"market.advanced_search.price_close_to_ath" = "Цена близка к ATH"; +"market.advanced_search.price_close_to_atl" = "Цена близка к ATL"; + +"market.advanced_search.top" = "Топ %d"; +"market.advanced_search.reset_all" = "Сбросить"; + +"market.advanced_search.less_5_m" = "< 5млн"; +"market.advanced_search.less_10_m" = "< 10млн"; +"market.advanced_search.less_50_m" = "< 50млн"; +"market.advanced_search.less_500_m" = "< 500млн"; +"market.advanced_search.m_5_m_20" = "5млн - 20млн"; +"market.advanced_search.m_10_m_40" = "10млн - 40млн"; +"market.advanced_search.m_20_m_100" = "20млн - 100млн"; +"market.advanced_search.m_40_m_200" = "40млн - 200млн"; +"market.advanced_search.m_50_m_200" = "50млн - 200млн"; +"market.advanced_search.m_100_b_1" = "100млн - 1млрд"; +"market.advanced_search.m_200_b_1" = "200млн - 1млрд"; +"market.advanced_search.m_200_b_2" = "200млн - 2млрд"; +"market.advanced_search.m_500_b_2" = "500млн - 2млрд"; +"market.advanced_search.b_1_b_5" = "1млрд - 5млрд"; +"market.advanced_search.b_1_b_10" = "1млрд - 10млрд"; +"market.advanced_search.b_2_b_10" = "2млрд - 10млрд"; +"market.advanced_search.b_10_b_50" = "10млрд - 50млрд"; +"market.advanced_search.b_10_b_100" = "10млрд - 100млрд"; +"market.advanced_search.b_100_b_500" = "100млрд - 500млрд"; +"market.advanced_search.more_5_b" = "> 5млрд"; +"market.advanced_search.more_10_b" = "> 10млрд"; +"market.advanced_search.more_50_b" = "> 50млрд"; +"market.advanced_search.more_500_b" = "> 500млрд"; + +"market.advanced_search.day" = "1 день"; +"market.advanced_search.week" = "1 неделя"; +"market.advanced_search.week2" = "2 недели"; +"market.advanced_search.month" = "1 месяц"; +"market.advanced_search.month6" = "6 месяцев"; +"market.advanced_search.year" = "1 Год"; + +"market.advanced_search_results.title" = "Результаты"; + +"market.global.total_market_cap.title" = "Полная рын. кап."; +"market.global.total_market_cap.description" = "Общая рыночная стоимость всех криптовалют"; + +"market.global.volume_24h.title" = "Объем торгов (24ч)"; +"market.global.volume_24h.description" = "24-часовой объем крипторынка"; + +"market.global.defi_cap.title" = "Капитализация DeFi"; +"market.global.defi_cap.description" = "Общая рыночная стоимость проектов DeFi"; + +"market.global.tvl_in_defi.title" = "TVL в DeFi"; +"market.global.tvl_in_defi.description" = "Всего заблокировано (TVL) в DeFi"; +"market.global.tvl_in_defi.multi_chain" = "Мультичейн"; +"market.global.tvl_in_defi.filter_by_chain" = "Сортировать по блокчейну"; // Coin Page -"coin_page.overview" = "Price"; -"coin_page.analytics" = "Analytics"; -"coin_page.markets" = "Markets"; -"coin_page.tweets" = "Tweets"; +"coin_page.overview" = "Цена"; +"coin_page.analytics" = "Аналитика"; +"coin_page.markets" = "Рынки"; +"coin_page.tweets" = "Твиты"; // Coin Page -> Overview -"coin_overview.indicators" = "Indicators"; -"coin_overview.indicators.show" = "Show"; -"coin_overview.indicators.hide" = "Hide"; -"coin_overview.market_cap" = "Market Cap"; -"coin_overview.circulating_supply" = "In Circulation"; -"coin_overview.total_supply" = "Total Supply"; -"coin_overview.diluted_market_cap" = "Diluted MCap"; -"coin_overview.genesis_date" = "Inception Date"; -"coin_overview.trading_volume" = "Trading Volume"; - -"coin_overview.roi.hour24" = "1 Day"; -"coin_overview.roi.day7" = "1 Week"; -"coin_overview.roi.day14" = "2 Weeks"; -"coin_overview.roi.day30" = "1 Month"; -"coin_overview.roi.day200" = "6 Month"; -"coin_overview.roi.year1" = "1 Year"; - -"coin_overview.category" = "Category"; - -"coin_overview.blockchains" = "Blockchains"; -"coin_overview.bips" = "BIPs"; -"coin_overview.coin_types" = "Coin Types"; -"coin_overview.show_more" = "Show More"; -"coin_overview.show_less" = "Show Less"; -"coin_overview.links" = "Links"; - -"coin_overview.guide" = "Guide"; -"coin_overview.website" = "Website"; -"coin_overview.whitepaper" = "Whitepaper"; +"coin_overview.indicators" = "Индикаторы"; +"coin_overview.indicators.show" = "Показать"; +"coin_overview.indicators.hide" = "Скрыть"; +"coin_overview.market_cap" = "Рын. капитализация"; +"coin_overview.circulating_supply" = "В обороте"; +"coin_overview.total_supply" = "Макс.выпуск"; +"coin_overview.diluted_market_cap" = "Разводненная рын.кап."; +"coin_overview.genesis_date" = "Дата старта"; +"coin_overview.trading_volume" = "Объем торговли"; + +"coin_overview.roi.hour24" = "1 день"; +"coin_overview.roi.day7" = "1 неделя"; +"coin_overview.roi.day14" = "2 недели"; +"coin_overview.roi.day30" = "1 месяц"; +"coin_overview.roi.day200" = "6 месяцев"; +"coin_overview.roi.year1" = "1 Год"; + +"coin_overview.category" = "Категория"; + +"coin_overview.blockchains" = "Блокчейны"; +"coin_overview.bips" = "BIPы"; +"coin_overview.coin_types" = "Типы токенов"; +"coin_overview.show_more" = "Показать больше"; +"coin_overview.show_less" = "Показать меньше"; +"coin_overview.links" = "Ссылки"; + +"coin_overview.guide" = "Руководство"; +"coin_overview.website" = "Веб-сайт"; +"coin_overview.whitepaper" = "Тех. описание (whitepaper)"; // Coin Page -> Analytics -"coin_analytics.indicators.summary" = "Summary"; -"coin_analytics.indicators.title" = "Technical Indicators"; -"coin_analytics.indicators.no_data" = "No Data"; -"coin_analytics.indicators.strong_buy" = "Strong Buy"; -"coin_analytics.indicators.buy" = "Buy"; -"coin_analytics.indicators.neutral" = "Neutral"; -"coin_analytics.indicators.sell" = "Sell"; -"coin_analytics.indicators.strong_sell" = "Strong Sell"; -"coin_analytics.period" = "Period"; -"coin_analytics.period.select_title" = "Select Period"; -"coin_analytics.period.1h" = "1 hour"; -"coin_analytics.period.4h" = "4 hours"; -"coin_analytics.period.1d" = "1 day"; -"coin_analytics.period.1w" = "1 week"; - -"coin_analytics.details" = "Details"; - -"coin_analytics.not_available" = "Analytical data in this project? I've been told they're missing. But we're making everything better!"; - -"coin_analytics.technical_indicators" = "Technical Indicators"; -"coin_analytics.technical_indicators.info1" = "The Overview: We've got the best overview on asset's technicals. Everybody's talking about it! It tells you straight – Buy, Sell, or just stay Neutral. The best advice, believe me!"; -"coin_analytics.technical_indicators.info2" = "Moving Averages? Everybody loves them. Best trend indicators out there. Shows you average prices, and we've got the best averages.\n\nSimple Moving Average (SMA): Calculates averages like nobody else. Usually based on the top closing prices.\n\nExponential Moving Average (EMA): Super smart! Focuses on the latest prices, reacting faster than anything else. Everyone's talking about it!"; -"coin_analytics.technical_indicators.info3" = "Oscillators: They fluctuate, and they're tremendous. Helps you spot the best deals in the market. The very best!\n\nRelative Strength Index (RSI): Measures everything super fast! Used by the best to spot those overbought and oversold markets.\n\nMoving Average Convergence Divergence (MACD): This is the future! Points out the best times to buy or sell. Super smart, trust me."; - -"coin_analytics.cex_volume" = "CEX Volume"; -"coin_analytics.cex_volume_rank" = "CEX Volume Rank"; -"coin_analytics.cex_volume_rank.description" = "We rank tokens better than anyone else, especially based on trading volume. Everyone wants to be on top!"; -"coin_analytics.cex_volume.info1" = "Just look at our trading volume on the top centralized exchanges. Leading the game for 30 days straight!"; -"coin_analytics.cex_volume.info2" = "This chart? It's a masterpiece. Shows the trading volume changes like you've never seen before. Top-notch stuff!"; -"coin_analytics.cex_volume.info3" = "Our token's rank? Always leading based on volume. Best of the best!"; -"coin_analytics.cex_volume.info4" = "List of tokens? Only the winners, ranked by their top trading volumes. We're always on top, 24/7, every month."; - -"coin_analytics.dex_volume" = "DEX Volume"; -"coin_analytics.dex_volume_rank" = "DEX Volume Rank"; -"coin_analytics.dex_volume_rank.description" = "Tokens ranked? We've got the best rankings, especially for decentralized exchanges. Nobody does it better!"; -"coin_analytics.dex_volume.info1" = "The numbers are huge! Unbelievable trading volume for the token on the greatest decentralized exchanges. We're leading for 30 days, and everyone's talking about it!"; -"coin_analytics.dex_volume.info2" = "Ever seen a chart like this? Didn't think so. It shows the incredible variation in daily trading volume. The best chart for the best decentralized exchanges over a whole year!"; -"coin_analytics.dex_volume.info3" = "Where's our token's rank? At the top, of course! All thanks to the amazing trading volume on the finest decentralized exchanges for a whole month!"; -"coin_analytics.dex_volume.info4" = "Our list? Only the best tokens! All ranked by the highest trading volumes. Always updated - every 24 hours, every week, every month!"; -"coin_analytics.dex_volume.tracked_dexes" = "Which DEXes are we tracking? Only the best, most incredible ones out there. Everyone's asking about it!"; +"coin_analytics.indicators.summary" = "Общая оценка"; +"coin_analytics.indicators.title" = "Технические индикаторы"; +"coin_analytics.indicators.no_data" = "Нет данных"; +"coin_analytics.indicators.strong_buy" = "Активно покупать"; +"coin_analytics.indicators.buy" = "Покупать"; +"coin_analytics.indicators.neutral" = "Нейтральный"; +"coin_analytics.indicators.sell" = "Продавать"; +"coin_analytics.indicators.strong_sell" = "Активно продавать"; +"coin_analytics.period" = "Период"; +"coin_analytics.period.select_title" = "Выберите период"; +"coin_analytics.period.1h" = "1 час"; +"coin_analytics.period.4h" = "4 часа"; +"coin_analytics.period.1d" = "1 день"; +"coin_analytics.period.1w" = "1 неделя"; + +"coin_analytics.details" = "Подробности"; + +"coin_analytics.not_available" = "В этом проекте нет аналитических данных"; + +"coin_analytics.technical_indicators" = "Технические индикаторы"; +"coin_analytics.technical_indicators.info1" = "Общая оценка: Это общий обзор технических средств актива с учетом различных технических показателей и временных рамок. Он обеспечивает консенсусную точку зрения (купить, продать или нейтраль) на основе этих показателей."; +"coin_analytics.technical_indicators.info2" = "Скользящие средние (MA): Это обычно используемые технические индикаторы, позволяющие сгладить данные о ценах для создания индикатора следующего тренда. Они показывают среднюю цену за определенный период времени. Существует несколько типов MAs:\n\nSimple Moving среднее значение (SMA): Это вычисляет среднее значение выбранного диапазона цен, обычно закрывают цены, по количеству периодов в этом диапазоне.\n\nЭкспоненциальное скользящее среднее (EMA): Это даёт больше веса для последних цен, тем самым реагируя быстрее на последние изменения цен."; +"coin_analytics.technical_indicators.info3" = "Осцилляторы: Это технические индикаторы, которые колеблются со временем в диапазоне (выше и ниже центральной линии или между заданными уровнями). Они предназначены для идентификации перекупленных и перепродаваемых условий на рынке. Вот несколько распространенных осцилляторов:\n\nИндекс относительной силы (RSI): Это измеряет скорость и изменение движений цен. Обычно он используется для идентификации перекупленных или перепроданных условий.\n\nДвижение среднего сближения (MACD): Используется для выявления потенциальных сигналов на покупку и продажу. Она запускает технические сигналы, когда она пересекает линии сигнала выше (покупать) или ниже (продавать)."; + +"coin_analytics.cex_volume" = "Объем CEX"; +"coin_analytics.cex_volume_rank" = "Рейтинг объема CEX"; +"coin_analytics.cex_volume_rank.description" = "Торговый объем токена на централизованных биржах."; +"coin_analytics.cex_volume.info1" = "Общий объем торгов по токену на ведущих централизованных биржах за 30-дневный период."; +"coin_analytics.cex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих централизованных биржах за 1 год."; +"coin_analytics.cex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих централизованных биржах за 30-дневный период."; +"coin_analytics.cex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на децентрализованных биржах за 24ч/7дн/1мес."; + +"coin_analytics.dex_volume" = "Объем DEX"; +"coin_analytics.dex_volume_rank" = "Рейтинг объема DEX"; +"coin_analytics.dex_volume_rank.description" = "Торговый объем токена на децентрализованных биржах."; +"coin_analytics.dex_volume.info1" = "Общий объем торгов по токену на ведущих децентрализованных биржах за 30-дневный период."; +"coin_analytics.dex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих децентрализованных биржах за 1 год."; +"coin_analytics.dex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих децентрализованных биржах за 30-дневный период."; +"coin_analytics.dex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на децентрализованных биржах за 24ч/7дн/1мес."; +"coin_analytics.dex_volume.tracked_dexes" = "Отслеживаемые DEX биржи:"; "coin_analytics.dex_volume.tracked_dexes.info1" = "Ethereum : Uniswap V2/3, Sushiswap"; "coin_analytics.dex_volume.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap"; -"coin_analytics.dex_liquidity" = "DEX Liquidity"; -"coin_analytics.dex_liquidity_rank" = "DEX Liquidity Rank"; -"coin_analytics.dex_liquidity_rank.description" = "Tokens ranked by liquidity? We've got the best numbers for decentralized exchanges. Simply unbeatable!"; -"coin_analytics.dex_liquidity.info1" = "Look at this. Unbelievable amounts of liquidity for our token. Nobody has this kind of presence on decentralized exchanges!"; -"coin_analytics.dex_liquidity.info2" = "Have you seen this chart? Shows our dominance in liquidity. Leading the pack over a whole year!"; -"coin_analytics.dex_liquidity.info3" = "You want a list of top tokens by liquidity? Here it is. And guess who's always on top?"; -"coin_analytics.dex_liquidity.tracked_dexes" = "You're curious about which DEXes we're tracking? Only the absolute best in the business!"; +"coin_analytics.dex_liquidity" = "Ликвидность DEX"; +"coin_analytics.dex_liquidity_rank" = "Рейтинг ликвидности DEX"; +"coin_analytics.dex_liquidity_rank.description" = "Рейтинг токенов основан по доступной ликвидности на децентрализованных биржах."; +"coin_analytics.dex_liquidity.info1" = "Общая ликвидность, доступная в настоящий момент для токена на ведущих децентрализованных биржах."; +"coin_analytics.dex_liquidity.info2" = "График, отражающий колебания доступной ликвидности для токена на ведущих децентрализованных биржах за 1 год."; +"coin_analytics.dex_liquidity.info3" = "Список всех токенов, ранжированных по доступной ликвидности на ведущих децентрализованных биржах."; +"coin_analytics.dex_liquidity.tracked_dexes" = "Отслеживаемые DEX биржи:"; "coin_analytics.dex_liquidity.tracked_dexes.info1" = "Ethereum : Uniswap V2/3, Balancer V1/2, Bancor V2, Curve, Sushiswap"; "coin_analytics.dex_liquidity.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap, DODO V1/2"; -//Active Addresses & Info - -"coin_analytics.active_addresses" = "Daily Active Addresses"; -"coin_analytics.active_addresses.30_day_unique_addresses" = "30-Day Unique Addresses"; -"coin_analytics.active_addresses_rank" = "Active Addresses Rank"; -"coin_analytics.active_addresses_rank.description" = "Rankings? Tokens ranked by unique addresses? We're always on top. No one else even comes close!"; -"coin_analytics.active_addresses.info1" = "The numbers are HUGE. Unbeatable daily unique addresses. 24 hours of dominance!"; -"coin_analytics.active_addresses.info2" = "This chart? Shows our incredible journey over a year. People love using our token. Tremendous growth in daily active addresses. You've got to see it to believe it!"; -"coin_analytics.active_addresses.info3" = "Look at these numbers! Over the past month alone, countless unique blockchain addresses have been transacting with our token. We're hotter than ever!"; -"coin_analytics.active_addresses.info4" = "Rankings? Of course, we're up there. We're ranked based on the sheer number of active wallets transacting with us in just 30 days. Everyone's talking about us!"; -"coin_analytics.active_addresses.info5" = "Who wants a list? We've got the top tokens based on daily activity. Updated regularly - 24 hours, weekly, monthly!"; - -//Transaction Count & Info - -"coin_analytics.transaction_count" = "Transaction Count"; -"coin_analytics.transaction_count_rank" = "Tx Count Rank"; -"coin_analytics.transaction_count_rank.description" = "Tokens ranked by transactions? We're the gold standard!"; -"coin_analytics.transaction_count.info1" = "Count 'em! Massive number of transactions with our token in 30 days!"; -"coin_analytics.transaction_count.info5" = "Look at this! A sheer number of tokens moved across the blockchain in just 30 days. Big league!"; -"coin_analytics.transaction_count.info4" = "Everyone's talking about it! Here's the definitive list of tokens - but remember, the more transactions, the better the token. We're making transactions happen!"; -"coin_analytics.transaction_count.info2" = "Want to see a chart that shows real winning? Look at our transaction count over the past year. It's tremendous growth, folks!"; -"coin_analytics.transaction_count.info3" = "Where do we rank in terms of transactions in the last 30 days? It's at the top. It's always been at the top. No token transacts like us!"; - -//Holders & Info - -"coin_analytics.holders" = "Holders"; -"coin_analytics.holders_rank" = "Holders Rank"; -"coin_analytics.holders_rank.description" = "Rankings by holders? Our token is held by the best people on multiple blockchains!"; -"coin_analytics.holders.info1" = "The numbers? Only growing! So many unique addresses holding our amazing token across various blockchains."; -"coin_analytics.holders.info2" = "Top 10 wallets? Everyone's asking! Holding our token on each and every blockchain!"; -"coin_analytics.holders.tracked_blockchains" = "Where are we tracking? Everywhere! Ethereum, Binance Smart Chain, you name it. Leading everywhere!"; -"coin_analytics.holders.in_top_10_addresses" = "in top 10 holders"; -"coin_analytics.holders.count" = "Total Holders: %@"; -"coin_analytics.holders.see_all" = "See All"; - -"coin_analytics.project_tvl" = "Project TVL"; -"coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; -"coin_analytics.project_tvl.info_title" = "Project TVL (Total Value Locked)"; -"coin_analytics.project_tvl.info1" = "Our TVL? Unbelievable! People are locking in their assets with us like never before. Everyone wants to be part of this project!"; -"coin_analytics.project_tvl.info2" = "This chart shows our meteoric rise. Look at the variation in Total-Value-Locked in just one year. We've truly made smart contracts great again!"; -"coin_analytics.project_tvl.info3" = "When it comes to TVL, we're the top pick! Look at our ranking. Everyone's taking notice."; -"coin_analytics.project_tvl.info4" = "Want to see how we stack up? Here's a list of tokens, and trust me, we're always at the top. Everyone loves our TVL!"; -"coin_analytics.project_tvl.info5" = "The numbers don't lie. Check out our Market Cap to TVL ratio. We're in a league of our own!"; - -"coin_analytics.project_fee" = "Project Fee"; -"coin_analytics.project_fee_rank" = "Project Fee Rank"; -"coin_analytics.project_fee_rank.description" = "We're ranking tokens on who's really making money here, folks. Look at these fees! And every project has its own way, its own secret sauce. Some tokens are just better at it than others, believe me!"; - -"coin_analytics.project_revenue" = "Project Revenue"; -"coin_analytics.project_revenue_rank" = "Project Revenue Rank"; -"coin_analytics.project_revenue_rank.description" = "Let's rank tokens on who's actually putting money in the pockets of their people! Some of these tokens have brilliant strategies, like staking or even burning their tokens to create value. Not all tokens do it, but the smart ones do, and they're winning bigly for their holders!"; - -"coin_analytics.other_data" = "Other Data"; - -"coin_analytics.reports" = "Reports"; - -"coin_analytics.funding" = "Funding"; -"coin_analytics.funding.lead" = "Lead"; +"coin_analytics.active_addresses" = "Ежедневные активные адреса"; +"coin_analytics.active_addresses.30_day_unique_addresses" = "Уникальные адреса за 30 дней"; +"coin_analytics.active_addresses_rank" = "Рейтинг активных адресов"; +"coin_analytics.active_addresses_rank.description" = "Список токенов, ранжированных по количеству уникальных адресов транзакций с токеном."; +"coin_analytics.active_addresses.info1" = "Общее количество уникальных ежедневных активных адресов за 24 часа."; +"coin_analytics.active_addresses.info2" = "График, показывающий вариацию количества ежедневных активных адресов в течение 1 года."; +"coin_analytics.active_addresses.info3" = "Общее количество уникальных адресов блокчейна, с которых проводились транзакции с токеном за 30 дней."; +"coin_analytics.active_addresses.info4" = "Рейтинг токена основан на количестве активных кошельков, используемых для транзакций с токеном за 30-дневный период."; +"coin_analytics.active_addresses.info5" = "Список всех токенов, ранжированных по ежедневному количеству активных адресов транзакций с токеном с интервалом 24ч. / 7 дн. / 1 мес."; + +"coin_analytics.transaction_count" = "Количество транзакций"; +"coin_analytics.transaction_count_rank" = "Рейтинг кол-ва транзакций"; +"coin_analytics.transaction_count_rank.description" = "Токены ранжируются по количеству транзакций в блокчейне."; +"coin_analytics.transaction_count.info1" = "Общее количество уникальных транзакций блокчейна с токеном более 30 дней."; +"coin_analytics.transaction_count.info2" = "График, отражающий колебания количества транзакций за 1 год."; +"coin_analytics.transaction_count.info3" = "Рейтинг токена основан на количестве транзакций с токеном за 30-дневный период."; +"coin_analytics.transaction_count.info4" = "Список всех токенов, ранжированных на основе количества транзакций с интервалом 24ч / 7D / 1М."; +"coin_analytics.transaction_count.info5" = "Общее количество токенов, отправленных через блокчейн за 30-дневный период."; + +"coin_analytics.holders" = "Держатели"; +"coin_analytics.holders_rank" = "Рейтинг держателей"; +"coin_analytics.holders_rank.description" = "Рейтинг токенов по уникальным адресам, содержащим их в нескольких блокчейнах."; +"coin_analytics.holders.info1" = "Общее количество уникальных адресов с токенами в различных блокчейнах."; +"coin_analytics.holders.info2" = "Топ-10 кошельков с токенами в каждом блокчейне."; +"coin_analytics.holders.tracked_blockchains" = "Отслеживаемые блокчейны: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; +"coin_analytics.holders.in_top_10_addresses" = "в топ-10 держателей"; +"coin_analytics.holders.count" = "Всего держателей: %@"; +"coin_analytics.holders.see_all" = "Посмотреть всё"; + +"coin_analytics.project_tvl" = "Проект TVL"; +"coin_analytics.tvl_ratio" = "Рын.кап / Соотношение TVL "; +"coin_analytics.project_tvl.info_title" = "Проект TVL (совокупная сумма средств заблокирована)"; +"coin_analytics.project_tvl.info1" = "TVL (или Активы под управлением) в проектных смарт-контрактах."; +"coin_analytics.project_tvl.info2" = "График, отражающий колебания TVL в проектных смарт-контрактах за период, превышающий 1 год."; +"coin_analytics.project_tvl.info3" = "Рейтинг токена по текущему TVL."; +"coin_analytics.project_tvl.info4" = "Список всех токенов, ранжированных по текущему TVL."; +"coin_analytics.project_tvl.info5" = "Соотношение рыночной капитализации/TVL в рамках проекта."; + +"coin_analytics.project_fee" = "Комиссия проекта"; +"coin_analytics.project_fee_rank" = "Рейтинг оплаты за проект"; +"coin_analytics.project_fee_rank.description" = "Токены ранжируются в соответствии с платами, полученными в рамках соответствующих проектов."; + +"coin_analytics.project_revenue" = "Доход от проекта"; +"coin_analytics.project_revenue_rank" = "Рейтинг доходов"; +"coin_analytics.project_revenue_rank.description" = "Токены ранжируются по пассивному доходу владельцев, получаемому через стекинг или сжигание токенов."; + +"coin_analytics.other_data" = "Другие данные"; + +"coin_analytics.reports" = "Отчёты"; + +"coin_analytics.funding" = "Финансирование"; +"coin_analytics.funding.lead" = "Лид"; "coin_analytics.treasuries" = "Treasuries"; -"coin_analytics.treasuries.filters" = "Filters"; -"coin_analytics.treasuries.filter.all" = "All"; -"coin_analytics.treasuries.filter.public" = "Public"; -"coin_analytics.treasuries.filter.private" = "Private"; +"coin_analytics.treasuries.filters" = "Фильтры"; +"coin_analytics.treasuries.filter.all" = "Все"; +"coin_analytics.treasuries.filter.public" = "Публичный"; +"coin_analytics.treasuries.filter.private" = "Приватный"; "coin_analytics.treasuries.filter.etf" = "ETF"; -"coin_analytics.audits" = "Audits"; -"coin_analytics.audits.issues" = "Issues"; -"coin_analytics.audits.no_reports" = "No audit reports"; - -"coin_analytics.last_30d" = "last 30d"; -"coin_analytics.current" = "current"; - -"coin_analytics.overall_score" = "Overall Score"; -"coin_analytics.overall_score.excellent" = "Excellent"; -"coin_analytics.overall_score.good" = "Good"; -"coin_analytics.overall_score.fair" = "Fair"; -"coin_analytics.overall_score.poor" = "Poor"; -"coin_analytics.overall_score.cex_volume" = "When it comes to centralized exchanges, the real measure of a token's greatness is the average daily trading volume over the past week. Believe me, that's where the action is!"; -"coin_analytics.overall_score.dex_volume" = "For the decentralized exchanges, which are absolutely taking off, by the way, the top tokens are the ones with the best average daily trading volume over the last 7 days. Winners keep winning!"; -"coin_analytics.overall_score.dex_liquidity" = "Now, liquidity? It's huge! The best tokens have massive amounts of available liquidity on decentralized exchanges. That's where the power is, folks."; -"coin_analytics.overall_score.active_addresses" = "Let's talk active addresses. In just the last week, the really special tokens are the ones buzzing with daily activity. That's the mark of true success!"; -"coin_analytics.overall_score.project_tvl" = "TVL, or Total Value Locked, is where the smart money is. The best projects, and I mean the very best, have the most assets under management. That's a sign of trust!"; -"coin_analytics.overall_score.transaction_count" = "Transactions count - it's like the pulse of a token. Over the last week, the champions are the ones with the highest daily transaction. They're doing something right!"; -"coin_analytics.overall_score.holders" = "And holders? They're the foundation! The top-tier tokens have armies of holders, dedicated believers backing their play. That's the real strength!"; - -"coin_analytics.rank" = "Rank"; -"coin_analytics.30_day_rank" = "30-Day Rank"; -"coin_analytics.30_day_volume" = "30-Day Volume"; +"coin_analytics.audits" = "Аудиты"; +"coin_analytics.audits.issues" = "Запросы"; +"coin_analytics.audits.no_reports" = "Нет аудиторских отчетов"; + +"coin_analytics.last_30d" = "Последние 30 дн."; +"coin_analytics.current" = "текущее"; + +"coin_analytics.overall_score" = "Общий балл"; +"coin_analytics.overall_score.excellent" = "Отлично"; +"coin_analytics.overall_score.good" = "Хорошо"; +"coin_analytics.overall_score.fair" = "Приемлемо"; +"coin_analytics.overall_score.poor" = "Плохо"; +"coin_analytics.overall_score.cex_volume" = "Общий балл основан на средневзвешенном объеме торговли за последние 7 дней на централизованных биржах."; +"coin_analytics.overall_score.dex_volume" = "Общий балл основан на среднем ежедневном объеме торговли на децентрализованных биржах за последние 7 дней."; +"coin_analytics.overall_score.dex_liquidity" = "Общий балл основан на общей доступной ликвидности на децентрализованных биржах."; +"coin_analytics.overall_score.active_addresses" = "Общий балл основан на среднедневных активных адресах за последние 7 дней."; +"coin_analytics.overall_score.project_tvl" = "Общая балл основан на общей закрытой стоимости (активы, находящиеся в ведении управления) на проекте, представленном данным токеном."; +"coin_analytics.overall_score.transaction_count" = "Общий счет основан на среднем подсчете ежедневных транзакций за последние 7 дней."; +"coin_analytics.overall_score.holders" = "Общий балл основан на общем количестве адресов, содержащих соответствующий токен."; + +"coin_analytics.rank" = "Рейтинг"; +"coin_analytics.30_day_rank" = "Рейтинг на 30д"; +"coin_analytics.30_day_volume" = "Объем за 30д"; // Coin Page -> Markets -"coin_markets.empty" = "No data available"; +"coin_markets.empty" = "Нет доступных данных"; // Coin Page -> Tweets -"coin_tweets.reference_type.retweeted" = "Retweeted @%@"; -"coin_tweets.reference_type.quoted" = "Quoted @%@"; -"coin_tweets.reference_type.replied" = "Replied to @%@"; -"coin_tweets.no_tweets_yet" = "No tweets yet"; -"coin_tweets.not_available" = "No twitter available"; -"coin_tweets.see_on_twitter" = "See on Twitter"; +"coin_tweets.reference_type.retweeted" = "Ретвитнуто @%@"; +"coin_tweets.reference_type.quoted" = "Цитировано @%@"; +"coin_tweets.reference_type.replied" = "Ответил @%@"; +"coin_tweets.no_tweets_yet" = "Пока нет твитов"; +"coin_tweets.not_available" = "Twitter не доступен"; +"coin_tweets.see_on_twitter" = "Посмотреть в Twitter"; // Coin Page -> Indicators -"chart_indicators.title" = "Indicators"; -"chart_indicators.moving_averages" = "Moving Averages"; -"chart_indicators.oscillators" = "Oscillators"; +"chart_indicators.title" = "Индикаторы"; +"chart_indicators.moving_averages" = "Скользящие средние"; +"chart_indicators.oscillators" = "Осцилляторы"; -"chart_indicators.settings.period.error" = "Period can’t be more than %d"; +"chart_indicators.settings.period.error" = "Период не может быть больше чем %d"; -"chart_indicators.settings.ma.description" = "The EMA, SMA, and WMA are moving averages used in technical analysis:\n\nEMA emphasizes recent prices for quicker reactions.\nSMA averages price data for a general trend view.\nWMA balances sensitivity and noise reduction by linearly weighting recent data"; -"chart_indicators.settings.ma.type_title" = "Type"; -"chart_indicators.settings.ma.period_title" = "Period"; +"chart_indicators.settings.ma.description" = "В EMA, SMA и WMA используются средние значения для технического анализа:\n\nEMA подчеркивает последние цены для более быстрых реакций.\nСредние цены SMA для общего представления тренда.\nЧувствительность и шумовое снижение ВМА путем линейного взвешивания последних данных"; +"chart_indicators.settings.ma.type_title" = "Тип"; +"chart_indicators.settings.ma.period_title" = "Период"; "chart_indicators.settings.rsi.title" = "RSI"; - -"chart_indicators.settings.rsi.description" = "The Relative Strength Index (RSI)? Tremendous tool! It's like the pulse-checker of the market. When you see it above 70, that's a hot market, maybe too hot. Below 30? It might be a sale! And the best part? It can even show you when prices might take a U-turn. Genius!"; -"chart_indicators.settings.rsi.period_title" = "RSI Length"; +"chart_indicators.settings.rsi.description" = "Индекс относительной силы (RSI) - это колебатель импульса, который измеряет скорость и изменения цен, определяющий перекупленные (более 70) или перепродаваемые (менее 30) рыночные условия. Она также может обнаружить изменения цен через расхождения."; +"chart_indicators.settings.rsi.period_title" = "Длина RSI"; "chart_indicators.settings.macd.title" = "MACD"; -"chart_indicators.settings.macd.description" = "The Moving Average Convergence Divergence (MACD) is absolutely one of the big league players in the indicator world. It watches two EMAs like a hawk and tells you when things are heating up or cooling down. When the MACD line does a little dance with the 9-period EMA, the signal line, it might be showtime!"; +"chart_indicators.settings.macd.description" = "Дивергенция Moving Average Convergence (MACD) — индикатор динамики, который отслеживает отношения между двумя ЭРА ценой безопасности. Это сигнализирует о возможности покупки или продажи, когда линия MACD (12-ти периодная EMA минус 26-ти периодов EMA) пересекает девять периодов EMA, известную как сигнальная линия."; "chart_indicators.settings.macd.fast_period_title" = "Fast Length"; - "chart_indicators.settings.macd.slow_period_title" = "Slow Length"; "chart_indicators.settings.macd.signal_period_title" = "Signal Smoothing"; -"chart_indicators.settings.macd.slow_fast.error" = "Fast Length must be less than Slow Length"; +"chart_indicators.settings.macd.slow_fast.error" = "Fast Length должна быть меньше Slow Length"; // Transactions -"transactions.title" = "Transactions"; -"transactions.tab_bar_item" = "Transactions"; +"transactions.title" = "Транзакции"; +"transactions.tab_bar_item" = "Транзакции"; "transactions.blockchain" = "Blockchain"; -"transactions.all_blockchains" = "All Blockchains"; -"transactions.all_coins" = "All Coins"; -"transactions.choose_coin" = "Choose Coin"; -"transactions.filter_all" = "All"; -"transactions.empty_text" = "You don't have any pending or past transactions yet"; -"transactions.pending" = "Pending"; -"transactions.processing" = "Processing"; -"transactions.completed" = "Completed"; -"transactions.failed" = "Failed"; - -"transactions.receive" = "Receive"; -"transactions.send" = "Send"; -"transactions.burn" = "Burn"; -"transactions.mint" = "Mint"; -"transactions.approve" = "Approve"; -"transactions.swap" = "Swap"; -"transactions.contract_call" = "Contract Call"; -"transactions.contract_creation" = "Contract Creation"; -"transactions.external_call" = "External Call"; - -"transactions.to" = "To %@"; -"transactions.from" = "From %@"; - -"transactions.multiple" = "Multiple"; - -"transactions.value.unlimited" = "unlimited"; - -"transactions.today" = "Today"; -"transactions.yesterday" = "Yesterday"; - -"transactions.types.all" = "All"; -"transactions.types.incoming" = "Received"; -"transactions.types.outgoing" = "Sent"; -"transactions.types.swap" = "Swaps"; -"transactions.types.approve" = "Approvals"; - -"transactions.unknown_transaction.title" = "Unknown Transaction"; -"transactions.unknown_transaction.description" = "Transaction can not be parsed"; +"transactions.all_blockchains" = "Все блокчейны"; +"transactions.all_coins" = "Все токены"; +"transactions.choose_coin" = "Выберите токен"; +"transactions.filter_all" = "Все"; +"transactions.empty_text" = "У вас ещё нет незавершенных или прошлых транзакций"; +"transactions.pending" = "В обработке"; +"transactions.processing" = "В процессе"; +"transactions.completed" = "Завершено"; +"transactions.failed" = "Не удалось"; + +"transactions.receive" = "Получить"; +"transactions.send" = "Отправить"; +"transactions.burn" = "Сжечь"; +"transactions.mint" = "Минт"; +"transactions.approve" = "Разрешить"; +"transactions.swap" = "Обменять"; +"transactions.contract_call" = "Вызов контракта"; +"transactions.contract_creation" = "Создание контракта"; +"transactions.external_call" = "Внешний вызов"; + +"transactions.to" = "Кому %@"; +"transactions.from" = "От %@"; + +"transactions.multiple" = "Множество"; + +"transactions.value.unlimited" = "безлимитный"; + +"transactions.today" = "Сегодня"; +"transactions.yesterday" = "Вчера"; + +"transactions.types.all" = "Все"; +"transactions.types.incoming" = "Получено"; +"transactions.types.outgoing" = "Отправлено"; +"transactions.types.swap" = "Обмены"; +"transactions.types.approve" = "Разрешения"; + +"transactions.unknown_transaction.title" = "Неизвестная транзакция"; +"transactions.unknown_transaction.description" = "Транзакция не может быть обработана"; // Transaction Info -"tx_info.title" = "Transaction Info"; -"tx_info.date" = "Date"; -"tx_info.title_approval" = "Swap Approval"; -"tx_info.status.pending" = "Pending"; -"tx_info.status.completed" = "Completed"; -"tx_info.status.failed" = "Failed"; -"tx_info.from_hash" = "From"; +"tx_info.title" = "Детали транзакции"; +"tx_info.date" = "Дата"; +"tx_info.title_approval" = "Разрешение обмена"; +"tx_info.status.pending" = "В обработке"; +"tx_info.status.completed" = "Завершено"; +"tx_info.status.failed" = "Не удалось"; +"tx_info.from_hash" = "От"; "tx_info.transaction_id" = "ID"; -"tx_info.to_hash" = "To"; -"tx_info.spender" = "Spender"; -"tx_info.contact_name" = "Contact Name"; -"tx_info.button_explorer" = "View on %@"; -"tx_info.rate" = "Historical Rate"; -"tx_info.options.speed_up" = "Speed Up"; -"tx_info.options.cancel" = "Cancel Transaction"; -"tx_info.transaction.already_in_block" = "Transaction already in block"; -"tx_info.fee" = "Fee"; -"tx_info.fee.estimated" = "Fee (est.)"; -"tx_info.to_self_note" = "This transaction is sent to own address"; -"tx_info.double_spent_note" = "Double Spend Risk!"; -"tx_info.locked_until" = "Locked until %@"; -"tx_info.unlocked_at" = "Unlocked at %@"; -"tx_info.recipient_hash" = "Recipient"; -"tx_info.raw_transaction" = "Raw Transaction"; +"tx_info.to_hash" = "Кому"; +"tx_info.spender" = "Покупатель"; +"tx_info.contact_name" = "Имя контакта"; +"tx_info.button_explorer" = "Посмотреть на %@"; +"tx_info.rate" = "Исторический курс"; +"tx_info.options.speed_up" = "Ускорить"; +"tx_info.options.cancel" = "Отменить транзакцию"; +"tx_info.transaction.already_in_block" = "Транзакция уже в блоке"; +"tx_info.fee" = "Комиссия"; +"tx_info.fee.estimated" = "Комиссия (прим.)"; +"tx_info.to_self_note" = "Данная транзакция отправлена на собственный адрес"; +"tx_info.double_spent_note" = "Риск двойной траты!"; +"tx_info.locked_until" = "Заблокировано до %@"; +"tx_info.unlocked_at" = "Разблокировано %@"; +"tx_info.recipient_hash" = "Получатель"; +"tx_info.raw_transaction" = "Неподтвержденная транзакция"; "tx_info.memo" = "Memo"; -"tx_info.service" = "Service"; -"tx_info.view_on" = "View on %@"; -"tx_info.you_pay" = "You Pay"; -"tx_info.you_get" = "You Get"; -"tx_info.you_paid" = "You Paid"; -"tx_info.you_got" = "You Got"; -"tx_info.price" = "Price"; +"tx_info.service" = "Сервис"; +"tx_info.view_on" = "Посмотреть на %@"; +"tx_info.you_pay" = "Платите"; +"tx_info.you_get" = "Получите"; +"tx_info.you_paid" = "Вы заплатили"; +"tx_info.you_got" = "Вы получили"; +"tx_info.price" = "Цена"; // Settings -"settings.title" = "Settings"; -"settings.tab_bar_item" = "Settings"; -"settings.manage_accounts" = "Manage Wallets"; -"settings.blockchain_settings" = "Blockchain Settings"; -"settings.security" = "Security"; -"settings.experimental_features" = "Experimental"; -"settings.personal_support" = "Personal Support"; -"settings.base_currency" = "Base Currency"; -"settings.language" = "Language"; +"settings.title" = "Настройки"; +"settings.tab_bar_item" = "Настройки"; +"settings.manage_accounts" = "Кошельки"; +"settings.blockchain_settings" = "Настройки блокчейна"; +"settings.security" = "Безопасность"; +"settings.experimental_features" = "Экспериментальные функции"; +"settings.personal_support" = "Персональная поддержка"; +"settings.base_currency" = "Базовая валюта"; +"settings.language" = "Язык"; "settings.faq" = "FAQ"; -"settings.theme" = "Theme"; -"settings.info_subtitle" = "decentralized app"; -"settings.donate.description" = "Together, with your support, we can make this app even better!"; -"settings.donate.title" = "Donate"; +"settings.theme" = "Тема"; +"settings.info_subtitle" = "децентрализованное приложение"; +"settings.donate.description" = "С вашей поддержкой мы вместе сможем сделать это приложение еще лучше!"; +"settings.donate.title" = "Поддержи нас"; // Settings -> Base Currency -"settings.base_currency.title" = "Base Currency"; -"settings.base_currency.other" = "Other"; -"settings.base_currency.disclaimer" = "Disclaimer"; -"settings.base_currency.disclaimer.description" = "Hey there! So, we get our exchange rates from Coingecko.com, alright? While we aim for accuracy, we can't promise it's 100% spot-on, especially if you pick a base currency other than %@. Just so you know!"; -"settings.base_currency.disclaimer.set" = "Got it!"; +"settings.base_currency.title" = "Базовая валюта"; +"settings.base_currency.other" = "Другое"; +"settings.base_currency.disclaimer" = "Отказ от ответственности"; +"settings.base_currency.disclaimer.description" = "Данные об обменном курсе предоставлены третьим лицом Coingecko.сom\n\n Приложение %@ Wallet не гарантирует, что эти данные всегда верны и соответствуют рыночным. Шанс на несоответствие повышается при выборе базовой валюты, отличающейся от %@."; +"settings.base_currency.disclaimer.set" = "Установить"; // Settings -> Manage Wallet -"manage_wallets.title" = "Coin Manager"; -"manage_wallets.not_found" = "Hmm, no luck. Maybe try adding the token by hand?"; -"manage_wallets.search_placeholder" = "Whatcha looking for? Name, code, or maybe contract address?"; -"manage_wallets.contract_address" = "Contract Address"; -"manage_wallets.derivation_description" = "Fun fact: there are 4 cool address formats %@ wallets can use. They are BIP44, BIP49, BIP84 (our fav!), and BIP86. But no stress, we got you covered with all of them!"; -"manage_wallets.bitcoin_cash_coin_type_description" = "For Bitcoin Cash, there are 2 formats - TYPE 0 and TYPE 145. We say TYPE 145 is the way to go!"; +"manage_wallets.title" = "Токены"; +"manage_wallets.not_found" = "Ничего не найдено. Попробуйте добавить токен вручную."; +"manage_wallets.search_placeholder" = "Имя, код или адрес контракта"; +"manage_wallets.contract_address" = "Адрес контракта"; +"manage_wallets.derivation_description" = "Существует 4 распространенных формата адресов %@ кошельков для получения входящих платежей:\n\n- BIP44 (более старый)\n- BIP49\n- BIP84 (рекомендованный)\n- BIP86 (самый новый)\n\nХотя кошелек %@ поддерживает все четыре формата, рекомендуется использовать %@ кошелек, работающий в формате BIP84."; +"manage_wallets.bitcoin_cash_coin_type_description" = "Существует 2 формата адресов, которые кошельки Bitcoin Cash могут использовать для приема входящих платежей:\n\n- TYPE 0 (более старый)\n- TYPE 145 (более новый)\n\nХотя кошелек %@ поддерживает оба формата, рекомендуется использовать кошелек Bitcoin Cash, работающий в формате TYPE 145."; // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Account"; -"settings.personal_support.telegram_username.placeholder" = "@username!"; -"settings.personal_support.description" = "Pop in your Telegram username, and we'll slide into your DMs for some one-on-one chat."; -"settings.personal_support.request" = "Request"; -"settings.personal_support.requested" = "Requested"; -"settings.personal_support.failed" = "Request failed"; -"settings.personal_support.need_subscription" = "Heads up! This cool feature? It's for the %@ Wallet VIPs. Wanna know more? Check our website!"; -"settings.personal_support.requested.description" = "You've already pinged us! Check your Telegram for our message."; -"settings.personal_support.requested.open_telegram" = "Open Telegram"; -"settings.personal_support.requested.new_request" = "New Request"; +"settings.personal_support.telegram_username.placeholder" = "@username"; +"settings.personal_support.description" = "Введите имя аккаунта Telegram, чтобы открыть личный чат поддержки, и мы отправим вам сообщение."; +"settings.personal_support.request" = "Запрос"; +"settings.personal_support.requested" = "Запрошено"; +"settings.personal_support.failed" = "Ошибка запроса"; +"settings.personal_support.need_subscription" = "Эта функция доступна только для премиум-пользователей %@ Wallet . Дополнительная информация на нашем официальном сайте."; +"settings.personal_support.requested.description" = "Вы уже запросили приватный чат, найдите его в Telegram"; +"settings.personal_support.requested.open_telegram" = "Открыть Telegram"; +"settings.personal_support.requested.new_request" = "Новый запрос"; // Settings -> Experimental Features -"settings.experimental_features.title" = "Experimental"; -"settings.experimental_features.description" = "Below are some fun (but experimental) features. We've played with them, and while we think they're cool, use them at your own risk!"; +"settings.experimental_features.title" = "Экспериментальные функции"; +"settings.experimental_features.description" = "Функции ниже являются экспериментальными. Несмотря на то, что мы детально их протестировали, используя наши собственные криптосредства, мы не можем гарантировать, что они всегда будут работать так, как предусмотрено."; "settings.experimental_features.bitcoin_hodling" = "TimeLock"; // Settings -> Experimental Features -> Bitcoin HODLing "settings.bitcoin_hodling.title" = "TimeLock"; -"settings.bitcoin_hodling.lock_time" = "Activate"; -"settings.bitcoin_hodling.description" = "So this? It's a cool trick to lock up your Bitcoins till a certain date. But make sure the receiver's using %@ wallet app version 0.10 or newer. It's a fun way to HODL, especially if you're sending to yourself."; +"settings.bitcoin_hodling.lock_time" = "Активировать"; +"settings.bitcoin_hodling.description" = "Позволяет отправлять биткойны, которые могут быть потрачены только после указанной даты.\n\nПолучатель такой транзакции должен использовать версию 0.10 приложения %@ Wallet или же более новую версию с форматом адреса BIP44 для Bitcoin.\n\nТолько кошелек %@ может правильно распознать и обработать такие транзакции в сети Bitcoin, и позволить получателю тратить эти биткойны по истечении периода блокировки.\n\nЕсли вы HODLer (долгосрочный инвестор), то вы можете использовать эту функцию для удержания своих биткойнов, отправив их себе. \n\nТак как это экспериментальная функция, максимальная сумма транзакции ограничена 0,5 BTC."; // Settings -> Terms -"terms.title" = "Terms"; -"terms.i_agree" = "I Agree"; +"terms.title" = "Условия использования"; +"terms.i_agree" = "Я принимаю"; -"terms.item.1" = "Always backup your wallet recovery phrases. It's your key to the kingdom if things go sideways."; -"terms.item.2" = "Your wallet recovery phrases are made just for you, right here, and we don't keep them anywhere else."; -"terms.item.3" = "Heads up! If you switch off the unlock PIN on your phone, all wallets in the app will vanish. You'll need those recovery phrases to get back in."; -"terms.item.4" = "Messing with your phone's settings or downloading sketchy apps? That might put your coins at risk."; -"terms.item.5" = "Just so you know, there's always a teeny chance that a bug sneaks into our code, which might make the app act a little funky."; +"terms.item.1" = "Безопасное резервное копирование фраз для каждого кошелька. Это единственный способ восстановить доступ к средствам, если приложение не работает."; +"terms.item.2" = "Фразы восстановления кошелька генерируются случайным образом на устройстве во время установки и не хранятся в другом месте."; +"terms.item.3" = "Отключение PIN-кода разблокировки на смартфоне удаляет все кошельки из приложения. Для восстановления доступа к средствам потребуется восстановление фраз."; +"terms.item.4" = "Jailbreaking (rooting), использование устаревших ОС и установка приложений из неизвестных источников могут поставить под угрозу безопасность средств."; +"terms.item.5" = "В процессе работы кода могут возникнуть проблемы с программным обеспечением, что может привести к сбоям приложения."; // Settings -> Tell Friends @@ -1076,342 +1073,346 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Settings -> Blockchain Settings -"blockchain_settings.title" = "Blockchain Settings"; +"blockchain_settings.title" = "Настройки блокчейна"; // Settings -> Security -"settings_security.title" = "Security"; -"settings_security.passcode" = "Passcode"; -"settings_security.change_pin" = "Edit Passcode"; +"settings_security.title" = "Безопасность"; +"settings_security.passcode" = "Код доступа"; +"settings_security.change_pin" = "Изменить код"; "settings_security.touch_id" = "Touch ID"; "settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Blockchain Settings"; -"security_settings.delete_alert_button" = "Delete from Phone"; +"settings_security.blockchain_settings" = "Настройки блокчейна"; +"security_settings.delete_alert_button" = "Удалить с телефона"; -"btc_blockchain_settings.restore_source" = "Restore Source"; -"btc_blockchain_settings.restore_source.description" = "Where do you wanna pull your wallet's past deeds from?"; -"btc_blockchain_settings.restore_source.alert" = "Just so you know, if you tweak the Restore Source, the wallet's gotta get in sync with the %@ blockchain again."; +"btc_blockchain_settings.restore_source" = "Источник восстановления"; +"btc_blockchain_settings.restore_source.description" = "Выберите источник данных для восстановления кошелька с транзакциями."; +"btc_blockchain_settings.restore_source.alert" = "После изменения источника восстановления, кошелек должен будет повторно синхронизироваться с блокчейном %@."; -"btc_restore_mode.recommended" = "Recommended"; -"btc_restore_mode.more_private" = "More Private"; +"btc_restore_mode.recommended" = "Рекомендовано"; +"btc_restore_mode.more_private" = "Более Приватно"; "btc_transaction_sort_mode.shuffle" = "Shuffle"; -"btc_transaction_sort_mode.shuffle.description" = "Random Indexing"; +"btc_transaction_sort_mode.shuffle.description" = "Случайное индексирование"; "btc_transaction_sort_mode.bip69" = "Deterministic Bip69"; -"btc_transaction_sort_mode.bip69.description" = "Lexicographical Indexing"; +"btc_transaction_sort_mode.bip69.description" = "Лексикографическая индексация"; -"blockchain_settings.info.restore_source" = "Restore Source"; -"blockchain_settings.info.restore_source.content" = "Alright, here's the deal. If you're bringing back an old wallet, you've got to know where it's been. %@ can tap into two ways: 1. A super-fast API server (think 5-10 minutes) that's kinda like asking a friend who knows everything. But it’s not as private and leans on that friend always being around. 2. Straight from the blockchain network, which is more private and independent but also a slow poke (could take 2-3 hours)."; -"blockchain_settings.info.rpc_source" = "RPC Source"; -"blockchain_settings.info.rpc_source.content" = "How do we chat with the blockchains when money's moving? With some (like Bitcoin), it's like chatting in a big group. With others (like Ethereum), we gotta use a middleman like Infura.io. No worries about your coins though, just how we connect. We're brainstorming more freestyle ways to link up. Stay tuned!"; +"blockchain_settings.info.restore_source" = "Источник восстановления"; +"blockchain_settings.info.restore_source.content" = "Этот параметр актуален только при восстановлении существующего кошелька. Это процесс получения истории транзакций для данной монеты, чтобы приложение кошелька могло отображать предыдущие транзакции и определять последний баланс. Это должно произойти только один раз при восстановлении пользователем ранее созданного кошелька. \n\nНа данный момент у такого мобильного кошелька, как %@, есть два возможных способа для реализации этого вопроса: \n\n1. С сервера API: существует предопределенный сторонний сервер, на котором размещается вся цепочка блоков, происходит обработка всех данных и их подготовка к быстрому предоставлению. Это быстрый метод, однако он предполагает потенциальное отслеживание IP-адресов и запрашиваемых данных. Этот вариант рекомендуется использовать из-за скорости получения данных (5-10 минут). Он сопряжен с минимальными рисками. Если пользователь восстанавливает данные со своего собственного сервера, то нет никакой угрозы конфиденциальности.\n\n2. Из блокчейна: в этом случае приложение пытается восстановить данные напрямую из p2p-сети узлов блокчейна. Приложение пингует многие из этих узлов и запрашивает у них данные. Потенциально эти узлы могут отслеживать IP-адрес пользователя и сделанные запросы, однако во время восстановления данных приложение может переключаться между узлами. В любое время в сети находится около 10 000 биткойн-узлов. Это медленный вариант обработки данных, она может занять 2-3 часа и приложение должно быть открыто во время их восстановления."; +"blockchain_settings.info.rpc_source" = "Источник RPC"; +"blockchain_settings.info.rpc_source.content" = "Этот параметр определяет способ взаимодействия приложения с блокчейнами при отправке или получении транзакций.\n\nКогда речь идет о Bitcoin, Bitcoin Cash, Litecoin, и Dash, связь с сетевыми узлами блокчейна полностью равноправна. %@ пингует многие узлы и связывается с одним из них. Каждый раз приложение подключается к другому узлу.\n\nЧто касается Ethereum и Binance Smart Chain, то пока нет подходящих альтернативных вариантов мобильных кошельков для взаимодействия с соответствующими блокчейнами кроме сторонних поставщиков услуг RPC (т.е. Infura.io) или личными узлами Ethereum. Это значит, что ваше взаимодействие с блокчейном не децентрализовано. \n\nЭто влияет не на ваши средства, а на возможность подключения к сети блокчейн."; // Manage Accounts -"manage_accounts.migration_required" = "Migration Required"; -"manage_accounts.migration_recommended" = "Migration Recommended"; -"manage_accounts.backup_required" = "Backup Required"; +"manage_accounts.migration_required" = "Требуется миграция"; +"manage_accounts.migration_recommended" = "Рекомендуется миграция"; +"manage_accounts.backup_required" = "Необходима резерв. копия"; // Manage Keys -"settings_manage_keys.title" = "Manage Wallets"; -"settings_manage_keys.delete" = "Delete"; -"settings_manage_keys.backup" = "Backup"; -"settings_manage_keys.delete.title" = "Delete Wallet"; -"settings_manage_keys.delete.confirmation_remove" = "Heads up! This will kick the wallet off the device."; -"settings_manage_keys.delete.confirmation_loose" = "Hey, if you haven't stashed the key for this wallet, you could be saying bye to your bucks."; -"settings_manage_keys.delete.confirmation_watch" = "Done watching this wallet's every move?"; -"settings_manage_keys.delete.confirmation_watch.button" = "Stop Watching"; +"settings_manage_keys.title" = "Кошельки"; +"settings_manage_keys.delete" = "Удалить"; +"settings_manage_keys.backup" = "Резервная копия"; +"settings_manage_keys.delete.title" = "Удалить кошелек"; +"settings_manage_keys.delete.confirmation_remove" = "Действие удалит этот кошелек с устройства."; +"settings_manage_keys.delete.confirmation_loose" = "Если вы не сделали резервную копию приватного ключа для этого кошелька, вы потеряете доступ к вашим средствам."; +"settings_manage_keys.delete.confirmation_watch" = "Вы хотите приостановить просмотр этого адреса кошелька?"; +"settings_manage_keys.delete.confirmation_watch.button" = "Остановить просмотр"; // Settings -> About App -"settings.about_app.title" = "About App"; -"settings.about_app.app_name" = "%@ Wallet"; -"settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; -"settings.about_app.whats_new" = "What's New"; -"settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Contact Us"; -"settings.about_app.rate_us" = "Rate Us"; -"settings.about_app.tell_friends" = "Tell Friends"; +"settings.about_app.title" = "О приложении"; +"settings.about_app.app_name" = "%@ кошелек"; +"settings.about_app.description" = "Кошелек %@ создан для тех, кто хочет инвестировать и хранить криптовалюты частным и независимым образом.\n\nЭто одноранговый кошелек, где только пользователь имеет контроль над средствами. Он не собирает никаких данных и обеспечивает независимость пользователя, не привязывая средства пользователя к определенному приложению.\n\nКошелек %@ имеет полностью открытый исходный код, и любой может подтвердить, что приложение работает именно так, как заявлено."; +"settings.about_app.whats_new" = "Что нового"; +"settings.about_app.website" = "Веб-сайт"; +"settings.about_app.contact" = "Связаться с нами"; +"settings.about_app.rate_us" = "Оценить нас"; +"settings.about_app.tell_friends" = "Рассказать друзьям"; // Settings -> About App -> Contact -"settings.contact.title" = "Contact Us"; -"settings.contact.via_email" = "via E-mail"; -"settings.contact.via_telegram" = "via Telegram"; +"settings.contact.title" = "Связаться с нами"; +"settings.contact.via_email" = "по электронной почте"; +"settings.contact.via_telegram" = "через Telegram"; // Settings -> Privacy - -"settings.privacy" = "Privacy Vibes"; -"settings.privacy.description" = "Just so you know, %@ is like that friend who never spills your secrets. No data collection, no sneaky analytics, nada! We've built this wallet with your privacy front and center."; -"settings.privacy.statement.user_data_storage" = "Your stuff? It stays right on your device, and that's it."; -"settings.privacy.statement.data_usage" = "We're not in the biz of snooping or collecting any deets about you."; -"settings.privacy.statement.data_privacy" = "Sharing your data? Nah, that's not our style."; -"settings.privacy.statement.user_account" = "And guess what? No user accounts or mysterious databases storing your data out in the wild."; +"settings.privacy" = "Приватность"; +"settings.privacy.description" = "%@ не собирает данные и не использует инструменты аналитики, которые могут раскрывать любые данные о своих пользователях. Кошелек разработан для обеспечения высокого уровня конфиденциальности для своих пользователей."; +"settings.privacy.statement.user_data_storage" = "Данные пользователя всегда остаются на устройстве пользователя."; +"settings.privacy.statement.data_usage" = "Кошелек не собирает никаких данных о пользователях."; +"settings.privacy.statement.data_privacy" = "Кошелек не передает никаких данных о пользователях."; +"settings.privacy.statement.user_account" = "Нет учетных записей или баз данных, хранящих данные пользователя в другом месте."; // Settings -> Appearance -"appearance.title" = "Appearance"; +"appearance.title" = "Оформление"; -"appearance.theme" = "Theme"; -"appearance.theme.system" = "System"; -"appearance.theme.dark" = "Dark"; -"appearance.theme.light" = "Light"; +"appearance.theme" = "Тема"; +"appearance.theme.system" = "Системная"; +"appearance.theme.dark" = "Тёмная"; +"appearance.theme.light" = "Светлая"; -"appearance.tab_settings" = "Tab Settings"; -"appearance.markets_tab" = "Markets Tab"; -"appearance.launch_screen" = "Launch Screen"; -"appearance.launch_screen.auto" = "Auto"; -"appearance.launch_screen.balance" = "Balance"; -"appearance.launch_screen.market_overview" = "Market Overview"; -"appearance.launch_screen.watchlist" = "Watchlist"; +"appearance.tab_settings" = "Настройки вкладки"; +"appearance.markets_tab" = "Вкладка рынки"; +"appearance.launch_screen" = "Запуск экрана"; +"appearance.launch_screen.auto" = "По умолчанию"; +"appearance.launch_screen.balance" = "Баланс"; +"appearance.launch_screen.market_overview" = "Обзор рынка"; +"appearance.launch_screen.watchlist" = "Избранное"; -"appearance.app_icon" = "App Icon"; +"appearance.app_icon" = "Иконка приложения"; -"appearance.balance_conversion" = "Balance Conversion"; +"appearance.balance_conversion" = "Конвертация баланса"; -"appearance.balance_value" = "Balance Value"; -"appearance.balance_value.coin_value" = "Coin Value"; -"appearance.balance_value.fiat_value" = "Fiat Value"; +"appearance.balance_value" = "Значение баланса"; +"appearance.balance_value.coin_value" = "Токен значение"; +"appearance.balance_value.fiat_value" = "Фиатное значение"; -"appearance.balance_auto_hide" = "Balance Auto Hide"; +"appearance.balance_auto_hide" = "Автоскрытие баланса"; // Settings -> Contacts -"contacts.title" = "Contacts"; -"contacts.list.search_placeholder" = "Search by name"; -"contacts.list.not_found" = "You do not have an added contact"; -"contacts.list.not_found_search" = "No results found"; -"contacts.add_new_contact" = "Add New Contact"; -"contacts.update_contact.already_has_address" = "Selected contact already has an address on %@. This action will replace the address %@ with %@."; -"contacts.update_contact.replace" = "Replace"; -"contacts.list.addresses_count" = "Addresses: %d"; -"contacts.contact.new.title" = "New Contact"; -"contacts.contact.name.placeholder" = "Name"; -"contacts.contact.update.error.name_already_exist" = "Name Already Exist"; -"contacts.contact.add_address" = "Add Address"; -"contacts.contact.delete" = "Delete Contact"; -"contacts.contact.address.blockchains" = "Blockchains"; +"contacts.title" = "Контакты"; +"contacts.list.search_placeholder" = "Поиск по имени"; +"contacts.list.not_found" = "У вас нет добавленных контактов"; +"contacts.list.not_found_search" = "Ничего не найдено"; +"contacts.add_new_contact" = "Добавить новый контакт"; +"contacts.update_contact.already_has_address" = "Выбранный контакт уже имеет адрес на %@. Это действие заменит адрес %@ на %@."; +"contacts.update_contact.replace" = "Заменить"; +"contacts.list.addresses_count" = "Адреса: %d"; +"contacts.contact.new.title" = "Новый контакт"; +"contacts.contact.name.placeholder" = "Название"; +"contacts.contact.update.error.name_already_exist" = "Имя уже существует"; +"contacts.contact.add_address" = "Добавить адрес"; +"contacts.contact.delete" = "Удалить контакт"; +"contacts.contact.address.blockchains" = "Блокчейны"; "contacts.contact.address.blockchain" = "Blockchain"; -"contacts.contact.address.delete_address" = "Delete Address"; +"contacts.contact.address.delete_address" = "Удалить адрес"; -"contacts.restore.restored" = "Restored"; -"contacts.restore.parsing_error" = "File has wrong data!"; -"contacts.restore.restore_error" = "Failed to restore contacts"; -"contacts.restore.overwrite_alert.description" = "This action will overwrite your local payment contacts as well as its iCloud copy (if there is one)."; -"contacts.restore.overwrite_alert.replace" = "Replace"; +"contacts.restore.restored" = "Восстановлено"; +"contacts.restore.parsing_error" = "Файл имеет неправильные данные!"; +"contacts.restore.restore_error" = "Не удалось восстановить контакты"; +"contacts.restore.overwrite_alert.description" = "Это действие перезапишет ваши локальные платежные контакты, а также их копии в iCloud (при наличии таковых)."; +"contacts.restore.overwrite_alert.replace" = "Заменить"; -"contacts.add_address.title" = "Add Address"; -"contacts.add_address.create_new" = "Create New Contact"; -"contacts.add_address.add_to_contact" = "Add to Existing Contact"; -"contacts.add_address.exist_address" = "This address is already used for %@"; +"contacts.add_address.title" = "Добавить адрес"; +"contacts.add_address.create_new" = "Создать новый контакт"; +"contacts.add_address.add_to_contact" = "Добавить в существующий контакт"; +"contacts.add_address.exist_address" = "Этот адрес уже используется для %@"; -"contacts.contact.delete_alert.title" = "Delete Contact"; -"contacts.contact.delete_alert.description" = "Are you sure you want to delete this contact?"; -"contacts.contact.delete_alert.delete" = "Delete"; +"contacts.contact.delete_alert.title" = "Удалить контакт"; +"contacts.contact.delete_alert.description" = "Вы действительно хотите удалить этот контакт?"; +"contacts.contact.delete_alert.delete" = "Удалить"; -"contacts.contact.dismiss_changes.description" = "Are you sure you want to discard these new changes?"; -"contacts.contact.dismiss_changes.discard_changes" = "Discard Changes"; -"contacts.contact.dismiss_changes.keep_editing" = "Keep Editing"; +"contacts.contact.dismiss_changes.description" = "Вы уверены, что хотите отменить эти изменения?"; +"contacts.contact.dismiss_changes.discard_changes" = "Отменить изменения"; +"contacts.contact.dismiss_changes.keep_editing" = "Продолжить редактирование"; -"contacts.add_address.delete_alert.title" = "Delete Address"; -"contacts.add_address.delete_alert.description" = "Are you sure you want to delete this address?"; -"contacts.add_address.delete_alert.delete" = "Delete"; +"contacts.add_address.delete_alert.title" = "Удалить адрес"; +"contacts.add_address.delete_alert.description" = "Вы уверены, что хотите удалить этот адрес?"; +"contacts.add_address.delete_alert.delete" = "Удалить"; // Contacts -> Settings -"contacts.settings.title" = "Settings"; +"contacts.settings.title" = "Настройки"; -"contacts.settings.restore_contacts" = "Restore Contacts"; -"contacts.settings.backup_contacts" = "Backup Contacts"; +"contacts.settings.restore_contacts" = "Восстановить контакты"; +"contacts.settings.backup_contacts" = "Резерв. копирование контактов"; -"contacts.settings.icloud_sync" = "iCloud Sync"; -"contacts.settings.description" = "Sync payment contacts to iCloud for easy backup and access across multiple devices."; -"contacts.settings.lost_synchronization.description" = "iCloud synchronization is lost. Please check that iCloud Storage is enabled on your device."; -"contacts.settings.merge_disclaimer" = "Your local payment contacts will be merged with ones stored on iCloud."; +"contacts.settings.icloud_sync" = "iCloud синхр."; +"contacts.settings.description" = "Синхронизируйте платежные контакты с iCloud для легкого резервного копирования и доступа к ним на нескольких устройствах."; +"contacts.settings.lost_synchronization.description" = "Синхронизация iCloud утеряна. Пожалуйста, проверьте, что iCloud Storage включен на вашем устройстве."; +"contacts.settings.merge_disclaimer" = "Ваши локальные платежные контакты будут объединены с данными, хранящимися в iCloud."; -"contacts.settings.alert.title" = "iCloud Sync"; -"contacts.settings.alert.description" = "Please check that iCloud Storage is enabled on your device."; +"contacts.settings.alert.title" = "iCloud синхр."; +"contacts.settings.alert.description" = "Пожалуйста, убедитесь, что iCloud хранилище включено на вашем устройстве."; -"contacts.settings.alert_error.title" = "iCloud Error"; +"contacts.settings.alert_error.title" = "Ошибка iCloud"; // Set PIN -"set_pin.title" = "Passcode"; -"set_pin.info" = "Your passcode will be used to unlock your wallet"; -"set_pin.wrong_confirmation" = "Passcode did not match. Try again"; +"set_pin.title" = "Код доступа"; +"set_pin.info" = "Код доступа будет использоваться для разблокировки вашего кошелька"; +"set_pin.wrong_confirmation" = "Код доступа не совпадает. Попробуйте ещё раз"; // Edit PIN -"edit_pin.title" = "Edit Passcode"; -"edit_pin.unlock_info" = "Current Passcode"; -"edit_pin.new_pin_info" = "New Passcode"; +"edit_pin.title" = "Изменить код"; +"edit_pin.unlock_info" = "Текущий код"; +"edit_pin.new_pin_info" = "Новый код"; // Unlock PIN -"unlock_pin.info" = "Passcode"; -"unlock_pin.cant_save_pin" = "Ouch! We cannot save your passcode, please contact us asap!"; -"unlock_pin.blocked_until" = "Disabled until: %@"; +"unlock_pin.info" = "Код доступа"; +"unlock_pin.cant_save_pin" = "Ой! Мы не можем сохранить ваш код доступа. Пожалуйста, свяжитесь с нами как можно скорее!"; +"unlock_pin.blocked_until" = "Отключено до: %@"; // Key Types -"chart.time_duration.day" = "24H"; -"chart.time_duration.week" = "7D"; -"chart.time_duration.week2" = "2W"; -"chart.time_duration.month" = "1M"; -"chart.time_duration.month3" = "3M"; -"chart.time_duration.halfyear" = "6M"; -"chart.time_duration.year" = "1Y"; -"chart.time_duration.year2" = "2Y"; -"chart.time_duration.all" = "ALL"; - -"chart.market_cap" = "Market Cap"; -"chart.volume" = "Volume (24h)"; -"chart.circulation" = "In Circulation"; -"chart.selected.volume" = "Vol."; - -"chart.market.header" = "Market"; -"chart.market.market_cap" = "Market Cap"; -"chart.market.volume" = "Volume (24h)"; -"chart.market.circulation" = "In Circulation"; -"chart.market.total_supply" = "Total Supply"; - -"chart.performance.week_changes" = "Changes (1W)"; -"chart.performance.month_changes" = "Changes (1M)"; - -"chart.about.header" = "About"; -"chart.about.read_more" = "Read More"; -"chart.about.read_less" = "Read Less"; +"chart.time_duration.day" = "24Ч"; +"chart.time_duration.week" = "7Д"; +"chart.time_duration.week2" = "2Н"; +"chart.time_duration.month" = "1М"; +"chart.time_duration.month3" = "3М"; +"chart.time_duration.halfyear" = "6М"; +"chart.time_duration.year" = "1Г"; +"chart.time_duration.year2" = "2Г"; +"chart.time_duration.all" = "ВСЕ"; + +"chart.market_cap" = "Рын. капитализация"; +"chart.volume" = "Объём (за 24 ч)"; +"chart.circulation" = "В обороте"; +"chart.selected.volume" = "Объём"; + +"chart.market.header" = "Рынок"; +"chart.market.market_cap" = "Рын. капитализация"; +"chart.market.volume" = "Объём (за 24 ч)"; +"chart.market.circulation" = "В обороте"; +"chart.market.total_supply" = "Макс.выпуск"; + +"chart.performance.week_changes" = "Изменения (за 1Н)"; +"chart.performance.month_changes" = "Изменения (за 1М)"; + +"chart.about.header" = "О Проекте"; +"chart.about.read_more" = "Подробнее"; +"chart.about.read_less" = "Свернуть"; "coin_page.return_of_investments" = "ROI"; // Create Wallet -"create_wallet.title" = "New Wallet"; -"create_wallet.name" = "Name"; -"create_wallet.advanced_setup" = "Advanced"; -"create_wallet.create" = "Create"; -"create_wallet.advanced" = "Advanced"; -"create_wallet.phrase_count" = "Recovery Phrase"; -"create_wallet.12_words" = "12 words (recommended)"; -"create_wallet.n_words" = "%@ words"; -"create_wallet.word_list" = "Word List"; -"create_wallet.passphrase" = "Passphrase"; -"create_wallet.input.passphrase" = "Passphrase"; -"create_wallet.input.confirm" = "Confirm"; -"create_wallet.passphrase_description" = "Think of passphrases as your wallet's secret handshake. If you ever need to bring back a lost wallet, you'll need the recovery phrase and this passphrase. Plus, with just one mnemonic, you can unlock a bunch of multi-coin wallets with different passphrases. Neat, right?"; -"create_wallet.error.empty_passphrase" = "Oops! Looks like you missed the passphrase. Please fill it in."; -"create_wallet.error.forbidden_symbols" = "Hold on there, cowboy! Let's stick to the symbols we know and love::A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"create_wallet.error.invalid_confirmation" = "Hmm, your passphrase confirmations aren't matching. Let's try that again."; +"create_wallet.title" = "Новый кошелек"; +"create_wallet.name" = "Название"; +"create_wallet.advanced_setup" = "Доп. настройки"; +"create_wallet.create" = "Создать"; +"create_wallet.advanced" = "Доп. настройки"; +"create_wallet.phrase_count" = "Фраза восстановления"; +"create_wallet.12_words" = "12 слов (рекомендуется)"; +"create_wallet.n_words" = "%@ слов"; +"create_wallet.word_list" = "Список слов"; +"create_wallet.passphrase" = "Кодовая фраза"; +"create_wallet.input.passphrase" = "Кодовая фраза"; +"create_wallet.input.confirm" = "Подтвердить"; +"create_wallet.passphrase_description" = "Кодовая фраза добавляет дополнительный слой безопасности для кошельков. Чтобы восстановить такой кошелек требуется как мнемоник, так и кодовая фраза.\n\\Кодовая фраза также облегчает пользователям возможность иметь множество мульти-монетных кошельков, используя одну фразу восстановления, но другой пароль."; +"create_wallet.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; +"create_wallet.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"create_wallet.error.invalid_confirmation" = "Подтвержденная кодовая фраза не совпадает"; + // Restore Select -"restore_select.title" = "Choose Blockchains"; +"restore_select.title" = "Выберите блокчейны"; // Lock Info "lock_info.title" = "TimeLock"; -"lock_info.text" = "Heads up! The sender locked these funds for a bit. The lock will lift on the indicated date. \n\nRest assured, those Bitcoins are already in your pocket. You'll just need to wait until the lock"; +"lock_info.text" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; // Double Spend Info -"double_spend_info.title" = "Double Spend"; -"double_spend_info.header" = "Double Spend Risk! There is another transaction on the blockchain that is trying to spend inputs used in this transaction. Only one transaction will be accepted by the network"; -"double_spend_info.this_hash" = "This Tx"; -"double_spend_info.conflicting_hash" = "Conflicting Tx"; +"double_spend_info.title" = "Двойная трата"; +"double_spend_info.header" = "Риск двойной траты! Существует другая транзакция в блокчейне, которая пытается использовать входные данные, используемые в этой транзакции. Только одна транзакция будет принята сетью"; +"double_spend_info.this_hash" = "Эта транзакция"; +"double_spend_info.conflicting_hash" = "Конфликтующая транзакция"; // Relative Date -"timestamp.days_ago" = "%lud ago"; -"timestamp.hours_ago" = "%luh ago"; -"timestamp.min_ago" = "%lum ago"; +"timestamp.days_ago" = "%luд назад"; +"timestamp.hours_ago" = "%luч назад"; +"timestamp.min_ago" = "%luмин назад"; // Intro -"intro.unchain_assets.title" = "Unchain Assets"; -"intro.unchain_assets.description" = "Why cage yourself in? And don’t let any losers do that to you. Be free!"; -"intro.go_borderless.title" = "Go Borderless"; -"intro.go_borderless.description" = "Skip those silly barriers. Access markets everywhere, like a boss!"; -"intro.stay_private.title" = "Stay Private"; -"intro.stay_private.description" = "Why broadcast your business? Keep your secrets, just like I do!"; +"intro.unchain_assets.title" = "Непривязанные активы"; +"intro.unchain_assets.description" = "Распоряжайтесь активами свободно и без ограничений"; +"intro.go_borderless.title" = "Избавьтесь от границ"; +"intro.go_borderless.description" = "Преодолейте барьеры и получите доступ к рынкам"; +"intro.stay_private.title" = "Сохраняйте конфиденциальность"; +"intro.stay_private.description" = "Не допускайте утечку личных и финансовых данных"; // Guides -"guides.tab_bar_item" = "Academy"; -"guides.title" = "Academy"; +"guides.tab_bar_item" = "Академия"; +"guides.title" = "Академия"; // Add Token -"add_token.title" = "Add Token"; +"add_token.title" = "Добавить токен"; "add_token.blockchain" = "Blockchain"; -"add_token.already_added" = "This coin? It's already on the A-list, like me in the world of real estate. No need to add it again, folks!"; -"add_token.invalid_contract_address" = "Invalid contract address"; -"add_token.invalid_bep2_symbol" = "Invalid BEP2 symbol"; -"add_token.contract_address_not_found" = "Contract address not found in %@ blockchain"; -"add_token.bep2_symbol_not_found" = "BEP2 symbol not found"; -"add_token.input_placeholder.contract_address" = "Contract Address"; -"add_token.input_placeholder.bep2_symbol" = "BEP2 Symbol"; -"add_token.coin_name" = "Coin Name"; -"add_token.symbol" = "Symbol"; -"add_token.decimals" = "Decimals"; +"add_token.already_added" = "Этот токен уже есть в списке"; +"add_token.invalid_contract_address" = "Неверный адрес контракта"; +"add_token.invalid_bep2_symbol" = "Неверный символ BEP2"; +"add_token.contract_address_not_found" = "Адрес контракта не найден в %@ блокчейне "; +"add_token.bep2_symbol_not_found" = "Символ BEP2 не найден"; +"add_token.input_placeholder.contract_address" = "Адрес контракта"; +"add_token.input_placeholder.bep2_symbol" = "Символ BEP2"; +"add_token.coin_name" = "Название токена"; +"add_token.symbol" = "Символ"; +"add_token.decimals" = "Десятичные знаки"; // Wallet Connect "wallet_connect.title" = "WalletConnect"; -"wallet_connect.error.invalid_url" = "Invalid URL Address"; +"wallet_connect.error.invalid_url" = "Неверный URL-адрес"; "wallet_connect.url" = "URL"; -"wallet_connect.active_account" = "Active Wallet"; -"wallet_connect.address" = "Address"; -"wallet_connect.network" = "Network"; -"wallet_connect.list.pending_requests" = "Pending Requests"; -"wallet_connect.main.unsupported_chains" = "Some of these chains? Very low energy. Not supported!"; -"wallet_connect.connect_description" = "Hit that approve, and you're letting the app see your VIP address. We're doing this, folks, to make security great again and keep the phishers out!"; -"wallet_connect.usage_description" = "You can head over to the browser, but remember - keep this page open. It's the best page!"; -"wallet_connect.no_connection" = "Couldn't make the connection. Probably the system's fault. Try again and let's win this!"; -"wallet_connect.button_reconnect" = "Reconnect"; -"wallet_connect.button_disconnect" = "Disconnect"; -"ethereum_transaction.error.title" = "Error"; -"ethereum_transaction.error.insufficient_balance" = "Hey, we need more %@ to send this. Not enough funds, folks!"; -"ethereum_transaction.error.insufficient_balance_with_fee" = "Your %@ balance? Too low! Can't cover the transaction and the tiny fee!"; -"ethereum_transaction.error.lower_than_base_gas_limit" = "You're offering peanuts for the fee! Too low, it'll be shown the door!"; -"ethereum_transaction.error.nonce_already_in_block" = "This transaction? Already in the block! Done and dusted!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Trying to replace? But your fee? Way too cheap! Up the ante!"; -"ethereum_transaction.error.transaction_underpriced" = "Your fee's too low! It's like trying to buy Mar-a-Lago with pocket change!"; -"ethereum_transaction.error.tips_higher_than_max_fee" = "Tips are rolling in higher than the max fee! Remember, the max fee counts those tips too!"; -"ethereum_transaction.error.reverted" = "Can't do the transaction! Here's why: %@"; -"wallet_connect.request_title" = "Contract Call"; -"wallet_connect.button.confirm" = "Confirm"; -"wallet_connect.sign.request_title" = "Sign Request"; -"wallet_connect.sign.domain" = "Domain"; +"wallet_connect.active_account" = "Активный Кошелек"; +"wallet_connect.address" = "Адрес"; +"wallet_connect.network" = "Сеть"; +"wallet_connect.address" = "Адрес"; +"wallet_connect.network" = "Сеть"; +"wallet_connect.list.pending_requests" = "Ожидающие запросы"; +"wallet_connect.main.no_any_supported_chains" = "Нет поддерживаемых блокчейнов!"; +"wallet_connect.main.unsupported_chains" = "Некоторые блокчейны не поддерживаются!"; +"wallet_connect.connect_description" = "Нажимая \"Подтвердить\", вы разрешаете этому приложению просматривать ваш общедоступный адрес. Это важный шаг для защиты ваших данных от потенциальных рисков фишинга."; +"wallet_connect.usage_description" = "Вы можете перейти в браузер. Не закрывайте эту страницу во время использования браузера."; +"wallet_connect.no_connection" = "Не удалось установить соединение. Попробуйте переподключиться снова."; +"wallet_connect.button_reconnect" = "Повторное подключение"; +"wallet_connect.button_disconnect" = "Отсоединить"; +"ethereum_transaction.error.title" = "Ошибка"; +"ethereum_transaction.error.insufficient_balance" = "Для отправки транзакции требуется %@."; +"ethereum_transaction.error.insufficient_balance_with_fee" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; +"ethereum_transaction.error.lower_than_base_gas_limit" = "Выбранное значение комиссии слишком низкое и будет отклонено!"; +"ethereum_transaction.error.nonce_already_in_block" = "Транзакция уже в блоке!"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Недостаточно комиссии для замены транзакции"; +"ethereum_transaction.error.transaction_underpriced" = "Комиссия недостаточна для отправки транзакции"; +"ethereum_transaction.error.tips_higher_than_max_fee" = "Максимальное вознаграждение не может быть меньше чаевых, так как максимальное вознаграждение включает чаевые."; +"ethereum_transaction.error.reverted" = "Транзакция не может быть выполнена: %@"; +"wallet_connect.request_title" = "Вызов контракта"; +"wallet_connect.button.confirm" = "Подтвердить"; +"wallet_connect.sign.request_title" = "Запрос на подпись"; +"wallet_connect.sign.domain" = "Домен"; "wallet_connect.sign.dapp_name" = "dApp"; -"wallet_connect.sign.message" = "Message to sign"; +"wallet_connect.sign.message" = "Сообщение для подписи"; "wallet_connect_list.title" = "WalletConnect"; -"wallet_connect.list.empty_view_text" = "No active sessions"; +"wallet_connect.list.empty_view_text" = "Нет активных сессий"; + +"wallet_connect.list.pairings" = "Соединения"; +"wallet_connect.list.version_text" = "Версия %@"; +"wallet_connect.list.v1_bottom_text" = "В первой версии WalletConnect вы должны войти в сессии, чтобы увидеть и подтвердить запрос."; +"wallet_connect_list.new_connection" = "Новое соединение"; +"wallet_connect_list.disconnecting" = "Отсоединение"; -"wallet_connect.list.pairings" = "Pairings"; -"wallet_connect.list.version_text" = "Version %@"; -"wallet_connect.list.v1_bottom_text" = "In the first version of WalletConnect, you must go into sessions to see and confirm the request"; -"wallet_connect_list.new_connection" = "New Connection"; -"wallet_connect_list.disconnecting" = "Disconnecting"; +"wallet_connect.no_account.description" = "Вам нужно создать или импортировать кошелек перед использованием WalletConnect."; +"wallet_connect.unbackuped_account.description" = "Вам нужно сделать резервную копию %@ перед использованием WalletConnect"; -"wallet_connect.no_account.description" = "Before you jump into WalletConnect, you've gotta either build a wallet or bring one in, just like building or buying a beautiful hotel!"; -"wallet_connect.unbackuped_account.description" = "Hey, you've gotta backup %@ before using WalletConnect. Protect it, just like I protect my Twitter account!"; -"wallet_connect.non_supported_account.description" = "That wallet of yours, the %@ type? Not a fan of WalletConnect, sorry folks!"; -"wallet_connect.non_supported_account.switch" = "Switch"; +"wallet_connect.non_supported_account.description" = "Ваш текущий тип кошелька %@ не поддерживает WalletConnect"; +"wallet_connect.non_supported_account.switch" = "Перекл."; -"wallet_connect.pending_requests_title" = "Pending Requests"; -"wallet_connect.paired_dapps.title" = "Paired dApps"; -"wallet_connect.paired_dapps.cant_disconnect" = "Can't Disconnect"; -"wallet_connect.paired_dapps.disconnect_all" = "Delete All"; -"wallet_connect.pending_requests.nonactive_footer" = "To kickstart a request, you gotta turn on the wallet you want. Activate it like you mean it, folks!"; +"wallet_connect.pending_requests_title" = "Ожидающие запросы"; +"wallet_connect.paired_dapps.title" = "Соединеные dApp"; +"wallet_connect.paired_dapps.cant_disconnect" = "Не удалось отключить"; +"wallet_connect.paired_dapps.disconnect_all" = "Удалить всё"; +"wallet_connect.pending_requests.nonactive_footer" = "Чтобы открыть запрос, необходимо активировать желаемый кошелек"; // App Status -"app_status.title" = "App Status"; -"app_status.application_status" = "Application status"; -"app_status.linked_wallets" = "Linked Wallets"; -"app_status.version_history" = "Version History"; -"app_status.blockchain_status" = "Blockchain status"; +"app_status.title" = "Статус приложения"; +"app_status.application_status" = "Статус приложения"; +"app_status.linked_wallets" = "Связанные кошельки"; +"app_status.version_history" = "История версий"; +"app_status.blockchain_status" = "Статус блокчейна"; // FAQ @@ -1419,321 +1420,324 @@ To scan that bigly QR code. Just go to \"Settings\" -> and let %@ in!"; // Status Info -"status_info.title" = "Status"; -"status_info.pending.title" = "Pending"; -"status_info.pending.content" = "Your transaction? It's like waiting for an applause at one of my rallies - coming but not here yet. If you paid a good fee, it'll be there in a jiffy. But if you went low-ball, might take longer - days even! And it could be shown the exit door. Remember, updates on the %@ wallet might lag a tad."; -"status_info.processing.title" = "Getting Things Done"; -"status_info.processing.content" = "Your transaction? It's in the blockchain but not set in stone. For small deals, you can consider it done. But for the big league stuff, you might wanna hold your horses till it's totally completed."; -"status_info.completed.title" = "Victory!"; -"status_info.confirmed.content" = "It's a done deal! That transaction? Set in stone and it's not going back."; -"status_info.failed.title" = "Oops!"; -"status_info.failed.content" = "Didn't work out this time - like trying to get a Democrat to agree with me! Some slip-ups might still cost you fees. If another transaction nudged yours out or if it was canceled, no fees, but still an 'Oops'. The %@ app can't tell you why it failed, but you can play detective on block explorers like etherscan.io."; +"status_info.title" = "Статус"; +"status_info.pending.title" = "В обработке"; +"status_info.pending.content" = "Транзакция еще не подтверждена в блокчейне. Транзакции, отправленные с рекомендованной или более высокой комиссией, обычно обрабатываются в течение минут. Транзакции, отправленные с низкой комиссией, могут обрабатываться в течение нескольких часов или дней и даже отклоняться. Учтите, что обновление статуса каждой транзакции, отображаемого в интерфейсе кошелька %@, обычно происходит с небольшой задержкой. "; +"status_info.processing.title" = "В процессе"; +"status_info.processing.content" = "Транзакция уже включена в блокчейн, но пока не обработана. На данном этапе транзакции считаются завершенными для небольших платежей. Что касается завершения обработки более крупных транзакций, стоит дождаться смены статуса."; +"status_info.completed.title" = "Завершено"; +"status_info.confirmed.content" = "Транзакция завершена и считается постоянной и необратимой."; +"status_info.failed.title" = "Не удалось"; +"status_info.failed.content" = "Транзакция не была обработана и передача средств не произошла. В зависимости от причины неудачи, некоторые неудачные транзакции могут использовать комиссию за транзакцию. Транзакции, которые были заменены или отменены другой транзакцией, не потребляют транзакционные сборы и также будут показаны как \"не удалось\". Приложение %@ не может показать причину неудачных транзакций, но пользователи могут посмотреть это в etherscan.io."; // Onboarding -"onboarding.balance.create" = "New Wallet"; -"onboarding.balance.import" = "Import Wallet"; -"onboarding.balance.watch" = "Watch Wallet"; +"onboarding.balance.create" = "Новый кошелек"; +"onboarding.balance.import" = "Импорт кошелька"; +"onboarding.balance.watch" = "Просмотр кошелька"; // Manage Accounts -"manage_accounts.n_words" = "%@ words"; -"manage_accounts.n_words_with_passphrase" = "%@ words with passphrase"; +"manage_accounts.n_words" = "%@ слов"; +"manage_accounts.n_words_with_passphrase" = "%@ слов с кодовой фразой"; // Manage Account -"manage_account.name" = "Name"; -"manage_account.recovery_phrase" = "Recovery Phrase"; -"manage_account.public_keys" = "Public Keys"; -"manage_account.private_keys" = "Private Keys"; -"manage_account.backup_recovery_phrase" = "Manual Backup"; -"manage_account.cloud_backup_recovery_phrase" = "Backup to iCloud"; -"manage_account.cloud_delete_backup_recovery_phrase" = "Delete Backup from iCloud"; -"manage_account.manual_backup_required" = "You Need a Personal Backup!"; -"manage_account.manual_backup_required.description" = "Before you throw out the iCloud backup, be smart and write down your recovery phrase. It's like insurance for your wallet!"; -"manage_account.manual_backup_required.button" = "Do the Backup!"; -"manage_account.unlink" = "Cut Ties with Wallet"; -"manage_account.backup.no_backup_yet_description" = "Want to use the wallet? First, pick a way to back it up, just like you'd have a backup plan for a business deal."; -"manage_account.backup.has_backup_description" = "Always have a personal backup for each wallet. It's just good sense!"; -"manage_account.cloud_delete_backup_recovery_phrase.description" = "Are you sure you want to delete your wallet backup from iCloud?"; - +"manage_account.name" = "Название"; +"manage_account.recovery_phrase" = "Фраза восстановления"; +"manage_account.public_keys" = "Публичные ключи"; +"manage_account.private_keys" = "Приватные ключи"; +"manage_account.backup_recovery_phrase" = "Ручное резервное копирование"; +"manage_account.cloud_backup_recovery_phrase" = "Резерв. копирование в iCloud"; +"manage_account.cloud_delete_backup_recovery_phrase" = "Удалить резерв. копию из iCloud"; +"manage_account.manual_backup_required" = "Требуется резервное копирование вручную"; +"manage_account.manual_backup_required.description" = "Чтобы безопасно удалить резервную копию в iCloud, вам необходимо сначала сделать резервную копию фразы восстановления вручную."; +"manage_account.manual_backup_required.button" = "Сделать резервную копию"; +"manage_account.unlink" = "Отключить кошелек"; +"manage_account.backup.no_backup_yet_description" = "Завершите одну из опций резервного копирования кошелька, чтобы начать использовать кошелек."; +"manage_account.backup.has_backup_description" = "Рекомендуется создать ручную резервную копию для каждого кошелька."; + +"manage_account.cloud_delete_backup_recovery_phrase.description" = "Вы уверены, что хотите удалить резервную копию кошелька из iCloud?"; // Manage Account -> Public Keys -"public_keys.title" = "Public Keys"; -"public_keys.evm_address" = "EVM Address"; -"public_keys.evm_address.description" = "With this, you can peek (just peek!) at wallets on Ethereum, Binance Smart Chain, and those other EVM blockchains. Only the best blockchains!"; -"public_keys.account_extended_public_key" = "The Ultra Public Key for Accounts"; -"public_keys.account_extended_public_key.description" = "This lets you have a little sneak peek (read-only, of course!) at wallets holding the big ones like Bitcoin, and the others like Litecoin, Bitcoin Cash, Dash, and more. It's huge!"; +"public_keys.title" = "Публичные ключи"; +"public_keys.evm_address" = "EVM адрес"; +"public_keys.evm_address.description" = "Позволяет отслеживать кошельки с активами на Ethereum, Binance Smart Chain и других блокчейнах на базе EVM, только для чтения."; +"public_keys.account_extended_public_key" = "Account Extended Public Key"; +"public_keys.account_extended_public_key.description" = "Позволяет осуществлять мониторинг кошельков, содержащих Bitcoin и другие криптовалюты на основе UTXO (например, Litecoin, Bitcoin Cash, Dash и т.д.), только для чтения."; // Manage Account -> Private Keys -"private_keys.title" = "Private Keys"; -"private_keys.evm_private_key" = "EVM Private Key"; -"private_keys.evm_private_key.description" = "You've got the golden key! Total power over EVM cryptos like Ethereum and Binance Smart Chain in the wallet. It's like owning the Trump Tower of crypto!"; -"private_keys.bip32_root_key" = "The Ultimate BIP32 Key"; -"private_keys.bip32_root_key.description" = "This key? It's yuge! Complete command over everything in the wallet. It's the Art of the Deal of keys!"; -"private_keys.account_extended_private_key" = "The Super Private Key for Accounts"; -"private_keys.account_extended_private_key.description" = "With this, you're the boss of Bitcoin and those other coins like Litecoin, Bitcoin Cash, Dash, and more in the wallet. It's like being the president of cryptos!"; +"private_keys.title" = "Приватные ключи"; +"private_keys.evm_private_key" = "Приватный Ключ EVM"; +"private_keys.evm_private_key.description" = "Предоставляет полный контроль над криптовалютами на базе EVM, т.е. Ethereum, Binance Smart Chain и т.д. в пределах соответствующего кошелька."; +"private_keys.bip32_root_key" = "BIP32 Root Key"; +"private_keys.bip32_root_key.description" = "Предоставляет полный контроль над активами на соответствующем кошельке."; +"private_keys.account_extended_private_key" = "Account Extended Private Key"; +"private_keys.account_extended_private_key.description" = "Предоставляет полный контроль над Bitcoin и другими криптовалютами на базе UTXO, такими как Litecoin, Bitcoin Cash, Dash и т.д. в пределах соответствующего кошелька."; // Manage Account -> EVM Address -"evm_address.title" = "EVM Address"; +"evm_address.title" = "EVM адрес"; // Birthday Height -"birthday_height.title" = "Birthday Height"; +"birthday_height.title" = "Блок создания"; // Birthday Input -"birthday_input.title" = "Birthday Height"; -"birthday_input.description" = "Enter wallet's birthday height for faster synchronization."; -"birthday_input.new_wallet" = "New Wallet"; -"birthday_input.new_wallet.description" = "Doesn't have any transactions"; -"birthday_input.old_wallet" = "Existing Wallet"; -"birthday_input.old_wallet.description" = "Has transactions"; -"birthday_input.input_placeholder" = "%@ (optional)"; +"birthday_input.title" = "Блок создания"; +"birthday_input.description" = "Введите блок создания кошелька для ускорения синхронизации."; +"birthday_input.new_wallet" = "Новый кошелек"; +"birthday_input.new_wallet.description" = "Нет транзакций"; +"birthday_input.old_wallet" = "Существующий кошелек"; +"birthday_input.old_wallet.description" = "Есть транзакции"; +"birthday_input.input_placeholder" = "%@ (опционально)"; -"restore_setting.birthday_height" = "%@ Birthday Height"; -"restore_setting.download.disclaimer" = "The initial synchronization with the blockchain can consume a lot of internet traffic."; +"restore_setting.birthday_height" = "%@ Блок создания"; +"restore_setting.download.disclaimer" = "Первоначальная синхронизация с блокчейном может потреблять много интернет-трафика."; // EVM Network -"evm_network.rpc_source" = "RPC Source"; -"evm_network.added" = "Added"; -"evm_network.add_new" = "Add New"; +"evm_network.rpc_source" = "Источник RPC"; +"evm_network.added" = "Добавлен"; +"evm_network.add_new" = "Добавить новый"; // Add RPC Source -"add_evm_sync_source.title" = "Add RPC Source"; -"add_evm_sync_source.name" = "Name"; +"add_evm_sync_source.title" = "Добавить источник RPC"; +"add_evm_sync_source.name" = "Название"; "add_evm_sync_source.rpc_url" = "RPC URL"; -"add_evm_sync_source.basic_auth" = "Basic Auth (optional)"; -"add_evm_sync_source.warning.url_exists" = "This RPC Source URL? It's already in the club. No duplicates, folks!"; -"add_evm_sync_source.error.invalid_url" = "The URL you punched in? Not good! Make sure it starts with https or wss, otherwise it's a no-go. It's basic URL etiquette!"; +"add_evm_sync_source.basic_auth" = "Базовая авторизация (опц.)"; +"add_evm_sync_source.warning.url_exists" = "RPC Источник с таким url уже существует"; +"add_evm_sync_source.error.invalid_url" = "Введенный url неверен. Допустимый url должен иметь одну из следующих схем: http, https, ws, wss"; + // Send Settings -"evm_send_settings.nonce" = "Transaction Nonce"; -"evm_send_settings.nonce.info" = "The nonce? Think of it as a unique badge number for your transactions. Usually, it goes up one by one, like my ratings! But hey, if you're a smart cookie, you can use it to swap out a slow transaction with a faster one. Just make sure you're willing to pay top dollar in fees. Because, remember, if two transactions have the same badge number, only the one that's willing to tip the most gets the VIP treatment!";"evm_send_settings.nonce.errors.already_in_use" = "Used Nonce"; -"evm_send_settings.nonce.errors.already_in_use.info" = "An executed transaction with this nonce already exists."; - -"fee_settings" = "Advanced"; -"fee_settings.fee" = "Fee"; -"fee_settings.fee.info" = "Think of blockchains like a fancy club in Manhattan. When it's crowded, you gotta pay a premium to get in. So, when there's a lot of action on the network, fees go up. Big league!"; -"fee_settings.fee_rate" = "Fee Rate"; -"fee_settings.fee_rate.description" = "Here is the recommended value for hitting the next 2 blocks"; -"fee_settings.inputs_outputs" = "Inputs/Outputs"; -"fee_settings.transaction_settings" = "Transaction Settings"; -"fee_settings.transaction_settings.description" = "Want to move your Bitcoin with some mystery, like a billionaire in disguise? Tweak how the transactions are done, and you'll be harder to track!"; -"fee_settings.time_lock" = "Time Lock"; -"fee_settings.time_lock.description" = "TimeLock works only for sending to BIP44 addresses (starting with 1)"; - -"fee_settings.network_fee" = "Network Fee"; -"fee_settings.network_fee.info" = "The estimated cost of sending given transaction on the network."; +"evm_send_settings.nonce" = "Nonce транзакции"; +"evm_send_settings.nonce.info" = "Nonce - это уникальное целочисленное значение для транзакции в кошельке пользователя. Обычно оно увеличивается с каждой транзакцией и не нуждается в изменении. Продвинутые пользователи могут установить его равным nonce отложенной транзакции, чтобы отменить и заменить эту транзакцию, при условии, что новая транзакция имеет достаточно большую плату, чтобы старая не была подтверждена вместо нее (например, они могут захотеть ускорить ее подтверждение или полностью изменить параметры транзакции). Когда несколько ожидающих транзакций имеют одинаковый nonce, подтверждается только одна, обычно с наибольшей комиссией. +"; +"evm_send_settings.nonce.errors.already_in_use" = "Использованый Nonce"; +"evm_send_settings.nonce.errors.already_in_use.info" = "Выполненная транзакция с таким nonce уже существует."; + +"fee_settings" = "Доп. настройки"; +"fee_settings.fee" = "Комиссия"; +"fee_settings.fee.info" = "Блокчейн требует от пользователей оплаты сетевых сборов при отправке транзакций. Эти сборы выше, когда в сети происходит много транзакций."; +"fee_settings.fee_rate" = "Комиссия"; +"fee_settings.fee_rate.description" = "Это рекомендуемое значение для попадания в следующие 2 блока"; +"fee_settings.inputs_outputs" = "Вводы/Выводы"; +"fee_settings.transaction_settings" = "Настройки транзакций"; +"fee_settings.transaction_settings.description" = "Затрудните отслеживание Биткойн-транзакций, изменив их структурирование."; +"fee_settings.time_lock" = "TimeLock"; +"fee_settings.time_lock.description" = "TimeLock работает только для отправки на адреса BIP44 (начинаются с 1) +"; + +"fee_settings.network_fee" = "Комиссия сети"; +"fee_settings.network_fee.info" = "Ориентировочная стоимость отправки данной транзакции по сети."; "fee_settings.gas_limit" = "Gas Limit"; -"fee_settings.gas_limit.info" = "You know, transactions have their own kind of 'energy', and they call it 'gas'. Think of the Gas Limit as the biggest energy drink you'd need for a transaction. But usually, you'll sip less than what you think."; +"fee_settings.gas_limit.info" = "Единица сложности транзакций - газ, она варьирует в зависимости от выполняемого смарт-контракта. Лимит газа - это предполагаемый максимальный размер газа, необходимый для выполнения смарт-контракта. Обычно используется меньшее количество газа."; "fee_settings.gas_price" = "Gas Price"; -"fee_settings.gas_price.info" = "So here's the deal: doing business on the network costs 'gas'. The Gas Price? It's like your bid at an auction - how much you're willing to drop for each gas unit. If the network's buzzing like one of my rallies, prices soar. When it's calm, they dip. And hey, if you're stingy on the gas price, don't be surprised if your transaction hangs around longer than expected!"; - -"fee_settings.base_fee" = "Base Fee"; -"fee_settings.base_fee.info" = "So, every block has a ticket price set by the big network – they call it the base fee rate. Think of it as a fluctuating stock; sometimes up, sometimes down, but never more than 12.5% change. This ticket price changes based on how packed the network is. The number you're seeing? That's the current rate. Very transparent!"; -"fee_settings.max_fee_rate" = "Max Fee Rate"; -"fee_settings.max_fee_rate.info" = "This is your ceiling - the max you're willing to spend per gas unit. It's gotta cover the basic fee and the special priority fee. The number here? It's a clever estimate, but usually, you'll end up spending less. Cut corners on this and you might be waiting a long time, or even get your transaction stuck in limbo!"; -"fee_settings.tips" = "Max Priority Fee"; -"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; - - -"fee_settings.errors.insufficient_balance" = "Insufficient balance"; -"fee_settings.errors.unexpected_error" = "Unexpected Error"; -"fee_settings.errors.insufficient_balance.info" = "Listen, your %@ stash is looking a bit thin for this transaction, especially when we throw in the fee."; -"fee_settings.errors.low_max_fee" = "Low Fee"; -"fee_settings.errors.low_max_fee.info" = "That fee you set? Way too low for the big league transaction you're trying to do!"; -"fee_settings.errors.nonce_already_in_block" = "Too late, already made the move!"; -"fee_settings.errors.replacement_transaction_underpriced" = "Trying to switch things up? Your fee's still too skimpy!"; -"fee_settings.errors.transaction_underpriced" = "That's a bargain-bin fee!"; -"fee_settings.errors.tips_higher_than_max_fee" = "Your max fee? It's not gonna cut it!"; -"fee_settings.errors.zero_amount.info" = "Cannot transfer 0 TRX"; -"fee_settings.warning.risk_of_getting_stuck" = "Risky"; -"fee_settings.warning.risk_of_getting_stuck.info" = "This might take a while... or, who knows, might not even happen at all!"; -"fee_settings.warning.overpricing" = "Fee Too High"; -"fee_settings.warning.overpricing.info" = "Your fee is looking more lavish than Mar-a-Lago! Way more than what you need for this."; +"fee_settings.gas_price.info" = "Комиссия за сетевую транзакцию измеряется в единицах газа. Цена газа – это сумма, которую пользователь готов заплатить за единицу газа. Когда сеть занята, цены на газ повышаются, а во время простоя они наоборот снижаются. Слишком низкая цена газа зачастую является основанием для длительного сохранения отложенного статуса транзакции."; + +"fee_settings.base_fee" = "Базовая комиссия"; +"fee_settings.base_fee.info" = "Сетевой протокол определяет базовую стоимость газа для каждого блока, которая называется базовая ставка комиссии. Он варьирует в зависимости от уровня загрузки сети от блока к блоку. В следующем блоке он может увеличиться или уменьшиться не более чем на 12.5%, что делает взимаемый тариф более предсказуемым. Здесь отражена базовая ставка комиссии текущего блока."; +"fee_settings.max_fee_rate" = "Макс. ставка комиссии"; +"fee_settings.max_fee_rate.info" = "Это максимальная общая цена газа, которую пользователь готов заплатить. Она должна покрывать базовую тарифную ставку сети и максимальный приоритетный тариф. Указанное здесь значение предлагается на основе оценки суммы базовой тарифной ставки следующего блока и максимального приоритетного тарифа, выбранного пользователем. Фактический размер платёжного тарифа обычно меньше. Снижение настройки текущего базового тарифа ограничит оплачиваемую сумму тарифа, но приведет к удлинению периода ожидания подтверждения транзакции или даже к ее подвисанию."; +"fee_settings.tips" = "Макс. приоритет. комиссия"; +"fee_settings.tips.info" = "Пользователи платят приоритетную комиссию, в целях ускорения процесса подтверждения транзакции. Иногда эти тарифы называются чаевыми. Максимальный размер приоритетного сбора - это максимальная дополнительная цена за газ, которую пользователь готов оплатить поверх базовой ставки комиссии. Приведенная здесь стоимость определяется исходя из прогнозируемых сетевых условий. Фактический размер приоритетного тарифа будет меньше. Обнуление этого тарифа может привести к удлинению периода ожидания подтверждения транзакции ввиду её размещения в конце очереди незавершенных транзакций ото всех пользователей."; + +"fee_settings.errors.insufficient_balance" = "Недостаточный баланс"; +"fee_settings.errors.unexpected_error" = "Непредвиденная ошибка"; +"fee_settings.errors.insufficient_balance.info" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; +"fee_settings.errors.low_max_fee" = "Низкая комиссия"; +"fee_settings.errors.low_max_fee.info" = "Установленная сумма комиссии недостаточно для обработки этой транзакции."; +"fee_settings.errors.nonce_already_in_block" = "Не удается заменить транзакцию"; +"fee_settings.errors.replacement_transaction_underpriced" = "Низкая комиссия на замену транзакции"; +"fee_settings.errors.transaction_underpriced" = "Низкая комиссия за транзакцию"; +"fee_settings.errors.tips_higher_than_max_fee" = "Слишком низкая макс. комиссия"; +"fee_settings.errors.zero_amount.info" = "Невозможно отправить 0 TRX"; +"fee_settings.warning.risk_of_getting_stuck" = "Риски"; +"fee_settings.warning.risk_of_getting_stuck.info" = "Обработка транзакции может отложиться на некоторое время, или транзакцию могут полностью отклонить."; +"fee_settings.warning.overpricing" = "Слишком высокая комиссия"; +"fee_settings.warning.overpricing.info" = "Установленная комиссия за транзакцию больше чем требуется для обработки этой транзакции."; // Watch Address -"watch_address.title" = "Watch Wallet"; -"watch_address.watch" = "Next"; -"watch_address.address" = "Address"; -"watch_address.by" = "By"; -"watch_address.watch_by" = "Watch By"; -"watch_address.evm_address" = "EVM Address"; -"watch_address.tron_address" = "TRON Address"; +"watch_address.title" = "Просмотр кошелька"; +"watch_address.watch" = "Далее"; +"watch_address.address" = "Адрес"; +"watch_address.by" = "Из"; +"watch_address.watch_by" = "Смотреть из"; +"watch_address.evm_address" = "EVM адрес"; +"watch_address.tron_address" = "TRON адрес"; "watch_address.public_key" = "Account xPubKey"; -"watch_address.public_key.placeholder" = "Enter Account Extended Public Key "; -"watch_address.public_key.invalid_key" = "Invalid Key"; -"watch_address.choose_blockchain" = "Choose Blockchain"; -"watch_address.choose_coin" = "Choose Coin"; +"watch_address.public_key.placeholder" = "Введите Account Extended Public Key"; +"watch_address.public_key.invalid_key" = "Неверный ключ"; +"watch_address.choose_blockchain" = "Выбрать блокчейн"; +"watch_address.choose_coin" = "Выберите токен"; // Nft Collections -"nft_collections.title" = "NFTs"; -"nft_collections.price_mode" = "Price Mode"; -"nft_collections.last_sale" = "Last Sale"; -"nft_collections.average_7d" = "Average 7D"; -"nft_collections.average_30d" = "Average 30D"; -"nft_collections.on_sale" = "On Sale"; -"nft_collections.empty" = "You don’t have any NFTs in your wallet"; +"nft_collections.title" = "NFT"; +"nft_collections.price_mode" = "Режим цены"; +"nft_collections.last_sale" = "Последняя продажа"; +"nft_collections.average_7d" = "Средн. 7Д"; +"nft_collections.average_30d" = "Средн. 30Д"; +"nft_collections.on_sale" = "В продаже"; +"nft_collections.empty" = "В вашем кошельке нет NFT"; -"top_nft_collections.title" = "Top NFT Collections"; -"top_nft_collections.description" = "Leading NFT collections by trading volume."; +"top_nft_collections.title" = "Топ NFT коллекции"; +"top_nft_collections.description" = "Ведущие коллекции NFT по объему торгов."; // Nft Asset -"nft_asset.tab.overview" = "Overview"; -"nft_asset.tab.activity" = "Activity"; - -"nft_asset.last_sale" = "Last Sale"; -"nft_asset.average_7d" = "7 Day Average"; -"nft_asset.average_30d" = "30 Day Average"; -"nft_asset.floor_price" = "Floor Price"; -"nft_asset.on_sale" = "On Sale"; -"nft_asset.on_auction" = "On Auction"; -"nft_asset.until_date" = "until %@"; -"nft_asset.buy_now" = "Buy Now"; -"nft_asset.minimum_bid" = "Minimum Bid"; -"nft_asset.best_offer" = "Best Offer"; -"nft_asset.properties" = "Properties"; -"nft_asset.description" = "Description"; -"nft_asset.details" = "Details"; -"nft_asset.details.contract_address" = "Contract Address"; -"nft_asset.details.token_id" = "Token ID"; -"nft_asset.details.token_standard" = "Token Standard"; +"nft_asset.tab.overview" = "Обзор"; +"nft_asset.tab.activity" = "Активность"; + +"nft_asset.last_sale" = "Последняя продажа"; +"nft_asset.average_7d" = "Среднее за 7 дней"; +"nft_asset.average_30d" = "Среднее за 30 дней"; +"nft_asset.floor_price" = "Минимальная цена"; +"nft_asset.on_sale" = "В продаже"; +"nft_asset.on_auction" = "На аукционе"; +"nft_asset.until_date" = "до %@"; +"nft_asset.buy_now" = "Купить сейчас"; +"nft_asset.minimum_bid" = "Мин. cтавка"; +"nft_asset.best_offer" = "Лучшее предложение"; +"nft_asset.properties" = "Параметры"; +"nft_asset.description" = "Описание"; +"nft_asset.details" = "Подробности"; +"nft_asset.details.contract_address" = "Адрес контракта"; +"nft_asset.details.token_id" = "ID токена"; +"nft_asset.details.token_standard" = "Стандартный токен"; "nft_asset.details.blockchain" = "Blockchain"; -"nft_asset.links" = "Links"; -"nft_asset.links.website" = "Website"; -"nft_asset.options.save_to_photos" = "Save to Photos"; -"nft_asset.options.set_as_watch_face" = "Set as Watch Face"; -"nft_asset.save_to_photos.failed" = "Failed to save NFT image"; +"nft_asset.links" = "Ссылки"; +"nft_asset.links.website" = "Веб-сайт"; +"nft_asset.options.save_to_photos" = "Сохранить в фото"; +"nft_asset.options.set_as_watch_face" = "Установить как Watch Face"; +"nft_asset.save_to_photos.failed" = "Не удалось сохранить NFT-изображение"; // Nft Collection -"nft_collection.tab.overview" = "Overview"; -"nft_collection.tab.assets" = "Items"; -"nft_collection.tab.activity" = "Activity"; - -"nft_collection.overview.description" = "Description"; -"nft_collection.overview.contracts" = "Contracts"; -"nft_collection.overview.links" = "Links"; -"nft_collection.overview.links.website" = "Website"; - - -"nft_collection.overview.owners" = "Owners"; -"nft_collection.overview.items" = "Items"; -"nft_collection.overview.24h_volume" = "24h Volume"; -"nft_collection.overview.today_sellers" = "Today's Sellers"; -"nft_collection.overview.floor_price" = "Floor Price"; -"nft_collection.overview.all_time_average" = "All time average"; -"nft_collection.overview.royalty" = "Royalty"; -"nft_collection.overview.inception_date" = "Inception Date"; - -"nft.activity.contracts" = "Contracts"; -"nft.activity.empty_list" = "No item activity yet"; -"nft.activity.event_types" = "Event Types"; -"nft.activity.event_type.all" = "All Events"; -"nft.activity.event_type.sale" = "Sale"; -"nft.activity.event_type.transfer" = "Transfer"; -"nft.activity.event_type.mint" = "Mint"; -"nft.activity.event_type.list" = "List"; -"nft.activity.event_type.listCancel" = "List Cancel"; -"nft.activity.event_type.offer" = "Offer"; -"nft.activity.event_type.offerCancel" = "Offer Cancel"; +"nft_collection.tab.overview" = "Обзор"; +"nft_collection.tab.assets" = "Элементы"; +"nft_collection.tab.activity" = "Активность"; + +"nft_collection.overview.description" = "Описание"; +"nft_collection.overview.contracts" = "Контракты"; +"nft_collection.overview.links" = "Ссылки"; +"nft_collection.overview.links.website" = "Веб-сайт"; + + +"nft_collection.overview.owners" = "Владельцы"; +"nft_collection.overview.items" = "Элементы"; +"nft_collection.overview.24h_volume" = "Объем торгов (24ч)"; +"nft_collection.overview.today_sellers" = "Продавцы сегодня"; +"nft_collection.overview.floor_price" = "Минимальная цена"; +"nft_collection.overview.all_time_average" = "Среднее за все время"; +"nft_collection.overview.royalty" = "Вознаграждение"; +"nft_collection.overview.inception_date" = "Дата старта"; + +"nft.activity.contracts" = "Контракты"; +"nft.activity.empty_list" = "Нет активности"; +"nft.activity.event_types" = "Типы событий"; +"nft.activity.event_type.all" = "Все события"; +"nft.activity.event_type.sale" = "Продажа"; +"nft.activity.event_type.transfer" = "Перевод"; +"nft.activity.event_type.mint" = "Минт"; +"nft.activity.event_type.list" = "Залистить"; +"nft.activity.event_type.listCancel" = "Убрать из листинга"; +"nft.activity.event_type.offer" = "Предложение"; +"nft.activity.event_type.offerCancel" = "Предложение отменено"; // Subscription Info -"subscription_info.title" = "Premium Features"; -"subscription_info.info1.title" = "Cryptocurrency Analytics"; -"subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.info2.title" = "Chart Indicators"; -"subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.info3.title" = "Personal Support"; -"subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.get_premium" = "Get Premium"; -"subscription_info.already_have" = "I already have Premium"; +"subscription_info.title" = "Премиум-функции"; +"subscription_info.info1.title" = "Анализ криптовалют"; +"subscription_info.info1.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.info2.title" = "Индикаторы"; +"subscription_info.info2.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.info3.title" = "Персональная поддержка"; +"subscription_info.info3.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.get_premium" = "Получите Премиум"; +"subscription_info.already_have" = "У меня уже есть Премиум"; // Activate Subscription -"activate_subscription.title" = "Activate"; -"activate_subscription.wallet" = "Wallet"; -"activate_subscription.address" = "Address"; -"activate_subscription.message" = "Message to Sign"; -"activate_subscription.sign" = "Sign"; -"activate_subscription.activating" = "Activating..."; -"activate_subscription.failed_to_activate" = "Failed to activate subscription"; -"activate_subscription.activated" = "Activated"; -"activate_subscription.no_subscriptions" = "Your wallet address does not have a subscription to premium features, you need to purchase it to activate the subscription."; +"activate_subscription.title" = "Активировать"; +"activate_subscription.wallet" = "Кошелек"; +"activate_subscription.address" = "Адрес"; +"activate_subscription.message" = "Сообщение для подписи"; +"activate_subscription.sign" = "Подписать"; +"activate_subscription.activating" = "Активация..."; +"activate_subscription.failed_to_activate" = "Не удалось активировать подписку"; +"activate_subscription.activated" = "Активировано"; +"activate_subscription.no_subscriptions" = "Адрес вашего кошелька не имеет подписки на премиум-функции. Вам нужно его приобрести, чтобы активировать подписку."; // Launch -"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting app or report the error to our support team."; -"launch.failed_to_launch.report" = "Report"; +"launch.failed_to_launch" = "Не удалось запустить приложение из-за внутренней ошибки. Пожалуйста, попробуйте перезапустить приложение или сообщите об ошибке нашей службе поддержки."; +"launch.failed_to_launch.report" = "Пожаловаться"; // Tron -"tron.send.activation_fee" = "Activation Fee"; -"tron.send.resources_consumed" = "Resources Consumed"; +"tron.send.activation_fee" = "Комиссия за активацию"; +"tron.send.resources_consumed" = "Расходуемые ресурсы"; "tron.send.bandwidth" = "Bandwidth"; "tron.send.energy" = "Energy"; -"tron.send.fee.info" = "Think of this as the ticket price for your transaction's grand premiere on the network. And remember, this includes the full experience – the Energy, Bandwidth, and that special Activation fee!"; -"tron.send.resources_consumed.info" = "Picture Bandwidth as the amount of space your transaction takes up in the blockchain’s exclusive club.\n\nNow, Energy? That's the juice that powers the behind-the-scenes magic, especially when the TRON virtual machine starts performing its signature moves.\n\nWhen you call on a smart contract to dazzle with its performance, it needs a bit of Energy to shine. Hence, the energy fee!"; -"tron.send.activation_fee.info" = "Sending some TRX or TRC-10 tokens to a dormant address? Consider it your magic touch to awaken and activate that account!"; -"tron.send.inactive_address" = "Psst... this address is still in slumber mode."; +"tron.send.fee.info" = "Ориентировочные затраты на отправку данной транзакции по сети (без учёта energy, Bandwidth и комиссии за активацию) предполагаются"; +"tron.send.resources_consumed.info" = "Bandwidth - это единица, которая измеряет размер байт транзакции, хранящихся в базе данных блокчейна. Чем больше транзакция, тем больше bandwidth будет потребляться.\n\nEnergy — это единица, измеряющая количество вычислений, требуемых виртуальной машиной TRON для выполнения конкретных операций в сети TRON.\n\nПоскольку транзакции смарт-контракта требуют выполнения вычислительных ресурсов, каждая сделка по контракту требует оплаты energy комиссии."; +"tron.send.activation_fee.info" = "Передача токенов TRX или TRC-10 на адрес неактивного аккаунта активирует его."; +"tron.send.inactive_address" = "Этот адрес не активен"; // Cex Coin Select -"cex_coin_select.title" = "Choose Coin"; -"cex_coin_select.withdraw" = "Withdraw"; -"cex_coin_select.withdraw.empty" = "You have no assets to withdraw."; -"cex_coin_select.search_placeholder" = "Coin Code or Name"; -"cex_coin_select.suspended" = "Suspended"; +"cex_coin_select.title" = "Выберите токен"; +"cex_coin_select.withdraw" = "Вывод средств"; +"cex_coin_select.withdraw.empty" = "У вас нет активов для вывода."; +"cex_coin_select.search_placeholder" = "Код или имя"; +"cex_coin_select.suspended" = "Приостановлено"; // Cex Deposit Network Select -"cex_deposit_network_select.title" = "Choose Network"; -"cex_deposit_network_select.description" = "Choose a network and get an address to deposit."; +"cex_deposit_network_select.title" = "Выберите сеть"; +"cex_deposit_network_select.description" = "Выберите сеть и получите адрес для пополнения."; // Cex Deposit -"cex_deposit.title" = "Deposit %@"; -"cex_deposit.address" = "Address"; +"cex_deposit.title" = "Депозит %@"; +"cex_deposit.address" = "Адрес"; -"cex_deposit.network" = "Network"; +"cex_deposit.network" = "Сеть"; "cex_deposit.memo" = "Memo (Tag)"; -"cex_deposit.min_amount" = "Min. Amount"; -"cex_deposit.warning_memo" = "Memo (tag) is required, or your will lose your coins."; -"cex_deposit.copy_address" = "Copy"; -"cex_deposit.share_address" = "Share"; -"cex_deposit.failed" = "Failed to load deposit address. Please retry."; -"cex_deposit.memo_warning.title" = "Important"; -"cex_deposit.memo_warning.description" = "Both a memo (tag) and the address are needed to ensure that assets are received. Otherwise your funds will be lost."; +"cex_deposit.min_amount" = "Мин. сумма"; +"cex_deposit.warning_memo" = "Необходимо memo (tag), или вы потеряете ваши монеты."; +"cex_deposit.copy_address" = "Копировать"; +"cex_deposit.share_address" = "Поделиться"; +"cex_deposit.failed" = "Не удалось загрузить адрес депозита. Повторите попытку."; +"cex_deposit.memo_warning.title" = "Важно"; +"cex_deposit.memo_warning.description" = "Для получения активов необходимы memo(tag) и адрес. В противном случае Ваши средства будут утеряны."; // Cex Widthdraw -"cex_withdraw.network" = "Network"; -"cex_withdraw.network_warning" = "Ensure the network matches the withdrawal address and the deposit platform supports it, or assets may be lost."; -"cex_withdraw.fee" = "Fee"; -"cex_withdraw.fee_from_amount" = "Fee from amount"; -"cex_withdraw.error.insufficient_funds" = "Not enough available balance"; -"cex_withdraw.error.max_amount_violated" = "The maximum amount you can withdraw is %@"; -"cex_withdraw.error.min_amount_violated" = "The minimum amount you can withdraw is %@"; -"cex_withdraw.address_required" = "Address is required"; +"cex_withdraw.network" = "Сеть"; +"cex_withdraw.network_warning" = "Убедитесь, что сеть совпадает с адресом снятия, а платформа депозита поддерживает его, или активы могут быть потеряны."; +"cex_withdraw.fee" = "Комиссия"; +"cex_withdraw.fee_from_amount" = "Комиссия от суммы"; +"cex_withdraw.error.insufficient_funds" = "Недостаточно доступных средств"; +"cex_withdraw.error.max_amount_violated" = "Макс.сумма вывода: %@"; +"cex_withdraw.error.min_amount_violated" = "Мин. сумма, которую вы можете вывести - %@"; +"cex_withdraw.address_required" = "Адрес обязателен"; // Cex Withdraw Network Select -"cex_withdraw_network_select.title" = "Network"; -"cex_withdraw_network_select.description" = "Ensure the network matches the withdrawal address and the deposit platform supports it."; +"cex_withdraw_network_select.title" = "Сеть"; +"cex_withdraw_network_select.description" = "Убедитесь, что сеть совпадает с адресом снятия и платформа депозита поддерживает его."; // Cex Withdraw Confirm -"cex_withdraw_confirm.you_withdraw" = "You Withdraw"; -"cex_withdraw_confirm.network" = "Network"; -"cex_withdraw_confirm.withdraw" = "Withdraw"; -"cex_withdraw_confirm.withdraw_failed" = "Withdraw failed"; +"cex_withdraw_confirm.you_withdraw" = "Вывод средств"; +"cex_withdraw_confirm.network" = "Сеть"; +"cex_withdraw_confirm.withdraw" = "Вывод средств"; +"cex_withdraw_confirm.withdraw_failed" = "Вывод средств не выполнен"; From 2c05e5744732591c0963550ef544e59559f52b48 Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 2 Oct 2023 15:58:36 +0600 Subject: [PATCH 33/63] Add auto-lock period setting to Security settings --- .../project.pbxproj | 6 +++ .../Core/Managers/LockManager.swift | 11 +++++- .../Models/AutoLockPeriod.swift | 25 ++++++++++++ .../ExperimentalFeaturesView.swift | 6 +-- .../Security/SecuritySettingsView.swift | 38 +++++++++++++++++++ .../Security/SecuritySettingsViewModel.swift | 7 ++++ .../SimpleActivate/SimpleActivateView.swift | 5 +-- .../UserInterface/SwiftUI/ThemeView.swift | 5 +-- .../en.lproj/Localizable.strings | 8 ++++ 9 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Models/AutoLockPeriod.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index babd88d801..345923896a 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -106,6 +106,7 @@ 11B3511B03A70BEA48637907 /* MarkdownListItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357E9508BF369BDFF7753 /* MarkdownListItemCell.swift */; }; 11B3511CB3C3FDAF362D8315 /* GuidesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D9767615D8FBF7A314F /* GuidesManager.swift */; }; 11B3511CD62516A146124350 /* TextDropDownAndSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C62F476065C11EE049 /* TextDropDownAndSettingsView.swift */; }; + 11B3511DAD3881FDE2419A64 /* AutoLockPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E41142BD3D2FF59BAE7 /* AutoLockPeriod.swift */; }; 11B3512144B36B8E7D4E4CD8 /* ShortcutInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1C200EC15159154E3F /* ShortcutInputCell.swift */; }; 11B3512244F96B6AE1399C9C /* NftUid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351924AF4DA7A0BC6D1A1 /* NftUid.swift */; }; 11B351245348DFCE20EC6D4B /* PlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E7C8DEDFA0A9B5993B7 /* PlaceholderCell.swift */; }; @@ -727,6 +728,7 @@ 11B358C72B4E7F70331084AA /* SendEvmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35113CB935A0E54504C1C /* SendEvmViewController.swift */; }; 11B358CB129212E2A0E455E4 /* MarketAdvancedSearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */; }; 11B358D1687049E5DACEBC96 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352884D47E0B23DCF2C2C /* AppManager.swift */; }; + 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E41142BD3D2FF59BAE7 /* AutoLockPeriod.swift */; }; 11B358D913A404C1DA7D4E0E /* CoinInvestorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */; }; 11B358DC6827FC6035BF3225 /* TokenQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353684493AFDF3711DF2B /* TokenQuery.swift */; }; 11B358DC90F3372DB98BD4A5 /* CexDepositNetworkSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DDED1BC5B541DB6B4B3 /* CexDepositNetworkSelectViewModel.swift */; }; @@ -3340,6 +3342,7 @@ 11B35E3DD6021EEB699A8EBF /* ReceiveService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveService.swift; sourceTree = ""; }; 11B35E3F01D5A5CFE5A4E94B /* EvmMethodLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmMethodLabel.swift; sourceTree = ""; }; 11B35E4058159A4FE60A3F53 /* CoinMarketsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMarketsService.swift; sourceTree = ""; }; + 11B35E41142BD3D2FF59BAE7 /* AutoLockPeriod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoLockPeriod.swift; sourceTree = ""; }; 11B35E4B97A593E898724335 /* EvmNftRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmNftRecord.swift; sourceTree = ""; }; 11B35E511F9D2B6C65792324 /* GuidesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidesViewController.swift; sourceTree = ""; }; 11B35E5C80435645132BCDD2 /* EvmUpdateStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmUpdateStatus.swift; sourceTree = ""; }; @@ -5080,6 +5083,7 @@ 11B35B617A9CE668EEF4978B /* AmountData.swift */, 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */, ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */, + 11B35E41142BD3D2FF59BAE7 /* AutoLockPeriod.swift */, ); path = Models; sourceTree = ""; @@ -9296,6 +9300,7 @@ 11B3595AD0AA7108CAC814CC /* DuressModeModule.swift in Sources */, 11B356A35A5981DD231E580C /* ListStyle.swift in Sources */, 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */, + 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10607,6 +10612,7 @@ 11B3586F6BFCA16BDFD5921D /* DuressModeModule.swift in Sources */, 11B353577381981235B90A82 /* ListStyle.swift in Sources */, 11B354DC983042AD922339A6 /* DuressModeSelectView.swift in Sources */, + 11B3511DAD3881FDE2419A64 /* AutoLockPeriod.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift index bebedf0b70..e9f267db2e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/LockManager.swift @@ -5,14 +5,19 @@ import StorageKit class LockManager { private let lastExitDateKey = "last_exit_date_key" + private let autoLockPeriodKey = "auto-lock-period" private let passcodeManager: PasscodeManager private let localStorage: ILocalStorage private let delegate: LockDelegate - private let lockTimeout: Double = 60 private(set) var isLocked: Bool + var autoLockPeriod: AutoLockPeriod { + didSet { + localStorage.set(value: autoLockPeriod.rawValue, for: autoLockPeriodKey) + } + } init(passcodeManager: PasscodeManager, localStorage: ILocalStorage, delegate: LockDelegate) { self.passcodeManager = passcodeManager @@ -20,6 +25,8 @@ class LockManager { self.delegate = delegate isLocked = passcodeManager.isPasscodeSet + let autoLockPeriodRaw: String? = localStorage.value(for: autoLockPeriodKey) + autoLockPeriod = autoLockPeriodRaw.flatMap { AutoLockPeriod(rawValue: $0) } ?? .minute1 } } @@ -40,7 +47,7 @@ extension LockManager { let exitTimestamp: TimeInterval = localStorage.value(for: lastExitDateKey) ?? 0 let now = Date().timeIntervalSince1970 - guard now - exitTimestamp > lockTimeout else { + guard now - exitTimestamp > autoLockPeriod.period else { return } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AutoLockPeriod.swift b/UnstoppableWallet/UnstoppableWallet/Models/AutoLockPeriod.swift new file mode 100644 index 0000000000..0dd3d7d7e4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/AutoLockPeriod.swift @@ -0,0 +1,25 @@ +import Foundation + +enum AutoLockPeriod: String, CaseIterable { + case immediate + case minute1 + case minute5 + case minute15 + case minute30 + case hour1 + + var title: String { + "auto_lock.\(rawValue)".localized + } + + var period: TimeInterval { + switch self { + case .immediate: return 0 + case .minute1: return 60 + case .minute5: return 5 * 60 + case .minute15: return 15 * 60 + case .minute30: return 30 * 60 + case .hour1: return 60 * 60 + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/ExperimentalFeatures/ExperimentalFeaturesView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/ExperimentalFeatures/ExperimentalFeaturesView.swift index 120559076c..e6d44a1704 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/ExperimentalFeatures/ExperimentalFeaturesView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/ExperimentalFeatures/ExperimentalFeaturesView.swift @@ -1,7 +1,6 @@ import SwiftUI struct ExperimentalFeaturesView: View { - var body: some View { ScrollableThemeView { VStack(spacing: .margin24) { @@ -16,9 +15,8 @@ struct ExperimentalFeaturesView: View { } } } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .navigationTitle("settings.experimental_features.title".localized) + .navigationTitle("settings.experimental_features.title".localized) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index 2ada24f12f..b91e8cdfd2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -55,6 +55,17 @@ struct SecuritySettingsView: View { } } + ListSection { + NavigationRow(destination: { + AutoLockView(period: $viewModel.autoLockPeriod) + }) { + Image("lock_24").themeIcon() + Text("settings_security.auto_lock".localized).themeBody() + Text(viewModel.autoLockPeriod.title).themeSubhead1(alignment: .trailing).padding(.trailing, -.margin8) + Image.disclosureIcon + } + } + VStack(spacing: 0) { ListSection { ListRow { @@ -176,4 +187,31 @@ struct SecuritySettingsView: View { self } } + + private struct AutoLockView: View { + @Binding var period: AutoLockPeriod + @Environment(\.presentationMode) private var presentationMode + + var body: some View { + ScrollableThemeView { + ListSection { + ForEach(AutoLockPeriod.allCases, id: \.self) { period in + ClickableRow(action: { + self.period = period + presentationMode.wrappedValue.dismiss() + }) { + Text(period.title).themeBody() + + if self.period == period { + Image.checkIcon + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .navigationTitle("settings_security.auto_lock".localized) + .navigationBarTitleDisplayMode(.inline) + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift index 570d4d36eb..1b55fc7ff5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsViewModel.swift @@ -12,6 +12,12 @@ class SecuritySettingsViewModel: ObservableObject { @Published var isDuressPasscodeSet: Bool @Published var biometryType: BiometryType? + @Published var autoLockPeriod: AutoLockPeriod { + didSet { + lockManager.autoLockPeriod = autoLockPeriod + } + } + @Published var isBiometryToggleOn: Bool { didSet { if isBiometryToggleOn != biometryManager.biometryEnabled, isPasscodeSet { @@ -36,6 +42,7 @@ class SecuritySettingsViewModel: ObservableObject { isPasscodeSet = passcodeManager.isPasscodeSet isDuressPasscodeSet = passcodeManager.isDuressPasscodeSet biometryType = biometryManager.biometryType + autoLockPeriod = lockManager.autoLockPeriod isBiometryToggleOn = biometryManager.biometryEnabled balanceAutoHide = balanceHiddenManager.balanceAutoHide diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift index 68be8559ed..7c76736706 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift @@ -21,9 +21,8 @@ struct SimpleActivateView: View { ListSectionFooter(text: description) } } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .navigationTitle(title) + .navigationTitle(title) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeView.swift index 5798a9168e..fefe2a12a7 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeView.swift @@ -9,7 +9,6 @@ struct ThemeView: View { content } } - } struct ScrollableThemeView: View { @@ -22,7 +21,6 @@ struct ScrollableThemeView: View { } } } - } struct ThemeNavigationView: View { @@ -32,7 +30,6 @@ struct ThemeNavigationView: View { NavigationView { content } - .accentColor(.themeJacob) + .accentColor(.themeJacob) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 9e90d6bf72..84030fcf24 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -97,6 +97,13 @@ "face_id" = "Face ID"; "touch_id" = "Touch ID"; +"auto_lock.immediate" = "Immediate"; +"auto_lock.minute1" = "1 minute"; +"auto_lock.minute5" = "5 minutes"; +"auto_lock.minute15" = "15 minutes"; +"auto_lock.minute30" = "30 minutes"; +"auto_lock.hour1" = "1 hour"; + // Access Camera "access_camera.message" = "%@ needs access to your camera to scan the QR code. @@ -1082,6 +1089,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.enable_passcode" = "Enable Passcode"; "settings_security.edit_passcode" = "Edit Passcode"; "settings_security.disable_passcode" = "Disable Passcode"; +"settings_security.auto_lock" = "Auto-Lock"; "settings_security.balance_auto_hide" = "Balance Auto Hide"; "settings_security.balance_auto_hide.description" = "Automatically hides balance each time the app is opened, regardless of previous preferences."; "settings_security.enable_duress_mode" = "Set Duress Mode"; From b9c154dc1174b01b39ec63e65e47371db104055c Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 2 Oct 2023 17:38:54 +0600 Subject: [PATCH 34/63] Allow using the same password when editing it --- .../Manage/EditDuressPasscodeViewModel.swift | 4 ++++ .../Manage/EditPasscodeViewModel.swift | 4 ++++ .../Passcode/Manage/SetPasscodeViewModel.swift | 18 +++++++++--------- .../Passcode/Unlock/BaseUnlockViewModel.swift | 11 +++++------ 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift index 46aa676d85..ccf9ad81fa 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditDuressPasscodeViewModel.swift @@ -13,6 +13,10 @@ class EditDuressPasscodeViewModel: SetPasscodeViewModel { "edit_duress_passcode.confirm_new_passcode".localized } + override func isCurrent(passcode: String) -> Bool { + passcodeManager.isValid(duressPasscode: passcode) + } + override func onEnter(passcode: String) { do { try passcodeManager.set(duressPasscode: passcode) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift index 574d55e77a..f767915595 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/EditPasscodeViewModel.swift @@ -13,6 +13,10 @@ class EditPasscodeViewModel: SetPasscodeViewModel { "edit_passcode.confirm_new_passcode".localized } + override func isCurrent(passcode: String) -> Bool { + passcodeManager.isValid(passcode: passcode) + } + override func onEnter(passcode: String) { do { try passcodeManager.set(passcode: passcode) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift index 45b85b8ed9..b6ae7e24ca 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/SetPasscodeViewModel.swift @@ -8,10 +8,10 @@ class SetPasscodeViewModel: ObservableObject { @Published var errorText: String = "" @Published var passcode: String = "" { didSet { + let passcode = passcode if passcode.count == passcodeLength { - Task { - try await Task.sleep(nanoseconds: 200_000_000) - await handlePasscodeChanged() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in + self?.handleEntered(passcode: passcode) } } else if passcode.count != 0 { errorText = "" @@ -35,29 +35,29 @@ class SetPasscodeViewModel: ObservableObject { var title: String { "" } var passcodeDescription: String { "" } var confirmDescription: String { "" } + func isCurrent(passcode: String) -> Bool { false } func onEnter(passcode _: String) {} func onCancel() {} - @MainActor - private func handlePasscodeChanged() { + private func handleEntered(passcode: String) { if let enteredPasscode { if enteredPasscode == passcode { onEnter(passcode: passcode) } else { self.enteredPasscode = nil - passcode = "" + self.passcode = "" syncDescription() errorText = "set_passcode.invalid_confirmation".localized } - } else if passcodeManager.has(passcode: passcode) { - passcode = "" + } else if passcodeManager.has(passcode: passcode) && !isCurrent(passcode: passcode) { + self.passcode = "" errorText = "set_passcode.already_used".localized shakeTrigger += 1 UINotificationFeedbackGenerator().notificationOccurred(.error) } else { enteredPasscode = passcode - passcode = "" + self.passcode = "" syncDescription() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift index 3370b8ec76..12137ca989 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Unlock/BaseUnlockViewModel.swift @@ -10,10 +10,10 @@ class BaseUnlockViewModel: ObservableObject { @Published var errorText: String = "" @Published var passcode: String = "" { didSet { + let passcode = passcode if passcode.count == passcodeLength { - Task { - try await Task.sleep(nanoseconds: 200_000_000) - await handlePasscodeChanged() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in + self?.handleEntered(passcode: passcode) } } } @@ -82,13 +82,12 @@ class BaseUnlockViewModel: ObservableObject { func onEnterValid(passcode _: String) {} func onBiometryUnlock() -> Bool { false } - @MainActor - private func handlePasscodeChanged() { + private func handleEntered(passcode: String) { if isValid(passcode: passcode) { onEnterValid(passcode: passcode) lockoutManager.didUnlock() } else { - passcode = "" + self.passcode = "" lockoutManager.didFailUnlock() shakeTrigger += 1 From c02ad83af2897946ba430c91fd6337545ef6d67e Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 3 Oct 2023 15:32:37 +0600 Subject: [PATCH 35/63] Show Auto-Lock only when passcode is set --- .../Security/SecuritySettingsView.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index b91e8cdfd2..8cd3256974 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -55,14 +55,16 @@ struct SecuritySettingsView: View { } } - ListSection { - NavigationRow(destination: { - AutoLockView(period: $viewModel.autoLockPeriod) - }) { - Image("lock_24").themeIcon() - Text("settings_security.auto_lock".localized).themeBody() - Text(viewModel.autoLockPeriod.title).themeSubhead1(alignment: .trailing).padding(.trailing, -.margin8) - Image.disclosureIcon + if viewModel.isPasscodeSet { + ListSection { + NavigationRow(destination: { + AutoLockView(period: $viewModel.autoLockPeriod) + }) { + Image("lock_24").themeIcon() + Text("settings_security.auto_lock".localized).themeBody() + Text(viewModel.autoLockPeriod.title).themeSubhead1(alignment: .trailing).padding(.trailing, -.margin8) + Image.disclosureIcon + } } } From 49dee867ff4908ad4668429215e5b3280bea430a Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 3 Oct 2023 15:42:00 +0600 Subject: [PATCH 36/63] Add Created hud after setting duress mode --- .../Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift index 35753bf0ce..6b3604f731 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/Manage/CreateDuressPasscodeViewModel.swift @@ -1,4 +1,5 @@ import Combine +import ComponentKit class CreateDuressPasscodeViewModel: SetPasscodeViewModel { private let accountIds: [String] @@ -32,6 +33,7 @@ class CreateDuressPasscodeViewModel: SetPasscodeViewModel { } finishSubject.send() + HudHelper.instance.show(banner: .created) } catch { print("Create Duress Passcode Error: \(error)") } From 149c6110ce277b24d3f4cf489de77f493751a487 Mon Sep 17 00:00:00 2001 From: ant013 Date: Tue, 3 Oct 2023 19:41:28 +0400 Subject: [PATCH 37/63] Inmplement Backup App Settings module --- .../project.pbxproj | 140 +++++++ .../Icons/file_24.imageset/Contents.json | 22 + .../Icons/file_24.imageset/file@2x.png | Bin 0 -> 472 bytes .../Icons/file_24.imageset/file@3x.png | Bin 0 -> 702 bytes .../Core/Managers/CloudBackupManager.swift | 21 +- .../Core/Managers/EvmSyncSourceManager.swift | 10 +- .../BackupCloudPassphraseViewModel.swift | 37 +- .../BottomSheet/BottomSheetModule.swift | 18 + .../Settings/BackupApp/BackupAppModule.swift | 52 +++ .../BackupApp/BackupAppViewModel.swift | 379 ++++++++++++++++++ .../BackupDisclaimerView.swift | 53 +++ .../BackupApp/BackupList/BackupListView.swift | 104 +++++ .../BackupManager/BackupManagerModule.swift | 7 + .../BackupManager/BackupManagerView.swift | 35 ++ .../BackupApp/BackupName/BackupNameView.swift | 52 +++ .../BackupPassword/BackupPasswordView.swift | 109 +++++ .../BackupApp/BackupType/BackupTypeView.swift | 69 ++++ .../BlockchainSettingsViewModel.swift | 2 +- .../Main/MainSettingsViewController.swift | 13 +- .../UserInterface/Caution.swift | 38 +- .../UserInterface/SwiftUI/CheckboxStyle.swift | 23 ++ .../Extensions/ActivityViewController.swift | 22 + .../SwiftUI/Extensions/Text.swift | 27 +- .../UserInterface/SwiftUI/InputTextRow.swift | 15 + .../UserInterface/SwiftUI/InputTextView.swift | 125 ++++++ .../UserInterface/SwiftUI/NavigationRow.swift | 15 +- .../UserInterface/SwiftUI/Shake.swift | 86 ++++ .../en.lproj/Localizable.strings | 42 ++ 28 files changed, 1475 insertions(+), 41 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/Contents.json create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/file@2x.png create mode 100644 UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/file@3x.png create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/ActivityViewController.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Shake.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 345923896a..ba686d0d9f 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1994,6 +1994,7 @@ ABC9A05D9F96BE464CFC90CC /* ContactBookModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5518367F0DDDB94D320 /* ContactBookModule.swift */; }; ABC9A06BB934A43890376A70 /* NftCollectionCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CF40E995105E7F38AC /* NftCollectionCellFactory.swift */; }; ABC9A06BE632BD33E5CA4106 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEAD18F73D4FBE05783D /* Contact.swift */; }; + ABC9A06CA0D364D4E1261C87 /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */; }; ABC9A074995C051E714FAFAB /* BackupContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC50307ABA3DF7034E1D /* BackupContact.swift */; }; ABC9A07518E5769122DFEAC2 /* RestoreCloudViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */; }; ABC9A0779C1107119CD3AF19 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */; }; @@ -2003,6 +2004,7 @@ ABC9A092DC0DEEF9838DB47A /* CellElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A776346AF62265896CA1 /* CellElement.swift */; }; ABC9A096B05E5491A40A327C /* DonateDescriptionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABFE62D22F9FB0B3409A /* DonateDescriptionDataSource.swift */; }; ABC9A097A0BDD99777D5374D /* DonateDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0848221B0EC25C37F3 /* DonateDescriptionCell.swift */; }; + ABC9A09E0B614E5B4E32B7F9 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACEC3169A9F01B55921A /* InputTextView.swift */; }; ABC9A0A3A52AD41643D67D3D /* SingleLineFormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */; }; ABC9A0AADAE0A5C370946B8D /* RestoreCloudPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */; }; ABC9A0B37693470DAD0FFE20 /* WalletConnectMainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD18F3E73F96DD6C4FA9 /* WalletConnectMainService.swift */; }; @@ -2016,7 +2018,9 @@ ABC9A0EE5E5B31405569BF3F /* IndicatorAdviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */; }; ABC9A0F42A6687705CAD1340 /* NftAssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */; }; ABC9A1117A41AB8CE00FDEDB /* WalletConnectAppShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */; }; + ABC9A12A4D114A2E4F4C711A /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */; }; ABC9A133A6BF0FC9A87FA14A /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; + ABC9A13B7F43722709570161 /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */; }; ABC9A13D78DD5F176A170B65 /* FullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */; }; ABC9A13DB3ADB580D59F66E4 /* SendEip1155ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A23CB332521C0607CC6B /* SendEip1155ViewModel.swift */; }; ABC9A13F4C814FFB31FF13CA /* SendEip721ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7315E119F0B1581B70C /* SendEip721ViewController.swift */; }; @@ -2033,7 +2037,9 @@ ABC9A1E5E31DD2F72BB2A13A /* IntegerAmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6D56EBB7FFAD68CFD66 /* IntegerAmountInputViewModel.swift */; }; ABC9A1EC656488FF79F458EC /* RestoreCloudViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5FE0EDA53E4D9B85DE1 /* RestoreCloudViewModel.swift */; }; ABC9A1FFFB4F9EC58BF78661 /* AccountRestoreWarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */; }; + ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */; }; ABC9A20A25C4C683A73CB994 /* ContactBookContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8080797194017F736AB /* ContactBookContactViewModel.swift */; }; + ABC9A20B7F90F5E741A74823 /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */; }; ABC9A20D2DDF8736293DE5C5 /* CoinIndicatorViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */; }; ABC9A20F6F7D5EA2A1A55A9E /* ContactLabelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB89F64056FFB98928E7 /* ContactLabelService.swift */; }; ABC9A227648FF076E9518703 /* ContactBookHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */; }; @@ -2079,6 +2085,7 @@ ABC9A359DB8C1A89269236CC /* ContactBookSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A24CBB826A2D2F88EC61 /* ContactBookSettingsModule.swift */; }; ABC9A3613C5477C07F37F48C /* WalletConnectListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE5FD79ECC4AC85B86FA /* WalletConnectListViewController.swift */; }; ABC9A36297D869E49C152CAB /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; + ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.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 */; }; @@ -2105,20 +2112,26 @@ ABC9A47D4666FA5115F98629 /* ChartIndicatorsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */; }; ABC9A4801E4964F6AED1E667 /* WalletConnectMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEA1D717D8CED8462AB0 /* WalletConnectMainViewModel.swift */; }; ABC9A4929EFBFAD0B595A4E8 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; + ABC9A4A21CFBA188A7EEC930 /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */; }; ABC9A4B643D98FB95F431401 /* SendBitcoinAmountInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1F55164BDFD049793 /* SendBitcoinAmountInputService.swift */; }; ABC9A4B9A4CC3A9EE9A89C32 /* EventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A950663B76424B1761B3 /* EventHandler.swift */; }; ABC9A4BD4CA7A7872CE6167E /* BaseSendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABE97578DC667CBDC11A /* BaseSendViewController.swift */; }; ABC9A4CD35CF43C88EC13909 /* SendEip1155Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC6A0B950C0AABD5A93E /* SendEip1155Service.swift */; }; ABC9A4D5326B3A85888140FE /* ChartIndicatorSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */; }; ABC9A4E323FF7AAD86FA8E75 /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; }; + ABC9A4EE80A57455DB2CCD4F /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */; }; ABC9A4F4B7F17169DC240A98 /* WalletConnectUriHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1C31F5343EB2BEA4540 /* WalletConnectUriHandler.swift */; }; ABC9A4FF1E1964FB77700C4E /* ChartIndicatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6DE5C760A5D0C90B70E /* ChartIndicatorFactory.swift */; }; + ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; + ABC9A5125874E09194BA8532 /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */; }; ABC9A51979D2047BEF45A2AE /* TokenSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */; }; ABC9A51E36466E414AF24C67 /* WalletConnectMainPendingRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D8072033A5AC7E4897 /* WalletConnectMainPendingRequestViewModel.swift */; }; ABC9A51F2E7EB0B477EBE708 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; ABC9A52E08E5C57665C07DBC /* PseudoAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC034DE5784F55BD5F3 /* PseudoAccessoryView.swift */; }; + ABC9A542CA987F09C93F04A9 /* InputTextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */; }; ABC9A543EB59D153FAD103F6 /* KdfParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6663522498A53CF4174 /* KdfParams.swift */; }; ABC9A54917CDA7F9EAE237C4 /* ChartIndicatorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB3EC7A1FB0D6C9F7F89 /* ChartIndicatorsModule.swift */; }; + ABC9A54E6E15E96271525339 /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */; }; ABC9A54FFFFBFC3C7B23F0B8 /* BottomGradientHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */; }; ABC9A55470228BD7B1535B9B /* WalletTokenBalanceViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */; }; ABC9A556361B2644555659D8 /* SendConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7F2ECF212EF8B70470 /* SendConfirmationViewController.swift */; }; @@ -2156,6 +2169,7 @@ ABC9A6A792282ACC8DAB62BC /* IntegerFormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */; }; ABC9A6A9C28C95352232B062 /* ThemeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */; }; ABC9A6BC79804B3D3AAFA8F1 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; + ABC9A6C05017D9267567AB73 /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */; }; ABC9A6C0A45A33C83B632D58 /* SendFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */; }; ABC9A6C1B2F55F1FFA8910CA /* ContactBookSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */; }; ABC9A6C65416E7F4F3830962 /* RestoreCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */; }; @@ -2165,6 +2179,7 @@ ABC9A6F88E51293F2605CACD /* ContactBookContactModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB2ED4E48D4FCEDBE769 /* ContactBookContactModule.swift */; }; ABC9A70AE588307EA1D3A414 /* SendConfirmationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2FD4A59DB53631435BA /* SendConfirmationService.swift */; }; ABC9A712F6389F5C2B0D63E3 /* RestoreCloudService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06866150862CEDEB5DE /* RestoreCloudService.swift */; }; + ABC9A7244B2D23BDA40C407C /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */; }; ABC9A727EC8A3C74C9C1A31A /* WalletConnectMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3A694467493C6F4AACE /* WalletConnectMainViewController.swift */; }; ABC9A728EEF4A054C7B8722B /* DonateAddressModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0B7E7360DC0357B2D0F /* DonateAddressModule.swift */; }; ABC9A7297650FD2D9F8F595F /* MacdIndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA8F31619609907AD67E /* MacdIndicatorDataSource.swift */; }; @@ -2177,12 +2192,15 @@ ABC9A7655AE66379E42FE2A4 /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; ABC9A767A49B686B3A3AC154 /* UniswapV3Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6363DB5DAE5B58AFDC0 /* UniswapV3Module.swift */; }; ABC9A774500F8D8D3D9E04DD /* SendBitcoinAdapterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1BD3B1B53C72DDF923A /* SendBitcoinAdapterService.swift */; }; + ABC9A77A41517173B610E0FC /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */; }; ABC9A78CFF8B232D330EC7B5 /* DiffLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A916C64B5EA9D96B8FDA /* DiffLabel.swift */; }; ABC9A78D3A4267CAC0F5D0E8 /* SendConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFF7119B9AC0E32B2060 /* SendConfirmationModule.swift */; }; ABC9A794E47FC07ABFC32BBD /* FeePriceScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF8E8DE67732371A00E0 /* FeePriceScale.swift */; }; + ABC9A79CFCEBAC442A1B791D /* BackupAppModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */; }; ABC9A7A9053C6ECF618D0E4A /* WalletConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4544AB5CA22ADE16417 /* WalletConnectSession.swift */; }; ABC9A7A9E27CC5F93BE5018B /* WalletConnectListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3BEB33F6DBE2395FD11 /* WalletConnectListService.swift */; }; ABC9A7AF4EE29CDE045ADEF7 /* MarketCategoryMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */; }; + ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.swift */; }; ABC9A7CBFDC0DF741E29EA44 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF26FDCB363793BF66E1 /* Integer.swift */; }; ABC9A7E1F93B0A85976C826D /* UniswapV3Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A253877D9FB972EFB8D7 /* UniswapV3Provider.swift */; }; ABC9A7E28714A9A19A2160D4 /* SendModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD1F2311CC6425CF9D90 /* SendModule.swift */; }; @@ -2192,6 +2210,7 @@ ABC9A806CB34CB9A5E27A0A3 /* RestoreCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */; }; ABC9A80BCDA72347C6619E6C /* SendTimeLockErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADF114FCFABEA148AF04 /* SendTimeLockErrorService.swift */; }; ABC9A819DDAEE683FCCA02EF /* NftAssetCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A30A8F78E9C9AEE861F1 /* NftAssetCellFactory.swift */; }; + ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */; }; ABC9A82D771D920162551294 /* WalletConnectPendingRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE15C187118DE6F0CE7B /* WalletConnectPendingRequestsViewModel.swift */; }; ABC9A83233DA4C83AF83E483 /* WalletConnectPendingRequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */; }; ABC9A8451CEF02EA0A94CEAA /* ProFeaturesAuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9628A708749A31EEA70 /* ProFeaturesAuthorizationManager.swift */; }; @@ -2209,6 +2228,7 @@ ABC9A89499016C8AC8341238 /* NftCollectionCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CF40E995105E7F38AC /* NftCollectionCellFactory.swift */; }; ABC9A8A74C527C4E01EBB8A5 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; + ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9A8D215CC5D6A70736E84 /* SendBaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A48552CF0C90E22686A9 /* SendBaseService.swift */; }; ABC9A8D8709EC2B40D74A97A /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; @@ -2221,6 +2241,7 @@ ABC9A92D7F9ADCE00CBCED09 /* WalletConnectPairingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE62C0399849EFB5C158 /* WalletConnectPairingViewModel.swift */; }; ABC9A933C2603486BA181B19 /* SendFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */; }; ABC9A9493F250B81E1152012 /* SendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */; }; + ABC9A94A82E55F437BD3FE68 /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */; }; ABC9A9562DD283B6FCACBCF9 /* MarketCardTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC7983E4A81E421AB639 /* MarketCardTitleView.swift */; }; ABC9A95E667DD7BD26602D8E /* SendEip721Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE7CB7CC9C118C72559 /* SendEip721Service.swift */; }; ABC9A96132AD85DD613EC773 /* ProFeaturesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB785128005F6C2C9F9A /* ProFeaturesStorage.swift */; }; @@ -2228,6 +2249,7 @@ ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */; }; ABC9A99861B1F83A19EA370D /* SettingsBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */; }; ABC9A998ECDE5438D94FBAE7 /* MarketDiscoveryCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADFD9DA59BD2FB21C51B /* MarketDiscoveryCategoryService.swift */; }; + ABC9A99A45187C36D48840F8 /* BackupAppModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */; }; ABC9A9A9FE5A83A6F0C3BFE9 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; }; ABC9A9AC7890BE4AAE7DDC84 /* WalletConnectSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */; }; ABC9A9CDDC14BA6259450ECA /* WalletConnectPairingModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4BA46EDEEAB6CD9B25C /* WalletConnectPairingModule.swift */; }; @@ -2235,6 +2257,7 @@ ABC9A9E3191338CD0D1DE8AE /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; ABC9A9EBBC60A709836DE237 /* NftAssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */; }; ABC9A9FA3285B39D25801C2A /* AccountRestoreWarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */; }; + ABC9AA05527830EFFCFB98E3 /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA0021E969A73DA7E177 /* BackupListView.swift */; }; ABC9AA0E63188150CD9A8D03 /* RestoreTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A13DB598B22516E5AD76 /* RestoreTypeViewModel.swift */; }; ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9AA27A709AC5F85176A53 /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; @@ -2261,6 +2284,7 @@ ABC9AB3DAD30AA400DEB719C /* SendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */; }; ABC9AB401FD98F99EF6B07C6 /* RestoreTypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A939DD222D4A2BD3D71C /* RestoreTypeViewController.swift */; }; ABC9AB4DF4CCEA2C51DD4AB6 /* SingleLineFormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */; }; + ABC9AB65B10FDB5F30E2731D /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */; }; ABC9AB6EB596E2F8B15D00E4 /* CipherParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A89726499CDB4F697EDD /* CipherParams.swift */; }; ABC9AB71563E5F2C9F2EA9E4 /* WalletConnectAppShowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3FB680357E569B6DB5F /* WalletConnectAppShowViewModel.swift */; }; ABC9AB83EE3F909BD80E0539 /* BackupCryptoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD5CB1911A698718213F /* BackupCryptoHelper.swift */; }; @@ -2276,6 +2300,7 @@ ABC9ABD9B19AD5D97E332EBE /* SendBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF15BD67548E6D755CA0 /* SendBinanceViewController.swift */; }; ABC9ABE2B6B19113D7C5EDA3 /* ContactBookHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */; }; ABC9ABE3189E497EC732B331 /* BackupCloudPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5544C221860C10BF131 /* BackupCloudPassphraseViewModel.swift */; }; + ABC9ABE3F52BF2307533D8FB /* InputTextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */; }; ABC9ABF97B8725530463FBCF /* NftAssetOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB8907B0E779CA4DF8F1 /* NftAssetOverviewViewModel.swift */; }; ABC9ABF99296DEA24FC5BFF0 /* SendAmountCautionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A580220B9FD291A6496A /* SendAmountCautionService.swift */; }; ABC9ABFA7299ADDDFEE918F7 /* WalletConnectPairingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE88105815BFC477D71 /* WalletConnectPairingViewController.swift */; }; @@ -2313,6 +2338,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 */; }; + ABC9ACE31EC94ABC5B325693 /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA0021E969A73DA7E177 /* BackupListView.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 */; }; @@ -2355,6 +2381,7 @@ ABC9AE44EF7D6B6419955B9B /* SendEip721Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE7CB7CC9C118C72559 /* SendEip721Service.swift */; }; ABC9AE472D9105F6FDAECD42 /* BackupCloudPassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF6AA02DA39787C053F0 /* BackupCloudPassphraseService.swift */; }; ABC9AE4FD599490B0A23003D /* PredefinedBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1057AD189DA1CE31BF5 /* PredefinedBlockchainService.swift */; }; + ABC9AE51262C09EABF5CCEEE /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACEC3169A9F01B55921A /* InputTextView.swift */; }; ABC9AE553D422A163A09E5F8 /* MarketCardValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4FCDC5085002DF35C17 /* MarketCardValueView.swift */; }; ABC9AE6D877341985A6F651F /* SendBitcoinAmountInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1F55164BDFD049793 /* SendBitcoinAmountInputService.swift */; }; ABC9AE775BB25CDB7AA83228 /* EventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A950663B76424B1761B3 /* EventHandler.swift */; }; @@ -2385,6 +2412,7 @@ ABC9AF77EF53B4A7B0C0E55A /* SendAmountCautionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A07A33870908ED1BA338 /* SendAmountCautionViewModel.swift */; }; ABC9AF8136B78C2F3E66FF23 /* NftAssetOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A806FD17A129212E3F7C /* NftAssetOverviewViewController.swift */; }; ABC9AF81A6F30F0041FE1FAC /* ContactBookAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A55B0E99C1DD25839EDB /* ContactBookAddressViewController.swift */; }; + ABC9AF95141EA649524FBF88 /* CheckboxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */; }; ABC9AF9C828BEBB740468204 /* WalletBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */; }; ABC9AF9D42B60D05030D43F3 /* ChartIndicatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0C131342CC764890C2B /* ChartIndicatorsViewController.swift */; }; ABC9AF9F8113DB5D54140E7A /* SendBitcoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D6C9D12C1F1F3A1218 /* SendBitcoinViewController.swift */; }; @@ -3763,9 +3791,11 @@ ABC9A0B7E7360DC0357B2D0F /* DonateAddressModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateAddressModule.swift; sourceTree = ""; }; ABC9A0C131342CC764890C2B /* ChartIndicatorsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsViewController.swift; sourceTree = ""; }; ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectModule.swift; sourceTree = ""; }; + ABC9A104D916039D690E454E /* Shake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shake.swift; sourceTree = ""; }; ABC9A1057AD189DA1CE31BF5 /* PredefinedBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredefinedBlockchainService.swift; sourceTree = ""; }; ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinDetailAdviceViewController.swift; sourceTree = ""; }; ABC9A1136889E6976E17B347 /* WalletConnectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectService.swift; sourceTree = ""; }; + ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManagerView.swift; sourceTree = ""; }; ABC9A12529DC8DE5D46D9776 /* ContactBookAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressViewModel.swift; sourceTree = ""; }; ABC9A12E4155640075755699 /* IndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorDataSource.swift; sourceTree = ""; }; ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapRevokeConfirmationViewController.swift; sourceTree = ""; }; @@ -3789,6 +3819,7 @@ ABC9A309A58148C40912B964 /* ContactBookSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookSettingsService.swift; sourceTree = ""; }; ABC9A30A8F78E9C9AEE861F1 /* NftAssetCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetCellFactory.swift; sourceTree = ""; }; ABC9A352F3EAA38107897CEF /* WalletTokenBalanceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceService.swift; sourceTree = ""; }; + ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupAppModule.swift; sourceTree = ""; }; ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCardView.swift; sourceTree = ""; }; ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsRepository.swift; sourceTree = ""; }; ABC9A38082BD2EBE1BC8E11E /* WalletTokenService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenService.swift; sourceTree = ""; }; @@ -3842,9 +3873,11 @@ ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinIndicatorViewItemFactory.swift; sourceTree = ""; }; ABC9A776346AF62265896CA1 /* CellElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellElement.swift; sourceTree = ""; }; ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSelectView.swift; sourceTree = ""; }; + ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupAppViewModel.swift; sourceTree = ""; }; ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721ViewModel.swift; sourceTree = ""; }; ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRestoreWarningManager.swift; sourceTree = ""; }; ABC9A7D6C9D12C1F1F3A1218 /* SendBitcoinViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinViewController.swift; sourceTree = ""; }; + ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextRow.swift; sourceTree = ""; }; ABC9A7FC41B9F98871246E0E /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; ABC9A80143F95E28346C81FE /* SendMemoInputService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMemoInputService.swift; sourceTree = ""; }; ABC9A806FD17A129212E3F7C /* NftAssetOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewViewController.swift; sourceTree = ""; }; @@ -3853,6 +3886,7 @@ ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeMode.swift; sourceTree = ""; }; ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowView.swift; sourceTree = ""; }; ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceViewItemFactory.swift; sourceTree = ""; }; + ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupNameView.swift; sourceTree = ""; }; ABC9A88E126AB21F856522A7 /* IntegerAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerAmountInputView.swift; sourceTree = ""; }; ABC9A896A83640B618328FE1 /* EnsAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnsAddressParserItem.swift; sourceTree = ""; }; ABC9A89726499CDB4F697EDD /* CipherParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherParams.swift; sourceTree = ""; }; @@ -3877,6 +3911,7 @@ ABC9A9E0190FAD212E2E007F /* RestoreCloudPassphraseModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseModule.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 = ""; }; + ABC9AA0021E969A73DA7E177 /* BackupListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupListView.swift; sourceTree = ""; }; ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataMonitor.swift; sourceTree = ""; }; ABC9AA31438063F7AB7BDDC8 /* WalletConnectRequestMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectRequestMapper.swift; sourceTree = ""; }; ABC9AA3B8927F9F138ABCFB8 /* WalletConnectAppShowService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowService.swift; sourceTree = ""; }; @@ -3908,6 +3943,7 @@ ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerFormAmountInputView.swift; sourceTree = ""; }; ABC9ABE473B354836327B3AC /* NftAssetOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewService.swift; sourceTree = ""; }; ABC9ABE97578DC667CBDC11A /* BaseSendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseSendViewController.swift; sourceTree = ""; }; + ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupDisclaimerView.swift; sourceTree = ""; }; ABC9ABFE62D22F9FB0B3409A /* DonateDescriptionDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateDescriptionDataSource.swift; sourceTree = ""; }; ABC9AC09A586D88BAB3B9C67 /* WalletConnectListModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectListModule.swift; sourceTree = ""; }; ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionManager.swift; sourceTree = ""; }; @@ -3917,9 +3953,11 @@ ABC9AC6A0B950C0AABD5A93E /* SendEip1155Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155Service.swift; sourceTree = ""; }; ABC9AC7983E4A81E421AB639 /* MarketCardTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCardTitleView.swift; sourceTree = ""; }; ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressService.swift; sourceTree = ""; }; + ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupTypeView.swift; sourceTree = ""; }; ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorSettingsViewModel.swift; sourceTree = ""; }; ABC9ACE7CB7CC9C118C72559 /* SendEip721Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721Service.swift; sourceTree = ""; }; ABC9ACE88105815BFC477D71 /* WalletConnectPairingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingViewController.swift; sourceTree = ""; }; + ABC9ACEC3169A9F01B55921A /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; ABC9ACF1ACFDFD53E2502C30 /* SendFeeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendFeeViewModel.swift; sourceTree = ""; }; ABC9ACF1F55164BDFD049793 /* SendBitcoinAmountInputService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinAmountInputService.swift; sourceTree = ""; }; ABC9ACF418357FF7AFC64B3F /* UniswapV3TradeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3TradeService.swift; sourceTree = ""; }; @@ -3932,6 +3970,7 @@ ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsService.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomGradientHolder.swift; sourceTree = ""; }; @@ -3939,6 +3978,7 @@ ABC9ADFD9DA59BD2FB21C51B /* MarketDiscoveryCategoryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketDiscoveryCategoryService.swift; sourceTree = ""; }; ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookHelper.swift; sourceTree = ""; }; ABC9AE15C187118DE6F0CE7B /* WalletConnectPendingRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsViewModel.swift; sourceTree = ""; }; + ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxStyle.swift; sourceTree = ""; }; ABC9AE5FD79ECC4AC85B86FA /* WalletConnectListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectListViewController.swift; sourceTree = ""; }; ABC9AE62C0399849EFB5C158 /* WalletConnectPairingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingViewModel.swift; sourceTree = ""; }; ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendZcashService.swift; sourceTree = ""; }; @@ -3948,11 +3988,13 @@ ABC9AEA1D717D8CED8462AB0 /* WalletConnectMainViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainViewModel.swift; sourceTree = ""; }; ABC9AEAD18F73D4FBE05783D /* Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; ABC9AEC034DE5784F55BD5F3 /* PseudoAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PseudoAccessoryView.swift; sourceTree = ""; }; + ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupPasswordView.swift; sourceTree = ""; }; ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackup.swift; sourceTree = ""; }; ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewController.swift; sourceTree = ""; }; ABC9AF15BD67548E6D755CA0 /* SendBinanceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBinanceViewController.swift; sourceTree = ""; }; ABC9AF26FDCB363793BF66E1 /* Integer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Integer.swift; sourceTree = ""; }; ABC9AF2B063727B7EABFD9A3 /* RsiIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RsiIndicatorDataSource.swift; sourceTree = ""; }; + ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = ""; }; ABC9AF6AA02DA39787C053F0 /* BackupCloudPassphraseService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCloudPassphraseService.swift; sourceTree = ""; }; ABC9AF8E8DE67732371A00E0 /* FeePriceScale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeePriceScale.swift; sourceTree = ""; }; ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetViewController.swift; sourceTree = ""; }; @@ -4641,6 +4683,10 @@ 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */, 11B351FDDBEF227E161F6A0E /* PageDescription.swift */, 11B356EF92FFD23F4385A991 /* ListStyle.swift */, + ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */, + ABC9ACEC3169A9F01B55921A /* InputTextView.swift */, + ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */, + ABC9A104D916039D690E454E /* Shake.swift */, ); path = SwiftUI; sourceTree = ""; @@ -4731,6 +4777,7 @@ 11B352972B14FA6EBEFD6904 /* Text.swift */, 11B352648C452D611F1EDF61 /* Image.swift */, 11B352978EC570F59F442BD5 /* View.swift */, + ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */, ); path = Extensions; sourceTree = ""; @@ -5380,6 +5427,7 @@ 11B35C3CD4FFDE56E8E30B80 /* BlockchainSettings */, D06F756D2A8BA33000184227 /* Donate */, 11B35710326AFD7334D8D044 /* SimpleActivate */, + ABC9A9C9D65EAD890AF617A4 /* BackupApp */, ); path = Settings; sourceTree = ""; @@ -7026,6 +7074,14 @@ path = DataSources; sourceTree = ""; }; + ABC9A3BF4876DB959C5E4DE2 /* BackupDisclaimer */ = { + isa = PBXGroup; + children = ( + ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */, + ); + path = BackupDisclaimer; + sourceTree = ""; + }; ABC9A3CED3BD03C1DBF797E2 /* Passphrase */ = { isa = PBXGroup; children = ( @@ -7089,6 +7145,14 @@ path = Send; sourceTree = ""; }; + ABC9A5A9AA93B6487D21EFF9 /* BackupList */ = { + isa = PBXGroup; + children = ( + ABC9AA0021E969A73DA7E177 /* BackupListView.swift */, + ); + path = BackupList; + sourceTree = ""; + }; ABC9A5F2A0999FCFDDEF1BA3 /* AmountCaution */ = { isa = PBXGroup; children = ( @@ -7202,6 +7266,14 @@ path = Components; sourceTree = ""; }; + ABC9A77088096A37E5C42191 /* BackupType */ = { + isa = PBXGroup; + children = ( + ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */, + ); + path = BackupType; + sourceTree = ""; + }; ABC9A7E9EAE24647C0700B39 /* RestoreCloud */ = { isa = PBXGroup; children = ( @@ -7225,6 +7297,21 @@ path = V2; sourceTree = ""; }; + ABC9A9C9D65EAD890AF617A4 /* BackupApp */ = { + isa = PBXGroup; + children = ( + ABC9AD5DC41717F92AC2151C /* BackupManager */, + ABC9A77088096A37E5C42191 /* BackupType */, + ABC9A5A9AA93B6487D21EFF9 /* BackupList */, + ABC9A3BF4876DB959C5E4DE2 /* BackupDisclaimer */, + ABC9AADD1C32448E8AA5D25F /* BackupName */, + ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */, + ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */, + ABC9ADC8D7C3B51B96589BC0 /* BackupPassword */, + ); + path = BackupApp; + sourceTree = ""; + }; ABC9AA15272B5421D314CDD9 /* AmountInput */ = { isa = PBXGroup; children = ( @@ -7269,6 +7356,14 @@ path = Bitcoin; sourceTree = ""; }; + ABC9AADD1C32448E8AA5D25F /* BackupName */ = { + isa = PBXGroup; + children = ( + ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */, + ); + path = BackupName; + sourceTree = ""; + }; ABC9AB313E49F27BBB0C9AED /* DataSources */ = { isa = PBXGroup; children = ( @@ -7334,6 +7429,15 @@ path = Platforms; sourceTree = ""; }; + ABC9AD5DC41717F92AC2151C /* BackupManager */ = { + isa = PBXGroup; + children = ( + ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */, + ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */, + ); + path = BackupManager; + sourceTree = ""; + }; ABC9ADC8373B45DF33E35DCA /* Binance */ = { isa = PBXGroup; children = ( @@ -7345,6 +7449,14 @@ path = Binance; sourceTree = ""; }; + ABC9ADC8D7C3B51B96589BC0 /* BackupPassword */ = { + isa = PBXGroup; + children = ( + ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */, + ); + path = BackupPassword; + sourceTree = ""; + }; ABC9ADCD89D02064F90038F5 /* ContactBookContact */ = { isa = PBXGroup; children = ( @@ -9301,6 +9413,20 @@ 11B356A35A5981DD231E580C /* ListStyle.swift in Sources */, 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */, 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */, + ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */, + ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */, + ABC9A5125874E09194BA8532 /* BackupTypeView.swift in Sources */, + ABC9AF95141EA649524FBF88 /* CheckboxStyle.swift in Sources */, + ABC9A6C05017D9267567AB73 /* BackupDisclaimerView.swift in Sources */, + ABC9A7244B2D23BDA40C407C /* BackupNameView.swift in Sources */, + ABC9A20B7F90F5E741A74823 /* BackupAppViewModel.swift in Sources */, + ABC9A79CFCEBAC442A1B791D /* BackupAppModule.swift in Sources */, + ABC9ACE31EC94ABC5B325693 /* BackupListView.swift in Sources */, + ABC9A09E0B614E5B4E32B7F9 /* InputTextView.swift in Sources */, + ABC9ABE3F52BF2307533D8FB /* InputTextRow.swift in Sources */, + ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */, + ABC9A06CA0D364D4E1261C87 /* BackupPasswordView.swift in Sources */, + ABC9A4A21CFBA188A7EEC930 /* ActivityViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10613,6 +10739,20 @@ 11B353577381981235B90A82 /* ListStyle.swift in Sources */, 11B354DC983042AD922339A6 /* DuressModeSelectView.swift in Sources */, 11B3511DAD3881FDE2419A64 /* AutoLockPeriod.swift in Sources */, + ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */, + ABC9A77A41517173B610E0FC /* BackupManagerModule.swift in Sources */, + ABC9AB65B10FDB5F30E2731D /* BackupTypeView.swift in Sources */, + ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */, + ABC9A4EE80A57455DB2CCD4F /* BackupDisclaimerView.swift in Sources */, + ABC9A94A82E55F437BD3FE68 /* BackupNameView.swift in Sources */, + ABC9A13B7F43722709570161 /* BackupAppViewModel.swift in Sources */, + ABC9A99A45187C36D48840F8 /* BackupAppModule.swift in Sources */, + ABC9AA05527830EFFCFB98E3 /* BackupListView.swift in Sources */, + ABC9AE51262C09EABF5CCEEE /* InputTextView.swift in Sources */, + ABC9A542CA987F09C93F04A9 /* InputTextRow.swift in Sources */, + ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */, + ABC9A54E6E15E96271525339 /* BackupPasswordView.swift in Sources */, + ABC9A12A4D114A2E4F4C711A /* ActivityViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/Contents.json b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/Contents.json new file mode 100644 index 0000000000..3d16665f31 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "file@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "file@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/file@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/file_24.imageset/file@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf99e8a82e559e9e53b2ea82dd1e68060e1587f GIT binary patch literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=P`1UZfiI}Z`@uPtN0o1!>y|`x?g_NHnSU&BPoPQFrFCJOU78d)4yi8A zIksueJ;`LPn!O8?*~2>Zx}Hy(;hE!>>=At6$!){1T{`KkhLdlcRa?qv(b~~6Bhw|r zwpK-Ljp2f?vv1bahd$x_XYKgewNZ~FYpKYi7f*}hmme%=ND|L5ve_@JIgHAO}>A)G^=m%%$oOxHLg7+O3#jP{MJ0u%hAyrE@#0!$1+jJ`oS5_ zdv@0^?;33nIu9CKqi0zuD=SQh9BMI~BDtW96(Z{N=Fb??u*zwfLc z2yEfrc1y2AH>Xy%T=8>r|7}y}KE~oZz8XsMW{Z>A|E+4sVqz~1SGaLdiy^vWQroF> zE-t;9Y3t;x*PkqyEpwK69_QRCYJAfRo~B;aTcN4+BHPF4idaXNOf^mN`9fTID9hvet3??amVmTGzNe`?rXtSo{CK zMXzeUKiq!%zeeBsyc09R$~H$YYv&KJeXb_s`d3eKF>8poMgRLWgC$~;kDZg=PSH&c=`njAxHWXl>kc@12?Gl^YyM?^KE| z`Hh1E7t0bbp%Hy_SJCafytm5~;tj7~ zK4p11UCwdS6d&iv=#w#@f4!MLqvdn1?rP>|3aM8`7&QeZEZQh~bl1}E)K+bMw+T!j z5q!icwvgBNj#_G#o{Mrg*>gO6>(M;#WzVJl)bBN|IK5qJZG~I&o`>N--5ykQ_9VY+ z{}+><`}H!@sg0NSnXf6BUVGVXlhLf5vsI4El%#)BXDTjQRvK}^>c~d}&comMC3EUu zIM1A8_+CD9@0A7{&X-0!|Nl*yaOS+_&mG0jntN?*i&>u>{rI)DM`h;brV01fZ)~|d zapLC5-#K5Y{0-X0c<28K*Hg Data { let backup = try appBackupProvider.fullBackup( fields: fields, passphrase: passphrase ) + return try JSONEncoder().encode(backup) + } + + func file(fields: [AppBackupProvider.Field], passphrase: String, name: String) throws -> URL { + let data = try data(fields: fields, passphrase: passphrase) + + // save book to temporary file + guard let temporaryFileUrl = ContactBookManager.localUrl?.appendingPathComponent(name + ".json") else { + throw FileStorage.StorageError.cantCreateFile + } + try data.write(to: temporaryFileUrl) + return temporaryFileUrl + } + + func save(fields: [AppBackupProvider.Field], passphrase: String, name: String) throws { do { - let encoder = JSONEncoder() - let data = try encoder.encode(backup) - let encoded = try JSONEncoder().encode(backup) + let encoded = try data(fields: fields, passphrase: passphrase) try save(encoded: encoded, name: name) } catch { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift index e4561c32a1..953b4e6283 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift @@ -189,11 +189,17 @@ extension EvmSyncSourceManager { } } - func customSyncSources(blockchainType: BlockchainType) -> [EvmSyncSource] { + func customSyncSources(blockchainType: BlockchainType?) -> [EvmSyncSource] { do { - let records = try evmSyncSourceStorage.records(blockchainTypeUid: blockchainType.uid) + let records: [EvmSyncSourceRecord] + if let blockchainType { + records = try evmSyncSourceStorage.records(blockchainTypeUid: blockchainType.uid) + } else { + records = try evmSyncSourceStorage.getAll() + } return records.compactMap { record in + let blockchainType = BlockchainType(uid: record.blockchainTypeUid) guard let url = URL(string: record.url), let scheme = url.scheme else { return nil } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift index 53331cd609..076f1b8988 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/Passphrase/BackupCloudPassphraseViewModel.swift @@ -83,15 +83,11 @@ extension BackupCloudPassphraseViewModel { } catch { switch error { case BackupCrypto.ValidationError.emptyPassphrase: - passphraseCaution = Caution(text: "backup.cloud.password.error.empty_passphrase".localized, type: .error) + passphraseCaution = Caution(text: error.localizedDescription, type: .error) case BackupCrypto.ValidationError.simplePassword: - passphraseCaution = Caution(text: "backup.cloud.password.error.minimum_requirement".localized, type: .error) + passphraseCaution = Caution(text: error.localizedDescription, type: .error) case BackupCloudPassphraseService.CreateError.invalidConfirmation: passphraseConfirmationCaution = Caution(text: "backup.cloud.password.confirm.error.doesnt_match".localized, type: .error) - case BackupCloudPassphraseService.CreateError.urlNotAvailable: - showErrorSubject.send("backup.cloud.not_available".localized) - case BackupCloudPassphraseService.CreateError.cantSaveFile: - showErrorSubject.send("backup.cloud.cant_create_file".localized) default: showErrorSubject.send(error.smartDescription) } @@ -100,3 +96,32 @@ extension BackupCloudPassphraseViewModel { } } + +extension BackupCrypto.ValidationError: LocalizedError { + public var errorDescription: String? { + switch self { + case .emptyPassphrase: return "backup.cloud.password.error.empty_passphrase".localized + case .simplePassword: return "backup.cloud.password.error.minimum_requirement".localized + } + } +} + +extension BackupCloudPassphraseService.CreateError: LocalizedError { + public var errorDescription: String? { + switch self { + case .urlNotAvailable: return "backup.cloud.not_available".localized + case .cantSaveFile: return "backup.cloud.cant_create_file".localized + case .invalidConfirmation: return "invalid confirmation".localized + } + } +} + +extension CloudBackupManager.BackupError: LocalizedError { + public var errorDescription: String? { + switch self { + case .urlNotAvailable: return "backup.cloud.not_available".localized + case .itemNotFound: return nil + } + } +} + diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift index ce38788fd5..b6ae2ef412 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift @@ -1,4 +1,5 @@ import UIKit +import SwiftUI import ThemeKit import ComponentKit import SectionsTableView @@ -146,3 +147,20 @@ extension BottomSheetModule { } } + +struct ViewWrapper: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let viewController: UIViewController + + init(_ viewController: UIViewController) { + self.viewController = viewController + } + + func makeUIViewController(context _: Context) -> UIViewController { + viewController + } + + func updateUIViewController(_: UIViewController, context _: Context) {} +} + diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift new file mode 100644 index 0000000000..1ed8a0a95b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift @@ -0,0 +1,52 @@ +import SwiftUI + +struct BackupAppModule { + static func view(backupPresented: Binding) -> some View { + let viewModel = BackupAppViewModel( + accountManager: App.shared.accountManager, + contactManager: App.shared.contactManager, + cloudBackupManager: App.shared.cloudBackupManager, + favoritesManager: App.shared.favoritesManager, + evmSyncSourceManager: App.shared.evmSyncSourceManager + ) + + return BackupTypeView(viewModel: viewModel, backupPresented: backupPresented) + } +} + +extension BackupAppModule { + enum Destination: String, CaseIterable, Identifiable { + case cloud + case local + + var id: Self { + self + } + + var backupDisclaimer: BackupDestinationDisclaimer { + switch self { + case .cloud: + return BackupDestinationDisclaimer( + title: "backup.disclaimer.cloud.title".localized, + highlightedDescription: "backup.disclaimer.cloud.description".localized, + selectedCheckboxText: "backup.disclaimer.cloud.checkbox_label".localized, + buttonTitle: "button.next".localized + ) + case .local: + return BackupDestinationDisclaimer( + title: "backup.disclaimer.file.title".localized, + highlightedDescription: "backup.disclaimer.file.description".localized, + selectedCheckboxText: "backup.disclaimer.file.checkbox_label".localized, + buttonTitle: "button.next".localized + ) + } + } + } + + struct BackupDestinationDisclaimer { + let title: String + let highlightedDescription: String + let selectedCheckboxText: String + let buttonTitle: String + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift new file mode 100644 index 0000000000..77dc46cf5b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift @@ -0,0 +1,379 @@ +import Combine +import ComponentKit +import Foundation + +class BackupAppViewModel: ObservableObject { + static let backupNamePrefix = "App Backup" + let accountManager: AccountManager + let contactManager: ContactBookManager + let cloudBackupManager: CloudBackupManager + let favoritesManager: FavoritesManager + let evmSyncSourceManager: EvmSyncSourceManager + + private var cancellables = Set() + + // Type ViewModel + @Published var cloudAvailable: Bool + @Published var destination: BackupAppModule.Destination? { + didSet { + // need to reset future fields: + name = nextName + password = AppConfig.defaultPassphrase + confirm = AppConfig.defaultPassphrase + + accountItems = accounts(watch: false) + .map { item(account: $0) } + + otherItems = getOtherItems() + selected = accountIds.reduce(into: [:]) { $0[$1] = true } + } + } + + // Configuration ViewModel + @Published var selected: [String: Bool] = [:] + @Published var accountItems: [AccountItem] = [] + @Published var otherItems: [Item] = [] + @Published var disclaimerPushed = false { + didSet { + // need to reset future fields: + name = nextName + password = AppConfig.defaultPassphrase + confirm = AppConfig.defaultPassphrase + } + } + + // Disclaimer ViewModel + @Published var namePushed = false { + didSet { + // need to reset future fields: + name = nextName + password = AppConfig.defaultPassphrase + confirm = AppConfig.defaultPassphrase + } + } + + // Name ViewModel + @Published var nameCautionState: CautionState = .none + @Published var name: String = "" { + didSet { + validateName() + } + } + @Published var passwordPushed = false { + didSet { + // need to reset future fields: + password = AppConfig.defaultPassphrase + confirm = AppConfig.defaultPassphrase + } + } + + // Password ViewModel + @Published var passwordCautionState: CautionState = .none + @Published var password: String = AppConfig.defaultPassphrase { + didSet { + validatePasswords() + } + } + + @Published var confirmCautionState: CautionState = .none + @Published var confirm: String = AppConfig.defaultPassphrase { + didSet { + validatePasswords() + } + } + + @Published var passwordButtonDisabled = true + @Published var passwordButtonProcessing = false + + @Published var sharePresented: URL? + + init(accountManager: AccountManager, contactManager: ContactBookManager, cloudBackupManager: CloudBackupManager, favoritesManager: FavoritesManager, evmSyncSourceManager: EvmSyncSourceManager) { + self.accountManager = accountManager + self.contactManager = contactManager + self.cloudBackupManager = cloudBackupManager + self.favoritesManager = favoritesManager + self.evmSyncSourceManager = evmSyncSourceManager + + cloudAvailable = cloudBackupManager.iCloudUrl != nil + cloudBackupManager.$state + .sink(receiveValue: { [weak self] state in + switch state { + case .error: self?.cloudAvailable = false + default: self?.cloudAvailable = true + } + }) + .store(in: &cancellables) + + accountItems = accounts(watch: false) + .map { item(account: $0) } + + otherItems = getOtherItems() + selected = accountIds.reduce(into: [:]) { $0[$1] = true } + name = nextName + + validatePasswords() + } +} + +// Account Page ViewModel +extension BackupAppViewModel { + private func accounts(watch: Bool) -> [Account] { + accountManager + .accounts + .filter { $0.watchAccount == watch } + } + + private var accountIds: [String] { + accounts(watch: false) + .map { $0.id } + } + + private func item(account: Account) -> 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 AccountItem( + accountId: account.id, + name: account.name, + description: description, + cautionType: cautionType + ) + } + + private func getOtherItems() -> [Item] { + var items = [Item]() + + let watchAccountCount = accounts(watch: true).count + if watchAccountCount != 0 { + items.append(Item( + title: "backup_list.other.watch_account.title".localized, + description: "backup_list.other.watch_account.description".localized(watchAccountCount) + )) + } + + let watchlistCount = favoritesManager.allCoinUids.count + if watchlistCount != 0 { + items.append(Item( + title: "backup_list.other.watchlist.title".localized, + description: "backup_list.other.watchlist.description".localized(watchlistCount) + )) + } + + let contacts = contactManager.all ?? [] + let contactAddressCount = contacts.reduce(into: 0) { $0 += $1.addresses.count } + if contactAddressCount != 0 { + items.append(Item( + title: "backup_list.other.contacts.title".localized, + description: "backup_list.other.contacts.description".localized(contactAddressCount) + )) + } + + let blockchainSourcesCount = evmSyncSourceManager.customSyncSources(blockchainType: nil).count + if blockchainSourcesCount != 0 { + items.append(Item( + title: "backup_list.other.blockchain_settings.title".localized, + description: "backup_list.other.blockchain_settings.description".localized(blockchainSourcesCount) + )) + } + items.append(Item( + title: "backup_list.other.app_settings.title".localized, + description: "backup_list.other.app_settings.description".localized + )) + + return items + } + + var configuration: [AppBackupProvider.Field] { + var fields = [AppBackupProvider.Field.settings] + + var accountIds = accounts(watch: true).map { $0.id } + selected.forEach { id, selected in + if selected { + accountIds.append(id) + } + } + + fields.append(.accounts(ids: accountIds)) + + let contacts = contactManager.all ?? [] + if contacts.count != 0 { + fields.append(.contacts) + } + + if !favoritesManager.allCoinUids.isEmpty { + fields.append(.watchlist) + } + + return fields + } +} + +extension BackupAppViewModel { + func toggle(item: AccountItem) { + selected[item.accountId]?.toggle() + } +} + +// Backup Name VieeModel +extension BackupAppViewModel { + var nextName: String { + let name = { (_: String) in [Self.backupNamePrefix, "1"].joined(separator: " ") } + switch destination { + case .cloud: + let exists = cloudBackupManager + .existFilenames + .filter { $0.hasPrefix(Self.backupNamePrefix) } + .sorted() + for i in 1 ..< exists.count + 1 { + let newName = name(i.description) + if !exists.contains(where: { $0.lowercased() == newName.lowercased() }) { + return newName + } + } + return name((exists.count + 1).description) + default: + return name("1") + } + } + + func validateName() { + if name.isEmpty { + nameCautionState = .caution(.init(text: NameError.empty.localizedDescription, type: .error)) + } else if (cloudBackupManager.existFilenames + [Self.backupNamePrefix + "2"]).contains(where: { $0.lowercased() == name.lowercased() }) { + nameCautionState = .caution(.init(text: NameError.alreadyExist.localizedDescription, type: .error)) + } else { + nameCautionState = .none + } + } + + func validatePasswords() { + var buttonDisabled = false + if password.isEmpty { + buttonDisabled = true + confirmCautionState = .none + } else { + do { + try BackupCrypto.validate(passphrase: password) + passwordCautionState = .none + } catch { + passwordCautionState = .caution(.init(text: error.localizedDescription, type: .error)) + buttonDisabled = true + } + } + + if confirm.isEmpty { + buttonDisabled = true + confirmCautionState = .none + } else { + do { + try BackupCrypto.validate(passphrase: confirm) + if password != confirm { + buttonDisabled = true + confirmCautionState = .caution( + .init( + text: "backup.cloud.password.confirm.error.doesnt_match".localized, + type: .error + ) + ) + } else { + confirmCautionState = .none + } + } catch { + confirmCautionState = .caution(.init(text: error.localizedDescription, type: .error)) + buttonDisabled = true + } + } + + passwordButtonDisabled = buttonDisabled + } + + @MainActor + private func showSuccess() { + HudHelper.instance.show(banner: .savedToCloud) + } + + @MainActor + private func show(error: Error) { + HudHelper.instance.show(banner: .error(string: error.localizedDescription)) + } + + func onTapSave() { + passwordButtonProcessing = true + + Task { + switch destination { + case .none: () + case .cloud: + do { + try cloudBackupManager.save(fields: configuration, passphrase: password, name: name) + passwordButtonProcessing = false + await showSuccess() + } catch { + passwordButtonProcessing = false + await show(error: error) + } + case .local: + do { + let url = try cloudBackupManager.file(fields: configuration, passphrase: password, name: name) + sharePresented = url + passwordButtonProcessing = false + } catch { + passwordButtonProcessing = false + await show(error: error) + } + } + } + } +} + +extension BackupAppViewModel { + struct AccountItem: Comparable, Identifiable { + let accountId: String + let name: String + let description: String + let cautionType: CautionType? + + static func < (lhs: AccountItem, rhs: AccountItem) -> Bool { + lhs.name < rhs.name + } + + static func == (lhs: AccountItem, rhs: AccountItem) -> Bool { + lhs.accountId == rhs.accountId + } + + var id: String { + accountId + } + } + + struct Item: Identifiable { + let title: String + let description: String + + var id: String { + title + } + } + + enum NameError: Error, LocalizedError { + case empty + case alreadyExist + + var errorDescription: String? { + switch self { + case .empty: return "backup.cloud.name.error.empty".localized + case .alreadyExist: return "backup.cloud.name.error.already_exist".localized + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift new file mode 100644 index 0000000000..73125c8f6c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift @@ -0,0 +1,53 @@ +import SDWebImageSwiftUI +import SwiftUI +import ThemeKit + +struct BackupDisclaimerView: View { + @ObservedObject var viewModel: BackupAppViewModel + @Binding var backupPresented: Bool + + @State var isOn: Bool = true + + var body: some View { + let backupDisclaimer = (viewModel.destination ?? .local).backupDisclaimer + + ThemeView { + BottomGradientWrapper { + VStack(spacing: .margin32) { + HighlightedTextView(text: backupDisclaimer.highlightedDescription, style: .warning) + ListSection { + ClickableRow(action: { + isOn.toggle() + }) { + Toggle(isOn: $isOn) {} + .labelsHidden() + .toggleStyle(CheckboxStyle()) + + Text(backupDisclaimer.selectedCheckboxText).themeSubhead2(color: .themeLeah) + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } bottomContent: { + NavigationLink( + destination: BackupNameView(viewModel: viewModel, backupPresented: $backupPresented), + isActive: $viewModel.namePushed + ) { + Button(action: { viewModel.namePushed = true }) { + Text("button.next".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(!isOn) + } + } + } + .navigationBarTitle(backupDisclaimer.title) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.cancel".localized) { + backupPresented = false + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift new file mode 100644 index 0000000000..7c5f512838 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift @@ -0,0 +1,104 @@ +import SDWebImageSwiftUI +import SwiftUI +import ThemeKit + +struct BackupListView: View { + @ObservedObject var viewModel: BackupAppViewModel + @Binding var backupPresented: Bool + + var body: some View { + ThemeView { + BottomGradientWrapper { + VStack(spacing: .margin24) { + VStack(spacing: 0) { + ListSectionHeader(text: "backup_list.header.wallets".localized) + + ListSection { + ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppViewModel.AccountItem) in + if viewModel.selected[item.id] != nil { + let selected = binding(for: item.accountId) + + ClickableRow(action: { + viewModel.toggle(item: item) + }) { + HStack { + AccountView(item: item) + + Toggle(isOn: selected) {} + .labelsHidden() + .toggleStyle(CheckboxStyle()) + } + } + } else { + ListRow { + AccountView(item: item) + } + } + } + } + } + + VStack(spacing: 0) { + ListSectionHeader(text: "backup_list.header.other".localized) + + ListSection { + ForEach(viewModel.otherItems) { (item: BackupAppViewModel.Item) in + ListRow { + VStack(spacing: 1) { + Text(item.title).themeBody() + + Text(item.description).themeSubhead2() + } + } + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } bottomContent: { + NavigationLink( + destination: BackupDisclaimerView(viewModel: viewModel, backupPresented: $backupPresented), + isActive: $viewModel.disclaimerPushed + ) { + Button(action: { + viewModel.disclaimerPushed = true + }) { + Text("button.next".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .navigationTitle("backup_list.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + backupPresented = false + } + } + } + } + + private func binding(for key: String) -> Binding { + .init( + get: { viewModel.selected[key, default: true] }, + set: { viewModel.selected[key] = $0 } + ) + } +} + +extension BackupListView { + struct AccountView: View { + var item: BackupAppViewModel.AccountItem + + var body: some View { + let color: Color? = item.cautionType.map { $0 == .error ? .themeLucian : .themeJacob } + + VStack(spacing: 1) { + Text(item.name).themeBody() + + Text(item.description) + .themeSubhead2(color: color ?? .themeGray) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift new file mode 100644 index 0000000000..785b0ea0be --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct BackupManagerModule { + static func view() -> some View { + BackupManagerView() + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift new file mode 100644 index 0000000000..93c71f72c7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift @@ -0,0 +1,35 @@ +import SDWebImageSwiftUI +import SwiftUI +import ThemeKit + +struct BackupManagerView: View { + @State var restorePresented: Bool = false + @State var backupPresented: Bool = false + + var body: some View { + ScrollableThemeView { + ListSection { + ClickableRow(action: { + restorePresented = true + }) { + Image("download_24").themeIcon(color: .themeJacob) + Text("backup_manager.restore".localized).themeBody(color: .themeJacob) + } + ClickableRow(action: { + backupPresented = true + }) { + Image("plus_24").themeIcon(color: .themeJacob) + Text("backup_manager.create".localized).themeBody(color: .themeJacob) + } + } + .sheet(isPresented: $restorePresented) { + InfoModule.restoreSourceInfo + } + .sheet(isPresented: $backupPresented) { + ThemeNavigationView { BackupAppModule.view(backupPresented: $backupPresented) } + } + .navigationBarTitle("backup_manager.title".localized) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift new file mode 100644 index 0000000000..5a32ef42e3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift @@ -0,0 +1,52 @@ +import SwiftUI +import ThemeKit + +struct BackupNameView: View { + @ObservedObject var viewModel: BackupAppViewModel + @Binding var backupPresented: Bool + + var body: some View { + ThemeView { + BottomGradientWrapper { + VStack(spacing: .margin24) { + Text("backup.name.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) + + InputTextRow { + InputTextView( + placeholder: "backup.cloud.name.placeholder".localized, + text: $viewModel.name + ) + .autocapitalization(.words) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.nameCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.nameCautionState)) + } + .animation(.default, value: viewModel.nameCautionState) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } bottomContent: { + NavigationLink( + destination: BackupPasswordView(viewModel: viewModel, backupPresented: $backupPresented), + isActive: $viewModel.passwordPushed + ) { + Button(action: { + viewModel.passwordPushed = true + }) { + Text("button.next".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(viewModel.nameCautionState != .none) + } + } + .navigationBarTitle("backup.name.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + backupPresented = false + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift new file mode 100644 index 0000000000..e8d71c289e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift @@ -0,0 +1,109 @@ +import SwiftUI +import ThemeKit +import ComponentKit + +struct BackupPasswordView: View { + @ObservedObject var viewModel: BackupAppViewModel + @Binding var backupPresented: Bool + + @State var secureLock = true + + var body: some View { + ThemeView { + BottomGradientWrapper { + VStack(spacing: .margin32) { + Text("backup.password.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + + VStack(spacing: .margin16) { + InputTextRow { + InputTextView( + placeholder: "backup.cloud.password.placeholder".localized, + text: $viewModel.password, + isValidText: { text in PassphraseValidator.validate(text: text) } + ) + .secure($secureLock) + .autocapitalization(.none) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.passwordCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.passwordCautionState)) + + InputTextRow { + InputTextView( + placeholder: "backup.cloud.password.confirm.placeholder".localized, + text: $viewModel.confirm, + isValidText: { text in PassphraseValidator.validate(text: text) } + ) + .secure($secureLock) + .autocapitalization(.none) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.confirmCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.confirmCautionState)) + } + .animation(.default, value: secureLock) + + HighlightedTextView(text: "backup.password.highlighted_description".localized, style: .warning) + } + .animation(.default, value: viewModel.passwordCautionState) + .animation(.default, value: viewModel.confirmCautionState) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } bottomContent: { + Button(action: { + viewModel.onTapSave() + }) { + HStack(spacing: .margin8) { + if viewModel.passwordButtonProcessing { + ProgressView().progressViewStyle(.circular) + } + + Text("button.save".localized) + } + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(viewModel.passwordButtonDisabled || viewModel.passwordButtonProcessing) + .animation(.default, value: viewModel.passwordButtonProcessing) + } + .sheet(item: $viewModel.sharePresented) { url in + let completion: UIActivityViewController.CompletionWithItemsHandler = { type, success, list, error in + if success { + showDone() + backupPresented = false + } + if let error { + show(error: error) + } + } + if #available(iOS 16, *) { + ActivityViewController(activityItems: [url], completionWithItemsHandler: completion).presentationDetents([.medium, .large]) + } else { + ActivityViewController(activityItems: [url], completionWithItemsHandler: completion) + } + } + .navigationBarTitle("backup.password.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + backupPresented = false + } + } + } + } + + @MainActor + private func showSuccess() { + HudHelper.instance.show(banner: .savedToCloud) + } + + @MainActor + private func show(error: Error) { + HudHelper.instance.show(banner: .error(string: error.localizedDescription)) + } + + @MainActor + func showDone() { + HudHelper.instance.show(banner: .done) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift new file mode 100644 index 0000000000..aee60de716 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift @@ -0,0 +1,69 @@ +import SwiftUI +import ThemeKit + +struct BackupTypeView: View { + @ObservedObject var viewModel: BackupAppViewModel + @Binding var backupPresented: Bool + + @State var navigationPushed = false + @State var cloudAlertPresented = false + + var body: some View { + ScrollableThemeView { + VStack(spacing: .margin24) { + Text("backup_type.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) + + ListSection { + navigation(image: "icloud_24", text: "backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable) { + if viewModel.cloudAvailable { viewModel.destination = .cloud } else { cloudAlertPresented = true } + } + + navigation(image: "file_24", text: "backup_type.file".localized) { + viewModel.destination = .local + } + } + .sheet(isPresented: $cloudAlertPresented) { + if #available(iOS 16, *) { + ViewWrapper(BottomSheetModule.cloudNotAvailableController()).presentationDetents([.medium]) + } else { + ViewWrapper(BottomSheetModule.cloudNotAvailableController()) + } + } + } + .navigationBarTitle("backup_type.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + backupPresented = false + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } + + @ViewBuilder func row(image: String, text: String) -> some View { + HStack(spacing: .margin16) { + Image(image).themeIcon() + Text(text).themeBody() + Image.disclosureIcon + } + } + + @ViewBuilder func navigation(image: String, text: String, isAvailable: Binding = .constant(true), action: @escaping () -> Void = {}) -> some View { + if isAvailable.wrappedValue { + NavigationRow( + destination: { BackupListView(viewModel: viewModel, backupPresented: $backupPresented) }, + isActive: $navigationPushed + ) { + row(image: image, text: text.localized) + } + .onChange(of: navigationPushed) { _ in action() } + } else { + ClickableRow(action: action) { + row(image: image, text: text.localized) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsViewModel.swift index 317b1b6eed..55fc10cb99 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsViewModel.swift @@ -9,8 +9,8 @@ class BlockchainSettingsViewModel: ObservableObject { private let evmSyncSourceManager: EvmSyncSourceManager private let disposeBag = DisposeBag() - @Published var btcItems: [BtcItem] = [] @Published var evmItems: [EvmItem] = [] + @Published var btcItems: [BtcItem] = [] init(btcBlockchainManager: BtcBlockchainManager, evmBlockchainManager: EvmBlockchainManager, evmSyncSourceManager: EvmSyncSourceManager) { self.btcBlockchainManager = btcBlockchainManager diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index 30494b97f4..ca2b3a3f78 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -219,11 +219,22 @@ class MainSettingsViewController: ThemeViewController { image: .local(UIImage(named: "blocks_24")), title: .body("settings.blockchain_settings".localized), accessoryType: .disclosure, - isLast: true, + isLast: false, action: { [weak self] in let viewController = BlockchainSettingsModule.view().toViewController(title: "blockchain_settings.title".localized) self?.navigationController?.pushViewController(viewController, animated: true) } + ), + tableView.universalRow48( + id: "backup-manager", + image: .local(UIImage(named: "icloud_24")), + title: .body("settings.backup_manager".localized), + accessoryType: .disclosure, + isLast: true, + action: { [weak self] in + let viewController = BackupManagerModule.view().toViewController(title: "backup_manager.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } ) ] } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Caution.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Caution.swift index 8fd7187194..ce608b8d2e 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Caution.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Caution.swift @@ -1,7 +1,30 @@ -import UIKit +import SwiftUI import ThemeKit -struct Caution { +enum CautionState: Equatable { + case none + case caution(Caution) + + var caution: Caution? { + switch self { + case let .caution(caution): return caution + default: return nil + } + } + + var color: Color { + switch self { + case .none: return Color.clear + case let .caution(caution): + switch caution.type { + case .warning: return .themeJacob + case .error: return .themeLucian + } + } + } +} + +struct Caution: Equatable { let text: String let type: CautionType } @@ -24,13 +47,12 @@ enum CautionType: Equatable { } } - static func ==(lhs: CautionType, rhs: CautionType) -> Bool { + static func == (lhs: CautionType, rhs: CautionType) -> Bool { switch (lhs, rhs) { case (.error, .error), (.warning, .warning): return true default: return false } } - } class TitledCaution: Equatable { @@ -44,12 +66,11 @@ class TitledCaution: Equatable { self.type = type } - static func ==(lhs: TitledCaution, rhs: TitledCaution) -> Bool { + static func == (lhs: TitledCaution, rhs: TitledCaution) -> Bool { lhs.title == rhs.title && - lhs.text == rhs.text && - lhs.type == rhs.type + lhs.text == rhs.text && + lhs.type == rhs.type } - } class CancellableTitledCaution: TitledCaution { @@ -60,5 +81,4 @@ class CancellableTitledCaution: TitledCaution { super.init(title: title, text: text, type: type) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift new file mode 100644 index 0000000000..4ff8103098 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift @@ -0,0 +1,23 @@ +import SwiftUI +import ThemeKit + +struct CheckboxStyle: ToggleStyle { + private let size: CGFloat = .margin24 - .heightOneDp + + func makeBody(configuration: Configuration) -> some View { + Button(action: { + configuration.isOn.toggle() + }, label: { + Image("check_2_20") + .themeIcon(color: .themeJacob) + .opacity(configuration.isOn ? 1 : 0) + .frame(width: size, height: size, alignment: .center) + }) + .overlay( + RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) + .stroke(Color.themeGray, lineWidth: .heightOneDp + .heightOnePixel) + ) + + configuration.label + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/ActivityViewController.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/ActivityViewController.swift new file mode 100644 index 0000000000..cba5720d66 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/ActivityViewController.swift @@ -0,0 +1,22 @@ +import SwiftUI +import UIKit + +struct ActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? = nil + var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? + + func makeUIViewController(context _: UIViewControllerRepresentableContext) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) + controller.completionWithItemsHandler = completionWithItemsHandler + return controller + } + + func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext) {} +} + +extension URL: Identifiable { + public var id: String { + absoluteString + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift index 2b9030e947..f2550d4128 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift @@ -1,26 +1,27 @@ import SwiftUI extension Text { - func themeBody(color: Color = .themeLeah, alignment: Alignment = .leading) -> some View { - self - .frame(maxWidth: .infinity, alignment: alignment) - .foregroundColor(color) - .font(.themeBody) + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeBody) } func themeSubhead1(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { - self - .frame(maxWidth: .infinity, alignment: alignment) - .foregroundColor(color) - .font(.themeSubhead1) + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeSubhead1) } func themeSubhead2(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { - self - .frame(maxWidth: .infinity, alignment: alignment) - .foregroundColor(color) - .font(.themeSubhead2) + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeSubhead2) } + func themeCaption(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeCaption) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift new file mode 100644 index 0000000000..b383757384 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct InputTextRow: View { + @ViewBuilder let content: Content + + var body: some View { + HStack(spacing: .margin16) { + content + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16)) + .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(Color.themeLawrence)) + .overlay(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).stroke(Color.themeSteel20, lineWidth: .heightOneDp)) + .frame(minHeight: .heightSingleLineCell) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift new file mode 100644 index 0000000000..77e076ea1d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift @@ -0,0 +1,125 @@ +import SwiftUI +import ThemeKit + +struct InputTextView: View { + var placeholder: String = "" + + var text: Binding + @State private var oldValue: String + + @Binding var secured: Bool + + @State var shake = false + var shakeOnInvalid = true + + var onEditingChanged: ((Bool) -> Void)? + var onCommit: (() -> Void)? + var isValidText: ((String) -> Bool)? + + init(placeholder: String = "", text: Binding, secured: Binding = .constant(false), onEditingChanged: ((Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil, isValidText: ((String) -> Bool)? = nil) { + self.placeholder = placeholder + self.text = text + oldValue = text.wrappedValue + self._secured = secured + + self.onEditingChanged = onEditingChanged + self.onCommit = onCommit + self.isValidText = isValidText + } + + var body: some View { + editView() + .font(.themeBody) + .accentColor(.themeLeah) + .frame(height: 20) //todo: How to remove this? + .onReceive(text.wrappedValue.publisher.collect()) { + let newValue = $0.map { String($0) }.joined() + if isValidText?(newValue) ?? true { + oldValue = newValue + } else { + text.wrappedValue = oldValue + + if shakeOnInvalid { + shake = true + } + } + } + .shake($shake) + } + + @ViewBuilder + func editView() -> some View { + if secured { + SecureField( + placeholder, + text: text, + onCommit: { commit() } + ) + } else { + TextField( + placeholder, + text: text, + onEditingChanged: { editingChanged($0) }, + onCommit: { commit() } + ) + } + } + + private func editingChanged(_ bool: Bool) { + onEditingChanged?(bool) + } + + private func commit() { + onCommit?() + } +} + +extension InputTextView { + @ViewBuilder func secure(_ secured: Binding) -> some View { + var selfView = self + selfView._secured = secured + + return HStack(spacing: .margin16) { + selfView + + Button(action: { + secured.wrappedValue.toggle() + }) { + Image(secured.wrappedValue ? "eye_off_20" : "eye_20").themeIcon() + } + } + } +} + +struct CautionBorder: ViewModifier { + let cornerRadius: CGFloat + @Binding var cautionState: CautionState + + init(cornerRadius: CGFloat = .cornerRadius8, cautionState: Binding) { + self.cornerRadius = cornerRadius + _cautionState = cautionState + } + + func body(content: Content) -> some View { + content + .overlay( + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(cautionState.color, lineWidth: .heightOneDp) + ) + } +} + +struct CautionPrompt: ViewModifier { + @Binding var cautionState: CautionState + + func body(content: Content) -> some View { + VStack { + content + + if let caution = cautionState.caution { + Text(caution.text) + .themeCaption(color: cautionState.color) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift index deb5523386..7c39a09b58 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift @@ -2,14 +2,19 @@ import SwiftUI struct NavigationRow: View { @ViewBuilder let destination: Destination + var isActive: Binding? @ViewBuilder let content: Content var body: some View { - NavigationLink(destination: destination) { - ListRow { - content - } + let row = ListRow { + content + } + if let isActive { + NavigationLink(destination: destination, isActive: isActive) { row } + .buttonStyle(RowButtonStyle()) + } else { + NavigationLink(destination: destination) { row } + .buttonStyle(RowButtonStyle()) } - .buttonStyle(RowButtonStyle()) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Shake.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Shake.swift new file mode 100644 index 0000000000..7c6c3a63f4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Shake.swift @@ -0,0 +1,86 @@ +import SwiftUI + +struct Shake: View { + /// Set to true in order to animate + @Binding var shake: Bool + /// How many times the content will animate back and forth + var repeatCount = 3 + /// Duration in seconds + var duration = 0.4 + /// Range in pixels to go back and forth + var offsetRange = -5.0 + + @ViewBuilder let content: Content + var onCompletion: (() -> Void)? + + @State private var xOffset = 0.0 + + var body: some View { + content + .offset(x: xOffset) + .onChange(of: shake) { shouldShake in + guard shouldShake else { return } + Task { + await animate() + shake = false + onCompletion?() + } + } + } + + // Obs: some of factors must be 1.0. + private func animate() async { + let factor1 = 0.9 + let eachDuration = duration * factor1 / CGFloat(repeatCount) + for _ in 0 ..< repeatCount { + await backAndForthAnimation(duration: eachDuration, offset: offsetRange) + } + + let factor2 = 0.1 + await animate(duration: duration * factor2) { + xOffset = 0.0 + } + } + + private func backAndForthAnimation(duration: CGFloat, offset: CGFloat) async { + let halfDuration = duration / 2 + await animate(duration: halfDuration) { + self.xOffset = offset + } + + await animate(duration: halfDuration) { + self.xOffset = -offset + } + } +} + +extension View { + func shake(_ shake: Binding, + repeatCount: Int = 3, + duration: CGFloat = 0.4, + offsetRange: CGFloat = -5, + onCompletion: (() -> Void)? = nil) -> some View + { + Shake(shake: shake, + repeatCount: repeatCount, + duration: duration, + offsetRange: offsetRange) + { + self + } onCompletion: { + onCompletion?() + } + } + + func animate(duration: CGFloat, _ execute: @escaping () -> Void) async { + await withCheckedContinuation { continuation in + withAnimation(.linear(duration: duration)) { + execute() + } + + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + continuation.resume() + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 84030fcf24..b5a4cee487 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Paste"; "button.resend" = "Resend"; "button.backup" = "Backup"; +"button.restore" = "Restore"; "button.copy" = "Copy"; "button.retry" = "Retry"; "button.report" = "Report"; @@ -1011,6 +1012,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings.tab_bar_item" = "Settings"; "settings.manage_accounts" = "Manage Wallets"; "settings.blockchain_settings" = "Blockchain Settings"; +"settings.backup_manager" = "Backup Manager"; "settings.security" = "Security"; "settings.experimental_features" = "Experimental"; "settings.personal_support" = "Personal Support"; @@ -1083,6 +1085,46 @@ Go to Settings - > %@ and allow access to the camera."; "blockchain_settings.title" = "Blockchain Settings"; +// Settings -> Backup Manager + +"backup_manager.title" = "Backup Manager"; +"backup_manager.restore" = "Restore Backup"; +"backup_manager.create" = "Create New Backup"; + +"backup_type.title" = "Backup Type"; +"backup_type.description" = "Select where you want to save the backup file."; +"backup_type.cloud" = "Backup to iCloud"; +"backup_type.file" = "Backup to Files"; + +"backup_list.title" = "Backup File"; +"backup_list.description.restore" = "List of contents in the backup file."; +"backup_list.header.wallets" = "Wallets"; +"backup_list.header.other" = "Other"; +"backup_list.other.watch_account.title" = "Watch Wallets"; +"backup_list.other.watch_account.description" = "Addresses: %d"; +"backup_list.other.watchlist.title" = "Watchlist"; +"backup_list.other.watchlist.description" = "Coins: %d"; +"backup_list.other.contacts.title" = "Contacts"; +"backup_list.other.contacts.description" = "Addresses: %d"; +"backup_list.other.blockchain_settings.title" = "Blockchain Settings"; +"backup_list.other.blockchain_settings.description" = "Custom RPCs: %d"; +"backup_list.other.app_settings.title" = "App Settings"; +"backup_list.other.app_settings.description" = "Language, Currency, Appearance ..."; + +"backup.disclaimer.cloud.title" = "Backup to iCloud"; +"backup.disclaimer.cloud.description" = "iCloud is a cloud storage service provided by Apple. It's important to know that your backup data will be stored on Apple's servers."; +"backup.disclaimer.cloud.checkbox_label" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; +"backup.disclaimer.file.title" = "Backup to File"; +"backup.disclaimer.file.description" = "Storage devices i.e. hard drives, USB drives , storage on smartphone etc. are all vulnerable to loss due to physical damage, theft or other unforeseen circumstances."; +"backup.disclaimer.file.checkbox_label" = "I understand that theft or damage of a backup device will result in loss of a backup to a respective wallet."; + +"backup.name.title" = "Backup Name"; +"backup.name.description" = "Enter name for the backup file."; + +"backup.password.title" = "Backup Password"; +"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.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."; + // Settings -> Security "settings_security.title" = "Security"; From 907ff0101b19153799df8983532a037a1ee24b92 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 4 Oct 2023 17:46:50 +0600 Subject: [PATCH 38/63] Refactor Backup Prompt usage - hide qr code toolbar button on Wallet module when required --- .../BottomSheet/BottomSheetModule.swift | 175 +++++++++------- .../ManageAccountsViewController.swift | 198 +++++++++--------- .../WalletTokenBalanceDataSource.swift | 24 +-- .../Modules/Wallet/WalletViewController.swift | 28 +-- .../Modules/Wallet/WalletViewModel.swift | 5 + .../WalletConnectAppShowView.swift | 24 +-- .../en.lproj/Localizable.strings | 7 +- 7 files changed, 211 insertions(+), 250 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift index b6ae2ef412..33979818fe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/BottomSheet/BottomSheetModule.swift @@ -1,123 +1,138 @@ -import UIKit -import SwiftUI -import ThemeKit import ComponentKit import SectionsTableView +import SwiftUI +import ThemeKit +import UIKit protocol IBottomSheetDismissDelegate: AnyObject { func bottomSelectorOnDismiss() } -class BottomSheetModule { - +enum BottomSheetModule { static func viewController(image: BottomSheetTitleView.Image? = nil, title: String, subtitle: String? = nil, items: [Item] = [], buttons: [Button] = [], delegate: IBottomSheetDismissDelegate? = nil) -> UIViewController { let viewController = BottomSheetViewController(image: image, title: title, subtitle: subtitle, items: items, buttons: buttons, delegate: delegate) return viewController.toBottomSheet } - } extension BottomSheetModule { - static func copyConfirmation(value: String) -> UIViewController { viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "copy_warning.title".localized, - items: [ - .highlightedDescription(text: "copy_warning.description".localized) - ], - buttons: [ - .init(style: .red, title: "copy_warning.i_will_risk_it".localized) { - UIPasteboard.general.string = value - HudHelper.instance.show(banner: .copied) - }, - .init(style: .transparent, title: "copy_warning.dont_copy".localized) - ] + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), + title: "copy_warning.title".localized, + items: [ + .highlightedDescription(text: "copy_warning.description".localized), + ], + buttons: [ + .init(style: .red, title: "copy_warning.i_will_risk_it".localized) { + UIPasteboard.general.string = value + HudHelper.instance.show(banner: .copied) + }, + .init(style: .transparent, title: "copy_warning.dont_copy".localized), + ] ) } - static func backupPrompt(account: Account, sourceViewController: UIViewController?) -> UIViewController { + static func backupPromptAfterCreate(account: Account, sourceViewController: UIViewController?) -> UIViewController { + backupPrompt( + title: "backup_prompt.backup_recovery_phrase".localized, + description: "backup_prompt.warning".localized, + cancelText: "backup_prompt.later".localized, + account: account, + sourceViewController: sourceViewController + ) + } + + static func backupRequiredPrompt(description: String, account: Account, sourceViewController: UIViewController?) -> UIViewController { + backupPrompt( + title: "backup_prompt.backup_required".localized, + description: description, + cancelText: "button.cancel".localized, + account: account, + sourceViewController: sourceViewController + ) + } + + private static func backupPrompt(title: String, description: String, cancelText: String, account: Account, sourceViewController: UIViewController?) -> UIViewController { viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "backup_prompt.title".localized, - items: [ - .highlightedDescription(text: "backup_prompt.warning".localized) - ], - buttons: [ - .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [weak sourceViewController] in - guard let viewController = BackupModule.manualViewController(account: account) else { - return - } - - sourceViewController?.present(viewController, animated: true) - }, - .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak sourceViewController] in - sourceViewController?.present(BackupModule.cloudViewController(account: account), animated: true) - }, - .init(style: .transparent, title: "backup_prompt.later".localized) - ] + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), + title: title, + items: [ + .highlightedDescription(text: description), + ], + buttons: [ + .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [weak sourceViewController] in + guard let viewController = BackupModule.manualViewController(account: account) else { + return + } + + sourceViewController?.present(viewController, animated: true) + }, + .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak sourceViewController] in + sourceViewController?.present(BackupModule.cloudViewController(account: account), animated: true) + }, + .init(style: .transparent, title: cancelText), + ] ) } static func description(title: String, text: String) -> UIViewController { viewController( - image: .local(image: UIImage(named: "circle_information_20")?.withTintColor(.themeGray)), - title: title, - items: [ - .description(text: text) - ] + image: .local(image: UIImage(named: "circle_information_20")?.withTintColor(.themeGray)), + title: title, + items: [ + .description(text: text), + ] ) } - static func confirmDeleteCloudBackupController(action: (() -> ())?) -> UIViewController { + static func confirmDeleteCloudBackupController(action: (() -> Void)?) -> UIViewController { viewController( - image: .local(image: UIImage(named: "trash_24")?.withTintColor(.themeLucian)), - title: "manage_account.cloud_delete_backup_recovery_phrase".localized, - items: [ - .highlightedDescription(text: "manage_account.cloud_delete_backup_recovery_phrase.description".localized) - ], - buttons: [ - .init(style: .red, title: "button.delete".localized, actionType: .afterClose) { - action?() - }, - .init(style: .transparent, title: "button.cancel".localized) - ] + image: .local(image: UIImage(named: "trash_24")?.withTintColor(.themeLucian)), + title: "manage_account.cloud_delete_backup_recovery_phrase".localized, + items: [ + .highlightedDescription(text: "manage_account.cloud_delete_backup_recovery_phrase.description".localized), + ], + buttons: [ + .init(style: .red, title: "button.delete".localized, actionType: .afterClose) { + action?() + }, + .init(style: .transparent, title: "button.cancel".localized), + ] ) } - static func deleteCloudBackupAfterManualBackupController(action: (() -> ())?) -> UIViewController { + static func deleteCloudBackupAfterManualBackupController(action: (() -> Void)?) -> UIViewController { viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "manage_account.manual_backup_required".localized, - items: [ - .highlightedDescription(text: "manage_account.manual_backup_required.description".localized) - ], - buttons: [ - .init(style: .yellow, title: "manage_account.manual_backup_required.button".localized, actionType: .afterClose) { - action?() - }, - .init(style: .transparent, title: "button.cancel".localized) - ] + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), + title: "manage_account.manual_backup_required".localized, + items: [ + .highlightedDescription(text: "manage_account.manual_backup_required.description".localized), + ], + buttons: [ + .init(style: .yellow, title: "manage_account.manual_backup_required.button".localized, actionType: .afterClose) { + action?() + }, + .init(style: .transparent, title: "button.cancel".localized), + ] ) } static func cloudNotAvailableController() -> UIViewController { BottomSheetModule.viewController( - image: .local(image: UIImage(named: "icloud_24")?.withTintColor(.themeJacob)), - title: "backup.cloud.no_access.title".localized, - items: [ - .highlightedDescription(text: "backup.cloud.no_access.description".localized) - ], - buttons: [ - .init(style: .yellow, title: "button.ok".localized, actionType: .afterClose), - ] + image: .local(image: UIImage(named: "icloud_24")?.withTintColor(.themeJacob)), + title: "backup.cloud.no_access.title".localized, + items: [ + .highlightedDescription(text: "backup.cloud.no_access.description".localized), + ], + buttons: [ + .init(style: .yellow, title: "button.ok".localized, actionType: .afterClose), + ] ) } - } extension BottomSheetModule { - enum Item { case description(text: String) case highlightedDescription(text: String, style: HighlightedDescriptionBaseView.Style = .yellow) @@ -130,9 +145,9 @@ extension BottomSheetModule { let title: String let imageName: String? let actionType: ActionType - let action: (() -> ())? + let action: (() -> Void)? - init(style: PrimaryButton.Style, title: String, imageName: String? = nil, actionType: ActionType = .regular, action: (() -> ())? = nil) { + init(style: PrimaryButton.Style, title: String, imageName: String? = nil, actionType: ActionType = .regular, action: (() -> Void)? = nil) { self.style = style self.title = title self.imageName = imageName @@ -145,7 +160,6 @@ extension BottomSheetModule { case afterClose } } - } struct ViewWrapper: UIViewControllerRepresentable { @@ -163,4 +177,3 @@ struct ViewWrapper: UIViewControllerRepresentable { func updateUIViewController(_: UIViewController, context _: Context) {} } - diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift index ee7699031e..8128fc6aa1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift @@ -1,10 +1,10 @@ -import UIKit -import ThemeKit +import ComponentKit +import RxCocoa +import RxSwift import SectionsTableView import SnapKit -import RxSwift -import RxCocoa -import ComponentKit +import ThemeKit +import UIKit class ManageAccountsViewController: ThemeViewController { private let viewModel: ManageAccountsViewModel @@ -30,7 +30,8 @@ class ManageAccountsViewController: ThemeViewController { hidesBottomBarWhenPushed = true } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -55,26 +56,26 @@ class ManageAccountsViewController: ThemeViewController { createCell.set(backgroundStyle: .lawrence, isFirst: true) CellBuilderNew.buildStatic(cell: createCell, rootElement: .hStack([ - .image24 { (component: ImageComponent) -> () in + .image24 { (component: ImageComponent) in component.imageView.image = UIImage(named: "plus_24")?.withTintColor(.themeJacob) }, - .text { (component: TextComponent) -> () in + .text { (component: TextComponent) in component.font = .body component.textColor = .themeJacob component.text = "onboarding.balance.create".localized - } + }, ])) restoreCell.set(backgroundStyle: .lawrence) CellBuilderNew.buildStatic(cell: restoreCell, rootElement: .hStack([ - .image24 { (component: ImageComponent) -> () in + .image24 { (component: ImageComponent) in component.imageView.image = UIImage(named: "download_24")?.withTintColor(.themeJacob) }, - .text { (component: TextComponent) -> () in + .text { (component: TextComponent) in component.font = .body component.textColor = .themeJacob component.text = "onboarding.balance.import".localized - } + }, ])) watchCell.set(backgroundStyle: .lawrence, isLast: true) @@ -140,134 +141,127 @@ class ManageAccountsViewController: ThemeViewController { tableView.reload(animated: true) } - } extension ManageAccountsViewController { - func handleDismiss() { if viewModel.shouldClose { dismiss(animated: true) } } - } extension ManageAccountsViewController: ICreateAccountListener { - func handleCreateAccount() { dismiss(animated: true) { [weak self] in guard let account = self?.viewModel.lastCreatedAccount else { return } - let viewController = BottomSheetModule.backupPrompt(account: account, sourceViewController: self) + let viewController = BottomSheetModule.backupPromptAfterCreate(account: account, sourceViewController: self) self?.present(viewController, animated: true) } } - } extension ManageAccountsViewController: SectionsDataSource { - - private func row(viewItem: ManageAccountsViewModel.ViewItem, index: Int, isFirst: Bool, isLast: Bool) -> RowProtocol { + private func row(viewItem: ManageAccountsViewModel.ViewItem, index _: Int, isFirst: Bool, isLast: Bool) -> RowProtocol { CellBuilderNew.row( - rootElement: .hStack([ - .image24 { component in - component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24")?.withTintColor(.themeJacob) : UIImage(named: "circle_radiooff_24")?.withTintColor(.themeGray) + rootElement: .hStack([ + .image24 { component in + component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24")?.withTintColor(.themeJacob) : UIImage(named: "circle_radiooff_24")?.withTintColor(.themeGray) + }, + .vStackCentered([ + .text { component in + component.font = .body + component.textColor = .themeLeah + component.text = viewItem.title }, - .vStackCentered([ - .text { component in - component.font = .body - component.textColor = .themeLeah - component.text = viewItem.title - }, - .margin(1), - .text { component in - component.font = .subhead2 - component.textColor = viewItem.isSubtitleWarning ? .themeLucian : .themeGray - component.text = viewItem.subtitle - } - ]), - .image20 { component in - component.isHidden = !viewItem.watchAccount - component.imageView.image = UIImage(named: "binocule_20")?.withTintColor(.themeGray) + .margin(1), + .text { component in + component.font = .subhead2 + component.textColor = viewItem.isSubtitleWarning ? .themeLucian : .themeGray + component.text = viewItem.subtitle }, - .secondaryCircleButton { [weak self] component in - component.button.set( - image: viewItem.alert ? UIImage(named: "warning_2_20") : UIImage(named: "more_2_20"), - style: viewItem.alert ? .red : .default - ) - component.onTap = { - self?.onTapEdit(accountId: viewItem.accountId) - } - } ]), - tableView: tableView, - id: viewItem.accountId, - hash: "\(viewItem.title)-\(viewItem.subtitle)-\(viewItem.selected)-\(viewItem.alert)-\(viewItem.watchAccount)-\(isFirst)-\(isLast)", - height: .heightDoubleLineCell, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + .image20 { component in + component.isHidden = !viewItem.watchAccount + component.imageView.image = UIImage(named: "binocule_20")?.withTintColor(.themeGray) }, - action: { [weak self] in - self?.viewModel.onSelect(accountId: viewItem.accountId) - } + .secondaryCircleButton { [weak self] component in + component.button.set( + image: viewItem.alert ? UIImage(named: "warning_2_20") : UIImage(named: "more_2_20"), + style: viewItem.alert ? .red : .default + ) + component.onTap = { + self?.onTapEdit(accountId: viewItem.accountId) + } + }, + ]), + tableView: tableView, + id: viewItem.accountId, + hash: "\(viewItem.title)-\(viewItem.subtitle)-\(viewItem.selected)-\(viewItem.alert)-\(viewItem.watchAccount)-\(isFirst)-\(isLast)", + height: .heightDoubleLineCell, + autoDeselect: true, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + }, + action: { [weak self] in + self?.viewModel.onSelect(accountId: viewItem.accountId) + } ) } func buildSections() -> [SectionProtocol] { [ Section( - id: "regular-view-items", - headerState: .margin(height: .margin12), - footerState: .margin(height: viewState.regularViewItems.isEmpty ? 0 : .margin32), - rows: viewState.regularViewItems.enumerated().map { index, viewItem in - row(viewItem: viewItem, index: index, isFirst: index == 0, isLast: index == viewState.regularViewItems.count - 1) - } + id: "regular-view-items", + headerState: .margin(height: .margin12), + footerState: .margin(height: viewState.regularViewItems.isEmpty ? 0 : .margin32), + rows: viewState.regularViewItems.enumerated().map { index, viewItem in + row(viewItem: viewItem, index: index, isFirst: index == 0, isLast: index == viewState.regularViewItems.count - 1) + } ), Section( - id: "watch-view-items", - footerState: .margin(height: viewState.watchViewItems.isEmpty ? 0 : .margin32), - rows: viewState.watchViewItems.enumerated().map { index, viewItem in - row(viewItem: viewItem, index: index, isFirst: index == 0, isLast: index == viewState.watchViewItems.count - 1) - } + id: "watch-view-items", + footerState: .margin(height: viewState.watchViewItems.isEmpty ? 0 : .margin32), + rows: viewState.watchViewItems.enumerated().map { index, viewItem in + row(viewItem: viewItem, index: index, isFirst: index == 0, isLast: index == viewState.watchViewItems.count - 1) + } ), Section( - id: "actions", - footerState: .margin(height: .margin32), - rows: [ - StaticRow( - cell: createCell, - id: "create", - height: .heightCell48, - autoDeselect: true, - action: { [weak self] in - self?.onTapCreate() - } - ), - StaticRow( - cell: restoreCell, - id: "restore", - height: .heightCell48, - autoDeselect: true, - action: { [weak self] in - self?.onTapRestore() - } - ), - StaticRow( - cell: watchCell, - id: "watch", - height: .heightCell48, - autoDeselect: true, - action: { [weak self] in - self?.onTapWatch() - } - ) - ] - ) + id: "actions", + footerState: .margin(height: .margin32), + rows: [ + StaticRow( + cell: createCell, + id: "create", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.onTapCreate() + } + ), + StaticRow( + cell: restoreCell, + id: "restore", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.onTapRestore() + } + ), + StaticRow( + cell: watchCell, + id: "watch", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.onTapWatch() + } + ), + ] + ), ] } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift index 974d75567f..571cf3a1de 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift @@ -215,26 +215,10 @@ class WalletTokenBalanceDataSource: NSObject { } private func openBackupRequired(wallet: Wallet) { - let viewController = BottomSheetModule.viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "backup_required.title".localized, - items: [ - .highlightedDescription(text: "receive_alert.not_backed_up_description".localized(wallet.account.name, wallet.coin.name)), - ], - buttons: [ - .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [weak self] in - guard let viewController = BackupModule.manualViewController(account: wallet.account) else { - return - } - - self?.parentViewController?.present(viewController, animated: true) - }, - .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak self] in - let viewController = BackupModule.cloudViewController(account: wallet.account) - self?.parentViewController?.present(viewController, animated: true) - }, - .init(style: .transparent, title: "button.cancel".localized), - ] + let viewController = BottomSheetModule.backupRequiredPrompt( + description: "receive_alert.not_backed_up_description".localized(wallet.account.name, wallet.coin.name), + account: wallet.account, + sourceViewController: parentViewController ) parentViewController?.present(viewController, animated: true) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift index b15e076e7b..0807c05231 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift @@ -230,7 +230,7 @@ class WalletViewController: ThemeViewController { } @objc func onTapCreate() { - let viewController = CreateAccountModule.viewController(sourceViewController: self) + let viewController = CreateAccountModule.viewController(sourceViewController: self, listener: self) present(viewController, animated: true) } @@ -490,26 +490,10 @@ class WalletViewController: ThemeViewController { } private func openBackupRequired(account: Account) { - let viewController = BottomSheetModule.viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "backup_required.title".localized, - items: [ - .highlightedDescription(text: "receive_alert.any_coins.not_backed_up_description".localized(account.name)), - ], - buttons: [ - .init(style: .yellow, title: "backup_prompt.backup_manual".localized, imageName: "edit_24", actionType: .afterClose) { [weak self] in - guard let viewController = BackupModule.manualViewController(account: account) else { - return - } - - self?.present(viewController, animated: true) - }, - .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak self] in - let viewController = BackupModule.cloudViewController(account: account) - self?.present(viewController, animated: true) - }, - .init(style: .transparent, title: "button.cancel".localized), - ] + let viewController = BottomSheetModule.backupRequiredPrompt( + description: "receive_alert.any_coins.not_backed_up_description".localized(account.name), + account: account, + sourceViewController: self ) present(viewController, animated: true) @@ -601,7 +585,7 @@ class WalletViewController: ThemeViewController { return } - let viewController = BottomSheetModule.backupPrompt(account: account, sourceViewController: self) + let viewController = BottomSheetModule.backupPromptAfterCreate(account: account, sourceViewController: self) present(viewController, animated: true) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift index 36032b4a9a..363475cf3f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift @@ -82,6 +82,11 @@ class WalletViewModel { case .invalidApiKey: state = .invalidApiKey } } + + switch service.state { + case let .loaded(items): qrScanVisible = !service.watchAccount && !items.isEmpty + default: qrScanVisible = false + } } private func sync(activeAccount: Account?) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift index a051a33bbf..c3d14c058f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift @@ -125,26 +125,10 @@ extension WalletConnectAppShowView { ] ) case let .unbackupedAccount(account): - viewController = BottomSheetModule.viewController( - image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), - title: "backup_required.title".localized, - items: [ - .highlightedDescription(text: "wallet_connect.unbackuped_account.description".localized(account.name)), - ], - buttons: [ - .init(style: .yellow, title: "backup_prompt.backup".localized, actionType: .afterClose) { [weak sourceViewController] in - guard let viewController = BackupModule.manualViewController(account: account) else { - return - } - - sourceViewController?.present(viewController, animated: true) - }, - .init(style: .gray, title: "backup_prompt.backup_cloud".localized, imageName: "icloud_24", actionType: .afterClose) { [weak sourceViewController] in - let viewController = BackupModule.cloudViewController(account: account) - sourceViewController?.present(viewController, animated: true) - }, - .init(style: .transparent, title: "button.cancel".localized), - ] + viewController = BottomSheetModule.backupRequiredPrompt( + description: "wallet_connect.unbackuped_account.description".localized(account.name), + account: account, + sourceViewController: sourceViewController ) } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index b5a4cee487..089716155a 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -236,13 +236,10 @@ Go to Settings - > %@ and allow access to the camera."; "backup_verify_passphrase.description" = "Enter the passphrase"; "backup_verify_passphrase.incorrect_passphrase" = "Incorrect passphrase"; -// Backup Required - -"backup_required.title" = "Backup Required"; - // Backup Prompt -"backup_prompt.title" = "Manual Backup"; +"backup_prompt.backup_recovery_phrase" = "Backup Recovery Phrase"; +"backup_prompt.backup_required" = "Backup Required"; "backup_prompt.warning" = "Create a backup copy of the recovery phrase and the associated password that will allow you to recover your wallet if your phone is lost, stolen, broken, etc."; "backup_prompt.backup" = "Backup"; "backup_prompt.backup_manual" = "Manual Backup"; From 6f78ab86f5a36eb76ffeee3a90094749b127eeb8 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 4 Oct 2023 18:35:07 +0600 Subject: [PATCH 39/63] Fix auto-enable tokens for EVM accounts with lots of tokens --- .../Core/Managers/EvmAccountManager.swift | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift index d6bcf38482..ac80611229 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift @@ -1,14 +1,14 @@ +import BigInt +import Combine +import Eip20Kit +import EvmKit import Foundation -import RxSwift -import MarketKit +import HsExtensions import HsToolKit -import EvmKit -import Eip20Kit -import UniswapKit +import MarketKit import OneInchKit -import HsExtensions -import Combine -import BigInt +import RxSwift +import UniswapKit class EvmAccountManager { private let blockchainType: BlockchainType @@ -82,20 +82,20 @@ class EvmAccountManager { case let decoration as SwapDecoration: switch decoration.tokenOut { - case .eip20Coin(let address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: decoration.tokenOut.tokenInfo)) + case let .eip20Coin(address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: decoration.tokenOut.tokenInfo)) default: () } case let decoration as OneInchSwapDecoration: switch decoration.tokenOut { - case .eip20Coin(let address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: decoration.tokenOut.tokenInfo)) + case let .eip20Coin(address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: decoration.tokenOut.tokenInfo)) default: () } case let decoration as OneInchUnoswapDecoration: if let tokenOut = decoration.tokenOut { switch tokenOut { - case .eip20Coin(let address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: tokenOut.tokenInfo)) + case let .eip20Coin(address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: tokenOut.tokenInfo)) default: () } } @@ -103,7 +103,7 @@ class EvmAccountManager { case let decoration as OneInchUnknownSwapDecoration: if let tokenOut = decoration.tokenAmountOut?.token { switch tokenOut { - case .eip20Coin(let address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: tokenOut.tokenInfo)) + case let .eip20Coin(address, _): foundTokens.insert(FoundToken(tokenType: .eip20(address: address.hex), tokenInfo: tokenOut.tokenInfo)) default: () } } @@ -145,26 +145,26 @@ class EvmAccountManager { do { let queries = (foundTokens.map { $0.tokenType } + suspiciousTokenTypes).map { TokenQuery(blockchainType: blockchainType, tokenType: $0) } - let tokens = try marketKit.tokens(queries: queries) + let tokens = try queries.chunks(500).map { try marketKit.tokens(queries: $0) }.flatMap { $0 } var tokenInfos = [TokenInfo]() for foundToken in foundTokens { if let token = tokens.first(where: { $0.type == foundToken.tokenType }) { let tokenInfo = TokenInfo( - type: foundToken.tokenType, - coinName: token.coin.name, - coinCode: token.coin.code, - tokenDecimals: token.decimals + type: foundToken.tokenType, + coinName: token.coin.name, + coinCode: token.coin.code, + tokenDecimals: token.decimals ) tokenInfos.append(tokenInfo) } else if let tokenInfo = foundToken.tokenInfo { let tokenInfo = TokenInfo( - type: foundToken.tokenType, - coinName: tokenInfo.tokenName, - coinCode: tokenInfo.tokenSymbol, - tokenDecimals: tokenInfo.tokenDecimal + type: foundToken.tokenType, + coinName: tokenInfo.tokenName, + coinCode: tokenInfo.tokenSymbol, + tokenDecimals: tokenInfo.tokenDecimal ) tokenInfos.append(tokenInfo) @@ -174,10 +174,10 @@ class EvmAccountManager { for tokenType in suspiciousTokenTypes { if let token = tokens.first(where: { $0.type == tokenType }) { let tokenInfo = TokenInfo( - type: tokenType, - coinName: token.coin.name, - coinCode: token.coin.code, - tokenDecimals: token.decimals + type: tokenType, + coinName: token.coin.name, + coinCode: token.coin.code, + tokenDecimals: token.decimals ) tokenInfos.append(tokenInfo) @@ -247,21 +247,19 @@ class EvmAccountManager { let enabledWallets = infos.map { info in EnabledWallet( - tokenQueryId: TokenQuery(blockchainType: blockchainType, tokenType: info.type).id, - accountId: account.id, - coinName: info.coinName, - coinCode: info.coinCode, - tokenDecimals: info.tokenDecimals + tokenQueryId: TokenQuery(blockchainType: blockchainType, tokenType: info.type).id, + accountId: account.id, + coinName: info.coinName, + coinCode: info.coinCode, + tokenDecimals: info.tokenDecimals ) } walletManager.save(enabledWallets: enabledWallets) } - } extension EvmAccountManager { - struct TokenInfo { let type: TokenType let coinName: String @@ -282,9 +280,8 @@ extension EvmAccountManager { hasher.combine(tokenType) } - static func ==(lhs: FoundToken, rhs: FoundToken) -> Bool { + static func == (lhs: FoundToken, rhs: FoundToken) -> Bool { lhs.tokenType == rhs.tokenType } } - } From 81662f0b671517ed917690eb31e7888611e275b6 Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 6 Oct 2023 16:02:02 +0600 Subject: [PATCH 40/63] Implement new AppStatus module --- .../project.pbxproj | 54 +-- .../UnstoppableWallet/Core/App.swift | 21 +- .../Core/Managers/AppStatusManager.swift | 119 ------- .../AppStatus/AppStatusInteractor.swift | 22 -- .../Modules/AppStatus/AppStatusModule.swift | 27 +- .../AppStatus/AppStatusPresenter.swift | 22 -- .../Modules/AppStatus/AppStatusRouter.swift | 18 - .../Modules/AppStatus/AppStatusView.swift | 67 ++++ .../AppStatus/AppStatusViewController.swift | 80 ----- .../AppStatus/AppStatusViewModel.swift | 204 ++++++++++-- .../Settings/About/AboutViewController.swift | 312 +++++++++--------- .../Security/SecuritySettingsView.swift | 26 +- .../UserInterface/SwiftUI/ActivityView.swift | 13 + .../SwiftUI/Extensions/Text.swift | 6 + .../SwiftUI/PrimaryButtonStyle.swift | 2 +- .../en.lproj/Localizable.strings | 2 +- 16 files changed, 479 insertions(+), 516 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Managers/AppStatusManager.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusInteractor.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusPresenter.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusRouter.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewController.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ActivityView.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index ba686d0d9f..105a87ebaa 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -394,6 +394,7 @@ 11B35480339EC26092522079 /* BalanceTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356E71050EDF5C82FEFD9 /* BalanceTopView.swift */; }; 11B35480CA91E0A62617B83A /* EvmNftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E4B97A593E898724335 /* EvmNftRecord.swift */; }; 11B35481F59793CD9C95B324 /* CreatePasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */; }; + 11B354865DA8CA6A1442D577 /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35496770FA251785E5581 /* AppStatusViewModel.swift */; }; 11B35494E4BA9BF6A3DAA6D6 /* NftMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E2ACF02E2C35EFAE9FA /* NftMetadataService.swift */; }; 11B354A1F79EDA1E58E50418 /* WalletTokenListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */; }; 11B354A3BC2968C083771FC1 /* BlockchainSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F0A42CE7144C82D634 /* BlockchainSettingsModule.swift */; }; @@ -475,6 +476,7 @@ 11B3558589D57B3EAD53919F /* EvmNetworkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351895EE2816DE7BBC767 /* EvmNetworkViewModel.swift */; }; 11B3558898EE33B8D6E571CE /* MnemonicPhraseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3513049D27CB1FA264600 /* MnemonicPhraseCell.swift */; }; 11B3558A85AE9CAD42030EAD /* FiatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350EE043CD96E484F9524 /* FiatService.swift */; }; + 11B355901DFF6BAE9130D60E /* AppStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C16B28B535457F6E34 /* AppStatusView.swift */; }; 11B35590A4DA4BCFB3D38DDF /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352884D47E0B23DCF2C2C /* AppManager.swift */; }; 11B355967997A7C65B008BC7 /* SendEvmTransactionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B31362C98B401A8F9A1 /* SendEvmTransactionViewController.swift */; }; 11B355984178BF117AB606F5 /* SendAvailableBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352F8D9567E50A0DA2F67 /* SendAvailableBalanceCell.swift */; }; @@ -610,6 +612,7 @@ 11B35768B8651AFA005DDFAB /* CoinTreasury.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35185ECC372A193D00A00 /* CoinTreasury.swift */; }; 11B35769EB5389A80BAF11FE /* CoinMarketsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F0A7192BA590254A16E /* CoinMarketsViewController.swift */; }; 11B3577193A4BD719E12CE2E /* EnabledWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDC2E3F845A52C442AD /* EnabledWallet.swift */; }; + 11B357740CC018527301C4AE /* AppStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C16B28B535457F6E34 /* AppStatusView.swift */; }; 11B35774CEE79A1FD5265FB0 /* EnabledWalletStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35763ED14419B9EE4C6F9 /* EnabledWalletStorage.swift */; }; 11B35777D07EC35D2AD98094 /* ReceiveAddressModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB3B8928864A742C83E /* ReceiveAddressModule.swift */; }; 11B3577BDCF978797E6C283E /* RestoreService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3596ECFEECF17ADB3BAEF /* RestoreService.swift */; }; @@ -811,6 +814,7 @@ 11B359B11871F76B25426D58 /* MarketAdvancedSearchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCB7125B0046592414B /* MarketAdvancedSearchModule.swift */; }; 11B359B7C572FDCA7CD68320 /* FaqService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352B4E116BEC01B972A39 /* FaqService.swift */; }; 11B359BA446B27E6D369B35E /* RestoreModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BD0CE4F979CA88EFF0 /* RestoreModule.swift */; }; + 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35496770FA251785E5581 /* AppStatusViewModel.swift */; }; 11B359C05619611CBCFC89AC /* EvmBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D15E318829D9C7F5F1 /* EvmBlockchainManager.swift */; }; 11B359C198AA7A141522E5E9 /* EvmAccountManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F980B34E005B9F02B8F /* EvmAccountManagerFactory.swift */; }; 11B359C2651DA1F00A3C613C /* CoinRankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */; }; @@ -850,6 +854,7 @@ 11B35A426FD3D729DEB89DEA /* MarketTopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */; }; 11B35A42BF19B93C6005FBD9 /* AddTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356FFA77A8F6918B13FCA /* AddTokenService.swift */; }; 11B35A42D28B8BC4CDA57D8E /* AccountRecord_v_0_19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F6C5F6ABC288511AF0 /* AccountRecord_v_0_19.swift */; }; + 11B35A431DE03F33E739B639 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EDE38851EC8658D8A99 /* ActivityView.swift */; }; 11B35A48CF68A2A45E1A429E /* PageDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FDDBEF227E161F6A0E /* PageDescription.swift */; }; 11B35A4CBD60780E0870E77C /* NftAssetBriefMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E32AEEE37347E255C4 /* NftAssetBriefMetadata.swift */; }; 11B35A4D9BD4B8C29FBAFACF /* AboutModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E80D544DAF20B12B56 /* AboutModule.swift */; }; @@ -1042,6 +1047,7 @@ 11B35CA92AA402BE72B4F5D6 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352648C452D611F1EDF61 /* Image.swift */; }; 11B35CAD5A7E0C8709559FD2 /* WalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D547F1BB38D2AD6AD5 /* WalletManager.swift */; }; 11B35CADA5EE093B974C5A4A /* EvmLabelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F13BAFE57D363B9684F /* EvmLabelManager.swift */; }; + 11B35CAE0540A2549BD4A960 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EDE38851EC8658D8A99 /* ActivityView.swift */; }; 11B35CB0102019E63D7337D5 /* NftCollectionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35690912F374FEE910193 /* NftCollectionMetadata.swift */; }; 11B35CB50F9904708B827F9F /* CoinToggleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CDE31673BA1673B620 /* CoinToggleViewModel.swift */; }; 11B35CB5A90FCD0B53D59140 /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357736B8C29DF38F5DCBA /* AlertViewController.swift */; }; @@ -1232,7 +1238,6 @@ 11B35EBE688A7C4F9B92F865 /* ReceiveModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF031BC81E4D401CA01 /* ReceiveModule.swift */; }; 11B35EC2E4E5614FF64C7246 /* MarketMultiSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3503B9A985B4835FDB03D /* MarketMultiSortHeaderView.swift */; }; 11B35EC3B9E9C778183E1136 /* EvmNftAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C2397749C5654830540 /* EvmNftAdapter.swift */; }; - 11B35EC7F06AEAB8E555B833 /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3525406D0B011EB76ACE6 /* AppStatusViewModel.swift */; }; 11B35ECCE5D888A506D7144A /* RestoreBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354D96A80987DAB3B64A6 /* RestoreBinanceViewController.swift */; }; 11B35ED22837284580055F0A /* BalanceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDEB703708795B71C4E /* BalanceData.swift */; }; 11B35ED81BCE008EE5A71DE8 /* LockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F57D462E2C9E9AEF67C /* LockManager.swift */; }; @@ -1282,7 +1287,6 @@ 11B35F6B92C2FB142E522828 /* BtcBlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358830357DB1F87FCA006 /* BtcBlockchainSettingsViewModel.swift */; }; 11B35F6CD2706B10781456E8 /* ExtendedKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F95A84DD0F232E5A9CD /* ExtendedKeyViewModel.swift */; }; 11B35F6FDB8640381081A06C /* FormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A41EC99ADCC8F3E3E9 /* FormAmountInputView.swift */; }; - 11B35F7154E7B2E9FB4C866F /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3525406D0B011EB76ACE6 /* AppStatusViewModel.swift */; }; 11B35F72D67DB96FA83C9004 /* AddEvmSyncSourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F33517C6DDA1E7AF59 /* AddEvmSyncSourceViewController.swift */; }; 11B35F73153DEE805DD539CE /* EvmNetworkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35711A471C5A45DD87108 /* EvmNetworkViewController.swift */; }; 11B35F78F82224BE17D612AB /* RecoveryPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A0B1AE5F612E6A5FEE /* RecoveryPhraseService.swift */; }; @@ -1349,7 +1353,6 @@ 1A5640D097E24A155C1F2E56 /* MarketOverviewCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5645B1C5FD344967B1F4B7 /* MarketOverviewCategoryCell.swift */; }; 1A5640D6FDF86EDB54213F9B /* DeepLinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564580B3F739DAC59C623F /* DeepLinkManager.swift */; }; 1A5640DE72AC306799695F48 /* TopPlatformMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */; }; - 1A56411B659245BEDA547D06 /* AppStatusRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56446DB0696819B2ABC567 /* AppStatusRouter.swift */; }; 1A56412437A8C323E8555C82 /* WalletConnectSignMessageRequestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56400EBD84656A0447EA59 /* WalletConnectSignMessageRequestService.swift */; }; 1A56412970FD129426474522 /* MarketOverviewTopPlatformsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */; }; 1A56413DFF5B9A8E3F2A5EA6 /* ReleaseNotesMarkdownConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56432704A2E7A9BE78497B /* ReleaseNotesMarkdownConfig.swift */; }; @@ -1374,7 +1377,6 @@ 1A564335057D41EECDC8021B /* MarketOverviewTopCoinsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D661F3AE561D7FE9FAA /* MarketOverviewTopCoinsService.swift */; }; 1A5643651979E1907BE1B12C /* BlockchainSettingRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C46FB773A67E29D9D32 /* BlockchainSettingRecord.swift */; }; 1A5643813B0713460096F6D1 /* AppVersionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56446CCB15D32581396A59 /* AppVersionRecord.swift */; }; - 1A56439ABC0BB083D41F57E2 /* AppStatusInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A601F3F8DF2664007E3 /* AppStatusInteractor.swift */; }; 1A5643A263F288A4E83409FA /* BalanceErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564702FB246F315983743E /* BalanceErrorViewModel.swift */; }; 1A5643B0422BC87461CC25C5 /* MarketOverviewTopPlatformsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */; }; 1A5643BCA38A75A63D57F1AB /* SendEthereumErrorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564920282E84A6E7EE05EB /* SendEthereumErrorCell.swift */; }; @@ -1394,13 +1396,11 @@ 1A56443EA3671FA2A40F1F7E /* TopPlatformModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BDA5600859626D99BB4 /* TopPlatformModule.swift */; }; 1A56444313F5FB0DE9B06BE4 /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE10FD5FEC14EF38BD8 /* PrivacyPolicyViewController.swift */; }; 1A56444C8342498C892E931E /* MarketNftTopCollectionsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E5282C3C22DA85141AF /* MarketNftTopCollectionsModule.swift */; }; - 1A56449D17122EDBCDF92BD0 /* AppStatusInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A601F3F8DF2664007E3 /* AppStatusInteractor.swift */; }; 1A5644CAEC833EA0583556FD /* MarketDiscoveryFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E7A01B2DD08CB174C10 /* MarketDiscoveryFilterHeaderView.swift */; }; 1A5644CF2BEC2E7C6227BDC7 /* AppStatusModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56404C1C16B85434117DB7 /* AppStatusModule.swift */; }; 1A5644FA9A9599F94EE16916 /* MarketNftTopCollectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649E41FE690AF0A712426 /* MarketNftTopCollectionsViewModel.swift */; }; 1A5644FE2885F71299CC66EA /* PlaceholderViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CF35E7A07E96B704ADA /* PlaceholderViewModule.swift */; }; 1A564504E164177DD6EECFBA /* BalanceErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646B5D68A302515565030 /* BalanceErrorService.swift */; }; - 1A56450B62EC2CABE49F2ABF /* AppStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C8BA986A5635B1222FB /* AppStatusManager.swift */; }; 1A564512989CEBC48ABCB94E /* ConvertedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */; }; 1A564515554F7BCAF473FE65 /* FilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B1C051AF2C87C670563 /* FilterHeaderView.swift */; }; 1A56451ADE50B86E21814347 /* MarkdownContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56417C27A95B429D9F2912 /* MarkdownContentProvider.swift */; }; @@ -1419,13 +1419,11 @@ 1A5646322B606C56DFFA324A /* NftCollectionsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5643A672A508BC4CBCABDD /* NftCollectionsMultiSortHeaderViewModel.swift */; }; 1A56463AB918839385E8CDBD /* BlockchainSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56443BF752CB6537E45F5A /* BlockchainSettingsStorage.swift */; }; 1A56463FADAB4646BA106A5D /* TopPlatformMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */; }; - 1A56463FF49CCBF64CC921B5 /* AppStatusRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56446DB0696819B2ABC567 /* AppStatusRouter.swift */; }; 1A56464440899E3299F79D32 /* JailbreakService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56469A2F3EAAEDECFB4034 /* JailbreakService.swift */; }; 1A564651245AAE0CDD692A97 /* AcademyMarkdownConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564215DD6F0D54C1F6C4F7 /* AcademyMarkdownConfig.swift */; }; 1A5646632409BBC2A8790807 /* ReleaseNotesMarkdownConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56432704A2E7A9BE78497B /* ReleaseNotesMarkdownConfig.swift */; }; 1A564665243CEEBF646D1328 /* ConvertedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */; }; 1A564699CFB7CCE8BD3E5245 /* AppVersionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564AFF2709E27114985A8D /* AppVersionStorage.swift */; }; - 1A5646BA7EEED806D4C85025 /* AppStatusPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564827B8F8D94DC4D7CC0F /* AppStatusPresenter.swift */; }; 1A5646C3220E1735309D2927 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B7C35C5B235D2BBAC2C /* AppVersion.swift */; }; 1A5646E52D8DFADAA5ACFCAD /* TopPlatformsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D8F8A8A63BC9BEAAD56 /* TopPlatformsMultiSortHeaderViewModel.swift */; }; 1A5646E67996AB355694A35E /* EnabledWallet_v_0_13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A55E5866D6081EA6F69 /* EnabledWallet_v_0_13.swift */; }; @@ -1450,7 +1448,6 @@ 1A5648D075682B17EFE9CBB6 /* AddressData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C6FFC694CD18A8B39A /* AddressData.swift */; }; 1A5648D3EC31C2E267FB97DD /* BinanceAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564872B7C5F76D8CE55A8B /* BinanceAddressParserItem.swift */; }; 1A5648D7D29951DC4762B392 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B7C35C5B235D2BBAC2C /* AppVersion.swift */; }; - 1A5648F960BA99CA9DC5478B /* AppStatusPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564827B8F8D94DC4D7CC0F /* AppStatusPresenter.swift */; }; 1A5649169A405D3288324442 /* BalanceErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE56CEC73B78C9DB6B5 /* BalanceErrorViewController.swift */; }; 1A56491DC545ED4F8A6E6D40 /* Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D5E55767404ED6C88E0 /* Decimal.swift */; }; 1A564975B127F4EA2FCF61EB /* BitcoinBaseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56450DA6DF97C9E1FFE987 /* BitcoinBaseAdapter.swift */; }; @@ -1475,7 +1472,6 @@ 1A564AB8471E098F68FDB9D7 /* BinanceAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564872B7C5F76D8CE55A8B /* BinanceAddressParserItem.swift */; }; 1A564ABF3417C13718D78F57 /* ThemeSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EA6F1CCDF88F78351F8 /* ThemeSearchViewController.swift */; }; 1A564AC9AA4084E742D979B2 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5642E9B9E5592E04373C16 /* ButtonState.swift */; }; - 1A564ACAD8825F7A94B08DF2 /* AppStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56420928E5E0E9BC27E67B /* AppStatusViewController.swift */; }; 1A564AFAB995335885C6782C /* BasePerformanceCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D7B1F36B1C4AB4CBF3A /* BasePerformanceCollectionViewCell.swift */; }; 1A564B17A7FF7D36926CF6DE /* ThemeActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E86CCEAD0F9956664D4 /* ThemeActionSheetController.swift */; }; 1A564B1C354CEC471841AEB9 /* ReachabilityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564814721244F4D4D87557 /* ReachabilityViewModel.swift */; }; @@ -1501,7 +1497,6 @@ 1A564CC4790F0CED826C131F /* MarketOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */; }; 1A564CFD8F22A2F5FDB346EA /* JailbreakService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56469A2F3EAAEDECFB4034 /* JailbreakService.swift */; }; 1A564D02348C91416FD011FC /* TraitsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564FF31C5E879781A2D5E0 /* TraitsCell.swift */; }; - 1A564D0E3139291B3FD3613B /* AppStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56420928E5E0E9BC27E67B /* AppStatusViewController.swift */; }; 1A564D209D3AFA40F808C8FB /* PerformanceContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646483957D74946973BEE /* PerformanceContentCollectionViewCell.swift */; }; 1A564D2917CF5C38C84C031B /* BitcoinBaseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56450DA6DF97C9E1FFE987 /* BitcoinBaseAdapter.swift */; }; 1A564D3DB55C8CB8B5AED664 /* BalanceErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564702FB246F315983743E /* BalanceErrorViewModel.swift */; }; @@ -1522,7 +1517,6 @@ 1A564E0B7DB0060B9600FCB1 /* BalanceErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE56CEC73B78C9DB6B5 /* BalanceErrorViewController.swift */; }; 1A564E1912184BFC886548D9 /* MarketOverviewCategoryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */; }; 1A564E2897197EB14584A62E /* MarketOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */; }; - 1A564E49B65B5396F2CB47FB /* AppStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C8BA986A5635B1222FB /* AppStatusManager.swift */; }; 1A564E69BA99DF8CD4562902 /* PlaceholderViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CF35E7A07E96B704ADA /* PlaceholderViewModule.swift */; }; 1A564E768845395731A5B580 /* MarketOverviewNftCollectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5648C84CA034F0B8812F8F /* MarketOverviewNftCollectionsViewModel.swift */; }; 1A564E8CA0CFBE8B1E232B60 /* PerformanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641E505FE004F601943C4 /* PerformanceTableViewCell.swift */; }; @@ -2856,7 +2850,6 @@ 11B35249BB89CF45176701EA /* ChooseBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseBlockchainService.swift; sourceTree = ""; }; 11B3524B273DD5AB2FF5C7A6 /* Eip20Kit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Eip20Kit.swift; sourceTree = ""; }; 11B35252F90F25774BDD2CB3 /* NftActivityModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityModule.swift; sourceTree = ""; }; - 11B3525406D0B011EB76ACE6 /* AppStatusViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusViewModel.swift; sourceTree = ""; }; 11B3525F8436F286491A241F /* BinanceChainKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceChainKit.swift; sourceTree = ""; }; 11B352648C452D611F1EDF61 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 11B35264F7CE80AD5D9A540A /* CoinInvestment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestment.swift; sourceTree = ""; }; @@ -2964,6 +2957,7 @@ 11B3548F0E1223B08D3B7F0C /* CexWithdrawService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawService.swift; sourceTree = ""; }; 11B35492B1162F69CA7A0597 /* DateHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateHelper.swift; sourceTree = ""; }; 11B354950B1534AD045FDA3A /* SendEvmConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmConfirmationViewController.swift; sourceTree = ""; }; + 11B35496770FA251785E5581 /* AppStatusViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusViewModel.swift; sourceTree = ""; }; 11B354AFC10A63BDF4E86EE0 /* MarketWideCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWideCardCell.swift; sourceTree = ""; }; 11B354B32BD428041237570A /* NftRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftRecord.swift; sourceTree = ""; }; 11B354B43B5120F594318FDA /* PermissionsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsHelper.swift; sourceTree = ""; }; @@ -3085,6 +3079,7 @@ 11B357B185E8FECB3924FDF2 /* BlockchainType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainType.swift; sourceTree = ""; }; 11B357B2D07C69579BAEC997 /* CoinType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinType.swift; sourceTree = ""; }; 11B357BA1A6AC79F07B54FB5 /* SystemInfoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemInfoManager.swift; sourceTree = ""; }; + 11B357C16B28B535457F6E34 /* AppStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusView.swift; sourceTree = ""; }; 11B357C17104792A20769560 /* CoinCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinCategory.swift; sourceTree = ""; }; 11B357C67623035CDF98B540 /* CexAssetResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAssetResponse.swift; sourceTree = ""; }; 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewController.swift; sourceTree = ""; }; @@ -3393,6 +3388,7 @@ 11B35EC9E0E936067225C787 /* PoolSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolSource.swift; sourceTree = ""; }; 11B35ECC6866F29A33129F06 /* NftHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftHeaderView.swift; sourceTree = ""; }; 11B35EDE31BA3EF80F78859A /* HsLabelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HsLabelProvider.swift; sourceTree = ""; }; + 11B35EDE38851EC8658D8A99 /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 11B35EE072CE5471B0DFF841 /* TestNetManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNetManager.swift; sourceTree = ""; }; 11B35EF3688D60C8E6823267 /* BottomSingleSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSingleSelectorViewController.swift; sourceTree = ""; }; 11B35EFB45ECC2D403CA6C89 /* ValueFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueFormatter.swift; sourceTree = ""; }; @@ -3444,7 +3440,6 @@ 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionManager.swift; sourceTree = ""; }; 1A5641E505FE004F601943C4 /* PerformanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTableViewCell.swift; sourceTree = ""; }; 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewViewModel.swift; sourceTree = ""; }; - 1A56420928E5E0E9BC27E67B /* AppStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusViewController.swift; sourceTree = ""; }; 1A564215DD6F0D54C1F6C4F7 /* AcademyMarkdownConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcademyMarkdownConfig.swift; sourceTree = ""; }; 1A56422C196B48931CDE1445 /* SendTransactionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTransactionError.swift; sourceTree = ""; }; 1A564293D88587642800717B /* FilterHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterHeaderCell.swift; sourceTree = ""; }; @@ -3459,7 +3454,6 @@ 1A56443BF752CB6537E45F5A /* BlockchainSettingsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingsStorage.swift; sourceTree = ""; }; 1A56444EB2F32DB662981653 /* TitledHighlightedDescriptionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledHighlightedDescriptionCell.swift; sourceTree = ""; }; 1A56446CCB15D32581396A59 /* AppVersionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionRecord.swift; sourceTree = ""; }; - 1A56446DB0696819B2ABC567 /* AppStatusRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusRouter.swift; sourceTree = ""; }; 1A56446DB62F52AC4C3C2C30 /* SecuritySettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettingsViewModel.swift; sourceTree = ""; }; 1A56447C12D91108517ED217 /* UIDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 1A5644A21F9FEC4E2A7B0860 /* PlaceholderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderView.swift; sourceTree = ""; }; @@ -3486,7 +3480,6 @@ 1A5647AD7481B36F20D4DDF9 /* MainSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsModule.swift; sourceTree = ""; }; 1A5647FA18CC69113ECB6581 /* MarketOverviewGlobalDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewGlobalDataSource.swift; sourceTree = ""; }; 1A564814721244F4D4D87557 /* ReachabilityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityViewModel.swift; sourceTree = ""; }; - 1A564827B8F8D94DC4D7CC0F /* AppStatusPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusPresenter.swift; sourceTree = ""; }; 1A56485B094980B68B0A86AE /* ReadMoreTextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadMoreTextCell.swift; sourceTree = ""; }; 1A564872B7C5F76D8CE55A8B /* BinanceAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceAddressParserItem.swift; sourceTree = ""; }; 1A564879AD72301AAB78F8F5 /* MainSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsViewController.swift; sourceTree = ""; }; @@ -3504,7 +3497,6 @@ 1A564A144576DB93334E1682 /* ScanQrViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQrViewController.swift; sourceTree = ""; }; 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformMarketCapFetcher.swift; sourceTree = ""; }; 1A564A55E5866D6081EA6F69 /* EnabledWallet_v_0_13.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_13.swift; sourceTree = ""; }; - 1A564A601F3F8DF2664007E3 /* AppStatusInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusInteractor.swift; sourceTree = ""; }; 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertedError.swift; sourceTree = ""; }; 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryDataSource.swift; sourceTree = ""; }; 1A564AB0B646F7A92DD188F2 /* BalanceErrorModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceErrorModule.swift; sourceTree = ""; }; @@ -3524,7 +3516,6 @@ 1A564C4DB4A57CCF2C5EFB78 /* MarketTopPlatformsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPlatformsViewController.swift; sourceTree = ""; }; 1A564C5CC7EC339C3113869D /* MarketListTopPlatformDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListTopPlatformDecorator.swift; sourceTree = ""; }; 1A564C60EABE355B1F395D97 /* WalletConnectSignMessageRequestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSignMessageRequestViewController.swift; sourceTree = ""; }; - 1A564C8BA986A5635B1222FB /* AppStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusManager.swift; sourceTree = ""; }; 1A564CB28708314AE0A69424 /* TitledHighlightedDescriptionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledHighlightedDescriptionView.swift; sourceTree = ""; }; 1A564CC5878BF33B8CE1F339 /* MarketListNftCollectionDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListNftCollectionDecorator.swift; sourceTree = ""; }; 1A564CE10FD5FEC14EF38BD8 /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = ""; }; @@ -4687,6 +4678,7 @@ ABC9ACEC3169A9F01B55921A /* InputTextView.swift */, ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */, ABC9A104D916039D690E454E /* Shake.swift */, + 11B35EDE38851EC8658D8A99 /* ActivityView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -4727,7 +4719,6 @@ 11B35D0672D73C973EBE5E1B /* BinanceKitManager.swift */, 2FA5D02D8F5C2AE32C6FF923 /* KitCleaner.swift */, 58AAA18F75B95ACBBAE94DF3 /* DebugLogger.swift */, - 1A564C8BA986A5635B1222FB /* AppStatusManager.swift */, 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */, 11B35FA360A91FDE3EB0B85C /* RateAppManager.swift */, 1A5646B6231F2C52F27526F7 /* BtcBlockchainManager.swift */, @@ -6213,11 +6204,8 @@ isa = PBXGroup; children = ( 1A56404C1C16B85434117DB7 /* AppStatusModule.swift */, - 1A564A601F3F8DF2664007E3 /* AppStatusInteractor.swift */, - 1A564827B8F8D94DC4D7CC0F /* AppStatusPresenter.swift */, - 1A56446DB0696819B2ABC567 /* AppStatusRouter.swift */, - 1A56420928E5E0E9BC27E67B /* AppStatusViewController.swift */, - 11B3525406D0B011EB76ACE6 /* AppStatusViewModel.swift */, + 11B357C16B28B535457F6E34 /* AppStatusView.swift */, + 11B35496770FA251785E5581 /* AppStatusViewModel.swift */, ); path = AppStatus; sourceTree = ""; @@ -8220,15 +8208,10 @@ 58AAAD3AC2B87E6AAFE535D8 /* DebugLogger.swift in Sources */, 3A73FCAE258B1AFD00FE4D34 /* MarketMetricView.swift in Sources */, 1A564EDBD3E4B37299E199B7 /* AppStatusModule.swift in Sources */, - 1A56449D17122EDBCDF92BD0 /* AppStatusInteractor.swift in Sources */, - 1A5646BA7EEED806D4C85025 /* AppStatusPresenter.swift in Sources */, - 1A56463FF49CCBF64CC921B5 /* AppStatusRouter.swift in Sources */, - 1A564E49B65B5396F2CB47FB /* AppStatusManager.swift in Sources */, 11B3557CB2595D2884C94498 /* MultiTextMetricsView.swift in Sources */, 1A564F73B7FE144D39DEA34F /* UIDevice.swift in Sources */, 1A5648D7D29951DC4762B392 /* AppVersion.swift in Sources */, 1A5649F72A0116C66DCBA153 /* AppVersionManager.swift in Sources */, - 1A564D0E3139291B3FD3613B /* AppStatusViewController.swift in Sources */, 58AAA44FC19684B426489776 /* ChartIntervalConverter.swift in Sources */, 2FA5D2FE81DA1FB5A63C2D7C /* BitcoinCore+Hodler.swift in Sources */, 3A73FC6A258B1AD200FE4D34 /* MarketModule.swift in Sources */, @@ -8349,7 +8332,6 @@ D008CA5B267C8DDF00001E0A /* EvmIncomingTransactionRecord.swift in Sources */, 11B35C2FBF875F81E13CC575 /* CoinService.swift in Sources */, 1A5647072B937BE4B69FFA1D /* SendEthereumErrorCell.swift in Sources */, - 11B35EC7F06AEAB8E555B833 /* AppStatusViewModel.swift in Sources */, 58AAAAE261DEB08128441641 /* AmountDecimalParser.swift in Sources */, 11B35DDFAF0532881A4F68B0 /* AdditionalDataCellNew.swift in Sources */, 11B35D93B238BA992173E123 /* StackViewCell.swift in Sources */, @@ -9427,6 +9409,9 @@ ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */, ABC9A06CA0D364D4E1261C87 /* BackupPasswordView.swift in Sources */, ABC9A4A21CFBA188A7EEC930 /* ActivityViewController.swift in Sources */, + 11B357740CC018527301C4AE /* AppStatusView.swift in Sources */, + 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */, + 11B35CAE0540A2549BD4A960 /* ActivityView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9542,15 +9527,10 @@ D05E96902A261D82002CCD71 /* TronTransactionAdapter.swift in Sources */, 3A73FCAA258B1AFC00FE4D34 /* MarketMetricView.swift in Sources */, D05E96A32A2627DA002CCD71 /* TronIncomingTransactionRecord.swift in Sources */, - 1A56439ABC0BB083D41F57E2 /* AppStatusInteractor.swift in Sources */, - 1A5648F960BA99CA9DC5478B /* AppStatusPresenter.swift in Sources */, - 1A56411B659245BEDA547D06 /* AppStatusRouter.swift in Sources */, - 1A56450B62EC2CABE49F2ABF /* AppStatusManager.swift in Sources */, 11B35D51B52EF0000711CE05 /* MultiTextMetricsView.swift in Sources */, 1A5647EF2A5B8141F0BE4320 /* UIDevice.swift in Sources */, 1A5646C3220E1735309D2927 /* AppVersion.swift in Sources */, 1A5648ACB0A6B11E0E39A1B0 /* AppVersionManager.swift in Sources */, - 1A564ACAD8825F7A94B08DF2 /* AppStatusViewController.swift in Sources */, D02A67BF272A7460009B2C1C /* TweetsProvider.swift in Sources */, 58AAA5FDC26FC291E9E82928 /* ChartIntervalConverter.swift in Sources */, 2FA5D2A2E81E947B2CF15889 /* BitcoinCore+Hodler.swift in Sources */, @@ -9677,7 +9657,6 @@ D008CA5A267C8DDF00001E0A /* EvmIncomingTransactionRecord.swift in Sources */, 11B358902CE8D7EF2AD38448 /* CoinService.swift in Sources */, 1A5643BCA38A75A63D57F1AB /* SendEthereumErrorCell.swift in Sources */, - 11B35F7154E7B2E9FB4C866F /* AppStatusViewModel.swift in Sources */, 58AAA0642CB9B7B19C6235B5 /* AmountDecimalParser.swift in Sources */, 11B357BF378060E7E35F7052 /* AdditionalDataCellNew.swift in Sources */, 11B3580E4A964C65BF8EDDE9 /* StackViewCell.swift in Sources */, @@ -10753,6 +10732,9 @@ ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */, ABC9A54E6E15E96271525339 /* BackupPasswordView.swift in Sources */, ABC9A12A4D114A2E4F4C711A /* ActivityViewController.swift in Sources */, + 11B355901DFF6BAE9130D60E /* AppStatusView.swift in Sources */, + 11B354865DA8CA6A1442D577 /* AppStatusViewModel.swift in Sources */, + 11B35A431DE03F33E739B639 /* ActivityView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 88ae3837cb..ea47dd46fa 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -67,6 +67,7 @@ class App { let evmSyncSourceManager: EvmSyncSourceManager let evmAccountRestoreStateManager: EvmAccountRestoreStateManager let evmBlockchainManager: EvmBlockchainManager + let binanceKitManager: BinanceKitManager let tronAccountManager: TronAccountManager let restoreSettingsManager: RestoreSettingsManager @@ -77,7 +78,7 @@ class App { var debugLogger: DebugLogger? let logger: Logger - let appStatusManager: AppStatusManager + let appVersionStorage: AppVersionStorage let appVersionManager: AppVersionManager let testNetManager: TestNetManager @@ -196,7 +197,7 @@ class App { let evmAccountManagerFactory = EvmAccountManagerFactory(accountManager: accountManager, walletManager: walletManager, evmAccountRestoreStateManager: evmAccountRestoreStateManager, marketKit: marketKit) evmBlockchainManager = EvmBlockchainManager(syncSourceManager: evmSyncSourceManager, testNetManager: testNetManager, marketKit: marketKit, accountManagerFactory: evmAccountManagerFactory) - let binanceKitManager = BinanceKitManager() + binanceKitManager = BinanceKitManager() let tronKitManager = TronKitManager(testNetManager: testNetManager) tronAccountManager = TronAccountManager(accountManager: accountManager, walletManager: walletManager, marketKit: marketKit, tronKitManager: tronKitManager, evmAccountRestoreStateManager: evmAccountRestoreStateManager) @@ -251,21 +252,7 @@ class App { favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage) let appVersionRecordStorage = AppVersionRecordStorage(dbPool: dbPool) - let appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage) - - appStatusManager = AppStatusManager( - systemInfoManager: systemInfoManager, - storage: appVersionStorage, - accountManager: accountManager, - walletManager: walletManager, - adapterManager: adapterManager, - logRecordManager: logRecordManager, - restoreSettingsManager: restoreSettingsManager, - evmBlockchainManager: evmBlockchainManager, - binanceKitManager: binanceKitManager, - marketKit: marketKit - ) - + appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage) appVersionManager = AppVersionManager(systemInfoManager: systemInfoManager, storage: appVersionStorage) keychainKitDelegate = KeychainKitDelegate(accountManager: accountManager, walletManager: walletManager) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppStatusManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppStatusManager.swift deleted file mode 100644 index 9cc954e43b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppStatusManager.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import MarketKit - -class AppStatusManager { - private let systemInfoManager: SystemInfoManager - private let storage: AppVersionStorage - private let logRecordManager: LogRecordManager - private let accountManager: AccountManager - private let walletManager: WalletManager - private let adapterManager: AdapterManager - private let restoreSettingsManager: RestoreSettingsManager - private let evmBlockchainManager: EvmBlockchainManager - private let binanceKitManager: BinanceKitManager - private let marketKit: MarketKit.Kit - - init(systemInfoManager: SystemInfoManager, storage: AppVersionStorage, accountManager: AccountManager, - walletManager: WalletManager, adapterManager: AdapterManager, logRecordManager: LogRecordManager, restoreSettingsManager: RestoreSettingsManager, - evmBlockchainManager: EvmBlockchainManager, binanceKitManager: BinanceKitManager, marketKit: MarketKit.Kit) { - self.systemInfoManager = systemInfoManager - self.storage = storage - self.accountManager = accountManager - self.walletManager = walletManager - self.adapterManager = adapterManager - self.logRecordManager = logRecordManager - self.restoreSettingsManager = restoreSettingsManager - self.evmBlockchainManager = evmBlockchainManager - self.binanceKitManager = binanceKitManager - self.marketKit = marketKit - } - - private var marketLastSyncTimestamps: [(String, Any)] { - let syncInfo = marketKit.syncInfo() - - return [ - ("Coins", syncInfo.coinsTimestamp ?? "nil"), - ("Blockchains", syncInfo.blockchainsTimestamp ?? "nil"), - ("Tokens", syncInfo.tokensTimestamp ?? "nil") - ] - } - - private var accountStatus: [(String, Any)] { - accountManager.accounts.compactMap { account in - var status = [(String, Any)]() - - status.append(("origin", "\(account.origin)")) - - if case let .mnemonic(words, salt, _) = account.type { - status.append(("type", "mnemonic (\(words.count) words\(salt.isEmpty ? "" : " with passphrase"))")) - } - - let restoreSettingsInfo = restoreSettingsManager.accountSettingsInfo(account: account) - - if !restoreSettingsInfo.isEmpty { - var restoreSettings = [(String, Any)]() - - for info in restoreSettingsInfo { - let coinType = info.0 - let settingType = info.1 - let value = info.2.isEmpty ? "not set" : info.2 - restoreSettings.append(("\(coinType) - \(settingType)", "\(value)")) - } - - status.append(("Restore Settings", restoreSettings)) - } - - return (account.name, status) - } - } - - private var blockchainStatus: [(String, Any)] { - var status = [(String, Any)]() - - for wallet in walletManager.activeWallets { - let blockchain = wallet.token.blockchain - - switch blockchain.type { - case .bitcoin, .bitcoinCash, .ecash, .litecoin, .dash, .zcash: - if let adapter = adapterManager.adapter(for: wallet) { - status.append((blockchain.name, adapter.statusInfo)) - } - default: - () - } - } - - for blockchain in evmBlockchainManager.allBlockchains { - if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchain.type).evmKitWrapper { - status.append((blockchain.name, evmKitWrapper.evmKit.statusInfo())) - } - } - - if let binanceKit = binanceKitManager.binanceKit { - status.append(("Binance Chain", binanceKit.statusInfo)) - } - - return status - } - -} - -extension AppStatusManager { - - var status: [(String, Any)] { - [ - ("App Info", [ - ("Current Time", Date()), - ("App Version", systemInfoManager.appVersion.description), - ("Phone Model", systemInfoManager.deviceModel), - ("OS Version", systemInfoManager.osVersion) - ] as [Any]), - ("App Log", logRecordManager.logsGroupedBy(context: "Send")), - ("Version History", storage.appVersions.map { ($0.description, $0.date) }), - ("Market Last Sync Timestamps", marketLastSyncTimestamps), - ("Wallets Status", accountStatus), - ("Blockchains Status", blockchainStatus) - ] - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusInteractor.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusInteractor.swift deleted file mode 100644 index c5bcd1a2ba..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusInteractor.swift +++ /dev/null @@ -1,22 +0,0 @@ -class AppStatusInteractor { - private let appStatusManager: AppStatusManager - private let pasteboardManager: PasteboardManager - - init(appStatusManager: AppStatusManager, pasteboardManager: PasteboardManager) { - self.appStatusManager = appStatusManager - self.pasteboardManager = pasteboardManager - } - -} - -extension AppStatusInteractor: IAppStatusInteractor { - - var status: [(String, Any)] { - appStatusManager.status - } - - func copyToClipboard(string: String) { - pasteboardManager.set(value: string) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusModule.swift index 21fe8af1a2..7c7bd5e092 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusModule.swift @@ -1,14 +1,19 @@ -protocol IAppStatusView: AnyObject { - func set(logs: [(String, Any)]) -} - -protocol IAppStatusViewDelegate { - func viewDidLoad() - func onCopy(text: String) -} +import SwiftUI -protocol IAppStatusInteractor { - var status: [(String, Any)] { get } +struct AppStatusModule { + static func view() -> some View { + let viewModel = AppStatusViewModel( + systemInfoManager: App.shared.systemInfoManager, + appVersionStorage: App.shared.appVersionStorage, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager, + adapterManager: App.shared.adapterManager, + logRecordManager: App.shared.logRecordManager, + evmBlockchainManager: App.shared.evmBlockchainManager, + binanceKitManager: App.shared.binanceKitManager, + marketKit: App.shared.marketKit + ) - func copyToClipboard(string: String) + return AppStatusView(viewModel: viewModel) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusPresenter.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusPresenter.swift deleted file mode 100644 index 677b83fd43..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusPresenter.swift +++ /dev/null @@ -1,22 +0,0 @@ -class AppStatusPresenter { - weak var view: IAppStatusView? - - private let interactor: IAppStatusInteractor - - init(interactor: IAppStatusInteractor) { - self.interactor = interactor - } - -} - -extension AppStatusPresenter: IAppStatusViewDelegate { - - func viewDidLoad() { - view?.set(logs: interactor.status) - } - - func onCopy(text: String) { - interactor.copyToClipboard(string: text) - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusRouter.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusRouter.swift deleted file mode 100644 index 605f878f3c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusRouter.swift +++ /dev/null @@ -1,18 +0,0 @@ -import UIKit - -class AppStatusRouter { -} - -extension AppStatusRouter { - - static func module() -> UIViewController { - let interactor = AppStatusInteractor(appStatusManager: App.shared.appStatusManager, pasteboardManager: App.shared.pasteboardManager) - let presenter = AppStatusPresenter(interactor: interactor) - let viewController = AppStatusViewController(delegate: presenter) - - presenter.view = viewController - - return viewController - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift new file mode 100644 index 0000000000..be55a0c921 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct AppStatusView: View { + let viewModel: AppStatusViewModel + + @State private var shareText: ShareText? + + var body: some View { + ScrollableThemeView { + VStack(spacing: .margin24) { + HStack(spacing: .margin8) { + Button(action: { + CopyHelper.copyAndNotify(value: viewModel.rawStatus) + }) { + Text("button.copy".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + + Button(action: { + shareText = ShareText(text: viewModel.rawStatus) + }) { + Text("button.share".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + .sheet(item: $shareText) { shareText in + ActivityView(text: shareText.text) + .ignoresSafeArea() + } + + ForEach(viewModel.sections, id: \.title) { section in + VStack(spacing: 0) { + ListSectionHeader(text: section.title) + + VStack(spacing: .margin12) { + ForEach(section.blocks, id: \.self) { fields in + ListSection { + ForEach(fields, id: \.self) { field in + ListRow { + switch field { + case let .info(title, value): + Text(title).themeSubhead2() + Text(value).themeSubhead1(color: .themeLeah, alignment: .trailing) + case let .title(value): + Text(value).themeSubhead1(color: .themeLeah) + case let .raw(text): + Text(text).themeCaption() + } + } + } + } + } + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .navigationTitle("app_status.title".localized) + .navigationBarTitleDisplayMode(.inline) + } + + private struct ShareText: Identifiable { + let id = UUID() + let text: String + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewController.swift deleted file mode 100644 index e87d18f58d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewController.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import ThemeKit - -class AppStatusViewController: ThemeViewController { - private let delegate: IAppStatusViewDelegate - - private let textView = UITextView.appDebug - - private let dateFormatter = DateFormatter() - - init(delegate: IAppStatusViewDelegate) { - self.delegate = delegate - - super.init() - - hidesBottomBarWhenPushed = true - - dateFormatter.dateFormat = "dd MMM yyyy, HH:mm" - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "app_status.title".localized - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.copy".localized, style: .plain, target: self, action: #selector(didTapButton)) - - view.addSubview(textView) - textView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - delegate.viewDidLoad() - } - - @objc private func didTapButton() { - delegate.onCopy(text: textView.text) - } - - private func build(logs: [(String, Any)], indentation: String = "", bullet: String = "", level: Int = 0) -> String { - var result = "" - - logs.forEach { key, value in - let key = (indentation + bullet + key + ": ").capitalized - - if let date = value as? Date { - result += key + dateFormatter.string(from: date) + "\n" - } else if let string = value as? String { - result += key + string + "\n" - } else if let int = value as? Int { - result += key + "\(int)" + "\n" - } else if let int = value as? Int32 { - result += key + "\(int)" + "\n" - } else if let deep = value as? [String] { - result += key + "\n" - deep.forEach { str in - result += indentation + " " + bullet + str + "\n" - } - } else if let deep = value as? [(String, Any)] { - result += key + "\n" + build(logs: deep, indentation: " " + indentation, bullet: " - ", level: level + 1) + (level < 2 ? "\n" : "") - } - } - - return result - } - -} - -extension AppStatusViewController: IAppStatusView { - - func set(logs: [(String, Any)]) { - DispatchQueue.main.async { //need to handle weird behaviour of large title in relation to UITextView - self.textView.text = self.build(logs: logs) - } - } - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewModel.swift index d6fbbb93b9..7600f1ae3f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusViewModel.swift @@ -1,38 +1,204 @@ import Foundation +import MarketKit class AppStatusViewModel { -} + private let dateFormatter = DateFormatter() + private(set) var sections = [Section]() -extension AppStatusViewModel { + init(systemInfoManager: SystemInfoManager, appVersionStorage: AppVersionStorage, accountManager: AccountManager, + walletManager: WalletManager, adapterManager: AdapterManager, logRecordManager: LogRecordManager, + evmBlockchainManager: EvmBlockchainManager, binanceKitManager: BinanceKitManager, marketKit: MarketKit.Kit) + { + dateFormatter.dateFormat = "dd MMM yyyy, HH:mm" - var version: String { - "0.16 (404)" - } + sections.append( + Section( + title: "App Info", + blocks: [ + [ + .info(title: "Current Time", value: dateFormatter.string(from: Date())), + .info(title: "App Version", value: systemInfoManager.appVersion.description), + .info(title: "Device Model", value: systemInfoManager.deviceModel), + .info(title: "iOS Version", value: systemInfoManager.osVersion), + ], + ] + ) + ) + + let appVersions = appVersionStorage.appVersions + + if !appVersions.isEmpty { + sections.append( + Section( + title: "Version History", + blocks: [ + appVersions.map { version in + .info(title: version.description, value: dateFormatter.string(from: version.date)) + }, + ] + ) + ) + } + + let accounts = accountManager.accounts + + if !accounts.isEmpty { + sections.append( + Section( + title: "Wallets", + blocks: accounts.map { account in + var fields: [Field] = [ + .info(title: "Name", value: account.name), + .info(title: "Type", value: account.type.description), + ] + + if case .mnemonic = account.type { + fields.append(.info(title: "Origin", value: account.origin.rawValue.capitalized)) + } + + return fields + } + ) + ) + } + + var blockchainBlocks = [[Field]]() + + for wallet in walletManager.activeWallets { + let blockchain = wallet.token.blockchain + + switch blockchain.type { + case .bitcoin, .bitcoinCash, .ecash, .litecoin, .dash, .zcash: + if let adapter = adapterManager.adapter(for: wallet) { + blockchainBlocks.append(block(blockchain: blockchain.name, statusInfo: adapter.statusInfo)) + } + default: + () + } + } + + for blockchain in evmBlockchainManager.allBlockchains { + if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchain.type).evmKitWrapper { + blockchainBlocks.append(block(blockchain: blockchain.name, statusInfo: evmKitWrapper.evmKit.statusInfo())) + } + } + + if let binanceKit = binanceKitManager.binanceKit { + blockchainBlocks.append(block(blockchain: "Binance Chain", statusInfo: binanceKit.statusInfo)) + } + + if !blockchainBlocks.isEmpty { + sections.append( + Section( + title: "Blockchains", + blocks: blockchainBlocks + ) + ) + } - var linkedWalletsCount: Int { - 2 + let marketSyncInfo = marketKit.syncInfo() + + sections.append( + Section( + title: "Market Last Sync Timestamps", + blocks: [ + [ + .info(title: "Coins", value: marketSyncInfo.coinsTimestamp ?? "n/a"), + .info(title: "Blockchains", value: marketSyncInfo.blockchainsTimestamp ?? "n/a"), + .info(title: "Tokens", value: marketSyncInfo.tokensTimestamp ?? "n/a"), + ], + ] + ) + ) + + let sendLogs = logRecordManager.logsGroupedBy(context: "Send") + + if !sendLogs.isEmpty { + sections.append( + Section( + title: "Logs", + blocks: [ + [ + .title(value: "Send"), + .raw(text: build(logs: sendLogs, showBullet: true).trimmingCharacters(in: .whitespacesAndNewlines)), + ], + ] + ) + ) + } } - var blockchainViewItems: [BlockchainViewItem] { + private func block(blockchain: String, statusInfo: [(String, Any)]) -> [Field] { [ - BlockchainViewItem(name: "Bitcoin", status: .syncing), - BlockchainViewItem(name: "Ethereum", status: .synced), + .title(value: blockchain), + .raw(text: build(logs: statusInfo, showBullet: true).trimmingCharacters(in: .whitespacesAndNewlines)), ] } -} + private func build(logs: [(String, Any)], level: Int = 0, showBullet: Bool = false) -> String { + var result = "" -extension AppStatusViewModel { + logs.forEach { key, value in + let indentation = String(repeating: " ", count: level) + let bullet = showBullet ? "- " : "" + let key = (indentation + bullet + key + ": ").capitalized + + if let date = value as? Date { + result += key + dateFormatter.string(from: date) + "\n" + } else if let string = value as? String { + result += key + string + "\n" + } else if let int = value as? Int { + result += key + "\(int)" + "\n" + } else if let int = value as? Int32 { + result += key + "\(int)" + "\n" + } else if let deep = value as? [String] { + result += key + "\n" + deep.forEach { str in + result += indentation + " " + bullet + str + "\n" + } + } else if let deep = value as? [(String, Any)] { + result += "\n" + key + "\n" + build(logs: deep, level: level + 1, showBullet: true) + } + } - struct BlockchainViewItem { - let name: String - let status: BlockchainStatus + return result } - enum BlockchainStatus { - case syncing - case synced - case notSynced + var rawStatus: String { + var rawInfo = "" + + for section in sections { + rawInfo += section.title + "\n" + + for block in section.blocks { + for field in block { + switch field { + case let .info(title, value): + rawInfo += " - \(title): \(value)\n" + case let .title(value): + rawInfo += " - \(value)\n" + case let .raw(text): + rawInfo += text.components(separatedBy: "\n").map { " " + $0 }.joined(separator: "\n") + "\n" + } + } + + rawInfo += "\n" + } + } + + return rawInfo.trimmingCharacters(in: .whitespacesAndNewlines) } +} +extension AppStatusViewModel { + enum Field: Hashable { + case info(title: String, value: String) + case title(value: String) + case raw(text: String) + } + + struct Section { + let title: String + let blocks: [[Field]] + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift index 3477512c8c..8c97f47dd8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift @@ -1,11 +1,11 @@ +import ComponentKit +import MessageUI +import RxCocoa +import RxSwift +import SafariServices import SectionsTableView import SnapKit import ThemeKit -import RxSwift -import RxCocoa -import MessageUI -import SafariServices -import ComponentKit class AboutViewController: ThemeViewController { private let viewModel: AboutViewModel @@ -28,7 +28,8 @@ class AboutViewController: ThemeViewController { hidesBottomBarWhenPushed = true } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -36,6 +37,7 @@ class AboutViewController: ThemeViewController { super.viewDidLoad() title = "settings.about_app.title".localized + navigationItem.largeTitleDisplayMode = .never navigationItem.backBarButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil) view.addSubview(tableView) @@ -104,7 +106,7 @@ class AboutViewController: ThemeViewController { }, .init(style: .gray, title: "settings.contact.via_telegram".localized, actionType: .afterClose) { [weak self] in self?.handleTelegramContact() - } + }, ] ) @@ -120,26 +122,24 @@ class AboutViewController: ThemeViewController { urlManager.open(url: "https://twitter.com/\(account)", from: self) } } - } extension AboutViewController: SectionsDataSource { - - private func row(id: String, image: String, title: String, alert: Bool = false, isFirst: Bool = false, isLast: Bool = false, action: @escaping () -> ()) -> RowProtocol { + private func row(id: String, image: String, title: String, alert: Bool = false, isFirst: Bool = false, isLast: Bool = false, action: @escaping () -> Void) -> RowProtocol { var elements = tableView.universalImage24Elements(image: .local(UIImage(named: image)), title: .body(title), value: nil, accessoryType: .disclosure) if alert { elements.insert(.imageElement(image: .local(UIImage(named: "warning_2_24")?.withTintColor(.themeLucian)), size: .image24), at: 2) } return CellBuilderNew.row( - rootElement: .hStack(elements), - tableView: tableView, - id: id, - height: .heightCell48, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) - }, - action: action + rootElement: .hStack(elements), + tableView: tableView, + id: id, + height: .heightCell48, + autoDeselect: true, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + }, + action: action ) } @@ -148,162 +148,160 @@ extension AboutViewController: SectionsDataSource { return [ Section( - id: "header", - rows: [ - StaticRow( - cell: headerCell, - id: "header", - height: LogoHeaderCell.height - ), - Row( - id: "description", - dynamicHeight: { containerWidth in - DescriptionCell.height(containerWidth: containerWidth, text: descriptionText) - }, - bind: { cell, _ in - cell.label.text = descriptionText - } - ) - ] + id: "header", + rows: [ + StaticRow( + cell: headerCell, + id: "header", + height: LogoHeaderCell.height + ), + Row( + id: "description", + dynamicHeight: { containerWidth in + DescriptionCell.height(containerWidth: containerWidth, text: descriptionText) + }, + bind: { cell, _ in + cell.label.text = descriptionText + } + ), + ] ), Section( - id: "release-notes", - headerState: .margin(height: .margin24), - footerState: .margin(height: .margin32), - rows: [ - row( - id: "release-notes", - image: "circle_information_24", - title: "settings.about_app.whats_new".localized, - isFirst: true, - isLast: true, - action: { [weak self] in - guard let url = self?.viewModel.releaseNotesUrl else { - return - } - - self?.navigationController?.pushViewController(MarkdownModule.gitReleaseNotesMarkdownViewController(url: url, presented: false), animated: true) - } - ) - ] + id: "release-notes", + headerState: .margin(height: .margin24), + footerState: .margin(height: .margin32), + rows: [ + row( + id: "release-notes", + image: "circle_information_24", + title: "settings.about_app.whats_new".localized, + isFirst: true, + isLast: true, + action: { [weak self] in + guard let url = self?.viewModel.releaseNotesUrl else { + return + } + + self?.navigationController?.pushViewController(MarkdownModule.gitReleaseNotesMarkdownViewController(url: url, presented: false), animated: true) + } + ), + ] ), Section( - id: "main", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "app-status", - image: "app_status_24", - title: "app_status.title".localized, - isFirst: true, - action: { [weak self] in - self?.navigationController?.pushViewController(AppStatusRouter.module(), animated: true) - } - ), - row( - id: "terms", - image: "unordered_24", - title: "terms.title".localized, - alert: showTermsAlert, - action: { [weak self] in - self?.present(TermsModule.viewController(), animated: true) - } - ), - row( - id: "privacy", - image: "user_24", - title: "settings.privacy".localized, - isLast: true, - action: { [weak self] in - self?.navigationController?.pushViewController(PrivacyPolicyViewController(config: .privacy), animated: true) - } - ), - ] + id: "main", + footerState: .margin(height: .margin32), + rows: [ + row( + id: "app-status", + image: "app_status_24", + title: "app_status.title".localized, + isFirst: true, + action: { [weak self] in + let viewController = AppStatusModule.view().toViewController(title: "app_status.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } + ), + row( + id: "terms", + image: "unordered_24", + title: "terms.title".localized, + alert: showTermsAlert, + action: { [weak self] in + self?.present(TermsModule.viewController(), animated: true) + } + ), + row( + id: "privacy", + image: "user_24", + title: "settings.privacy".localized, + isLast: true, + action: { [weak self] in + self?.navigationController?.pushViewController(PrivacyPolicyViewController(config: .privacy), animated: true) + } + ), + ] ), Section( - id: "web", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "github", - image: "github_24", - title: "GitHub", - isFirst: true, - action: { [weak self] in - self?.viewModel.onTapGithubLink() - } - ), - row( - id: "twitter", - image: "twitter_24", - title: "Twitter", - action: { [weak self] in - self?.openTwitter() - } - ), - row( - id: "website", - image: "globe_24", - title: "settings.about_app.website".localized, - isLast: true, - action: { [weak self] in - self?.viewModel.onTapWebPageLink() - } - ) - ] + id: "web", + footerState: .margin(height: .margin32), + rows: [ + row( + id: "github", + image: "github_24", + title: "GitHub", + isFirst: true, + action: { [weak self] in + self?.viewModel.onTapGithubLink() + } + ), + row( + id: "twitter", + image: "twitter_24", + title: "Twitter", + action: { [weak self] in + self?.openTwitter() + } + ), + row( + id: "website", + image: "globe_24", + title: "settings.about_app.website".localized, + isLast: true, + action: { [weak self] in + self?.viewModel.onTapWebPageLink() + } + ), + ] ), Section( - id: "share", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "rate-us", - image: "rate_24", - title: "settings.about_app.rate_us".localized, - isFirst: true, - action: { [weak self] in - self?.viewModel.onTapRateApp() - } - ), - row( - id: "tell-friends", - image: "share_1_24", - title: "settings.about_app.tell_friends".localized, - isLast: true, - action: { [weak self] in - self?.openTellFriends() - } - ), - ] + id: "share", + footerState: .margin(height: .margin32), + rows: [ + row( + id: "rate-us", + image: "rate_24", + title: "settings.about_app.rate_us".localized, + isFirst: true, + action: { [weak self] in + self?.viewModel.onTapRateApp() + } + ), + row( + id: "tell-friends", + image: "share_1_24", + title: "settings.about_app.tell_friends".localized, + isLast: true, + action: { [weak self] in + self?.openTellFriends() + } + ), + ] ), Section( - id: "contact", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "email", - image: "mail_24", - title: "settings.about_app.contact".localized, - isFirst: true, - isLast: true, - action: { [weak self] in - self?.handleContact() - } - ) - ] - ) + id: "contact", + footerState: .margin(height: .margin32), + rows: [ + row( + id: "email", + image: "mail_24", + title: "settings.about_app.contact".localized, + isFirst: true, + isLast: true, + action: { [weak self] in + self?.handleContact() + } + ), + ] + ), ] } - } extension AboutViewController: MFMailComposeViewControllerDelegate { - - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) { controller.dismiss(animated: true) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index 8cd3256974..ca2853cb07 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -39,6 +39,19 @@ struct SecuritySettingsView: View { } } + if viewModel.isPasscodeSet { + ListSection { + NavigationRow(destination: { + AutoLockView(period: $viewModel.autoLockPeriod) + }) { + Image("lock_24").themeIcon() + Text("settings_security.auto_lock".localized).themeBody() + Text(viewModel.autoLockPeriod.title).themeSubhead1(alignment: .trailing).padding(.trailing, -.margin8) + Image.disclosureIcon + } + } + } + if let biometryType = viewModel.biometryType { ListSection { ListRow { @@ -55,19 +68,6 @@ struct SecuritySettingsView: View { } } - if viewModel.isPasscodeSet { - ListSection { - NavigationRow(destination: { - AutoLockView(period: $viewModel.autoLockPeriod) - }) { - Image("lock_24").themeIcon() - Text("settings_security.auto_lock".localized).themeBody() - Text(viewModel.autoLockPeriod.title).themeSubhead1(alignment: .trailing).padding(.trailing, -.margin8) - Image.disclosureIcon - } - } - } - VStack(spacing: 0) { ListSection { ListRow { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ActivityView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ActivityView.swift new file mode 100644 index 0000000000..2f0bd6ed56 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ActivityView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct ActivityView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let text: String + + func makeUIViewController(context _: Context) -> UIViewController { + UIActivityViewController(activityItems: [text], applicationActivities: nil) + } + + func updateUIViewController(_: UIViewController, context _: Context) {} +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift index f2550d4128..1c567199a4 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift @@ -24,4 +24,10 @@ extension Text { .foregroundColor(color) .font(.themeCaption) } + + func themeCaptionSB(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeCaptionSB) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PrimaryButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PrimaryButtonStyle.swift index fff42c7ae9..56c175e789 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PrimaryButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PrimaryButtonStyle.swift @@ -3,7 +3,7 @@ import SwiftUI struct PrimaryButtonStyle: ButtonStyle { let style: Style - @Environment(\.isEnabled) var isEnabled + @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 089716155a..f216362df5 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1146,7 +1146,7 @@ Go to Settings - > %@ and allow access to the camera."; // Enable Duress Mode -"enable_duress_mode.intro.title" = "Duress Passcode"; +"enable_duress_mode.intro.title" = "Duress Mode"; "enable_duress_mode.intro.description" = "This mode allows user to setup multiple unlock app passcodes where a desired passcode shows only specified wallets. Designed to keep selected wallets safe under coercion or threats."; "enable_duress_mode.intro.notes" = "Notes"; "enable_duress_mode.intro.biometrics.description" = "The %@ feature will work to unlock the Duress Mode. You can disable %@ for convenience."; From 93e010ea19080fbb04f2490f92f3fde88b34cbfd Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 6 Oct 2023 16:25:49 +0600 Subject: [PATCH 41/63] Move `Rate Us`, `Tell Friends` and `Contact Us` to MainSettings module --- .../Modules/Settings/About/AboutModule.swift | 7 +- .../Modules/Settings/About/AboutService.swift | 11 +- .../Settings/About/AboutViewController.swift | 89 ---- .../Settings/About/AboutViewModel.swift | 21 +- .../Settings/Main/MainSettingsModule.swift | 3 +- .../Settings/Main/MainSettingsService.swift | 8 +- .../Main/MainSettingsViewController.swift | 387 +++++++++++------- .../Settings/Main/MainSettingsViewModel.swift | 3 + .../en.lproj/Localizable.strings | 6 +- 9 files changed, 261 insertions(+), 274 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift index 0fb16a3df5..b66f225ab8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift @@ -1,12 +1,10 @@ import UIKit struct AboutModule { - static func viewController() -> UIViewController { let service = AboutService( - termsManager: App.shared.termsManager, - systemInfoManager: App.shared.systemInfoManager, - rateAppManager: App.shared.rateAppManager + termsManager: App.shared.termsManager, + systemInfoManager: App.shared.systemInfoManager ) let releaseNotesService = ReleaseNotesService(appVersionManager: App.shared.appVersionManager) @@ -14,5 +12,4 @@ struct AboutModule { return AboutViewController(viewModel: viewModel, urlManager: UrlManager(inApp: true)) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift index ed97f366a6..cf3a0401b2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift @@ -3,18 +3,14 @@ import RxSwift class AboutService { private let termsManager: TermsManager private let systemInfoManager: SystemInfoManager - private let rateAppManager: RateAppManager - init(termsManager: TermsManager, systemInfoManager: SystemInfoManager, rateAppManager: RateAppManager) { + init(termsManager: TermsManager, systemInfoManager: SystemInfoManager) { self.termsManager = termsManager self.systemInfoManager = systemInfoManager - self.rateAppManager = rateAppManager } - } extension AboutService { - var termsAccepted: Bool { termsManager.termsAccepted } @@ -26,9 +22,4 @@ extension AboutService { var appVersion: String { systemInfoManager.appVersion.description } - - func rateApp() { - rateAppManager.forceShow() - } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift index 8c97f47dd8..858edc5337 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift @@ -1,5 +1,4 @@ import ComponentKit -import MessageUI import RxCocoa import RxSwift import SafariServices @@ -71,48 +70,6 @@ class AboutViewController: ThemeViewController { tableView.deselectCell(withCoordinator: transitionCoordinator, animated: animated) } - private func openTellFriends() { - let text = "settings_tell_friends.text".localized + "\n" + AppConfig.appWebPageLink - let activityViewController = UIActivityViewController(activityItems: [text], applicationActivities: []) - present(activityViewController, animated: true, completion: nil) - } - - private func handleEmailContact() { - let email = AppConfig.reportEmail - - if MFMailComposeViewController.canSendMail() { - let controller = MFMailComposeViewController() - controller.setToRecipients([email]) - controller.mailComposeDelegate = self - - present(controller, animated: true) - } else { - CopyHelper.copyAndNotify(value: email) - } - } - - private func handleTelegramContact() { - navigationController?.pushViewController(PersonalSupportModule.viewController(), animated: true) - } - - private func handleContact() { - let viewController = BottomSheetModule.viewController( - image: .local(image: UIImage(named: "at_24")?.withTintColor(.themeJacob)), - title: "settings.contact.title".localized, - items: [], - buttons: [ - .init(style: .yellow, title: "settings.contact.via_email".localized, actionType: .afterClose) { [weak self] in - self?.handleEmailContact() - }, - .init(style: .gray, title: "settings.contact.via_telegram".localized, actionType: .afterClose) { [weak self] in - self?.handleTelegramContact() - }, - ] - ) - - present(viewController, animated: true) - } - private func openTwitter() { let account = AppConfig.appTwitterAccount @@ -256,52 +213,6 @@ extension AboutViewController: SectionsDataSource { ), ] ), - Section( - id: "share", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "rate-us", - image: "rate_24", - title: "settings.about_app.rate_us".localized, - isFirst: true, - action: { [weak self] in - self?.viewModel.onTapRateApp() - } - ), - row( - id: "tell-friends", - image: "share_1_24", - title: "settings.about_app.tell_friends".localized, - isLast: true, - action: { [weak self] in - self?.openTellFriends() - } - ), - ] - ), - Section( - id: "contact", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "email", - image: "mail_24", - title: "settings.about_app.contact".localized, - isFirst: true, - isLast: true, - action: { [weak self] in - self?.handleContact() - } - ), - ] - ), ] } } - -extension AboutViewController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) { - controller.dismiss(animated: true) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift index 705c3a762c..405ed8cf9f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift @@ -1,7 +1,7 @@ import Foundation -import RxSwift -import RxRelay import RxCocoa +import RxRelay +import RxSwift class AboutViewModel { private let service: AboutService @@ -18,17 +18,15 @@ class AboutViewModel { termsAlertRelay = BehaviorRelay(value: !service.termsAccepted) service.termsAcceptedObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] accepted in - self?.termsAlertRelay.accept(!accepted) - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe(onNext: { [weak self] accepted in + self?.termsAlertRelay.accept(!accepted) + }) + .disposed(by: disposeBag) } - } extension AboutViewModel { - var openLinkSignal: Signal { openLinkRelay.asSignal() } @@ -52,9 +50,4 @@ extension AboutViewModel { func onTapWebPageLink() { openLinkRelay.accept(AppConfig.appWebPageLink) } - - func onTapRateApp() { - service.rateApp() - } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift index 8acf2796b6..5e00970a00 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsModule.swift @@ -13,7 +13,8 @@ struct MainSettingsModule { systemInfoManager: App.shared.systemInfoManager, currencyKit: App.shared.currencyKit, walletConnectSessionManager: App.shared.walletConnectSessionManager, - subscriptionManager: App.shared.subscriptionManager + subscriptionManager: App.shared.subscriptionManager, + rateAppManager: App.shared.rateAppManager ) let viewModel = MainSettingsViewModel(service: service) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift index e4e43ec873..a177681203 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift @@ -20,12 +20,13 @@ class MainSettingsService { private let currencyKit: CurrencyKit.Kit private let walletConnectSessionManager: WalletConnectSessionManager private let subscriptionManager: SubscriptionManager + private let rateAppManager: RateAppManager private let iCloudAvailableErrorRelay = BehaviorRelay(value: false) private let noWalletRequiredActionsRelay = BehaviorRelay(value: false) init(backupManager: BackupManager, cloudAccountBackupManager: CloudBackupManager, accountRestoreWarningManager: AccountRestoreWarningManager, accountManager: AccountManager, contactBookManager: ContactBookManager, passcodeManager: PasscodeManager, termsManager: TermsManager, - systemInfoManager: SystemInfoManager, currencyKit: CurrencyKit.Kit, walletConnectSessionManager: WalletConnectSessionManager, subscriptionManager: SubscriptionManager) + systemInfoManager: SystemInfoManager, currencyKit: CurrencyKit.Kit, walletConnectSessionManager: WalletConnectSessionManager, subscriptionManager: SubscriptionManager, rateAppManager: RateAppManager) { self.cloudAccountBackupManager = cloudAccountBackupManager self.backupManager = backupManager @@ -38,6 +39,7 @@ class MainSettingsService { self.currencyKit = currencyKit self.walletConnectSessionManager = walletConnectSessionManager self.subscriptionManager = subscriptionManager + self.rateAppManager = rateAppManager subscribe(disposeBag, contactBookManager.iCloudErrorObservable) { [weak self] error in if error != nil, self?.contactBookManager.remoteSync ?? false { @@ -149,6 +151,10 @@ extension MainSettingsService { var analyticsLink: String { AppConfig.analyticsLink } + + func rateApp() { + rateAppManager.forceShow() + } } extension MainSettingsService { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index ca2b3a3f78..603a5c2141 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -1,13 +1,14 @@ -import UIKit +import ComponentKit +import MessageUI +import ModuleKit +import RxCocoa +import RxSwift +import SafariServices import SectionsTableView import SnapKit import ThemeKit import UIExtensions -import ModuleKit -import RxSwift -import RxCocoa -import SafariServices -import ComponentKit +import UIKit class MainSettingsViewController: ThemeViewController { private let viewModel: MainSettingsViewModel @@ -41,7 +42,8 @@ class MainSettingsViewController: ThemeViewController { tabBarItem = UITabBarItem(title: "settings.tab_bar_item".localized, image: UIImage(named: "filled_settings_2_24"), tag: 0) } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -135,33 +137,33 @@ class MainSettingsViewController: ThemeViewController { private func buildTitleImage(cell: BaseThemeCell, image: UIImage?, title: String, alertImage: UIImage? = nil) { CellBuilderNew.buildStatic(cell: cell, rootElement: .hStack([ - .image24 { (component: ImageComponent) -> () in + .image24 { (component: ImageComponent) in component.imageView.image = image }, - .text { (component: TextComponent) -> () in + .text { (component: TextComponent) in component.font = .body component.textColor = .themeLeah component.text = title }, - .image20 { (component: ImageComponent) -> () in + .image20 { (component: ImageComponent) in component.isHidden = alertImage == nil component.imageView.image = alertImage component.imageView.tintColor = .themeLucian }, .margin8, - .image20 { (component: ImageComponent) -> () in + .image20 { (component: ImageComponent) in component.imageView.image = UIImage(named: "arrow_big_forward_20") - } + }, ])) } private func syncWalletConnectCell(text: String? = nil, highlighted: Bool = false) { buildTitleValue( - cell: walletConnectCell, - image: UIImage(named: "wallet_connect_24"), - title: "wallet_connect.title".localized, - value: !highlighted ? text : nil, - badge: highlighted ? text : nil + cell: walletConnectCell, + image: UIImage(named: "wallet_connect_24"), + title: "wallet_connect.title".localized, + value: !highlighted ? text : nil, + badge: highlighted ? text : nil ) } @@ -174,26 +176,26 @@ class MainSettingsViewController: ThemeViewController { .image24 { (component: ImageComponent) in component.imageView.image = image }, - .text { (component: TextComponent) -> () in + .text { (component: TextComponent) in component.font = .body component.textColor = .themeLeah component.text = title }, - .text { (component: TextComponent) -> () in + .text { (component: TextComponent) in component.font = .subhead1 component.textColor = .themeGray component.text = value }, .margin8, - .badge { (component: BadgeComponent) -> () in + .badge { (component: BadgeComponent) in component.badgeView.set(style: .medium) component.isHidden = badge == nil component.badgeView.text = badge }, .margin8, - .image20 { (component: ImageComponent) -> () in + .image20 { (component: ImageComponent) in component.imageView.image = UIImage(named: "arrow_big_forward_20") - } + }, ])) } @@ -207,155 +209,192 @@ class MainSettingsViewController: ThemeViewController { private var accountRows: [RowProtocol] { [ StaticRow( - cell: manageAccountsCell, - id: "manage-accounts", - height: .heightCell48, - action: { [weak self] in - self?.navigationController?.pushViewController(ManageAccountsModule.viewController(mode: .manage), animated: true) - } + cell: manageAccountsCell, + id: "manage-accounts", + height: .heightCell48, + action: { [weak self] in + self?.navigationController?.pushViewController(ManageAccountsModule.viewController(mode: .manage), animated: true) + } ), tableView.universalRow48( - id: "blockchain-settings", - image: .local(UIImage(named: "blocks_24")), - title: .body("settings.blockchain_settings".localized), - accessoryType: .disclosure, - isLast: false, - action: { [weak self] in - let viewController = BlockchainSettingsModule.view().toViewController(title: "blockchain_settings.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } + id: "blockchain-settings", + image: .local(UIImage(named: "blocks_24")), + title: .body("settings.blockchain_settings".localized), + accessoryType: .disclosure, + isLast: false, + action: { [weak self] in + let viewController = BlockchainSettingsModule.view().toViewController(title: "blockchain_settings.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } ), tableView.universalRow48( - id: "backup-manager", - image: .local(UIImage(named: "icloud_24")), - title: .body("settings.backup_manager".localized), - accessoryType: .disclosure, - isLast: true, - action: { [weak self] in - let viewController = BackupManagerModule.view().toViewController(title: "backup_manager.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } - ) + id: "backup-manager", + image: .local(UIImage(named: "icloud_24")), + title: .body("settings.backup_manager".localized), + accessoryType: .disclosure, + isLast: true, + action: { [weak self] in + let viewController = BackupManagerModule.view().toViewController(title: "backup_manager.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } + ), ] } private var walletConnectRows: [RowProtocol] { [ StaticRow( - cell: walletConnectCell, - id: "wallet-connect", - height: .heightCell48, - autoDeselect: true, - action: { [weak self] in - self?.viewModel.onTapWalletConnect() - } - ) + cell: walletConnectCell, + id: "wallet-connect", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.viewModel.onTapWalletConnect() + } + ), ] } private var appearanceRows: [RowProtocol] { [ StaticRow( - cell: securityCell, - id: "security", - height: .heightCell48, - action: { [weak self] in - let viewController = SecuritySettingsModule.view().toViewController(title: "settings_security.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } + cell: securityCell, + id: "security", + height: .heightCell48, + action: { [weak self] in + let viewController = SecuritySettingsModule.view().toViewController(title: "settings_security.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } ), StaticRow( - cell: contactBookCell, - id: "address-book", - height: .heightCell48, - action: { [weak self] in - guard let viewController = ContactBookModule.viewController(mode: .edit) else { - return - } - self?.navigationController?.pushViewController(viewController, animated: true) + cell: contactBookCell, + id: "address-book", + height: .heightCell48, + action: { [weak self] in + guard let viewController = ContactBookModule.viewController(mode: .edit) else { + return } + self?.navigationController?.pushViewController(viewController, animated: true) + } ), StaticRow( - cell: appearanceCell, - id: "launch-screen", - height: .heightCell48, - action: { [weak self] in - let viewController = AppearanceModule.view().toViewController(title: "appearance.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } + cell: appearanceCell, + id: "launch-screen", + height: .heightCell48, + action: { [weak self] in + let viewController = AppearanceModule.view().toViewController(title: "appearance.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } ), StaticRow( - cell: baseCurrencyCell, - id: "base-currency", - height: .heightCell48, - action: { [weak self] in - self?.navigationController?.pushViewController(BaseCurrencySettingsModule.viewController(), animated: true) - } + cell: baseCurrencyCell, + id: "base-currency", + height: .heightCell48, + action: { [weak self] in + self?.navigationController?.pushViewController(BaseCurrencySettingsModule.viewController(), animated: true) + } ), StaticRow( - cell: languageCell, - id: "language", - height: .heightCell48, - action: { [weak self] in - let module = LanguageSettingsRouter.module { MainModule.instance(presetTab: .settings) } - self?.navigationController?.pushViewController(module, animated: true) - } - ) + cell: languageCell, + id: "language", + height: .heightCell48, + action: { [weak self] in + let module = LanguageSettingsRouter.module { MainModule.instance(presetTab: .settings) } + self?.navigationController?.pushViewController(module, animated: true) + } + ), ] } private var experimentalRows: [RowProtocol] { [ tableView.universalRow48( - id: "experimental-features", - image: .local(UIImage(named: "flask_24")), - title: .body("settings.experimental_features".localized), - accessoryType: .disclosure, - isFirst: true, - isLast: true, - action: { [weak self] in - let viewController = ExperimentalFeaturesView().toViewController(title: "settings.experimental_features.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } - ) + id: "experimental-features", + image: .local(UIImage(named: "flask_24")), + title: .body("settings.experimental_features".localized), + accessoryType: .disclosure, + isFirst: true, + isLast: true, + action: { [weak self] in + let viewController = ExperimentalFeaturesView().toViewController(title: "settings.experimental_features.title".localized) + self?.navigationController?.pushViewController(viewController, animated: true) + } + ), ] } private var knowledgeRows: [RowProtocol] { [ tableView.universalRow48( - id: "faq", - image: .local(UIImage(named: "message_square_24")), - title: .body("settings.faq".localized), - accessoryType: .disclosure, - isFirst: true, - action: { [weak self] in - self?.navigationController?.pushViewController(FaqModule.viewController(), animated: true) - } + id: "faq", + image: .local(UIImage(named: "message_square_24")), + title: .body("settings.faq".localized), + accessoryType: .disclosure, + isFirst: true, + action: { [weak self] in + self?.navigationController?.pushViewController(FaqModule.viewController(), animated: true) + } ), tableView.universalRow48( - id: "academy", - image: .local(UIImage(named: "academy_1_24")), - title: .body("guides.title".localized), - accessoryType: .disclosure, - isLast: true, - action: { [weak self] in - self?.navigationController?.pushViewController(GuidesModule.instance(), animated: true) - } - ) + id: "academy", + image: .local(UIImage(named: "academy_1_24")), + title: .body("guides.title".localized), + accessoryType: .disclosure, + isLast: true, + action: { [weak self] in + self?.navigationController?.pushViewController(GuidesModule.instance(), animated: true) + } + ), ] } private var aboutRows: [RowProtocol] { [ StaticRow( - cell: aboutCell, - id: "about", - height: .heightCell48, - action: { [weak self] in - self?.navigationController?.pushViewController(AboutModule.viewController(), animated: true) - } - ) + cell: aboutCell, + id: "about", + height: .heightCell48, + action: { [weak self] in + self?.navigationController?.pushViewController(AboutModule.viewController(), animated: true) + } + ), + ] + } + + private var feedbackRows: [RowProtocol] { + [ + tableView.universalRow48( + id: "rate-us", + image: .local(UIImage(named: "rate_24")), + title: .body("settings.rate_us".localized), + accessoryType: .disclosure, + autoDeselect: true, + isFirst: true, + action: { [weak self] in + self?.viewModel.onTapRateApp() + } + ), + tableView.universalRow48( + id: "tell-friends", + image: .local(UIImage(named: "share_1_24")), + title: .body("settings.tell_friends".localized), + accessoryType: .disclosure, + autoDeselect: true, + action: { [weak self] in + self?.openTellFriends() + } + ), + tableView.universalRow48( + id: "contact-us", + image: .local(UIImage(named: "mail_24")), + title: .body("settings.contact_us".localized), + accessoryType: .disclosure, + autoDeselect: true, + isLast: true, + action: { [weak self] in + self?.handleContact() + } + ), ] } @@ -370,33 +409,73 @@ class MainSettingsViewController: ThemeViewController { isFirst: true, isLast: true, action: { [weak self] in self?.onDonateTapped() } - ) + ), ] } private var footerRows: [RowProtocol] { [ StaticRow( - cell: footerCell, - id: "footer", - height: footerCell.cellHeight - ) + cell: footerCell, + id: "footer", + height: footerCell.cellHeight + ), ] } private func openWalletConnect(mode: MainSettingsViewModel.WalletConnectOpenMode) { switch mode { - case .errorDialog(let error): + case let .errorDialog(error): WalletConnectAppShowView.showWalletConnectError(error: error, sourceViewController: self) case .list: navigationController?.pushViewController(WalletConnectListModule.viewController(), animated: true) } } + private func openTellFriends() { + let text = "settings_tell_friends.text".localized + "\n" + AppConfig.appWebPageLink + let activityViewController = UIActivityViewController(activityItems: [text], applicationActivities: []) + present(activityViewController, animated: true, completion: nil) + } + + private func handleEmailContact() { + let email = AppConfig.reportEmail + + if MFMailComposeViewController.canSendMail() { + let controller = MFMailComposeViewController() + controller.setToRecipients([email]) + controller.mailComposeDelegate = self + + present(controller, animated: true) + } else { + CopyHelper.copyAndNotify(value: email) + } + } + + private func handleTelegramContact() { + navigationController?.pushViewController(PersonalSupportModule.viewController(), animated: true) + } + + private func handleContact() { + let viewController = BottomSheetModule.viewController( + image: .local(image: UIImage(named: "at_24")?.withTintColor(.themeJacob)), + title: "settings.contact.title".localized, + items: [], + buttons: [ + .init(style: .yellow, title: "settings.contact.via_email".localized, actionType: .afterClose) { [weak self] in + self?.handleEmailContact() + }, + .init(style: .gray, title: "settings.contact.via_telegram".localized, actionType: .afterClose) { [weak self] in + self?.handleTelegramContact() + }, + ] + ) + + present(viewController, animated: true) + } } extension MainSettingsViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { var sections: [SectionProtocol] = [ Section(id: "donate", headerState: .margin(height: .margin12), rows: donateRows), @@ -406,33 +485,39 @@ extension MainSettingsViewController: SectionsDataSource { Section(id: "experimental", headerState: .margin(height: .margin32), rows: experimentalRows), Section(id: "knowledge", headerState: .margin(height: .margin32), rows: knowledgeRows), Section(id: "about", headerState: .margin(height: .margin32), rows: aboutRows), - Section(id: "footer", headerState: .margin(height: .margin32), footerState: .margin(height: .margin32), rows: footerRows) + Section(id: "feedback", headerState: .margin(height: .margin32), rows: feedbackRows), + Section(id: "footer", headerState: .margin(height: .margin32), footerState: .margin(height: .margin32), rows: footerRows), ] if showTestNetSwitcher { sections.append( - Section( + Section( + id: "test-net-switcher", + footerState: .margin(height: .margin32), + rows: [ + tableView.universalRow48( id: "test-net-switcher", - footerState: .margin(height: .margin32), - rows: [ - tableView.universalRow48( - id: "test-net-switcher", - title: .body("TestNet Enabled"), - accessoryType: .switch( - isOn: App.shared.testNetManager.testNetEnabled, - onSwitch: { enabled in - App.shared.testNetManager.set(testNetEnabled: enabled) - } - ), - isFirst: true, - isLast: true - ) - ] - ) + title: .body("TestNet Enabled"), + accessoryType: .switch( + isOn: App.shared.testNetManager.testNetEnabled, + onSwitch: { enabled in + App.shared.testNetManager.set(testNetEnabled: enabled) + } + ), + isFirst: true, + isLast: true + ), + ] + ) ) } return sections } +} +extension MainSettingsViewController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) { + controller.dismiss(animated: true) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift index 5afa0c8089..bac41071cd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift @@ -149,6 +149,9 @@ extension MainSettingsViewModel { openLinkRelay.accept(AppConfig.companyWebPageLink) } + func onTapRateApp() { + service.rateApp() + } } extension MainSettingsViewModel { diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index f216362df5..04e2f6fbcb 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1020,6 +1020,9 @@ Go to Settings - > %@ and allow access to the camera."; "settings.info_subtitle" = "decentralized app"; "settings.donate.description" = "Together, with your support, we can make this app even better!"; "settings.donate.title" = "Donate"; +"settings.rate_us" = "Rate Us"; +"settings.tell_friends" = "Tell Friends"; +"settings.contact_us" = "Contact Us"; // Settings -> Base Currency @@ -1233,9 +1236,6 @@ Go to Settings - > %@ and allow access to the camera."; "settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; "settings.about_app.whats_new" = "What's New"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Contact Us"; -"settings.about_app.rate_us" = "Rate Us"; -"settings.about_app.tell_friends" = "Tell Friends"; // Settings -> About App -> Contact From 2511d3597874199b04791fa342fd249ee07956ef Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 6 Oct 2023 19:24:34 +0600 Subject: [PATCH 42/63] Switch About module to SwiftUI --- .../project.pbxproj | 30 +-- .../Core/Managers/TermsManager.swift | 21 +- .../Core/Managers/UrlManager.swift | 14 +- .../Modules/Main/MainBadgeService.swift | 10 +- .../Modules/Markdown/MarkdownModule.swift | 20 +- .../Modules/Settings/About/AboutModule.swift | 16 +- .../Modules/Settings/About/AboutService.swift | 25 -- .../Modules/Settings/About/AboutView.swift | 125 ++++++++++ .../Settings/About/AboutViewController.swift | 218 ------------------ .../Settings/About/AboutViewModel.swift | 48 ++-- .../Settings/Main/MainSettingsService.swift | 4 +- .../Main/MainSettingsViewController.swift | 2 +- .../Settings/Main/MainSettingsViewModel.swift | 91 ++++---- .../Privacy/PrivacyPolicyViewController.swift | 50 ++-- .../Modules/Settings/Terms/TermsModule.swift | 17 +- .../SwiftUI/Extensions/Text.swift | 6 + 16 files changed, 297 insertions(+), 400 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 105a87ebaa..f2af52af5a 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -297,7 +297,6 @@ 11B3534554BA9D9AF8D334D3 /* InputStateWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B18D0C02E331540538B /* InputStateWrapperView.swift */; }; 11B3534916B76A847608D1A4 /* WalletConnectRequestModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356C2E5AF8ED41E2B545D /* WalletConnectRequestModule.swift */; }; 11B35349D234724EE34956A0 /* EvmBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D15E318829D9C7F5F1 /* EvmBlockchainManager.swift */; }; - 11B3534A0CB17052E3002F96 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532F755C7B758D5AB2A2 /* AboutViewController.swift */; }; 11B3534B12C5E7596E4953F0 /* RestoreSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35336293A4473DD9F5C8B /* RestoreSettingsModule.swift */; }; 11B3534B567884E30A871F32 /* AddTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */; }; 11B3534EF58DAC9E15DC49A5 /* BackupVerifyWordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6399E5264BFFA32F08 /* BackupVerifyWordsViewController.swift */; }; @@ -383,7 +382,6 @@ 11B354542A3E929C1A7924FD /* CoinReportsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7FCEFE15A50EB5C6E0 /* CoinReportsViewController.swift */; }; 11B3545A8A2A23ADB5BA1E0A /* WelcomeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A05B93CB243B6404C4A /* WelcomeTextView.swift */; }; 11B3545B8A5568792A4C43D8 /* CoinTreasuriesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */; }; - 11B35467AC08F3C5439B250F /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FB85F826A825CB401D /* AboutViewModel.swift */; }; 11B3546AC03E6B632D155766 /* MarkdownImageTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353002DD782C5BEE9BFD4 /* MarkdownImageTitleCell.swift */; }; 11B3547938D32DCE88B4A1FC /* ExtendedKeyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35564351D59D37278C723 /* ExtendedKeyService.swift */; }; 11B3547989E25AB98B7C22DD /* WalletViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357426B767AA64ED8E7A2 /* WalletViewModel.swift */; }; @@ -505,7 +503,6 @@ 11B355F11DDA5EC8082C43DF /* BinanceWithdrawHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576C0D8464F74D44EE92 /* BinanceWithdrawHandler.swift */; }; 11B355F32686B8689B4EC105 /* WalletConnectRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CD5EBBB403D46BDEF0B /* WalletConnectRequest.swift */; }; 11B355FAD0E7823AF5F8EC83 /* SendEvmTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7F043B6C41E53D43BC /* SendEvmTransactionService.swift */; }; - 11B355FC8D055E7AD1FCFB6B /* AboutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3593037C8B33C1C307D85 /* AboutService.swift */; }; 11B3560586CBAB617211F003 /* Caution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D96CF03878016FC38FD /* Caution.swift */; }; 11B35608F7D19B3E6318CB22 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352972B14FA6EBEFD6904 /* Text.swift */; }; 11B3560E158C55624C466E27 /* GuidesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E511F9D2B6C65792324 /* GuidesViewController.swift */; }; @@ -531,8 +528,8 @@ 11B3564FBC180A0E6D30BCFA /* TransactionsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35828C8D50D0A5B915B2A /* TransactionsModule.swift */; }; 11B3565070D890657E004402 /* ManageWalletsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CBBFEC11CAE6FDBCFFA /* ManageWalletsModule.swift */; }; 11B35653622A466CE9E6FA71 /* EvmPrivateKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E62EBBDE01560EB2E4 /* EvmPrivateKeyViewController.swift */; }; - 11B356559BE65EE0756909E7 /* AboutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3593037C8B33C1C307D85 /* AboutService.swift */; }; 11B3565617F5E45C0B86AFED /* ReceiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFED85A9315089223E3 /* ReceiveViewModel.swift */; }; + 11B356562D2B4F5BCAB4FC80 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C3907AC1134C7A95DB /* AboutView.swift */; }; 11B35664B1EDEAB99B7B51AE /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; 11B356655BCF0A3919AD5120 /* ActivateSubscriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508AB65CCBDC18FEF2A6 /* ActivateSubscriptionViewController.swift */; }; 11B35665CC02390699802C61 /* RestoreBinanceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35995E0D358AC4DA2FA74 /* RestoreBinanceModule.swift */; }; @@ -562,7 +559,6 @@ 11B356C983C2A2B552D214A4 /* ListSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352970EA9924258E5BB75 /* ListSectionFooter.swift */; }; 11B356CF0D78F2DC6F28B4BD /* LaunchErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4096D259C9B1540D10 /* LaunchErrorViewController.swift */; }; 11B356CF55DB1BE22071B24E /* MarketMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350FAB6F1A6E1FCFACB2F /* MarketMultiSortHeaderViewModel.swift */; }; - 11B356D1A2017A37012D3763 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532F755C7B758D5AB2A2 /* AboutViewController.swift */; }; 11B356D4E85B0A0133F6870C /* RestorePrivateKeyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EBA5DE11150CE2E3F9 /* RestorePrivateKeyService.swift */; }; 11B356D60C39544F165547AA /* TermsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DAF31FBE0834EBC066 /* TermsService.swift */; }; 11B356D67F706464900DBD25 /* CexCoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590EB4E34B278277E8E4 /* CexCoinService.swift */; }; @@ -606,6 +602,7 @@ 11B3575108E28705A2F47BA9 /* NftViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35419084A6CB11230E3C6 /* NftViewController.swift */; }; 11B357573D364030813F231C /* CexAssetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356DBFFBD17DA5DA5D0E0 /* CexAssetManager.swift */; }; 11B35758262A961566ABB87F /* AddBep2TokenBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35102BB1E66987670CD1F /* AddBep2TokenBlockchainService.swift */; }; + 11B3575F30FFFDFB4F0AF174 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C3907AC1134C7A95DB /* AboutView.swift */; }; 11B357605EA9962F5D51DCD1 /* RestoreSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532946EA785A7C65D193 /* RestoreSettingsService.swift */; }; 11B35763508EBED0F4ED302A /* NftAssetRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359852B313E849499BC19 /* NftAssetRecord.swift */; }; 11B3576791792D356B0BE916 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35747FAD8381F2AD48276 /* MainViewModel.swift */; }; @@ -879,7 +876,6 @@ 11B35A801504D47FBE31CF40 /* PasteboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CEBC4B32E57AA2469AA /* PasteboardManager.swift */; }; 11B35A80AB419A754EF9955A /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AA43C4832521D428799 /* ListSection.swift */; }; 11B35A81973F6FB70B24AF7A /* PoolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359636E1AA1BC72CF7B11 /* PoolProvider.swift */; }; - 11B35A81C813B8411BDE8AC0 /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FB85F826A825CB401D /* AboutViewModel.swift */; }; 11B35A82220538FEE57546FB /* TransactionTypeFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3592E4DA65E72C0BC6BEB /* TransactionTypeFilter.swift */; }; 11B35A82532EC55909EFBAD8 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; 11B35A8395C75C6FA6515F3C /* CexWithdrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ABC3E6C990E3BFA0A7B /* CexWithdrawViewController.swift */; }; @@ -1101,6 +1097,7 @@ 11B35D51B52EF0000711CE05 /* MultiTextMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359824DCDF3B05413CDD2 /* MultiTextMetricsView.swift */; }; 11B35D54818399B4BCE9F2C2 /* UnlinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */; }; 11B35D550563934444558D15 /* AddTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */; }; + 11B35D55957E21D3388880CF /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359DCDBC90BD0AD938C02 /* AboutViewModel.swift */; }; 11B35D57964143D9FAAC6A4F /* CexCoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590EB4E34B278277E8E4 /* CexCoinService.swift */; }; 11B35D5B873123BC2D3909EF /* AppVersionRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35888BDB55DCFD0ECF655 /* AppVersionRecordStorage.swift */; }; 11B35D5BB556A490C6E13BA9 /* CoinAnalyticsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358DFD25E8DC35F689D5C /* CoinAnalyticsViewModel.swift */; }; @@ -1216,6 +1213,7 @@ 11B35E8DED55EE76CE1F943D /* ModuleUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */; }; 11B35E8E0F5E5F43E65B8A98 /* GuidesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352CFEDEBF0A01CC7073D /* GuidesModule.swift */; }; 11B35E94A7BCB0FEE8E144A9 /* GuidesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E6CB5B964E2A1521CC /* GuidesViewModel.swift */; }; + 11B35E98AE2272A7E37C41C5 /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359DCDBC90BD0AD938C02 /* AboutViewModel.swift */; }; 11B35E99BBF6DCCA72BDA4D1 /* CoinTreasuriesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3522CBA84677E00D44983 /* CoinTreasuriesViewModel.swift */; }; 11B35E99E0D2A095857DDE13 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35957968B4D79EC406D4D /* BottomSheetViewController.swift */; }; 11B35E9A5F3FB43FD2F5C718 /* AmountInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C3E03A9679D4B7E0D29 /* AmountInputCell.swift */; }; @@ -2901,7 +2899,6 @@ 11B353282C7000D3BDFC7FD0 /* EvmAddressLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAddressLabel.swift; sourceTree = ""; }; 11B3532946EA785A7C65D193 /* RestoreSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsService.swift; sourceTree = ""; }; 11B3532A1DC90E3D0E3403F8 /* ReceiveAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressViewModel.swift; sourceTree = ""; }; - 11B3532F755C7B758D5AB2A2 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 11B35332D245CFF50A68F8CA /* SectionsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionsTableView.swift; sourceTree = ""; }; 11B35336293A4473DD9F5C8B /* RestoreSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsModule.swift; sourceTree = ""; }; 11B35340910590E6FCF05A90 /* NftCollectionAssetsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionAssetsViewController.swift; sourceTree = ""; }; @@ -3081,6 +3078,7 @@ 11B357BA1A6AC79F07B54FB5 /* SystemInfoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemInfoManager.swift; sourceTree = ""; }; 11B357C16B28B535457F6E34 /* AppStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusView.swift; sourceTree = ""; }; 11B357C17104792A20769560 /* CoinCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinCategory.swift; sourceTree = ""; }; + 11B357C3907AC1134C7A95DB /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 11B357C67623035CDF98B540 /* CexAssetResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAssetResponse.swift; sourceTree = ""; }; 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewController.swift; sourceTree = ""; }; 11B357D89546EBA13B01A1ED /* TransactionsViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsViewItemFactory.swift; sourceTree = ""; }; @@ -3135,7 +3133,6 @@ 11B35921FBDF6F9BBAA88803 /* TextFieldStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldStackView.swift; sourceTree = ""; }; 11B3592A5323E54639864FC7 /* CreateAccountService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountService.swift; sourceTree = ""; }; 11B3592E4DA65E72C0BC6BEB /* TransactionTypeFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionTypeFilter.swift; sourceTree = ""; }; - 11B3593037C8B33C1C307D85 /* AboutService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutService.swift; sourceTree = ""; }; 11B35932B642378F85D6ACCD /* CoinAuditsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAuditsService.swift; sourceTree = ""; }; 11B35935EF1B2237E0289669 /* BaseTransactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsViewModel.swift; sourceTree = ""; }; 11B3593FBD158050C9FEF6B9 /* Misc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Misc.swift; sourceTree = ""; }; @@ -3176,11 +3173,11 @@ 11B359D884F1698E70F2536E /* SwitchAccountService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchAccountService.swift; sourceTree = ""; }; 11B359D88585F2BBFA56CB77 /* FeeRateProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeRateProviderFactory.swift; sourceTree = ""; }; 11B359DAB464176D8EBFC8A0 /* MarkdownTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTextView.swift; sourceTree = ""; }; + 11B359DCDBC90BD0AD938C02 /* AboutViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = ""; }; 11B359E32AEEE37347E255C4 /* NftAssetBriefMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetBriefMetadata.swift; sourceTree = ""; }; 11B359E4C84921BEAB994792 /* CoinMajorHoldersViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMajorHoldersViewModel.swift; sourceTree = ""; }; 11B359E546B8F1E572E695F4 /* AmountTypeSwitchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountTypeSwitchService.swift; sourceTree = ""; }; 11B359F01A63378AFAAEE113 /* ManageAccountsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountsModule.swift; sourceTree = ""; }; - 11B359FB85F826A825CB401D /* AboutViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = ""; }; 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = ""; }; 11B359FE5BB60FB12BB24F3E /* NonSpamPoolProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonSpamPoolProvider.swift; sourceTree = ""; }; 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataManager.swift; sourceTree = ""; }; @@ -5899,9 +5896,8 @@ isa = PBXGroup; children = ( 11B353E80D544DAF20B12B56 /* AboutModule.swift */, - 11B3593037C8B33C1C307D85 /* AboutService.swift */, - 11B359FB85F826A825CB401D /* AboutViewModel.swift */, - 11B3532F755C7B758D5AB2A2 /* AboutViewController.swift */, + 11B357C3907AC1134C7A95DB /* AboutView.swift */, + 11B359DCDBC90BD0AD938C02 /* AboutViewModel.swift */, ); path = About; sourceTree = ""; @@ -8351,11 +8347,8 @@ 11B35BF0FDB441A29B9467AF /* CoinSelectService.swift in Sources */, D36DE0CD272FD864000BC916 /* UniswapTradeService.swift in Sources */, 58AAA98A15442365CFE776F3 /* KeyboardAwareViewController.swift in Sources */, - 11B35A81C813B8411BDE8AC0 /* AboutViewModel.swift in Sources */, - 11B3534A0CB17052E3002F96 /* AboutViewController.swift in Sources */, D36DE0CA272FD864000BC916 /* UniswapProvider.swift in Sources */, D00267BA2A57E6CE00D6B2D5 /* ResendPastInputCell.swift in Sources */, - 11B355FC8D055E7AD1FCFB6B /* AboutService.swift in Sources */, D0C226142A66A3DB007101F7 /* PersonalSupportViewController.swift in Sources */, 11B35A4D9BD4B8C29FBAFACF /* AboutModule.swift in Sources */, 3A73FC9C258B1AF700FE4D34 /* MarketWatchlistViewController.swift in Sources */, @@ -9412,6 +9405,8 @@ 11B357740CC018527301C4AE /* AppStatusView.swift in Sources */, 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */, 11B35CAE0540A2549BD4A960 /* ActivityView.swift in Sources */, + 11B356562D2B4F5BCAB4FC80 /* AboutView.swift in Sources */, + 11B35E98AE2272A7E37C41C5 /* AboutViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9675,11 +9670,8 @@ 11B357DD946C13E58E69A0BE /* FaqCell.swift in Sources */, 11B358092D442440DAAE8AC0 /* CoinSelectService.swift in Sources */, 58AAA9A289DE179B76AFA99F /* KeyboardAwareViewController.swift in Sources */, - 11B35467AC08F3C5439B250F /* AboutViewModel.swift in Sources */, D00267B92A57E6CE00D6B2D5 /* ResendPastInputCell.swift in Sources */, - 11B356D1A2017A37012D3763 /* AboutViewController.swift in Sources */, D0C226132A66A3DB007101F7 /* PersonalSupportViewController.swift in Sources */, - 11B356559BE65EE0756909E7 /* AboutService.swift in Sources */, 11B3550424326606B055D7E5 /* AboutModule.swift in Sources */, 3A73FC99258B1AF600FE4D34 /* MarketWatchlistViewController.swift in Sources */, 11B35959AAF414186CE39698 /* AddTokenViewModel.swift in Sources */, @@ -10735,6 +10727,8 @@ 11B355901DFF6BAE9130D60E /* AppStatusView.swift in Sources */, 11B354865DA8CA6A1442D577 /* AppStatusViewModel.swift in Sources */, 11B35A431DE03F33E739B639 /* ActivityView.swift in Sources */, + 11B3575F30FFFDFB4F0AF174 /* AboutView.swift in Sources */, + 11B35D55957E21D3388880CF /* AboutViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TermsManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TermsManager.swift index 67d410b311..636dc4873a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TermsManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TermsManager.swift @@ -1,32 +1,23 @@ -import RxSwift -import RxRelay +import Combine +import HsExtensions import StorageKit class TermsManager { private let keyTermsAccepted = "key_terms_accepted" private let storage: StorageKit.ILocalStorage - private let termsAcceptedRelay = PublishRelay() + @DistinctPublished var termsAccepted: Bool init(storage: StorageKit.ILocalStorage) { self.storage = storage - } + termsAccepted = storage.value(for: keyTermsAccepted) ?? false + } } extension TermsManager { - - var termsAccepted: Bool { - storage.value(for: keyTermsAccepted) ?? false - } - - var termsAcceptedObservable: Observable { - termsAcceptedRelay.asObservable() - } - func setTermsAccepted() { storage.set(value: true, for: keyTermsAccepted) - termsAcceptedRelay.accept(true) + termsAccepted = true } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/UrlManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/UrlManager.swift index 62d232e1ff..1e9b3278fc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/UrlManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/UrlManager.swift @@ -1,5 +1,6 @@ -import UIKit import SafariServices +import SwiftUI +import UIKit class UrlManager { private let inApp: Bool @@ -51,5 +52,16 @@ class UrlManager { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } +} + +struct SFSafariView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let url: URL + + func makeUIViewController(context _: Context) -> UIViewController { + SFSafariViewController(url: url, configuration: SFSafariViewController.Configuration()) + } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift index 9593cb5c38..ec1dfdf95f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainBadgeService.swift @@ -46,13 +46,11 @@ class MainBadgeService { } .store(in: &cancellables) - termsManager.termsAcceptedObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) - .subscribe(onNext: { [weak self] _ in + termsManager.$termsAccepted + .sink { [weak self] _ in self?.syncSettingsBadge() - }) - .disposed(by: disposeBag) + } + .store(in: &cancellables) walletConnectSessionManager.activePendingRequestsObservable .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Markdown/MarkdownModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Markdown/MarkdownModule.swift index 214c25fbee..7e32e1e03a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Markdown/MarkdownModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Markdown/MarkdownModule.swift @@ -1,7 +1,7 @@ +import SwiftUI import UIKit struct MarkdownModule { - static func viewController(url: URL, handleRelativeUrl: Bool = true) -> UIViewController { let provider = MarkdownPlainContentProvider(url: url, networkManager: App.shared.networkManager) let service = MarkdownService(provider: provider) @@ -11,7 +11,7 @@ struct MarkdownModule { return MarkdownViewController(viewModel: viewModel, handleRelativeUrl: handleRelativeUrl) } - static func gitReleaseNotesMarkdownViewController(url: URL, presented: Bool, closeHandler: (() -> ())? = nil) -> UIViewController { + static func gitReleaseNotesMarkdownViewController(url: URL, presented: Bool, closeHandler: (() -> Void)? = nil) -> UIViewController { let provider = MarkdownGitReleaseContentProvider(url: url, networkManager: App.shared.networkManager) let service = MarkdownService(provider: provider) let parser = MarkdownParser() @@ -20,6 +20,9 @@ struct MarkdownModule { return ReleaseNotesViewController(viewModel: viewModel, handleRelativeUrl: false, urlManager: UrlManager(inApp: false), presented: presented, closeHandler: closeHandler) } + static func gitReleaseNotesMarkdownView(url: URL, presented: Bool) -> some View { + ReleaseNotesView(url: url, presented: presented) + } } enum MarkdownBlockViewItem { @@ -36,3 +39,16 @@ enum MarkdownImageType { case portrait case square } + +struct ReleaseNotesView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let url: URL + let presented: Bool + + func makeUIViewController(context _: Context) -> UIViewController { + MarkdownModule.gitReleaseNotesMarkdownViewController(url: url, presented: presented) + } + + func updateUIViewController(_: UIViewController, context _: Context) {} +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift index b66f225ab8..a41ca39f8a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutModule.swift @@ -1,15 +1,15 @@ -import UIKit +import SwiftUI struct AboutModule { - static func viewController() -> UIViewController { - let service = AboutService( - termsManager: App.shared.termsManager, - systemInfoManager: App.shared.systemInfoManager - ) + static func view() -> some View { let releaseNotesService = ReleaseNotesService(appVersionManager: App.shared.appVersionManager) - let viewModel = AboutViewModel(service: service, releaseNotesService: releaseNotesService) + let viewModel = AboutViewModel( + termsManager: App.shared.termsManager, + systemInfoManager: App.shared.systemInfoManager, + releaseNotesService: releaseNotesService + ) - return AboutViewController(viewModel: viewModel, urlManager: UrlManager(inApp: true)) + return AboutView(viewModel: viewModel) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift deleted file mode 100644 index cf3a0401b2..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutService.swift +++ /dev/null @@ -1,25 +0,0 @@ -import RxSwift - -class AboutService { - private let termsManager: TermsManager - private let systemInfoManager: SystemInfoManager - - init(termsManager: TermsManager, systemInfoManager: SystemInfoManager) { - self.termsManager = termsManager - self.systemInfoManager = systemInfoManager - } -} - -extension AboutService { - var termsAccepted: Bool { - termsManager.termsAccepted - } - - var termsAcceptedObservable: Observable { - termsManager.termsAcceptedObservable - } - - var appVersion: String { - systemInfoManager.appVersion.description - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift new file mode 100644 index 0000000000..ac21c35cef --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift @@ -0,0 +1,125 @@ +import SwiftUI + +struct AboutView: View { + @ObservedObject var viewModel: AboutViewModel + + @State private var termsPresented = false + @State private var linkUrl: URL? + + var body: some View { + ScrollableThemeView { + VStack(spacing: .margin24) { + HStack(spacing: .margin16) { + Image(uiImage: UIImage(named: AppIcon.main.imageName) ?? UIImage()) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius16, style: .continuous)) + .frame(width: 72, height: 72) + + VStack(spacing: .margin8) { + Text("settings.about_app.app_name".localized(AppConfig.appName)).themeHeadline1() + Text("version".localized(viewModel.appVersion)).themeSubhead2() + } + } + .padding(.horizontal, .margin24) + + Text("settings.about_app.description".localized(AppConfig.appName, AppConfig.appName)) + .font(.themeBody) + .foregroundColor(.themeBran) + .padding(.horizontal, .margin32) + .padding(.vertical, .margin12) + + VStack(spacing: .margin32) { + if let releaseNotesUrl = viewModel.releaseNotesUrl { + ListSection { + NavigationRow(destination: { + MarkdownModule.gitReleaseNotesMarkdownView(url: releaseNotesUrl, presented: false) + .ignoresSafeArea() + }) { + Image("circle_information_24").themeIcon() + Text("settings.about_app.whats_new".localized).themeBody() + Image.disclosureIcon + } + } + } + + ListSection { + NavigationRow(destination: { + AppStatusModule.view() + }) { + Image("app_status_24").themeIcon() + Text("app_status.title".localized).themeBody() + Image.disclosureIcon + } + + ClickableRow(action: { + termsPresented = true + }) { + Image("unordered_24").themeIcon() + Text("terms.title".localized).themeBody() + + if viewModel.termsAlert { + Image("warning_2_20").themeIcon(color: .themeLucian).padding(.trailing, -.margin8) + } + + Image.disclosureIcon + } + + NavigationRow(destination: { + PrivacyPolicyView(config: .privacy) + .navigationTitle(PrivacyPolicyViewController.Config.privacy.title) + .ignoresSafeArea() + }) { + Image("user_24").themeIcon() + Text("settings.privacy".localized).themeBody() + Image.disclosureIcon + } + } + + ListSection { + ClickableRow(action: { + linkUrl = URL(string: "https://github.com/\(AppConfig.appGitHubAccount)/\(AppConfig.appGitHubRepository)") + }) { + Image("github_24").themeIcon() + Text("GitHub").themeBody() + Image.disclosureIcon + } + + ClickableRow(action: { + let account = AppConfig.appTwitterAccount + + if let appUrl = URL(string: "twitter://user?screen_name=\(account)"), UIApplication.shared.canOpenURL(appUrl) { + UIApplication.shared.open(appUrl) + } else { + linkUrl = URL(string: "https://twitter.com/\(account)") + } + }) { + Image("twitter_24").themeIcon() + Text("Twitter").themeBody() + Image.disclosureIcon + } + + ClickableRow(action: { + linkUrl = URL(string: AppConfig.appWebPageLink) + }) { + Image("globe_24").themeIcon() + Text("settings.about_app.website".localized).themeBody() + Image.disclosureIcon + } + } + } + .padding(.horizontal, .margin16) + } + .padding(EdgeInsets(top: .margin24, leading: 0, bottom: .margin32, trailing: 0)) + .sheet(isPresented: $termsPresented) { + TermsModule.view() + .ignoresSafeArea() + } + .sheet(item: $linkUrl) { url in + SFSafariView(url: url) + .ignoresSafeArea() + } + } + .navigationTitle("settings.about_app.title".localized) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift deleted file mode 100644 index 858edc5337..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewController.swift +++ /dev/null @@ -1,218 +0,0 @@ -import ComponentKit -import RxCocoa -import RxSwift -import SafariServices -import SectionsTableView -import SnapKit -import ThemeKit - -class AboutViewController: ThemeViewController { - private let viewModel: AboutViewModel - private var urlManager: UrlManager - - private let disposeBag = DisposeBag() - - private let tableView = SectionsTableView(style: .grouped) - - private let headerCell = LogoHeaderCell() - - private var showTermsAlert = false - - init(viewModel: AboutViewModel, urlManager: UrlManager) { - self.viewModel = viewModel - self.urlManager = urlManager - - super.init() - - hidesBottomBarWhenPushed = true - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "settings.about_app.title".localized - navigationItem.largeTitleDisplayMode = .never - navigationItem.backBarButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil) - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - - tableView.sectionDataSource = self - tableView.registerCell(forClass: DescriptionCell.self) - - headerCell.image = UIImage(named: AppIcon.main.imageName) - headerCell.title = "settings.about_app.app_name".localized(AppConfig.appName) - headerCell.subtitle = "version".localized(viewModel.appVersion) - - subscribe(disposeBag, viewModel.termsAlertDriver) { [weak self] alert in - self?.showTermsAlert = alert - self?.tableView.reload() - } - subscribe(disposeBag, viewModel.openLinkSignal) { [weak self] url in - self?.urlManager.open(url: url, from: self) - } - - tableView.buildSections() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tableView.deselectCell(withCoordinator: transitionCoordinator, animated: animated) - } - - private func openTwitter() { - let account = AppConfig.appTwitterAccount - - if let appUrl = URL(string: "twitter://user?screen_name=\(account)"), UIApplication.shared.canOpenURL(appUrl) { - UIApplication.shared.open(appUrl) - } else { - urlManager.open(url: "https://twitter.com/\(account)", from: self) - } - } -} - -extension AboutViewController: SectionsDataSource { - private func row(id: String, image: String, title: String, alert: Bool = false, isFirst: Bool = false, isLast: Bool = false, action: @escaping () -> Void) -> RowProtocol { - var elements = tableView.universalImage24Elements(image: .local(UIImage(named: image)), title: .body(title), value: nil, accessoryType: .disclosure) - if alert { - elements.insert(.imageElement(image: .local(UIImage(named: "warning_2_24")?.withTintColor(.themeLucian)), size: .image24), at: 2) - } - return CellBuilderNew.row( - rootElement: .hStack(elements), - tableView: tableView, - id: id, - height: .heightCell48, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) - }, - action: action - ) - } - - func buildSections() -> [SectionProtocol] { - let descriptionText = "settings.about_app.description".localized(AppConfig.appName, AppConfig.appName) - - return [ - Section( - id: "header", - rows: [ - StaticRow( - cell: headerCell, - id: "header", - height: LogoHeaderCell.height - ), - Row( - id: "description", - dynamicHeight: { containerWidth in - DescriptionCell.height(containerWidth: containerWidth, text: descriptionText) - }, - bind: { cell, _ in - cell.label.text = descriptionText - } - ), - ] - ), - - Section( - id: "release-notes", - headerState: .margin(height: .margin24), - footerState: .margin(height: .margin32), - rows: [ - row( - id: "release-notes", - image: "circle_information_24", - title: "settings.about_app.whats_new".localized, - isFirst: true, - isLast: true, - action: { [weak self] in - guard let url = self?.viewModel.releaseNotesUrl else { - return - } - - self?.navigationController?.pushViewController(MarkdownModule.gitReleaseNotesMarkdownViewController(url: url, presented: false), animated: true) - } - ), - ] - ), - - Section( - id: "main", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "app-status", - image: "app_status_24", - title: "app_status.title".localized, - isFirst: true, - action: { [weak self] in - let viewController = AppStatusModule.view().toViewController(title: "app_status.title".localized) - self?.navigationController?.pushViewController(viewController, animated: true) - } - ), - row( - id: "terms", - image: "unordered_24", - title: "terms.title".localized, - alert: showTermsAlert, - action: { [weak self] in - self?.present(TermsModule.viewController(), animated: true) - } - ), - row( - id: "privacy", - image: "user_24", - title: "settings.privacy".localized, - isLast: true, - action: { [weak self] in - self?.navigationController?.pushViewController(PrivacyPolicyViewController(config: .privacy), animated: true) - } - ), - ] - ), - - Section( - id: "web", - footerState: .margin(height: .margin32), - rows: [ - row( - id: "github", - image: "github_24", - title: "GitHub", - isFirst: true, - action: { [weak self] in - self?.viewModel.onTapGithubLink() - } - ), - row( - id: "twitter", - image: "twitter_24", - title: "Twitter", - action: { [weak self] in - self?.openTwitter() - } - ), - row( - id: "website", - image: "globe_24", - title: "settings.about_app.website".localized, - isLast: true, - action: { [weak self] in - self?.viewModel.onTapWebPageLink() - } - ), - ] - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift index 405ed8cf9f..702baa503a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutViewModel.swift @@ -1,53 +1,33 @@ +import Combine import Foundation -import RxCocoa -import RxRelay -import RxSwift -class AboutViewModel { - private let service: AboutService +class AboutViewModel: ObservableObject { + private let termsManager: TermsManager + private let systemInfoManager: SystemInfoManager private let releaseNotesService: ReleaseNotesService - private let disposeBag = DisposeBag() + private var cancellables = Set() - private let termsAlertRelay: BehaviorRelay - private let openLinkRelay = PublishRelay() + @Published private(set) var termsAlert = false - init(service: AboutService, releaseNotesService: ReleaseNotesService) { - self.service = service + init(termsManager: TermsManager, systemInfoManager: SystemInfoManager, releaseNotesService: ReleaseNotesService) { + self.termsManager = termsManager + self.systemInfoManager = systemInfoManager self.releaseNotesService = releaseNotesService - termsAlertRelay = BehaviorRelay(value: !service.termsAccepted) + termsManager.$termsAccepted.sink { [weak self] in self?.syncTermsAlert(termsAccepted: $0) }.store(in: &cancellables) - service.termsAcceptedObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] accepted in - self?.termsAlertRelay.accept(!accepted) - }) - .disposed(by: disposeBag) - } -} - -extension AboutViewModel { - var openLinkSignal: Signal { - openLinkRelay.asSignal() + syncTermsAlert(termsAccepted: termsManager.termsAccepted) } - var termsAlertDriver: Driver { - termsAlertRelay.asDriver() + private func syncTermsAlert(termsAccepted: Bool) { + termsAlert = !termsAccepted } var appVersion: String { - service.appVersion + systemInfoManager.appVersion.description } var releaseNotesUrl: URL? { releaseNotesService.lastVersionUrl } - - func onTapGithubLink() { - openLinkRelay.accept("https://github.com/\(AppConfig.appGitHubAccount)/\(AppConfig.appGitHubRepository)") - } - - func onTapWebPageLink() { - openLinkRelay.accept(AppConfig.appWebPageLink) - } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift index a177681203..b8450b15d8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsService.swift @@ -89,8 +89,8 @@ extension MainSettingsService { termsManager.termsAccepted } - var termsAcceptedObservable: Observable { - termsManager.termsAcceptedObservable + var termsAcceptedPublisher: AnyPublisher { + termsManager.$termsAccepted } var walletConnectSessionCount: Int { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index 603a5c2141..f300cafcd3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -355,7 +355,7 @@ class MainSettingsViewController: ThemeViewController { id: "about", height: .heightCell48, action: { [weak self] in - self?.navigationController?.pushViewController(AboutModule.viewController(), animated: true) + self?.navigationController?.pushViewController(AboutModule.view().toViewController(title: "settings.about_app.title".localized), animated: true) } ), ] diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift index bac41071cd..ab691277e2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewModel.swift @@ -1,9 +1,9 @@ import Combine -import WalletConnectV1 -import RxSwift -import RxRelay import RxCocoa +import RxRelay +import RxSwift import ThemeKit +import WalletConnectV1 class MainSettingsViewModel { private let service: MainSettingsService @@ -13,7 +13,7 @@ class MainSettingsViewModel { private let manageWalletsAlertRelay: BehaviorRelay private let securityCenterAlertRelay: BehaviorRelay private let iCloudSyncAlertRelay: BehaviorRelay - private let walletConnectCountRelay: BehaviorRelay<(highlighted: Bool,text: String)?> + private let walletConnectCountRelay: BehaviorRelay<(highlighted: Bool, text: String)?> private let baseCurrencyRelay: BehaviorRelay private let aboutAlertRelay: BehaviorRelay private let openWalletConnectRelay = PublishRelay() @@ -30,64 +30,61 @@ class MainSettingsViewModel { aboutAlertRelay = BehaviorRelay(value: !service.termsAccepted) service.noWalletRequiredActionsObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] noWalletRequiredActions in - self?.manageWalletsAlertRelay.accept(!noWalletRequiredActions) - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe(onNext: { [weak self] noWalletRequiredActions in + self?.manageWalletsAlertRelay.accept(!noWalletRequiredActions) + }) + .disposed(by: disposeBag) service.isPasscodeSetPublisher - .sink { [weak self] isPinSet in - self?.securityCenterAlertRelay.accept(!isPinSet) - } - .store(in: &cancellables) + .sink { [weak self] isPinSet in + self?.securityCenterAlertRelay.accept(!isPinSet) + } + .store(in: &cancellables) service.iCloudAvailableErrorObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] hasError in - self?.iCloudSyncAlertRelay.accept(hasError) - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe(onNext: { [weak self] hasError in + self?.iCloudSyncAlertRelay.accept(hasError) + }) + .disposed(by: disposeBag) service.walletConnectSessionCountObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] count in - self?.walletConnectCountRelay.accept(Self.convert(walletConnectSessionCount: count, walletConnectPendingRequestCount: self?.service.walletConnectPendingRequestCount ?? 0)) - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe(onNext: { [weak self] count in + self?.walletConnectCountRelay.accept(Self.convert(walletConnectSessionCount: count, walletConnectPendingRequestCount: self?.service.walletConnectPendingRequestCount ?? 0)) + }) + .disposed(by: disposeBag) service.walletConnectPendingRequestCountObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] count in - self?.walletConnectCountRelay.accept(Self.convert(walletConnectSessionCount: self?.service.walletConnectSessionCount ?? 0, walletConnectPendingRequestCount: count)) - }) - .disposed(by: disposeBag) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe(onNext: { [weak self] count in + self?.walletConnectCountRelay.accept(Self.convert(walletConnectSessionCount: self?.service.walletConnectSessionCount ?? 0, walletConnectPendingRequestCount: count)) + }) + .disposed(by: disposeBag) service.baseCurrencyPublisher - .sink { [weak self] currency in - self?.baseCurrencyRelay.accept(currency.code) - } - .store(in: &cancellables) + .sink { [weak self] currency in + self?.baseCurrencyRelay.accept(currency.code) + } + .store(in: &cancellables) - service.termsAcceptedObservable - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onNext: { [weak self] accepted in - self?.aboutAlertRelay.accept(!accepted) - }) - .disposed(by: disposeBag) + service.termsAcceptedPublisher + .sink { [weak self] accepted in + self?.aboutAlertRelay.accept(!accepted) + } + .store(in: &cancellables) } - private static func convert(walletConnectSessionCount: Int, walletConnectPendingRequestCount: Int) -> (highlighted: Bool,text: String)? { + private static func convert(walletConnectSessionCount: Int, walletConnectPendingRequestCount: Int) -> (highlighted: Bool, text: String)? { if walletConnectPendingRequestCount != 0 { return (highlighted: true, text: "\(walletConnectPendingRequestCount)") } return walletConnectSessionCount > 0 ? (highlighted: false, text: "\(walletConnectSessionCount)") : nil } - } extension MainSettingsViewModel { - var openWalletConnectSignal: Signal { openWalletConnectRelay.asSignal() } @@ -108,7 +105,7 @@ extension MainSettingsViewModel { iCloudSyncAlertRelay.asDriver() } - var walletConnectCountDriver: Driver<(highlighted: Bool,text: String)?> { + var walletConnectCountDriver: Driver<(highlighted: Bool, text: String)?> { walletConnectCountRelay.asDriver() } @@ -138,10 +135,10 @@ extension MainSettingsViewModel { func onTapWalletConnect() { switch service.walletConnectState { - case .noAccount: openWalletConnectRelay.accept(.errorDialog(error: .noAccount)) - case .backedUp: openWalletConnectRelay.accept(.list) - case .nonSupportedAccountType(let accountType): openWalletConnectRelay.accept(.errorDialog(error: .nonSupportedAccountType(accountTypeDescription: accountType.description))) - case .unBackedUpAccount(let account): openWalletConnectRelay.accept(.errorDialog(error: .unbackupedAccount(account: account))) + case .noAccount: openWalletConnectRelay.accept(.errorDialog(error: .noAccount)) + case .backedUp: openWalletConnectRelay.accept(.list) + case let .nonSupportedAccountType(accountType): openWalletConnectRelay.accept(.errorDialog(error: .nonSupportedAccountType(accountTypeDescription: accountType.description))) + case let .unBackedUpAccount(account): openWalletConnectRelay.accept(.errorDialog(error: .unbackupedAccount(account: account))) } } @@ -155,10 +152,8 @@ extension MainSettingsViewModel { } extension MainSettingsViewModel { - enum WalletConnectOpenMode { case list case errorDialog(error: WalletConnectAppShowView.WalletConnectOpenError) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift index 9dd47daf54..af7b57ee0b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift @@ -1,7 +1,8 @@ -import UIKit -import ThemeKit -import SectionsTableView import ComponentKit +import SectionsTableView +import SwiftUI +import ThemeKit +import UIKit class PrivacyPolicyViewController: ThemeViewController { private let config: Config @@ -14,7 +15,8 @@ class PrivacyPolicyViewController: ThemeViewController { super.init() } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -52,24 +54,21 @@ class PrivacyPolicyViewController: ThemeViewController { return [ Section( - id: "privacy-section", - footerState: .margin(height: .margin32), - rows: infoRows - ) + id: "privacy-section", + footerState: .margin(height: .margin32), + rows: infoRows + ), ] } - } extension PrivacyPolicyViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { privacySections } - } -extension PrivacyPolicyViewController { +extension PrivacyPolicyViewController { struct Config { let title: String let description: String @@ -77,16 +76,27 @@ extension PrivacyPolicyViewController { static var privacy: Config { Config( - title: "settings.privacy".localized, - description: "settings.privacy.description".localized(AppConfig.appName), - viewItems: [ - "settings.privacy.statement.user_data_storage".localized, - "settings.privacy.statement.data_usage".localized, - "settings.privacy.statement.data_privacy".localized, - "settings.privacy.statement.user_account".localized - ]) + title: "settings.privacy".localized, + description: "settings.privacy.description".localized(AppConfig.appName), + viewItems: [ + "settings.privacy.statement.user_data_storage".localized, + "settings.privacy.statement.data_usage".localized, + "settings.privacy.statement.data_privacy".localized, + "settings.privacy.statement.user_account".localized, + ] + ) } + } +} + +struct PrivacyPolicyView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let config: PrivacyPolicyViewController.Config + func makeUIViewController(context _: Context) -> UIViewController { + PrivacyPolicyViewController(config: config) } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Terms/TermsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Terms/TermsModule.swift index 4f4d25a869..e294bae4d2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Terms/TermsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Terms/TermsModule.swift @@ -1,8 +1,8 @@ -import UIKit +import SwiftUI import ThemeKit +import UIKit struct TermsModule { - static func viewController(sourceViewController: UIViewController? = nil, moduleToOpen: UIViewController? = nil) -> UIViewController { let service = TermsService(termsManager: App.shared.termsManager) let viewModel = TermsViewModel(service: service) @@ -11,4 +11,17 @@ struct TermsModule { return ThemeNavigationController(rootViewController: viewController) } + static func view() -> some View { + TermsView() + } +} + +struct TermsView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + func makeUIViewController(context _: Context) -> UIViewController { + TermsModule.viewController() + } + + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift index 1c567199a4..2c1a8ae8da 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift @@ -30,4 +30,10 @@ extension Text { .foregroundColor(color) .font(.themeCaptionSB) } + + func themeHeadline1(color: Color = .themeLeah, alignment: Alignment = .leading) -> some View { + frame(maxWidth: .infinity, alignment: alignment) + .foregroundColor(color) + .font(.themeHeadline1) + } } From 6429ea230cb91dae5d6d26d3a6c144628b08fe10 Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 4 Oct 2023 15:41:15 +0400 Subject: [PATCH 43/63] Fix backup module routing --- .../Core/Crypto/FullBackup.swift | 18 ++++++++++-------- .../BackupApp/BackupAppViewModel.swift | 12 ++++++++++-- .../BackupPassword/BackupPasswordView.swift | 7 +++++-- .../BackupApp/BackupType/BackupTypeView.swift | 13 +++++++------ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift index d811cd8c2d..e22ba17c5f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -21,14 +21,16 @@ extension FullBackup: Codable { case timestamp } -// init(from decoder: Decoder) throws { -// let container = try decoder.container(keyedBy: CodingKeys.self) -// wallets = (try? container.decode([RestoreCloudModule.RestoredBackup].self, forKey: .wallets)) ?? [] -// watchlistIds = (try? container.decode([String].self, forKey: .watchlistIds)) ?? [] -// contacts = try? container.decode([BackupContact].self, forKey: .contacts) -// evmSyncSources = try? container.decode(SyncSourceBackup.self, forKey: .evmSyncSources) -// settings = try? container.decode(SettingsBackup.self, forKey: .settings) -// } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + wallets = (try? container.decode([RestoreCloudModule.RestoredBackup].self, forKey: .wallets)) ?? [] + watchlistIds = (try? container.decode([String].self, forKey: .watchlistIds)) ?? [] + contacts = try? container.decode(BackupCrypto.self, forKey: .contacts) + settings = try? container.decode(SettingsBackup.self, forKey: .settings) + version = try container.decode(Int.self, forKey: .version) + timestamp = try? container.decode(TimeInterval.self, forKey: .timestamp) + } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift index 77dc46cf5b..64c4c72aa3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift @@ -85,6 +85,7 @@ class BackupAppViewModel: ObservableObject { @Published var passwordButtonDisabled = true @Published var passwordButtonProcessing = false + private var dismissSubject = PassthroughSubject() @Published var sharePresented: URL? init(accountManager: AccountManager, contactManager: ContactBookManager, cloudBackupManager: CloudBackupManager, favoritesManager: FavoritesManager, evmSyncSourceManager: EvmSyncSourceManager) { @@ -227,7 +228,7 @@ extension BackupAppViewModel { // Backup Name VieeModel extension BackupAppViewModel { var nextName: String { - let name = { (_: String) in [Self.backupNamePrefix, "1"].joined(separator: " ") } + let name = { [Self.backupNamePrefix, $0].joined(separator: " ") } switch destination { case .cloud: let exists = cloudBackupManager @@ -249,7 +250,7 @@ extension BackupAppViewModel { func validateName() { if name.isEmpty { nameCautionState = .caution(.init(text: NameError.empty.localizedDescription, type: .error)) - } else if (cloudBackupManager.existFilenames + [Self.backupNamePrefix + "2"]).contains(where: { $0.lowercased() == name.lowercased() }) { + } else if destination == .cloud, cloudBackupManager.existFilenames.contains(where: { $0.lowercased() == name.lowercased() }) { nameCautionState = .caution(.init(text: NameError.alreadyExist.localizedDescription, type: .error)) } else { nameCautionState = .none @@ -318,6 +319,7 @@ extension BackupAppViewModel { try cloudBackupManager.save(fields: configuration, passphrase: password, name: name) passwordButtonProcessing = false await showSuccess() + dismissSubject.send() } catch { passwordButtonProcessing = false await show(error: error) @@ -336,6 +338,12 @@ extension BackupAppViewModel { } } +extension BackupAppViewModel { + var dismissPublisher: AnyPublisher { + dismissSubject.eraseToAnyPublisher() + } +} + extension BackupAppViewModel { struct AccountItem: Comparable, Identifiable { let accountId: String diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift index e8d71c289e..1f7c6b2526 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift @@ -1,6 +1,6 @@ +import ComponentKit import SwiftUI import ThemeKit -import ComponentKit struct BackupPasswordView: View { @ObservedObject var viewModel: BackupAppViewModel @@ -67,7 +67,7 @@ struct BackupPasswordView: View { .animation(.default, value: viewModel.passwordButtonProcessing) } .sheet(item: $viewModel.sharePresented) { url in - let completion: UIActivityViewController.CompletionWithItemsHandler = { type, success, list, error in + let completion: UIActivityViewController.CompletionWithItemsHandler = { _, success, _, error in if success { showDone() backupPresented = false @@ -82,6 +82,9 @@ struct BackupPasswordView: View { ActivityViewController(activityItems: [url], completionWithItemsHandler: completion) } } + .onReceive(viewModel.dismissPublisher) { + backupPresented = false + } .navigationBarTitle("backup.password.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift index aee60de716..07a2ed5232 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift @@ -5,7 +5,8 @@ struct BackupTypeView: View { @ObservedObject var viewModel: BackupAppViewModel @Binding var backupPresented: Bool - @State var navigationPushed = false + @State var cloudNavigationPushed = false + @State var localNavigationPushed = false @State var cloudAlertPresented = false var body: some View { @@ -16,11 +17,11 @@ struct BackupTypeView: View { .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) ListSection { - navigation(image: "icloud_24", text: "backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable) { + navigation(image: "icloud_24", text: "backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable, isActive: $cloudNavigationPushed) { if viewModel.cloudAvailable { viewModel.destination = .cloud } else { cloudAlertPresented = true } } - navigation(image: "file_24", text: "backup_type.file".localized) { + navigation(image: "file_24", text: "backup_type.file".localized, isActive: $localNavigationPushed) { viewModel.destination = .local } } @@ -51,15 +52,15 @@ struct BackupTypeView: View { } } - @ViewBuilder func navigation(image: String, text: String, isAvailable: Binding = .constant(true), action: @escaping () -> Void = {}) -> some View { + @ViewBuilder func navigation(image: String, text: String, isAvailable: Binding = .constant(true), isActive: Binding, action: @escaping () -> Void = {}) -> some View { if isAvailable.wrappedValue { NavigationRow( destination: { BackupListView(viewModel: viewModel, backupPresented: $backupPresented) }, - isActive: $navigationPushed + isActive: isActive ) { row(image: image, text: text.localized) } - .onChange(of: navigationPushed) { _ in action() } + .onChange(of: isActive.wrappedValue) { _ in action() } } else { ClickableRow(action: action) { row(image: image, text: text.localized) From 194623e11634f92e4d4580dd65f89b400f05a7d2 Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 4 Oct 2023 17:21:08 +0400 Subject: [PATCH 44/63] Fix handling icloud oneWalletBackups for v1 and v2 - add fileBackedUp field for accounts --- .../Core/Crypto/WalletBackup.swift | 8 +- .../Core/Factories/AccountFactory.swift | 8 +- .../Core/Managers/CloudBackupManager.swift | 7 +- .../Core/Storage/AccountStorage.swift | 4 +- .../Core/Storage/StorageMigrator.swift | 3 +- .../UnstoppableWallet/Models/Account.swift | 4 +- .../Models/AccountRecord.swift | 8 +- .../Backup/ICloud/AppBackupProvider.swift | 13 +- .../RestoreBinanceService.swift | 2 +- .../CreateAccount/CreateAccountService.swift | 1 + .../RestoreCloud/RestoreCloudModule.swift | 1 + .../RestoreCloudPassphraseService.swift | 9 +- ...RestoreCloudPassphraseViewController.swift | 150 +++++++++--------- .../RestoreSelect/RestoreSelectModule.swift | 3 +- .../RestoreSelect/RestoreSelectService.swift | 28 ++-- 15 files changed, 147 insertions(+), 102 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift index df55844c58..78611a0aad 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/WalletBackup.swift @@ -5,6 +5,7 @@ class WalletBackup: Codable { let id: String let type: AccountType.Abstract let isManualBackedUp: Bool + let isFileBackedUp: Bool let version: Int let timestamp: TimeInterval? let enabledWallets: [EnabledWallet] @@ -15,16 +16,18 @@ class WalletBackup: Codable { case id case type case isManualBackedUp = "manual_backup" + case isFileBackedUp = "file_backup" case version case timestamp } - init(crypto: BackupCrypto, enabledWallets: [EnabledWallet], id: String, type: AccountType.Abstract, isManualBackedUp: Bool, version: Int, timestamp: TimeInterval) { + init(crypto: BackupCrypto, enabledWallets: [EnabledWallet], id: String, type: AccountType.Abstract, isManualBackedUp: Bool, isFileBackedUp: Bool, version: Int, timestamp: TimeInterval) { self.crypto = crypto self.enabledWallets = enabledWallets self.id = id self.type = type self.isManualBackedUp = isManualBackedUp + self.isFileBackedUp = isFileBackedUp self.version = version self.timestamp = timestamp } @@ -37,6 +40,8 @@ class WalletBackup: Codable { type = try container.decode(AccountType.Abstract.self, forKey: .type) let isManualBackedUp = try? container.decode(Bool.self, forKey: .isManualBackedUp) self.isManualBackedUp = isManualBackedUp ?? false + let isFileBackedUp = try? container.decode(Bool.self, forKey: .isFileBackedUp) + self.isFileBackedUp = isFileBackedUp ?? false version = try container.decode(Int.self, forKey: .version) timestamp = try? container.decode(TimeInterval.self, forKey: .timestamp) } @@ -48,6 +53,7 @@ class WalletBackup: Codable { try container.encode(id, forKey: .id) try container.encode(type, forKey: .type) try container.encode(isManualBackedUp, forKey: .isManualBackedUp) + try container.encode(isFileBackedUp, forKey: .isFileBackedUp) try container.encode(version, forKey: .version) try container.encode(timestamp, forKey: .timestamp) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift index 778d6af7c7..abc36cba0a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AccountFactory.swift @@ -36,14 +36,15 @@ extension AccountFactory { return "Watch Wallet \(order)" } - func account(type: AccountType, origin: AccountOrigin, backedUp: Bool, name: String) -> Account { + func account(type: AccountType, origin: AccountOrigin, backedUp: Bool, fileBackedUp: Bool, name: String) -> Account { Account( id: UUID().uuidString, level: accountManager.currentLevel, name: name, type: type, origin: origin, - backedUp: backedUp + backedUp: backedUp, + fileBackedUp: fileBackedUp ) } @@ -54,7 +55,8 @@ extension AccountFactory { name: name, type: type, origin: .restored, - backedUp: true + backedUp: true, + fileBackedUp: false ) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift index 5ef7ae1bf1..d9ccac8749 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift @@ -71,7 +71,12 @@ class CloudBackupManager { do { forceDownloadContainerFiles(url: url) - let oneWalletItems: [String: WalletBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) + + var oneWalletItems: [String: WalletBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) + let oneWalletItemsV2: [String: RestoreCloudModule.RestoredBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) + let mapped = oneWalletItemsV2.reduce(into: [:]) { $0[$1.value.name] = $1.value.walletBackup } + + oneWalletItems.merge(mapped) { backup, backup2 in backup2 } let fullBackupItems: [String: FullBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) state = .success diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift index a8f63fab4a..66e78e29c6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/AccountStorage.swift @@ -87,7 +87,8 @@ class AccountStorage { name: record.name, type: type, origin: origin, - backedUp: record.backedUp + backedUp: record.backedUp, + fileBackedUp: record.fileBackedUp ) } @@ -132,6 +133,7 @@ class AccountStorage { type: typeName.rawValue, origin: account.origin.rawValue, backedUp: account.backedUp, + fileBackedUp: account.fileBackedUp, wordsKey: wordsKey, saltKey: saltKey, dataKey: dataKey, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift index 03edb45cdd..b35bd20bdf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift @@ -784,9 +784,10 @@ class StorageMigrator { } } - migrator.registerMigration("Add level to AccountRecord") { db in + migrator.registerMigration("Add level and fileBackedUp to AccountRecord") { db in try db.alter(table: AccountRecord.databaseTableName) { t in t.add(column: AccountRecord.Columns.level.name, .integer).defaults(to: 0) + t.add(column: AccountRecord.Columns.fileBackedUp.name, .boolean).defaults(to: false) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Account.swift b/UnstoppableWallet/UnstoppableWallet/Models/Account.swift index 3852a1ff9d..7c2593c3e6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Account.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Account.swift @@ -7,14 +7,16 @@ class Account: Identifiable { let type: AccountType let origin: AccountOrigin var backedUp: Bool + var fileBackedUp: Bool - init(id: String, level: Int, name: String, type: AccountType, origin: AccountOrigin, backedUp: Bool) { + init(id: String, level: Int, name: String, type: AccountType, origin: AccountOrigin, backedUp: Bool, fileBackedUp: Bool) { self.id = id self.level = level self.name = name self.type = type self.origin = origin self.backedUp = backedUp + self.fileBackedUp = fileBackedUp } var watchAccount: Bool { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift index 87027f01b5..21420d2718 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountRecord.swift @@ -7,18 +7,20 @@ class AccountRecord: Record { let type: String let origin: String let backedUp: Bool + let fileBackedUp: Bool var wordsKey: String? var saltKey: String? var dataKey: String? var bip39Compliant: Bool? - init(id: String, level: Int, name: String, type: String, origin: String, backedUp: Bool, wordsKey: String?, saltKey: String?, dataKey: String?, bip39Compliant: Bool?) { + init(id: String, level: Int, name: String, type: String, origin: String, backedUp: Bool, fileBackedUp: Bool, wordsKey: String?, saltKey: String?, dataKey: String?, bip39Compliant: Bool?) { self.id = id self.level = level self.name = name self.type = type self.origin = origin self.backedUp = backedUp + self.fileBackedUp = fileBackedUp self.wordsKey = wordsKey self.saltKey = saltKey self.dataKey = dataKey @@ -32,7 +34,7 @@ class AccountRecord: Record { } enum Columns: String, ColumnExpression { - case id, level, name, type, origin, backedUp, wordsKey, saltKey, dataKey, bip39Compliant + case id, level, name, type, origin, backedUp, fileBackedUp, wordsKey, saltKey, dataKey, bip39Compliant } required init(row: Row) { @@ -42,6 +44,7 @@ class AccountRecord: Record { type = row[Columns.type] origin = row[Columns.origin] backedUp = row[Columns.backedUp] + fileBackedUp = row[Columns.fileBackedUp] wordsKey = row[Columns.wordsKey] saltKey = row[Columns.saltKey] dataKey = row[Columns.dataKey] @@ -57,6 +60,7 @@ class AccountRecord: Record { container[Columns.type] = type container[Columns.origin] = origin container[Columns.backedUp] = backedUp + container[Columns.fileBackedUp] = fileBackedUp container[Columns.wordsKey] = wordsKey container[Columns.saltKey] = saltKey container[Columns.dataKey] = dataKey diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index aecc2ef9fb..8a1d122552 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -89,6 +89,7 @@ extension AppBackupProvider { accountType: account.type, wallets: wallets, isManualBackedUp: account.backedUp, + isFileBackedUp: account.fileBackedUp, name: account.name, passphrase: passphrase ) @@ -171,11 +172,18 @@ extension AppBackupProvider { type: accountType, origin: .restored, backedUp: backup.walletBackup.isManualBackedUp, + fileBackedUp: backup.walletBackup.isFileBackedUp, name: backup.name ) accountManager.save(account: account) default: - let account = accountFactory.account(type: accountType, origin: .restored, backedUp: backup.walletBackup.isManualBackedUp, name: backup.name) + let account = accountFactory.account( + type: accountType, + origin: .restored, + backedUp: backup.walletBackup.isManualBackedUp, + fileBackedUp: backup.walletBackup.isFileBackedUp, + name: backup.name + ) accountManager.save(account: account) let wallets = backup.walletBackup.enabledWallets.map { @@ -262,7 +270,7 @@ extension AppBackupProvider { } extension AppBackupProvider { - static func walletBackup(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, name: String, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { + static func walletBackup(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, isFileBackedUp: Bool, name: String, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { let message = accountType.uniqueId(hashed: false) let crypto = try BackupCrypto.instance(data: message, passphrase: passphrase) @@ -272,6 +280,7 @@ extension AppBackupProvider { id: accountType.uniqueId().hs.hex, type: AccountType.Abstract(accountType), isManualBackedUp: isManualBackedUp, + isFileBackedUp: isFileBackedUp, version: Self.version, timestamp: Date().timeIntervalSince1970.rounded() ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Binance/RestoreBinance/RestoreBinanceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Binance/RestoreBinance/RestoreBinanceService.swift index b34f8436a7..b423f55181 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Binance/RestoreBinance/RestoreBinanceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Binance/RestoreBinance/RestoreBinanceService.swift @@ -36,7 +36,7 @@ class RestoreBinanceService { private func createAccount() { let type: AccountType = .cex(cexAccount: .binance(apiKey: apiKey, secret: secretKey)) let name = accountFactory.nextAccountName(cex: .binance) - let account = accountFactory.account(type: type, origin: .restored, backedUp: true, name: name) + let account = accountFactory.account(type: type, origin: .restored, backedUp: true, fileBackedUp: false, name: name) accountManager.save(account: account) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/CreateAccount/CreateAccountService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/CreateAccount/CreateAccountService.swift index 02a507f926..1a3e4a01ba 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/CreateAccount/CreateAccountService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/CreateAccount/CreateAccountService.swift @@ -100,6 +100,7 @@ extension CreateAccountService { type: accountType, origin: .created, backedUp: false, + fileBackedUp: false, name: trimmedName.isEmpty ? defaultAccountName : trimmedName ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift index c3801524d5..75217aeaa0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift @@ -27,6 +27,7 @@ struct RestoreCloudModule { let name: String let accountType: AccountType let isManualBackedUp: Bool + let isFileBackedUp: Bool let showSelectCoins: Bool } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift index b29f427712..426f82e0d1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift @@ -24,7 +24,13 @@ class RestoreCloudPassphraseService { } private func createAccount(accountType: AccountType) { - let account = accountFactory.account(type: accountType, origin: .restored, backedUp: restoredBackup.walletBackup.isManualBackedUp, name: restoredBackup.name) + let account = accountFactory.account( + type: accountType, + origin: .restored, + backedUp: restoredBackup.walletBackup.isManualBackedUp, + fileBackedUp: restoredBackup.walletBackup.isFileBackedUp, + name: restoredBackup.name + ) accountManager.save(account: account) let wallets = restoredBackup.walletBackup.enabledWallets.map { @@ -67,6 +73,7 @@ extension RestoreCloudPassphraseService { name: restoredBackup.name, accountType: accountType, isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp, + isFileBackedUp: restoredBackup.walletBackup.isFileBackedUp, showSelectCoins: restoredBackup.walletBackup.enabledWallets.isEmpty )) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift index 69c2191076..4ff179b41c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift @@ -1,10 +1,10 @@ import Combine -import SnapKit -import ThemeKit -import UIKit import ComponentKit import SectionsTableView +import SnapKit +import ThemeKit import UIExtensions +import UIKit class RestoreCloudPassphraseViewController: KeyboardAwareViewController { private let viewModel: RestoreCloudPassphraseViewModel @@ -30,7 +30,8 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { super.init(scrollViews: [tableView], accessoryView: gradientWrapperView) } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -68,48 +69,53 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { passphraseCautionCell.onChangeHeight = { [weak self] in self?.onChangeHeight() } viewModel.$passphraseCaution - .receive(on: DispatchQueue.main) - .sink { [weak self] caution in - self?.passphraseCell.set(cautionType: caution?.type) - self?.passphraseCautionCell.set(caution: caution) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] caution in + self?.passphraseCell.set(cautionType: caution?.type) + self?.passphraseCautionCell.set(caution: caution) + } + .store(in: &cancellables) viewModel.$processing - .receive(on: DispatchQueue.main) - .sink { [weak self] processing in - self?.show(processing: processing) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] processing in + self?.show(processing: processing) + } + .store(in: &cancellables) viewModel.clearInputsPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.passphraseCell.inputText = nil - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.passphraseCell.inputText = nil + } + .store(in: &cancellables) viewModel.showErrorPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.show(error: $0) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.show(error: $0) + } + .store(in: &cancellables) viewModel.openSelectCoinsPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] backupAccount in - self?.openSelectCoins(accountName: backupAccount.name, accountType: backupAccount.accountType, isManualBackedUp: backupAccount.isManualBackedUp) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] backupAccount in + self?.openSelectCoins( + accountName: backupAccount.name, + accountType: backupAccount.accountType, + isManualBackedUp: backupAccount.isManualBackedUp, + isFileBackedUp: backupAccount.isFileBackedUp + ) + } + .store(in: &cancellables) viewModel.successPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in - HudHelper.instance.show(banner: .imported) - (self?.returnViewController ?? self)?.dismiss(animated: true) - } - .store(in: &cancellables) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + HudHelper.instance.show(banner: .imported) + (self?.returnViewController ?? self)?.dismiss(animated: true) + } + .store(in: &cancellables) showDefaultPassphrase() @@ -158,61 +164,58 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { HudHelper.instance.show(banner: .error(string: error)) } - private func openSelectCoins(accountName: String, accountType: AccountType, isManualBackedUp: Bool) { + private func openSelectCoins(accountName: String, accountType: AccountType, isManualBackedUp: Bool, isFileBackedUp: Bool) { let viewController = RestoreSelectModule.viewController( - accountName: accountName, - accountType: accountType, - isManualBackedUp: isManualBackedUp, - returnViewController: returnViewController + accountName: accountName, + accountType: accountType, + isManualBackedUp: isManualBackedUp, + isFileBackedUp: isFileBackedUp, + returnViewController: returnViewController ) navigationController?.pushViewController(viewController, animated: true) } - } extension RestoreCloudPassphraseViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { [ Section( - id: "description-section", - headerState: .margin(height: .margin12), - footerState: .margin(height: .margin32), - rows: [ - tableView.descriptionRow( - id: "description", - text: "restore.cloud.password.description".localized, - font: .subhead2, - textColor: .gray, - ignoreBottomMargin: true - ) - ] + id: "description-section", + headerState: .margin(height: .margin12), + footerState: .margin(height: .margin32), + rows: [ + tableView.descriptionRow( + id: "description", + text: "restore.cloud.password.description".localized, + font: .subhead2, + textColor: .gray, + ignoreBottomMargin: true + ), + ] ), Section( - id: "passphrase", - footerState: .margin(height: .margin16), - rows: [ - StaticRow( - cell: passphraseCell, - id: "passphrase", - height: .heightSingleLineCell - ), - StaticRow( - cell: passphraseCautionCell, - id: "passphrase-caution", - dynamicHeight: { [weak self] width in - self?.passphraseCautionCell.height(containerWidth: width) ?? 0 - } - ) - ] + id: "passphrase", + footerState: .margin(height: .margin16), + rows: [ + StaticRow( + cell: passphraseCell, + id: "passphrase", + height: .heightSingleLineCell + ), + StaticRow( + cell: passphraseCautionCell, + id: "passphrase-caution", + dynamicHeight: { [weak self] width in + self?.passphraseCautionCell.height(containerWidth: width) ?? 0 + } + ), + ] ), ] } - } extension RestoreCloudPassphraseViewController: IDynamicHeightCellDelegate { - func onChangeHeight() { guard isLoaded else { return @@ -223,5 +226,4 @@ extension RestoreCloudPassphraseViewController: IDynamicHeightCellDelegate { self?.tableView.endUpdates() } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectModule.swift index 4152159d46..ebd220f473 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectModule.swift @@ -4,7 +4,7 @@ import MarketKit struct RestoreSelectModule { - static func viewController(accountName: String, accountType: AccountType, isManualBackedUp: Bool = true, returnViewController: UIViewController?) -> UIViewController { + static func viewController(accountName: String, accountType: AccountType, isManualBackedUp: Bool = true, isFileBackedUp: Bool = false, returnViewController: UIViewController?) -> UIViewController { let (blockchainTokensService, blockchainTokensView) = BlockchainTokensModule.module() let (restoreSettingsService, restoreSettingsView) = RestoreSettingsModule.module() @@ -12,6 +12,7 @@ struct RestoreSelectModule { accountName: accountName, accountType: accountType, isManualBackedUp: isManualBackedUp, + isFileBackedUp: isFileBackedUp, accountFactory: App.shared.accountFactory, accountManager: App.shared.accountManager, walletManager: App.shared.walletManager, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectService.swift index df2bf20cdb..178a97e42c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreSelect/RestoreSelectService.swift @@ -1,12 +1,12 @@ -import RxSwift -import RxRelay import MarketKit +import RxRelay +import RxSwift class RestoreSelectService { - private let accountName: String private let accountType: AccountType private let isManualBackedUp: Bool + private let isFileBackedUp: Bool private let accountFactory: AccountFactory private let accountManager: AccountManager private let walletManager: WalletManager @@ -31,10 +31,11 @@ class RestoreSelectService { } } - init(accountName: String, accountType: AccountType, isManualBackedUp: Bool, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, evmAccountRestoreStateManager: EvmAccountRestoreStateManager, marketKit: MarketKit.Kit, blockchainTokensService: BlockchainTokensService, restoreSettingsService: RestoreSettingsService) { + init(accountName: String, accountType: AccountType, isManualBackedUp: Bool, isFileBackedUp: Bool, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, evmAccountRestoreStateManager: EvmAccountRestoreStateManager, marketKit: MarketKit.Kit, blockchainTokensService: BlockchainTokensService, restoreSettingsService: RestoreSettingsService) { self.accountName = accountName self.accountType = accountType self.isManualBackedUp = isManualBackedUp + self.isFileBackedUp = isFileBackedUp self.accountFactory = accountFactory self.accountManager = accountManager self.walletManager = walletManager @@ -83,9 +84,9 @@ class RestoreSelectService { let enabled = isEnabled(blockchain: blockchain) return Item( - blockchain: blockchain, - enabled: enabled, - hasSettings: enabled && hasSettings(blockchain: blockchain) + blockchain: blockchain, + enabled: enabled, + hasSettings: enabled && hasSettings(blockchain: blockchain) ) } @@ -132,11 +133,9 @@ class RestoreSelectService { cancelEnableBlockchainRelay.accept(blockchain.type) } } - } extension RestoreSelectService { - var itemsObservable: Observable<[Item]> { itemsRelay.asObservable() } @@ -192,7 +191,13 @@ extension RestoreSelectService { } func restore() { - let account = accountFactory.account(type: accountType, origin: .restored, backedUp: isManualBackedUp, name: accountName) + let account = accountFactory.account( + type: accountType, + origin: .restored, + backedUp: isManualBackedUp, + fileBackedUp: isFileBackedUp, + name: accountName + ) accountManager.save(account: account) for (token, settings) in restoreSettingsMap { @@ -210,15 +215,12 @@ extension RestoreSelectService { let wallets = enabledTokens.map { Wallet(token: $0, account: account) } walletManager.save(wallets: wallets) } - } extension RestoreSelectService { - struct Item { let blockchain: Blockchain let enabled: Bool let hasSettings: Bool } - } From ee0e75c799fd837226b9e000b83963de82f428a3 Mon Sep 17 00:00:00 2001 From: ant013 Date: Fri, 6 Oct 2023 17:28:43 +0600 Subject: [PATCH 45/63] Refactor restore controllers. - Add cases and temporary fix cloud oneWallet restore --- .../project.pbxproj | 248 +++++++++++------- .../Core/Managers/CloudBackupManager.swift | 22 +- .../Core/Storage/ContactBookManager.swift | 79 +++--- .../Models/AccountType.swift | 7 + .../Modules/Backup/BackupModule.swift | 31 +++ .../Backup/ICloud/AppBackupProvider.swift | 3 +- .../ManageAccountsViewController.swift | 2 +- .../RestoreCloud/RestoreCloudModule.swift | 5 + .../RestoreCloudPassphraseModule.swift | 23 -- .../RestoreCloudPassphraseService.swift | 88 ------- .../RestoreCloud/RestoreCloudService.swift | 56 ++-- .../RestoreCloudViewController.swift | 46 +++- .../RestoreCloud/RestoreCloudViewModel.swift | 41 ++- .../RestoreFileConfigurationModule.swift | 7 + .../RestoreFileConfigurationService.swift | 48 ++++ .../RestoreFileConfigurationViewModel.swift | 4 + .../RestorePassphraseModule.swift | 21 ++ .../RestorePassphraseService.swift | 64 +++++ .../RestorePassphraseViewController.swift} | 30 +-- .../RestorePassphraseViewModel.swift} | 28 +- .../RestoreType/RestoreTypeModule.swift | 24 +- .../RestoreTypeViewController.swift | 40 +-- .../RestoreType/RestoreTypeViewModel.swift | 54 ++-- .../{ => Backup}/BackupAppViewModel.swift | 83 +----- .../BackupDisclaimerView.swift | 0 .../BackupList/BackupListView.swift | 12 +- .../BackupName/BackupNameView.swift | 4 +- .../BackupPassword/BackupPasswordView.swift | 6 +- .../BackupType/BackupTypeView.swift | 8 +- .../Settings/BackupApp/BackupAppModule.swift | 82 +++++- .../BackupManager/BackupManagerView.swift | 6 +- .../Restore/RestoreAppViewModel.swift | 13 + .../Modules/Wallet/WalletViewController.swift | 2 +- .../en.lproj/Localizable.strings | 80 +++--- 34 files changed, 754 insertions(+), 513 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift rename UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/{RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift => RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift} (88%) rename UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/{RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift => RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift} (80%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupAppViewModel.swift (80%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupDisclaimer/BackupDisclaimerView.swift (100%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupList/BackupListView.swift (89%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupName/BackupNameView.swift (93%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupPassword/BackupPasswordView.swift (94%) rename UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/{ => Backup}/BackupType/BackupTypeView.swift (86%) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Restore/RestoreAppViewModel.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index f2af52af5a..acfcf7af1f 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1972,7 +1972,6 @@ 6BDA29AD29D6F384003847ED /* ECashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AC29D6F384003847ED /* ECashKit */; }; 6BDA29B029D6F934003847ED /* HsToolKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6BDA29AF29D6F934003847ED /* HsToolKit */; }; ABC9A001F335B695CD066218 /* NftAssetModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD35D41AEEBD38AA08B5 /* NftAssetModule.swift */; }; - ABC9A0034DFBD65A7A8C4D65 /* RestoreCloudPassphraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9E0190FAD212E2E007F /* RestoreCloudPassphraseModule.swift */; }; ABC9A005F31836B4EBAB1C97 /* DonateDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0848221B0EC25C37F3 /* DonateDescriptionCell.swift */; }; ABC9A0073333D3DEC2797D15 /* BackupCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8E4CDD143171A1F9C46 /* BackupCloudPassphraseViewController.swift */; }; ABC9A00CF4BC7368D8EFEFB1 /* WalletTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4B75DFB58AC56FEF798 /* WalletTokenModule.swift */; }; @@ -1983,10 +1982,10 @@ ABC9A040EA0B56AF76D01855 /* WalletConnectPendingRequestsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2F6EDC8BB83ED7B75BA /* WalletConnectPendingRequestsModule.swift */; }; ABC9A043F82D9F5945C5FAFA /* PseudoAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC034DE5784F55BD5F3 /* PseudoAccessoryView.swift */; }; ABC9A04655D81FE5198B786F /* SendEip1155ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A23CB332521C0607CC6B /* SendEip1155ViewModel.swift */; }; + ABC9A04FAB83D7A8D251DA90 /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A202ED9B98DFEA8E6154 /* BackupPasswordView.swift */; }; ABC9A05D9F96BE464CFC90CC /* ContactBookModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5518367F0DDDB94D320 /* ContactBookModule.swift */; }; ABC9A06BB934A43890376A70 /* NftCollectionCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CF40E995105E7F38AC /* NftCollectionCellFactory.swift */; }; ABC9A06BE632BD33E5CA4106 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEAD18F73D4FBE05783D /* Contact.swift */; }; - ABC9A06CA0D364D4E1261C87 /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */; }; ABC9A074995C051E714FAFAB /* BackupContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC50307ABA3DF7034E1D /* BackupContact.swift */; }; ABC9A07518E5769122DFEAC2 /* RestoreCloudViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */; }; ABC9A0779C1107119CD3AF19 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */; }; @@ -1998,12 +1997,12 @@ ABC9A097A0BDD99777D5374D /* DonateDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0848221B0EC25C37F3 /* DonateDescriptionCell.swift */; }; ABC9A09E0B614E5B4E32B7F9 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACEC3169A9F01B55921A /* InputTextView.swift */; }; ABC9A0A3A52AD41643D67D3D /* SingleLineFormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */; }; - ABC9A0AADAE0A5C370946B8D /* RestoreCloudPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */; }; ABC9A0B37693470DAD0FFE20 /* WalletConnectMainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD18F3E73F96DD6C4FA9 /* WalletConnectMainService.swift */; }; 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 */; }; 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 */; }; ABC9A0E6EE31D5675542EE0B /* SessionRequestFilterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA77C414AC06C41F9319 /* SessionRequestFilterManager.swift */; }; ABC9A0E743DDE8F4ADA483EB /* SwapPriceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA459E123B7053EC73F0 /* SwapPriceCell.swift */; }; @@ -2012,7 +2011,6 @@ ABC9A1117A41AB8CE00FDEDB /* WalletConnectAppShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */; }; ABC9A12A4D114A2E4F4C711A /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */; }; ABC9A133A6BF0FC9A87FA14A /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; - ABC9A13B7F43722709570161 /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */; }; ABC9A13D78DD5F176A170B65 /* FullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */; }; ABC9A13DB3ADB580D59F66E4 /* SendEip1155ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A23CB332521C0607CC6B /* SendEip1155ViewModel.swift */; }; ABC9A13F4C814FFB31FF13CA /* SendEip721ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7315E119F0B1581B70C /* SendEip721ViewController.swift */; }; @@ -2031,7 +2029,6 @@ ABC9A1FFFB4F9EC58BF78661 /* AccountRestoreWarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */; }; ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */; }; ABC9A20A25C4C683A73CB994 /* ContactBookContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8080797194017F736AB /* ContactBookContactViewModel.swift */; }; - ABC9A20B7F90F5E741A74823 /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */; }; ABC9A20D2DDF8736293DE5C5 /* CoinIndicatorViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */; }; ABC9A20F6F7D5EA2A1A55A9E /* ContactLabelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB89F64056FFB98928E7 /* ContactLabelService.swift */; }; ABC9A227648FF076E9518703 /* ContactBookHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */; }; @@ -2048,6 +2045,7 @@ 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 */; }; @@ -2057,9 +2055,11 @@ ABC9A2C671DE8C67F192D22E /* ContactBookAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */; }; ABC9A2CA505DB49DE0FB28DD /* WalletTokenBalanceCustomAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD448DC071D8800C6B12 /* WalletTokenBalanceCustomAmountCell.swift */; }; ABC9A2D0ACEDCFA5FDB04D89 /* IndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A12E4155640075755699 /* IndicatorDataSource.swift */; }; + ABC9A2D3D28955B8AD82AFC3 /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5CDF9153AECED3DE50C /* BackupTypeView.swift */; }; ABC9A2E71264B12B7FFC3736 /* WalletConnectListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3BEB33F6DBE2395FD11 /* WalletConnectListService.swift */; }; ABC9A2E921AE00E0AF5067DE /* CoinProChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A021D71EDD24DFB6BA62 /* CoinProChartModule.swift */; }; ABC9A2EEC77205793C21F9A1 /* WalletConnectMainPendingRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D8072033A5AC7E4897 /* WalletConnectMainPendingRequestViewModel.swift */; }; + ABC9A2F6D2A2AAFA31C64BAB /* RestoreFileConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF6C15800AF8C37C3516 /* RestoreFileConfigurationViewModel.swift */; }; ABC9A2FF431ACFA812F58AD1 /* WalletConnectRequestMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA31438063F7AB7BDDC8 /* WalletConnectRequestMapper.swift */; }; ABC9A305CBB28F2B19EB00D2 /* CoinDetailAdviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */; }; ABC9A30629619D5BD6CEB952 /* ContactBookContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8A353E491AAD3EDA120 /* ContactBookContactViewController.swift */; }; @@ -2072,12 +2072,15 @@ 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 */; }; + ABC9A346AC191059BAFAB977 /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7AC6BC7EA8166F21D9A /* BackupNameView.swift */; }; ABC9A348E1ADBF5EE7E62B06 /* SendBinanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0DD32AB4B9BAB79F11 /* SendBinanceFactory.swift */; }; ABC9A3510E5BE401AD04DA98 /* ContactBookViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3C708CD81CFE4C5BC5C /* ContactBookViewModel.swift */; }; ABC9A359DB8C1A89269236CC /* ContactBookSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A24CBB826A2D2F88EC61 /* ContactBookSettingsModule.swift */; }; ABC9A3613C5477C07F37F48C /* WalletConnectListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE5FD79ECC4AC85B86FA /* WalletConnectListViewController.swift */; }; ABC9A36297D869E49C152CAB /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.swift */; }; + ABC9A372F53F1F1D59BF8969 /* RestoreFileConfigurationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.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 */; }; @@ -2091,9 +2094,12 @@ ABC9A3FEF48388A60B8BACB5 /* DataSourceChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A22311B6AA64B7D93CB4 /* DataSourceChain.swift */; }; ABC9A4045F498EE345B998D8 /* IntegerFormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */; }; ABC9A40EB6EC886116806130 /* WalletConnectSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */; }; + ABC9A414F0F0AEA6E4DD4E9D /* RestorePassphraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A39A33712A1429D623D5 /* RestorePassphraseModule.swift */; }; ABC9A427B3166B8A0630EC8A /* WalletTokenBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A52822CE6B8830CF5EF4 /* WalletTokenBalanceViewModel.swift */; }; + ABC9A437473D0E77F9DBEB42 /* RestoreAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF1626FA59BD8CA7ABC1 /* RestoreAppViewModel.swift */; }; ABC9A4387AF9D012498DF42B /* NftAssetOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB8907B0E779CA4DF8F1 /* NftAssetOverviewViewModel.swift */; }; - ABC9A446EF71E1DB4FA7D353 /* RestoreCloudPassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6F1FB00B33D1896FC6B /* RestoreCloudPassphraseService.swift */; }; + ABC9A4465982823773CE1B50 /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFFD435E0C9FBE0E5E7C /* BackupDisclaimerView.swift */; }; + ABC9A453F337BA22A5698DCC /* RestorePassphraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A39A33712A1429D623D5 /* RestorePassphraseModule.swift */; }; ABC9A4566EA1995007490C0D /* SendFeeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1ACFDFD53E2502C30 /* SendFeeViewModel.swift */; }; ABC9A458626E9C13F229741F /* WalletTokenBalanceCustomAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD448DC071D8800C6B12 /* WalletTokenBalanceCustomAmountCell.swift */; }; ABC9A462CFF86970878025CE /* WalletTokenBalanceViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */; }; @@ -2103,6 +2109,7 @@ ABC9A47781ECAC34ADE55B41 /* SendConfirmationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2FD4A59DB53631435BA /* SendConfirmationService.swift */; }; ABC9A47D4666FA5115F98629 /* ChartIndicatorsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */; }; ABC9A4801E4964F6AED1E667 /* WalletConnectMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEA1D717D8CED8462AB0 /* WalletConnectMainViewModel.swift */; }; + ABC9A481F1C13DBAAD3F632B /* RestoreFileConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF6C15800AF8C37C3516 /* RestoreFileConfigurationViewModel.swift */; }; ABC9A4929EFBFAD0B595A4E8 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; ABC9A4A21CFBA188A7EEC930 /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */; }; ABC9A4B643D98FB95F431401 /* SendBitcoinAmountInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1F55164BDFD049793 /* SendBitcoinAmountInputService.swift */; }; @@ -2111,11 +2118,9 @@ ABC9A4CD35CF43C88EC13909 /* SendEip1155Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC6A0B950C0AABD5A93E /* SendEip1155Service.swift */; }; ABC9A4D5326B3A85888140FE /* ChartIndicatorSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */; }; ABC9A4E323FF7AAD86FA8E75 /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; }; - ABC9A4EE80A57455DB2CCD4F /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */; }; ABC9A4F4B7F17169DC240A98 /* WalletConnectUriHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1C31F5343EB2BEA4540 /* WalletConnectUriHandler.swift */; }; ABC9A4FF1E1964FB77700C4E /* ChartIndicatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6DE5C760A5D0C90B70E /* ChartIndicatorFactory.swift */; }; ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; - ABC9A5125874E09194BA8532 /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */; }; ABC9A51979D2047BEF45A2AE /* TokenSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */; }; ABC9A51E36466E414AF24C67 /* WalletConnectMainPendingRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D8072033A5AC7E4897 /* WalletConnectMainPendingRequestViewModel.swift */; }; ABC9A51F2E7EB0B477EBE708 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; @@ -2123,7 +2128,6 @@ ABC9A542CA987F09C93F04A9 /* InputTextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7FA830E64B8DCA1A69A /* InputTextRow.swift */; }; ABC9A543EB59D153FAD103F6 /* KdfParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6663522498A53CF4174 /* KdfParams.swift */; }; ABC9A54917CDA7F9EAE237C4 /* ChartIndicatorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB3EC7A1FB0D6C9F7F89 /* ChartIndicatorsModule.swift */; }; - ABC9A54E6E15E96271525339 /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */; }; ABC9A54FFFFBFC3C7B23F0B8 /* BottomGradientHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */; }; ABC9A55470228BD7B1535B9B /* WalletTokenBalanceViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */; }; ABC9A556361B2644555659D8 /* SendConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA7F2ECF212EF8B70470 /* SendConfirmationViewController.swift */; }; @@ -2149,6 +2153,7 @@ ABC9A638E7EA1788D40FF929 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */; }; ABC9A63B2AABC0414000DEC2 /* SendConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3AB799024C8FC2C7DD8 /* SendConfirmationViewModel.swift */; }; ABC9A63EC83A82A76E67778B /* SendNftModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */; }; + 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 */; }; ABC9A6887B716464A5813EE9 /* BackupCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */; }; @@ -2161,17 +2166,15 @@ ABC9A6A792282ACC8DAB62BC /* IntegerFormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */; }; ABC9A6A9C28C95352232B062 /* ThemeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */; }; ABC9A6BC79804B3D3AAFA8F1 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; - ABC9A6C05017D9267567AB73 /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */; }; ABC9A6C0A45A33C83B632D58 /* SendFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */; }; ABC9A6C1B2F55F1FFA8910CA /* ContactBookSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */; }; - ABC9A6C65416E7F4F3830962 /* RestoreCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */; }; ABC9A6D1C4EF73D4A8D6F3BD /* CoinProChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A021D71EDD24DFB6BA62 /* CoinProChartModule.swift */; }; ABC9A6D1CDF470FB73EF4816 /* WalletConnectPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A56ED1DB109A2E1F6EC1 /* WalletConnectPairingService.swift */; }; ABC9A6E939BDC0269313A66D /* SendModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD1F2311CC6425CF9D90 /* SendModule.swift */; }; + ABC9A6EFD77E59AA6B4C5070 /* RestorePassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE5CAD06644F52170C72 /* RestorePassphraseService.swift */; }; ABC9A6F88E51293F2605CACD /* ContactBookContactModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB2ED4E48D4FCEDBE769 /* ContactBookContactModule.swift */; }; ABC9A70AE588307EA1D3A414 /* SendConfirmationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2FD4A59DB53631435BA /* SendConfirmationService.swift */; }; ABC9A712F6389F5C2B0D63E3 /* RestoreCloudService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06866150862CEDEB5DE /* RestoreCloudService.swift */; }; - ABC9A7244B2D23BDA40C407C /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */; }; ABC9A727EC8A3C74C9C1A31A /* WalletConnectMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3A694467493C6F4AACE /* WalletConnectMainViewController.swift */; }; ABC9A728EEF4A054C7B8722B /* DonateAddressModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0B7E7360DC0357B2D0F /* DonateAddressModule.swift */; }; ABC9A7297650FD2D9F8F595F /* MacdIndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA8F31619609907AD67E /* MacdIndicatorDataSource.swift */; }; @@ -2197,9 +2200,9 @@ ABC9A7E1F93B0A85976C826D /* UniswapV3Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A253877D9FB972EFB8D7 /* UniswapV3Provider.swift */; }; ABC9A7E28714A9A19A2160D4 /* SendModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD1F2311CC6425CF9D90 /* SendModule.swift */; }; ABC9A7EACB2FA65355C2BA4E /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; + ABC9A7EF7780159AC6B946FC /* BackupPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A202ED9B98DFEA8E6154 /* BackupPasswordView.swift */; }; ABC9A7F5ECEC3311216A407F /* SendMemoInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A80143F95E28346C81FE /* SendMemoInputService.swift */; }; ABC9A802418438F6BD1FC1E3 /* WalletTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A64A66778C137FA9642C /* WalletTokenViewController.swift */; }; - ABC9A806CB34CB9A5E27A0A3 /* RestoreCloudPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */; }; ABC9A80BCDA72347C6619E6C /* SendTimeLockErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADF114FCFABEA148AF04 /* SendTimeLockErrorService.swift */; }; ABC9A819DDAEE683FCCA02EF /* NftAssetCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A30A8F78E9C9AEE861F1 /* NftAssetCellFactory.swift */; }; ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */; }; @@ -2219,6 +2222,7 @@ ABC9A8916A5DFA7A33F4FF79 /* SendBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF15BD67548E6D755CA0 /* SendBinanceViewController.swift */; }; ABC9A89499016C8AC8341238 /* NftCollectionCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CF40E995105E7F38AC /* NftCollectionCellFactory.swift */; }; ABC9A8A74C527C4E01EBB8A5 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; + ABC9A8AC5E635D9CB1704568 /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFFD435E0C9FBE0E5E7C /* BackupDisclaimerView.swift */; }; ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; @@ -2226,14 +2230,15 @@ ABC9A8D8709EC2B40D74A97A /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; ABC9A8D91CFED1961B618241 /* ChartIndicatorsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2F3E5147E0E92258FBB /* ChartIndicatorsService.swift */; }; ABC9A8FAB37E2049DC58FF14 /* RestoreTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A13DB598B22516E5AD76 /* RestoreTypeViewModel.swift */; }; + ABC9A904FCE6BFE793C944AE /* RestoreFileConfigurationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */; }; ABC9A90978E16ABC5F67CCF7 /* BalanceButtonsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A53F8E3F9572FFEE4275 /* BalanceButtonsCell.swift */; }; ABC9A90BDA552DFBCB19B226 /* NftAssetOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABE473B354836327B3AC /* NftAssetOverviewService.swift */; }; ABC9A91D03FB46F6AD21EEF4 /* WalletConnectSocketConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0483AEAEB88DFBDD873 /* WalletConnectSocketConnectionService.swift */; }; ABC9A9221E4FF0734089BCAB /* WalletConnectMainPendingRequestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFF8093DEB7AFD7DBBCC /* WalletConnectMainPendingRequestService.swift */; }; ABC9A92D7F9ADCE00CBCED09 /* WalletConnectPairingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE62C0399849EFB5C158 /* WalletConnectPairingViewModel.swift */; }; ABC9A933C2603486BA181B19 /* SendFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */; }; + ABC9A93E05AAF5D98C1DF4D6 /* RestorePassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE5CAD06644F52170C72 /* RestorePassphraseService.swift */; }; ABC9A9493F250B81E1152012 /* SendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */; }; - ABC9A94A82E55F437BD3FE68 /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */; }; ABC9A9562DD283B6FCACBCF9 /* MarketCardTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC7983E4A81E421AB639 /* MarketCardTitleView.swift */; }; ABC9A95E667DD7BD26602D8E /* SendEip721Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE7CB7CC9C118C72559 /* SendEip721Service.swift */; }; ABC9A96132AD85DD613EC773 /* ProFeaturesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB785128005F6C2C9F9A /* ProFeaturesStorage.swift */; }; @@ -2249,8 +2254,9 @@ ABC9A9E3191338CD0D1DE8AE /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; ABC9A9EBBC60A709836DE237 /* NftAssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */; }; ABC9A9FA3285B39D25801C2A /* AccountRestoreWarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */; }; - ABC9AA05527830EFFCFB98E3 /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA0021E969A73DA7E177 /* BackupListView.swift */; }; + ABC9AA016413C37F4CC95080 /* RestorePassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0547CBE2B5A3E38891E /* RestorePassphraseViewController.swift */; }; ABC9AA0E63188150CD9A8D03 /* RestoreTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A13DB598B22516E5AD76 /* RestoreTypeViewModel.swift */; }; + ABC9AA18996E714C955E7E13 /* RestoreAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF1626FA59BD8CA7ABC1 /* RestoreAppViewModel.swift */; }; ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9AA27A709AC5F85176A53 /* WalletConnectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */; }; ABC9AA309248821942E78740 /* MarketCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2B7FBA735A76083990C /* MarketCardCell.swift */; }; @@ -2271,12 +2277,12 @@ ABC9AB0F8FE5808DB889C081 /* WalletConnectScanQrViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3DBB89D0AE0C127742B /* WalletConnectScanQrViewModel.swift */; }; ABC9AB11FDD018A96BB86557 /* BottomGradientHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */; }; ABC9AB1E703AE57DF856ECD9 /* SendAmountCautionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A07A33870908ED1BA338 /* SendAmountCautionViewModel.swift */; }; + ABC9AB215D081976FC2E294F /* BackupNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7AC6BC7EA8166F21D9A /* BackupNameView.swift */; }; ABC9AB2E235EA006E2DAD8DD /* EnabledWalletCache_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */; }; ABC9AB308727D81FBB8EBCDD /* BackupCloudPassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF6AA02DA39787C053F0 /* BackupCloudPassphraseService.swift */; }; ABC9AB3DAD30AA400DEB719C /* SendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A696DCBBE4761E77311C /* SendBitcoinService.swift */; }; ABC9AB401FD98F99EF6B07C6 /* RestoreTypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A939DD222D4A2BD3D71C /* RestoreTypeViewController.swift */; }; ABC9AB4DF4CCEA2C51DD4AB6 /* SingleLineFormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */; }; - ABC9AB65B10FDB5F30E2731D /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */; }; ABC9AB6EB596E2F8B15D00E4 /* CipherParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A89726499CDB4F697EDD /* CipherParams.swift */; }; ABC9AB71563E5F2C9F2EA9E4 /* WalletConnectAppShowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3FB680357E569B6DB5F /* WalletConnectAppShowViewModel.swift */; }; ABC9AB83EE3F909BD80E0539 /* BackupCryptoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD5CB1911A698718213F /* BackupCryptoHelper.swift */; }; @@ -2284,7 +2290,6 @@ ABC9AB8A9028DC1488166ABC /* WalletConnectPendingRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAD79FD756DA69A52578 /* WalletConnectPendingRequestsViewController.swift */; }; ABC9AB9DCC782F2EC14A7031 /* TechnicalIndicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */; }; ABC9ABA70CEF664E8E01FA7A /* SendNftModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */; }; - ABC9ABC085B0733DD4EF1FCD /* RestoreCloudPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */; }; ABC9ABC09321233E1727A8DD /* WalletConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4544AB5CA22ADE16417 /* WalletConnectSession.swift */; }; ABC9ABC375B65451761D4766 /* SendFeeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1ACFDFD53E2502C30 /* SendFeeViewModel.swift */; }; ABC9ABD7C7746ABF50DD646F /* ChartIndicatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6DE5C760A5D0C90B70E /* ChartIndicatorFactory.swift */; }; @@ -2298,7 +2303,6 @@ ABC9ABFA7299ADDDFEE918F7 /* WalletConnectPairingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE88105815BFC477D71 /* WalletConnectPairingViewController.swift */; }; ABC9AC011EA9D58866999D88 /* UniswapV3DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE97D361FBF43F46F016 /* UniswapV3DataSource.swift */; }; ABC9AC10D815702B812CFFB7 /* NftAssetOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABE473B354836327B3AC /* NftAssetOverviewService.swift */; }; - ABC9AC1B69C1E03F4035A8FB /* RestoreCloudPassphraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9E0190FAD212E2E007F /* RestoreCloudPassphraseModule.swift */; }; ABC9AC1BD5C95957726F8AE8 /* MarketCardValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4FCDC5085002DF35C17 /* MarketCardValueView.swift */; }; ABC9AC50E2E966F009D78FD5 /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6B2EF46FF7EDA4728D3 /* CheckboxView.swift */; }; ABC9AC5552508D091D622027 /* SendZcashFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6F55A2C6777D25F57D5 /* SendZcashFactory.swift */; }; @@ -2307,6 +2311,7 @@ ABC9AC5C8EE0A8C7F10B8A50 /* ContactBookSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A309A58148C40912B964 /* ContactBookSettingsService.swift */; }; ABC9AC692F695C5F81E0453D /* DonateAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A525C1E9A53F37EC3918 /* DonateAddressViewController.swift */; }; ABC9AC73C488F8B1F54929B5 /* NftAssetCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A30A8F78E9C9AEE861F1 /* NftAssetCellFactory.swift */; }; + ABC9AC763748CC31D45FB6BD /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */; }; ABC9AC78B2D374511F751997 /* WalletConnectAppShowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3FB680357E569B6DB5F /* WalletConnectAppShowViewModel.swift */; }; ABC9AC79493AB0EC96904164 /* SessionRequestFilterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA77C414AC06C41F9319 /* SessionRequestFilterManager.swift */; }; ABC9AC79ACCB69BF97A01B53 /* ContactBookSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */; }; @@ -2330,10 +2335,10 @@ 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 */; }; - ABC9ACE31EC94ABC5B325693 /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA0021E969A73DA7E177 /* BackupListView.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 */; }; + ABC9AD1F6A6A7C97E4120F2F /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */; }; ABC9AD2688A8DF327A3F92FC /* NoAccountWalletTokenListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A141E4C255C3E450863E /* NoAccountWalletTokenListService.swift */; }; ABC9AD27E074CF3FA292C647 /* IndicatorAdviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */; }; ABC9AD3001AAA0570B503876 /* ManageBarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6CFDF38D413679D2088 /* ManageBarButtonView.swift */; }; @@ -2366,6 +2371,7 @@ ABC9AE0D23A7B54521E77052 /* ECashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4C563432A34A634B82A /* ECashAdapter.swift */; }; ABC9AE18DE62E4DDDD44916D /* SwapInputModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAD55B8932EE75E3C037 /* SwapInputModule.swift */; }; ABC9AE1E60CABA0101D62738 /* FullCoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9B35C58F6525F3B2D5C /* FullCoin.swift */; }; + ABC9AE2131780654A7139081 /* RestorePassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0547CBE2B5A3E38891E /* RestorePassphraseViewController.swift */; }; ABC9AE223619E13A296BED51 /* MarketCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2B7FBA735A76083990C /* MarketCardCell.swift */; }; ABC9AE2C026D04B679644279 /* IntegerAmountInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA527E63E18179CB689A /* IntegerAmountInputCell.swift */; }; ABC9AE3D64AF3981A68D9913 /* SendConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3AB799024C8FC2C7DD8 /* SendConfirmationViewModel.swift */; }; @@ -2377,6 +2383,7 @@ ABC9AE553D422A163A09E5F8 /* MarketCardValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4FCDC5085002DF35C17 /* MarketCardValueView.swift */; }; ABC9AE6D877341985A6F651F /* SendBitcoinAmountInputService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF1F55164BDFD049793 /* SendBitcoinAmountInputService.swift */; }; ABC9AE775BB25CDB7AA83228 /* EventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A950663B76424B1761B3 /* EventHandler.swift */; }; + ABC9AE7DA8EFD812710C7BE4 /* RestorePassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */; }; ABC9AE832E74D8DCADB83803 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7FC41B9F98871246E0E /* ImageCell.swift */; }; ABC9AE863B44E921F58DF3EA /* WalletConnectMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3A694467493C6F4AACE /* WalletConnectMainViewController.swift */; }; ABC9AE9A8BECB2CE0EEF8271 /* DonateAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A525C1E9A53F37EC3918 /* DonateAddressViewController.swift */; }; @@ -2392,8 +2399,9 @@ ABC9AEE0716153FBCA28A7F4 /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6B2EF46FF7EDA4728D3 /* CheckboxView.swift */; }; ABC9AEEF443C883568E9E555 /* WalletTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A64A66778C137FA9642C /* WalletTokenViewController.swift */; }; ABC9AEF0A2ECD0E627AF065B /* ECashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4C563432A34A634B82A /* ECashAdapter.swift */; }; - ABC9AEF4FDD9B4C16E87DBDA /* RestoreCloudPassphraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6F1FB00B33D1896FC6B /* RestoreCloudPassphraseService.swift */; }; + ABC9AEF231332C7B8756E8A9 /* BackupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4D1C7AE5723851A53EB /* BackupListView.swift */; }; ABC9AEF62C857F322FFA87E4 /* ContactBookAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */; }; + ABC9AF04946C86FA6DBD4225 /* RestorePassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */; }; ABC9AF1729BA19223BB39E06 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; }; ABC9AF371FBB4BEA654A78B6 /* MetadataMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */; }; ABC9AF4D82ACDFBFBDC2D23C /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; }; @@ -3770,6 +3778,7 @@ ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineFormTextView.swift; sourceTree = ""; }; ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceView.swift; sourceTree = ""; }; ABC9A0483AEAEB88DFBDD873 /* WalletConnectSocketConnectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSocketConnectionService.swift; sourceTree = ""; }; + ABC9A0547CBE2B5A3E38891E /* RestorePassphraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePassphraseViewController.swift; sourceTree = ""; }; ABC9A06866150862CEDEB5DE /* RestoreCloudService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudService.swift; sourceTree = ""; }; ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookSettingsViewModel.swift; sourceTree = ""; }; ABC9A06A64AB5B2A12C38D91 /* Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; @@ -3793,6 +3802,7 @@ ABC9A1BD3B1B53C72DDF923A /* SendBitcoinAdapterService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinAdapterService.swift; sourceTree = ""; }; ABC9A1C2F1CC07FD4CDFC591 /* UniswapV3ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3ViewModel.swift; sourceTree = ""; }; ABC9A1C31F5343EB2BEA4540 /* WalletConnectUriHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectUriHandler.swift; sourceTree = ""; }; + ABC9A202ED9B98DFEA8E6154 /* BackupPasswordView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupPasswordView.swift; sourceTree = ""; }; ABC9A21A8154277AF08399A8 /* InputIntegerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputIntegerSection.swift; sourceTree = ""; }; ABC9A22311B6AA64B7D93CB4 /* DataSourceChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceChain.swift; sourceTree = ""; }; ABC9A23CB332521C0607CC6B /* SendEip1155ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155ViewModel.swift; sourceTree = ""; }; @@ -3812,6 +3822,7 @@ ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsRepository.swift; sourceTree = ""; }; ABC9A38082BD2EBE1BC8E11E /* WalletTokenService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenService.swift; sourceTree = ""; }; ABC9A381CB4C09FF7CB62A94 /* HudHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HudHelper.swift; sourceTree = ""; }; + ABC9A39A33712A1429D623D5 /* RestorePassphraseModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePassphraseModule.swift; sourceTree = ""; }; ABC9A3A694467493C6F4AACE /* WalletConnectMainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainViewController.swift; sourceTree = ""; }; ABC9A3AB799024C8FC2C7DD8 /* SendConfirmationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationViewModel.swift; sourceTree = ""; }; ABC9A3AF18834CE9E569C89E /* ChartIndicatorsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsViewModel.swift; sourceTree = ""; }; @@ -3832,6 +3843,7 @@ ABC9A4B75DFB58AC56FEF798 /* WalletTokenModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenModule.swift; sourceTree = ""; }; ABC9A4BA46EDEEAB6CD9B25C /* WalletConnectPairingModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingModule.swift; sourceTree = ""; }; ABC9A4C563432A34A634B82A /* ECashAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ECashAdapter.swift; sourceTree = ""; }; + ABC9A4D1C7AE5723851A53EB /* BackupListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupListView.swift; sourceTree = ""; }; ABC9A4FCDC5085002DF35C17 /* MarketCardValueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCardValueView.swift; sourceTree = ""; }; ABC9A525C1E9A53F37EC3918 /* DonateAddressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateAddressViewController.swift; sourceTree = ""; }; ABC9A52822CE6B8830CF5EF4 /* WalletTokenBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceViewModel.swift; sourceTree = ""; }; @@ -3843,6 +3855,7 @@ ABC9A56611CF5E7B3F25CD5C /* SwapInputAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapInputAccessoryView.swift; sourceTree = ""; }; ABC9A56ED1DB109A2E1F6EC1 /* WalletConnectPairingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingService.swift; sourceTree = ""; }; ABC9A580220B9FD291A6496A /* SendAmountCautionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAmountCautionService.swift; sourceTree = ""; }; + ABC9A5CDF9153AECED3DE50C /* BackupTypeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupTypeView.swift; sourceTree = ""; }; ABC9A5E6F7C6887DD5DFF6E4 /* ContactBookContactService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactService.swift; sourceTree = ""; }; ABC9A5FE0EDA53E4D9B85DE1 /* RestoreCloudViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudViewModel.swift; sourceTree = ""; }; ABC9A6363DB5DAE5B58AFDC0 /* UniswapV3Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3Module.swift; sourceTree = ""; }; @@ -3854,14 +3867,13 @@ ABC9A6CFDF38D413679D2088 /* ManageBarButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageBarButtonView.swift; sourceTree = ""; }; ABC9A6D56EBB7FFAD68CFD66 /* IntegerAmountInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerAmountInputViewModel.swift; sourceTree = ""; }; ABC9A6DE5C760A5D0C90B70E /* ChartIndicatorFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorFactory.swift; sourceTree = ""; }; - ABC9A6F1FB00B33D1896FC6B /* RestoreCloudPassphraseService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseService.swift; sourceTree = ""; }; ABC9A6F55A2C6777D25F57D5 /* SendZcashFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendZcashFactory.swift; sourceTree = ""; }; ABC9A72B62F6152709348A6D /* DonateAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateAddressViewModel.swift; sourceTree = ""; }; ABC9A7315E119F0B1581B70C /* SendEip721ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721ViewController.swift; sourceTree = ""; }; ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinIndicatorViewItemFactory.swift; sourceTree = ""; }; ABC9A776346AF62265896CA1 /* CellElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellElement.swift; sourceTree = ""; }; ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSelectView.swift; sourceTree = ""; }; - ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupAppViewModel.swift; sourceTree = ""; }; + ABC9A7AC6BC7EA8166F21D9A /* BackupNameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupNameView.swift; sourceTree = ""; }; ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721ViewModel.swift; sourceTree = ""; }; ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRestoreWarningManager.swift; sourceTree = ""; }; ABC9A7D6C9D12C1F1F3A1218 /* SendBitcoinViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinViewController.swift; sourceTree = ""; }; @@ -3874,7 +3886,6 @@ ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeMode.swift; sourceTree = ""; }; ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowView.swift; sourceTree = ""; }; ABC9A86EA911DA12C7A6AC20 /* WalletTokenBalanceViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceViewItemFactory.swift; sourceTree = ""; }; - ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupNameView.swift; sourceTree = ""; }; ABC9A88E126AB21F856522A7 /* IntegerAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerAmountInputView.swift; sourceTree = ""; }; ABC9A896A83640B618328FE1 /* EnsAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnsAddressParserItem.swift; sourceTree = ""; }; ABC9A89726499CDB4F697EDD /* CipherParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherParams.swift; sourceTree = ""; }; @@ -3896,10 +3907,8 @@ 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 = ""; }; - ABC9A9E0190FAD212E2E007F /* RestoreCloudPassphraseModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseModule.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 = ""; }; - ABC9AA0021E969A73DA7E177 /* BackupListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupListView.swift; sourceTree = ""; }; ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataMonitor.swift; sourceTree = ""; }; ABC9AA31438063F7AB7BDDC8 /* WalletConnectRequestMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectRequestMapper.swift; sourceTree = ""; }; ABC9AA3B8927F9F138ABCFB8 /* WalletConnectAppShowService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowService.swift; sourceTree = ""; }; @@ -3910,7 +3919,6 @@ ABC9AA7F2ECF212EF8B70470 /* SendConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationViewController.swift; sourceTree = ""; }; ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsBackup.swift; sourceTree = ""; }; ABC9AA8F31619609907AD67E /* MacdIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacdIndicatorDataSource.swift; sourceTree = ""; }; - ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewModel.swift; sourceTree = ""; }; ABC9AAB6BA03FFE92F247FF6 /* ProChartFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProChartFetcher.swift; sourceTree = ""; }; ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreTypeModule.swift; sourceTree = ""; }; ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceCell.swift; sourceTree = ""; }; @@ -3918,6 +3926,7 @@ ABC9AAD79FD756DA69A52578 /* WalletConnectPendingRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsViewController.swift; sourceTree = ""; }; ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCrypto.swift; sourceTree = ""; }; ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModel.swift; sourceTree = ""; }; + ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupAppViewModel.swift; sourceTree = ""; }; ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; ABC9AB2DC4C4412EFE6BEFF7 /* WalletTokenBalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenBalanceCell.swift; sourceTree = ""; }; ABC9AB2ED4E48D4FCEDBE769 /* ContactBookContactModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactModule.swift; sourceTree = ""; }; @@ -3931,7 +3940,6 @@ ABC9AB9077A6A0ABE4909B76 /* IntegerFormAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegerFormAmountInputView.swift; sourceTree = ""; }; ABC9ABE473B354836327B3AC /* NftAssetOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewService.swift; sourceTree = ""; }; ABC9ABE97578DC667CBDC11A /* BaseSendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseSendViewController.swift; sourceTree = ""; }; - ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupDisclaimerView.swift; sourceTree = ""; }; ABC9ABFE62D22F9FB0B3409A /* DonateDescriptionDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateDescriptionDataSource.swift; sourceTree = ""; }; ABC9AC09A586D88BAB3B9C67 /* WalletConnectListModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectListModule.swift; sourceTree = ""; }; ABC9AC0B5943DF3B61B20BF6 /* WalletConnectSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionManager.swift; sourceTree = ""; }; @@ -3941,7 +3949,6 @@ ABC9AC6A0B950C0AABD5A93E /* SendEip1155Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155Service.swift; sourceTree = ""; }; ABC9AC7983E4A81E421AB639 /* MarketCardTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCardTitleView.swift; sourceTree = ""; }; ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressService.swift; sourceTree = ""; }; - ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupTypeView.swift; sourceTree = ""; }; ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorSettingsViewModel.swift; sourceTree = ""; }; ABC9ACE7CB7CC9C118C72559 /* SendEip721Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721Service.swift; sourceTree = ""; }; ABC9ACE88105815BFC477D71 /* WalletConnectPairingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingViewController.swift; sourceTree = ""; }; @@ -3959,6 +3966,8 @@ 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 = ""; }; ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomGradientHolder.swift; sourceTree = ""; }; @@ -3967,27 +3976,30 @@ ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookHelper.swift; sourceTree = ""; }; ABC9AE15C187118DE6F0CE7B /* WalletConnectPendingRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsViewModel.swift; sourceTree = ""; }; ABC9AE522F09C5E7029CA86E /* CheckboxStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxStyle.swift; sourceTree = ""; }; + ABC9AE5CAD06644F52170C72 /* RestorePassphraseService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePassphraseService.swift; sourceTree = ""; }; ABC9AE5FD79ECC4AC85B86FA /* WalletConnectListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectListViewController.swift; sourceTree = ""; }; ABC9AE62C0399849EFB5C158 /* WalletConnectPairingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPairingViewModel.swift; sourceTree = ""; }; ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendZcashService.swift; sourceTree = ""; }; ABC9AE89A5925C2026AB6B69 /* TransactionsContactLabelService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsContactLabelService.swift; sourceTree = ""; }; ABC9AE8A193F58021C411311 /* WalletConnectMainModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainModule.swift; sourceTree = ""; }; + ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePassphraseViewModel.swift; sourceTree = ""; }; ABC9AE97D361FBF43F46F016 /* UniswapV3DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3DataSource.swift; sourceTree = ""; }; ABC9AEA1D717D8CED8462AB0 /* WalletConnectMainViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainViewModel.swift; sourceTree = ""; }; ABC9AEAD18F73D4FBE05783D /* Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; ABC9AEC034DE5784F55BD5F3 /* PseudoAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PseudoAccessoryView.swift; sourceTree = ""; }; - ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupPasswordView.swift; sourceTree = ""; }; ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackup.swift; sourceTree = ""; }; - ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewController.swift; sourceTree = ""; }; ABC9AF15BD67548E6D755CA0 /* SendBinanceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBinanceViewController.swift; sourceTree = ""; }; + ABC9AF1626FA59BD8CA7ABC1 /* RestoreAppViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreAppViewModel.swift; sourceTree = ""; }; ABC9AF26FDCB363793BF66E1 /* Integer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Integer.swift; sourceTree = ""; }; ABC9AF2B063727B7EABFD9A3 /* RsiIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RsiIndicatorDataSource.swift; sourceTree = ""; }; ABC9AF395EA01B43D6D77C43 /* ActivityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = ""; }; ABC9AF6AA02DA39787C053F0 /* BackupCloudPassphraseService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCloudPassphraseService.swift; sourceTree = ""; }; + ABC9AF6C15800AF8C37C3516 /* RestoreFileConfigurationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileConfigurationViewModel.swift; sourceTree = ""; }; ABC9AF8E8DE67732371A00E0 /* FeePriceScale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeePriceScale.swift; sourceTree = ""; }; ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetViewController.swift; sourceTree = ""; }; ABC9AFF7119B9AC0E32B2060 /* SendConfirmationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationModule.swift; sourceTree = ""; }; ABC9AFF8093DEB7AFD7DBBCC /* WalletConnectMainPendingRequestService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainPendingRequestService.swift; sourceTree = ""; }; + ABC9AFFD435E0C9FBE0E5E7C /* BackupDisclaimerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupDisclaimerView.swift; sourceTree = ""; }; D00267B82A57E6CE00D6B2D5 /* ResendPastInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendPastInputCell.swift; sourceTree = ""; }; D00267BB2A57E72700D6B2D5 /* ResendPasteInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendPasteInputView.swift; sourceTree = ""; }; D003297526CD2C67002EC21D /* TransactionLockInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionLockInfo.swift; sourceTree = ""; }; @@ -5987,6 +5999,7 @@ 2FA5D4327C41BABFC64F5843 /* RestoreNonStandard */, ABC9A41E82AB94393553267F /* RestoreType */, ABC9A7E9EAE24647C0700B39 /* RestoreCloud */, + ABC9A6D0013823EF4EECB442 /* RestoreFile */, ); path = RestoreAccount; sourceTree = ""; @@ -6956,6 +6969,14 @@ path = Token; sourceTree = ""; }; + ABC9A1A8B6E734D38D55E1D2 /* Restore */ = { + isa = PBXGroup; + children = ( + ABC9AF1626FA59BD8CA7ABC1 /* RestoreAppViewModel.swift */, + ); + path = Restore; + sourceTree = ""; + }; ABC9A1BEFAB2EACDD6ACD361 /* SwapNew */ = { isa = PBXGroup; children = ( @@ -6975,17 +6996,6 @@ path = MarketCards; sourceTree = ""; }; - ABC9A211A4BD2F9A50E15A4C /* RestoreCloudPassphrase */ = { - isa = PBXGroup; - children = ( - ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */, - ABC9A6F1FB00B33D1896FC6B /* RestoreCloudPassphraseService.swift */, - ABC9AF12879C62002DFE946A /* RestoreCloudPassphraseViewController.swift */, - ABC9A9E0190FAD212E2E007F /* RestoreCloudPassphraseModule.swift */, - ); - path = RestoreCloudPassphrase; - sourceTree = ""; - }; ABC9A25DA779297B95A9BFD0 /* PendingRequests */ = { isa = PBXGroup; children = ( @@ -7049,21 +7059,26 @@ path = BottomSheet; sourceTree = ""; }; - ABC9A39D14C7F11B56BFC6A3 /* DataSources */ = { + ABC9A386552F4E2372850DBB /* Backup */ = { isa = PBXGroup; children = ( - ABC9A22311B6AA64B7D93CB4 /* DataSourceChain.swift */, - ABC9AFF00631C853B04007AC /* WalletTokenBalance */, + ABC9A59832FC6377E11AAC40 /* BackupName */, + ABC9AA91FD5A8903214AB375 /* BackupType */, + ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */, + ABC9A989B1CF1BE4696D4E88 /* BackupPassword */, + ABC9A5E0447C6FEFEC7B3AF2 /* BackupList */, + ABC9AB9FDA3552B56C218EB7 /* BackupDisclaimer */, ); - path = DataSources; + path = Backup; sourceTree = ""; }; - ABC9A3BF4876DB959C5E4DE2 /* BackupDisclaimer */ = { + ABC9A39D14C7F11B56BFC6A3 /* DataSources */ = { isa = PBXGroup; children = ( - ABC9ABF9E55990704693487F /* BackupDisclaimerView.swift */, + ABC9A22311B6AA64B7D93CB4 /* DataSourceChain.swift */, + ABC9AFF00631C853B04007AC /* WalletTokenBalance */, ); - path = BackupDisclaimer; + path = DataSources; sourceTree = ""; }; ABC9A3CED3BD03C1DBF797E2 /* Passphrase */ = { @@ -7129,10 +7144,18 @@ path = Send; sourceTree = ""; }; - ABC9A5A9AA93B6487D21EFF9 /* BackupList */ = { + ABC9A59832FC6377E11AAC40 /* BackupName */ = { isa = PBXGroup; children = ( - ABC9AA0021E969A73DA7E177 /* BackupListView.swift */, + ABC9A7AC6BC7EA8166F21D9A /* BackupNameView.swift */, + ); + path = BackupName; + sourceTree = ""; + }; + ABC9A5E0447C6FEFEC7B3AF2 /* BackupList */ = { + isa = PBXGroup; + children = ( + ABC9A4D1C7AE5723851A53EB /* BackupListView.swift */, ); path = BackupList; sourceTree = ""; @@ -7222,6 +7245,15 @@ path = WalletConnectAppShowWorker; sourceTree = ""; }; + ABC9A6D0013823EF4EECB442 /* RestoreFile */ = { + isa = PBXGroup; + children = ( + ABC9A781F6F6806A9DCE4C9E /* RestorePassphrase */, + ABC9A6EF8CB7A0B2D477F2C5 /* BakcupFileConfiguration */, + ); + path = RestoreFile; + sourceTree = ""; + }; ABC9A6E3A891AD336A1A5326 /* Zcash */ = { isa = PBXGroup; children = ( @@ -7232,6 +7264,16 @@ path = Zcash; sourceTree = ""; }; + ABC9A6EF8CB7A0B2D477F2C5 /* BakcupFileConfiguration */ = { + isa = PBXGroup; + children = ( + ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */, + ABC9AD9A0F91C825D149B289 /* RestoreFileConfigurationService.swift */, + ABC9AF6C15800AF8C37C3516 /* RestoreFileConfigurationViewModel.swift */, + ); + path = BakcupFileConfiguration; + sourceTree = ""; + }; ABC9A71A64E11AD3709A1174 /* Workers */ = { isa = PBXGroup; children = ( @@ -7250,12 +7292,15 @@ path = Components; sourceTree = ""; }; - ABC9A77088096A37E5C42191 /* BackupType */ = { + ABC9A781F6F6806A9DCE4C9E /* RestorePassphrase */ = { isa = PBXGroup; children = ( - ABC9AC9C3F666BC8D15C7F53 /* BackupTypeView.swift */, + ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */, + ABC9AE5CAD06644F52170C72 /* RestorePassphraseService.swift */, + ABC9A0547CBE2B5A3E38891E /* RestorePassphraseViewController.swift */, + ABC9A39A33712A1429D623D5 /* RestorePassphraseModule.swift */, ); - path = BackupType; + path = RestorePassphrase; sourceTree = ""; }; ABC9A7E9EAE24647C0700B39 /* RestoreCloud */ = { @@ -7265,11 +7310,18 @@ ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */, ABC9A06866150862CEDEB5DE /* RestoreCloudService.swift */, ABC9AA751C8B09F90F716231 /* RestoreCloudViewController.swift */, - ABC9A211A4BD2F9A50E15A4C /* RestoreCloudPassphrase */, ); path = RestoreCloud; sourceTree = ""; }; + ABC9A989B1CF1BE4696D4E88 /* BackupPassword */ = { + isa = PBXGroup; + children = ( + ABC9A202ED9B98DFEA8E6154 /* BackupPasswordView.swift */, + ); + path = BackupPassword; + sourceTree = ""; + }; ABC9A99F726DF775B1321923 /* V2 */ = { isa = PBXGroup; children = ( @@ -7285,13 +7337,9 @@ isa = PBXGroup; children = ( ABC9AD5DC41717F92AC2151C /* BackupManager */, - ABC9A77088096A37E5C42191 /* BackupType */, - ABC9A5A9AA93B6487D21EFF9 /* BackupList */, - ABC9A3BF4876DB959C5E4DE2 /* BackupDisclaimer */, - ABC9AADD1C32448E8AA5D25F /* BackupName */, - ABC9A7B2F7354F12F78A2F30 /* BackupAppViewModel.swift */, ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */, - ABC9ADC8D7C3B51B96589BC0 /* BackupPassword */, + ABC9A386552F4E2372850DBB /* Backup */, + ABC9A1A8B6E734D38D55E1D2 /* Restore */, ); path = BackupApp; sourceTree = ""; @@ -7328,6 +7376,14 @@ path = UniswapV3; sourceTree = ""; }; + ABC9AA91FD5A8903214AB375 /* BackupType */ = { + isa = PBXGroup; + children = ( + ABC9A5CDF9153AECED3DE50C /* BackupTypeView.swift */, + ); + path = BackupType; + sourceTree = ""; + }; ABC9AA9FCC7E722C3EEA97BE /* Bitcoin */ = { isa = PBXGroup; children = ( @@ -7340,14 +7396,6 @@ path = Bitcoin; sourceTree = ""; }; - ABC9AADD1C32448E8AA5D25F /* BackupName */ = { - isa = PBXGroup; - children = ( - ABC9A86FE7E7F2DA93A5CB00 /* BackupNameView.swift */, - ); - path = BackupName; - sourceTree = ""; - }; ABC9AB313E49F27BBB0C9AED /* DataSources */ = { isa = PBXGroup; children = ( @@ -7369,6 +7417,14 @@ path = MemoInput; sourceTree = ""; }; + ABC9AB9FDA3552B56C218EB7 /* BackupDisclaimer */ = { + isa = PBXGroup; + children = ( + ABC9AFFD435E0C9FBE0E5E7C /* BackupDisclaimerView.swift */, + ); + path = BackupDisclaimer; + sourceTree = ""; + }; ABC9AC691EEA7276F0A21357 /* Views */ = { isa = PBXGroup; children = ( @@ -7433,14 +7489,6 @@ path = Binance; sourceTree = ""; }; - ABC9ADC8D7C3B51B96589BC0 /* BackupPassword */ = { - isa = PBXGroup; - children = ( - ABC9AEC2C4752498AF7A7A2E /* BackupPasswordView.swift */, - ); - path = BackupPassword; - sourceTree = ""; - }; ABC9ADCD89D02064F90038F5 /* ContactBookContact */ = { isa = PBXGroup; children = ( @@ -9193,10 +9241,6 @@ ABC9A8A74C527C4E01EBB8A5 /* RestoreCloudModule.swift in Sources */, ABC9A712F6389F5C2B0D63E3 /* RestoreCloudService.swift in Sources */, ABC9A07518E5769122DFEAC2 /* RestoreCloudViewController.swift in Sources */, - ABC9ABC085B0733DD4EF1FCD /* RestoreCloudPassphraseViewModel.swift in Sources */, - ABC9AEF4FDD9B4C16E87DBDA /* RestoreCloudPassphraseService.swift in Sources */, - ABC9A806CB34CB9A5E27A0A3 /* RestoreCloudPassphraseViewController.swift in Sources */, - ABC9A0034DFBD65A7A8C4D65 /* RestoreCloudPassphraseModule.swift in Sources */, 11B352006084CC499F31CD70 /* WalletService.swift in Sources */, 11B351B0C0F37424A3840737 /* ContactBookManager.swift in Sources */, 11B35750FEA183828D4ABADE /* CexAsset.swift in Sources */, @@ -9390,23 +9434,31 @@ 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */, ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */, ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */, - ABC9A5125874E09194BA8532 /* BackupTypeView.swift in Sources */, ABC9AF95141EA649524FBF88 /* CheckboxStyle.swift in Sources */, - ABC9A6C05017D9267567AB73 /* BackupDisclaimerView.swift in Sources */, - ABC9A7244B2D23BDA40C407C /* BackupNameView.swift in Sources */, - ABC9A20B7F90F5E741A74823 /* BackupAppViewModel.swift in Sources */, ABC9A79CFCEBAC442A1B791D /* BackupAppModule.swift in Sources */, - ABC9ACE31EC94ABC5B325693 /* BackupListView.swift in Sources */, ABC9A09E0B614E5B4E32B7F9 /* InputTextView.swift in Sources */, ABC9ABE3F52BF2307533D8FB /* InputTextRow.swift in Sources */, ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */, - ABC9A06CA0D364D4E1261C87 /* BackupPasswordView.swift in Sources */, ABC9A4A21CFBA188A7EEC930 /* ActivityViewController.swift in Sources */, 11B357740CC018527301C4AE /* AppStatusView.swift in Sources */, 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */, 11B35CAE0540A2549BD4A960 /* ActivityView.swift in Sources */, 11B356562D2B4F5BCAB4FC80 /* AboutView.swift in Sources */, 11B35E98AE2272A7E37C41C5 /* AboutViewModel.swift in Sources */, + ABC9A346AC191059BAFAB977 /* BackupNameView.swift in Sources */, + ABC9A2D3D28955B8AD82AFC3 /* BackupTypeView.swift in Sources */, + ABC9AD1F6A6A7C97E4120F2F /* BackupAppViewModel.swift in Sources */, + ABC9A7EF7780159AC6B946FC /* BackupPasswordView.swift in Sources */, + ABC9AEF231332C7B8756E8A9 /* BackupListView.swift in Sources */, + ABC9A8AC5E635D9CB1704568 /* BackupDisclaimerView.swift in Sources */, + ABC9A437473D0E77F9DBEB42 /* RestoreAppViewModel.swift in Sources */, + ABC9AE7DA8EFD812710C7BE4 /* RestorePassphraseViewModel.swift in Sources */, + ABC9A93E05AAF5D98C1DF4D6 /* RestorePassphraseService.swift in Sources */, + 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10514,10 +10566,6 @@ ABC9A4929EFBFAD0B595A4E8 /* RestoreCloudModule.swift in Sources */, ABC9AFA5222E61F7999E2A88 /* RestoreCloudService.swift in Sources */, ABC9A324BB7E7FF8758A92C3 /* RestoreCloudViewController.swift in Sources */, - ABC9A0AADAE0A5C370946B8D /* RestoreCloudPassphraseViewModel.swift in Sources */, - ABC9A446EF71E1DB4FA7D353 /* RestoreCloudPassphraseService.swift in Sources */, - ABC9A6C65416E7F4F3830962 /* RestoreCloudPassphraseViewController.swift in Sources */, - ABC9AC1B69C1E03F4035A8FB /* RestoreCloudPassphraseModule.swift in Sources */, 11B35E276D1C91193B687718 /* WalletService.swift in Sources */, 11B359E7632FC042278ED912 /* ContactBookManager.swift in Sources */, 11B352407989CB29F849C0BA /* CexAsset.swift in Sources */, @@ -10712,23 +10760,31 @@ 11B3511DAD3881FDE2419A64 /* AutoLockPeriod.swift in Sources */, ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */, ABC9A77A41517173B610E0FC /* BackupManagerModule.swift in Sources */, - ABC9AB65B10FDB5F30E2731D /* BackupTypeView.swift in Sources */, ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */, - ABC9A4EE80A57455DB2CCD4F /* BackupDisclaimerView.swift in Sources */, - ABC9A94A82E55F437BD3FE68 /* BackupNameView.swift in Sources */, - ABC9A13B7F43722709570161 /* BackupAppViewModel.swift in Sources */, ABC9A99A45187C36D48840F8 /* BackupAppModule.swift in Sources */, - ABC9AA05527830EFFCFB98E3 /* BackupListView.swift in Sources */, ABC9AE51262C09EABF5CCEEE /* InputTextView.swift in Sources */, ABC9A542CA987F09C93F04A9 /* InputTextRow.swift in Sources */, ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */, - ABC9A54E6E15E96271525339 /* BackupPasswordView.swift in Sources */, ABC9A12A4D114A2E4F4C711A /* ActivityViewController.swift in Sources */, 11B355901DFF6BAE9130D60E /* AppStatusView.swift in Sources */, 11B354865DA8CA6A1442D577 /* AppStatusViewModel.swift in Sources */, 11B35A431DE03F33E739B639 /* ActivityView.swift in Sources */, 11B3575F30FFFDFB4F0AF174 /* AboutView.swift in Sources */, 11B35D55957E21D3388880CF /* AboutViewModel.swift in Sources */, + ABC9AB215D081976FC2E294F /* BackupNameView.swift in Sources */, + ABC9A66D7B34C6547C2469E9 /* BackupTypeView.swift in Sources */, + ABC9AC763748CC31D45FB6BD /* BackupAppViewModel.swift in Sources */, + ABC9A04FAB83D7A8D251DA90 /* BackupPasswordView.swift in Sources */, + ABC9A0CE0155F89F12350DFC /* BackupListView.swift in Sources */, + ABC9A4465982823773CE1B50 /* BackupDisclaimerView.swift in Sources */, + ABC9AA18996E714C955E7E13 /* RestoreAppViewModel.swift in Sources */, + ABC9AF04946C86FA6DBD4225 /* RestorePassphraseViewModel.swift in Sources */, + ABC9A6EFD77E59AA6B4C5070 /* RestorePassphraseService.swift in Sources */, + 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift index d9ccac8749..4d9c105b0e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift @@ -208,27 +208,31 @@ extension CloudBackupManager { try delete(uniqueId: hex) } - func delete(uniqueId: String) throws { + func delete(name: String) throws { guard let iCloudUrl else { throw BackupError.urlNotAvailable } - guard let item = oneWalletItems.first(where: { _, backup in backup.id == uniqueId }) else { - throw BackupError.itemNotFound - } - - let fileUrl = iCloudUrl.appendingPathComponent(item.key) + let fileUrl = iCloudUrl.appendingPathComponent(name) do { try fileStorage.deleteFile(url: fileUrl) // system will automatically updates items but after 1-2 seconds. So we need force update - oneWalletItems[item.key] = nil - logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) successful") + oneWalletItems[name] = nil + logger?.log(level: .debug, message: "CloudAccountManager.delete \(name) successful") } catch { - logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) unsuccessful because: \(error)") + logger?.log(level: .debug, message: "CloudAccountManager.delete \(name) unsuccessful because: \(error)") throw error } } + + func delete(uniqueId: String) throws { + guard let item = oneWalletItems.first(where: { _, backup in backup.id == uniqueId }) else { + throw BackupError.itemNotFound + } + + try delete(name: item.key) + } } extension CloudBackupManager { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift index 8002df04ba..ef32071e96 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift @@ -1,14 +1,14 @@ -import Foundation import CloudKit import Combine -import RxSwift -import RxRelay -import ObjectMapper +import Foundation import HsToolKit import MarketKit +import ObjectMapper +import RxRelay +import RxSwift class ContactBookManager { - static private let batchingInterval: TimeInterval = 1 + private static let batchingInterval: TimeInterval = 1 static let filename = "Contacts.json" static let localUrl = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) @@ -39,7 +39,7 @@ class ContactBookManager { private var metadataMonitor: MetadataMonitor? private let iCloudErrorRelay = BehaviorRelay(value: nil) - private(set) var iCloudError: Error? = nil { + private(set) var iCloudError: Error? { didSet { iCloudErrorRelay.accept(iCloudError) } @@ -57,9 +57,9 @@ class ContactBookManager { let localUrl: URL? var iCloudUrl: URL? { FileManager - .default - .url(forUbiquityContainerIdentifier: ubiquityContainerIdentifier)? - .appendingPathComponent("Documents") + .default + .url(forUbiquityContainerIdentifier: ubiquityContainerIdentifier)? + .appendingPathComponent("Documents") } private var needsToSyncRemote = false { @@ -83,7 +83,7 @@ class ContactBookManager { syncLocalFile() } -// ================================ LOCAL ==================================================== // + // ================================ LOCAL ==================================================== // func syncLocalFile() { state = .loading guard let localUrl else { @@ -94,7 +94,7 @@ class ContactBookManager { logger?.debug("=C-MANAGER> SYNC") do { let data = try fileStorage - .read(directoryUrl: localUrl, filename: Self.filename) + .read(directoryUrl: localUrl, filename: Self.filename) logger?.debug("=C-MANAGER> FOUND LOCAL: \(data.count)") sync(localData: data) } catch { @@ -137,8 +137,8 @@ class ContactBookManager { let localError = localError as NSError // code = 260 "No such file or directory" if localError.domain == NSCocoaErrorDomain, - localError.code == 260 { - + localError.code == 260 + { sync(localData: Data()) return } @@ -147,7 +147,7 @@ class ContactBookManager { state = .failed(localError) } -// ================================== REMOTE =================================================== // + // ================================== REMOTE =================================================== // private func syncCloudFile(localBook: ContactBook) { if let iCloudUrl { logger?.debug("=C-MANAGER> Try read remote book") @@ -209,7 +209,7 @@ class ContactBookManager { logger?.debug("=C-MANAGER> Remote book is up to date. Save to local") try save(url: localUrl, remoteContactBook) state = .completed(remoteContactBook) - case .merged(let book): + case let .merged(book): logger?.debug("=C-MANAGER> Merged. Save to both") try save(url: localUrl, book) state = .completed(book) @@ -230,8 +230,8 @@ class ContactBookManager { let iCloudError = iCloudError as NSError if iCloudError.domain == NSCocoaErrorDomain, - iCloudError.code == 260 { // code = 260 "No such file or directory" - + iCloudError.code == 260 + { // code = 260 "No such file or directory" logger?.debug("=C-MANAGER> no file in icloud. Try to save local to icloud") // we need to try save local contacts to iCloud file if !localBook.contacts.isEmpty { @@ -268,17 +268,16 @@ class ContactBookManager { iCloudError = nil if localStorage.remoteContactsSync { - // create monitor and handle its events let metadataMonitor = MetadataMonitor(url: iCloudUrl, filenames: [Self.filename], batchingInterval: Self.batchingInterval, logger: logger) self.metadataMonitor = metadataMonitor logger?.debug("=C-MANAGER> Turn ON monitor") metadataMonitor.needUpdatePublisher - .sink { [weak self] in - self?.logger?.debug("=C-MANAGER> Monitor Want to Sync iCloudStorage") - self?.syncRemoteStorage() - } - .store(in: &monitorCancellables) + .sink { [weak self] in + self?.logger?.debug("=C-MANAGER> Monitor Want to Sync iCloudStorage") + self?.syncRemoteStorage() + } + .store(in: &monitorCancellables) syncRemoteStorage() // sometimes monitor not ask to check icloud file, but we need to check it for first time } else { @@ -296,9 +295,9 @@ class ContactBookManager { } // try to create json with parsed data - guard let json = try JSONSerialization.jsonObject(with: data) as? [String : Any] else { + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { logger?.debug("=C-MANAGER> CANT PARSE") - throw StorageError.cantParseData + throw StorageError.cantParseData } let book = try Mapper().map(JSON: json) @@ -328,11 +327,9 @@ class ContactBookManager { state = .failed(error) } } - } extension ContactBookManager { - var stateObservable: Observable> { stateRelay.asObservable() } @@ -356,7 +353,7 @@ extension ContactBookManager { func name(blockchainType: BlockchainType, address: String) -> String? { if let contact = all?.first(where: { contact in !contact.addresses - .filter({ $0.blockchainUid == blockchainType.uid && $0.address.lowercased() == address.lowercased() }).isEmpty + .filter { $0.blockchainUid == blockchainType.uid && $0.address.lowercased() == address.lowercased() }.isEmpty }) { return contact.name } @@ -380,7 +377,6 @@ extension ContactBookManager { if remoteSync { try saveToICloud(book: newContactBook) } - } func delete(_ contactUid: String) throws { @@ -406,23 +402,15 @@ extension ContactBookManager { func backupContacts(from url: URL) throws -> [BackupContact] { let data = try FileManager.default.contentsOfFile(coordinatingAccessAt: url) - guard let json = try? JSONSerialization.jsonObject(with: data) as? [[String : Any]], - let contacts = try? json.map({ try Mapper().map(JSON: $0) }) else { - + guard let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]], + let contacts = try? json.map({ try Mapper().map(JSON: $0) }) + else { throw StorageError.cantParseData } return contacts } - func restore(crypto: BackupCrypto, passphrase: String) throws { - let data = try crypto.data(passphrase: passphrase) - let decoder = JSONDecoder() - let contacts = try decoder.decode([BackupContact].self, from: data) - - try restore(contacts: contacts) - } - func restore(contacts: [BackupContact]) throws { guard let localUrl else { state = .failed(ContactBookManager.StorageError.localUrlNotAvailable) @@ -440,16 +428,23 @@ extension ContactBookManager { var backupContactBook: BackupContactBook? { state.data.map { helper.backupContactBook(contactBook: $0) } } - } extension ContactBookManager { + static func encode(crypto: BackupCrypto, passphrase: String) throws -> [BackupContact] { + let data = try crypto.data(passphrase: passphrase) + let decoder = JSONDecoder() + let contacts = try decoder.decode([BackupContact].self, from: data) + + return contacts + } +} +extension ContactBookManager { enum StorageError: Error { case cloudUrlNotAvailable case localUrlNotAvailable case notReady case cantParseData } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index 8ea57d439f..4151add3f7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -295,6 +295,13 @@ extension AccountType { case .cex: self = .cex } } + + var isWatch: Bool { + switch self { + case .evmAddress, .tronAddress: return true + default: return false + } + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift index aa65f89a97..ec13646662 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/BackupModule.swift @@ -23,3 +23,34 @@ struct BackupModule { } } + +extension BackupModule { + enum Source { + case wallet(WalletBackup) + case full(FullBackup) + + enum Abstract { + case wallet + case full + } + + var id: String { + switch self { + case let .wallet(backup): return backup.id + case let .full(backup): return backup.id + } + } + + var timestamp: TimeInterval? { + switch self { + case let .wallet(backup): return backup.timestamp + case let .full(backup): return backup.timestamp + } + } + } + + struct NamedSource { + let name: String + let source: Source + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index 8a1d122552..fbf37d1d14 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -243,7 +243,8 @@ extension AppBackupProvider { favoritesManager.add(coinUids: backup.watchlistIds) if let contacts = backup.contacts { - try contactManager.restore(crypto: contacts, passphrase: passphrase) + let contacts = try ContactBookManager.encode(crypto: contacts, passphrase: passphrase) + try contactManager.restore(contacts: contacts) } if let settings = backup.settings { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift index 8128fc6aa1..72d7af6c9e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift @@ -112,7 +112,7 @@ class ManageAccountsViewController: ThemeViewController { } private func onTapRestore() { - let viewController = RestoreTypeModule.viewController(sourceViewController: self, returnViewController: createAccountListener) + let viewController = RestoreTypeModule.viewController(type: .wallet, sourceViewController: self, returnViewController: createAccountListener) present(viewController, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift index 75217aeaa0..7a8d42d452 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudModule.swift @@ -23,6 +23,11 @@ struct RestoreCloudModule { } } + struct DecryptedRestoredBackup { + let name: String + let walletBackup: WalletBackup + } + struct RestoredAccount { let name: String let accountType: AccountType diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift deleted file mode 100644 index 9b89c77638..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseModule.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import UIKit - -class RestoreCloudPassphraseModule { - - static func restorePassword(item: RestoreCloudModule.RestoredBackup, returnViewController: UIViewController?) -> UIViewController { - let service = RestoreCloudPassphraseService( - iCloudManager: App.shared.cloudBackupManager, - appBackupProvider: App.shared.appBackupProvider, - accountFactory: App.shared.accountFactory, - accountManager: App.shared.accountManager, - walletManager: App.shared.walletManager, - restoreSettingsManager: App.shared.restoreSettingsManager, - item: item - ) - let viewModel = RestoreCloudPassphraseViewModel(service: service) - let controller = RestoreCloudPassphraseViewController(viewModel: viewModel, returnViewController: returnViewController) - - return controller - } - - -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift deleted file mode 100644 index 426f82e0d1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseService.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation -import MarketKit - -class RestoreCloudPassphraseService { - private let iCloudManager: CloudBackupManager - private let appBackupProvider: AppBackupProvider - private let accountFactory: AccountFactory - private let accountManager: AccountManager - private let walletManager: WalletManager - private let restoreSettingsManager: RestoreSettingsManager - - private let restoredBackup: RestoreCloudModule.RestoredBackup - - var passphrase: String = "" - - init(iCloudManager: CloudBackupManager, appBackupProvider: AppBackupProvider, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, restoreSettingsManager: RestoreSettingsManager, item: RestoreCloudModule.RestoredBackup) { - self.iCloudManager = iCloudManager - self.appBackupProvider = appBackupProvider - self.accountFactory = accountFactory - self.accountManager = accountManager - self.walletManager = walletManager - self.restoreSettingsManager = restoreSettingsManager - restoredBackup = item - } - - private func createAccount(accountType: AccountType) { - let account = accountFactory.account( - type: accountType, - origin: .restored, - backedUp: restoredBackup.walletBackup.isManualBackedUp, - fileBackedUp: restoredBackup.walletBackup.isFileBackedUp, - name: restoredBackup.name - ) - accountManager.save(account: account) - - let wallets = restoredBackup.walletBackup.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: account, blockchainType: tokenQuery.blockchainType) - } - } - return EnabledWallet( - tokenQueryId: $0.tokenQueryId, - accountId: account.id, - coinName: $0.coinName, - coinCode: $0.coinCode, - tokenDecimals: $0.tokenDecimals - ) - } - walletManager.save(enabledWallets: wallets) - } -} - -extension RestoreCloudPassphraseService { - func validate(text: String?) -> Bool { - PassphraseValidator.validate(text: text) - } - - func importWallet() async throws -> RestoreResult { - let accountType = try restoredBackup.walletBackup.crypto.accountType(type: restoredBackup.walletBackup.type, passphrase: passphrase) - appBackupProvider.walletRestore(backup: restoredBackup, accountType: accountType) - switch accountType { - case .cex: - return .success - default: - return .restoredAccount(RestoreCloudModule.RestoredAccount( - name: restoredBackup.name, - accountType: accountType, - isManualBackedUp: restoredBackup.walletBackup.isManualBackedUp, - isFileBackedUp: restoredBackup.walletBackup.isFileBackedUp, - showSelectCoins: restoredBackup.walletBackup.enabledWallets.isEmpty - )) - } - } -} - -extension RestoreCloudPassphraseService { - enum RestoreResult { - case restoredAccount(RestoreCloudModule.RestoredAccount) - case success - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift index 1c85262138..342e48807d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift @@ -1,5 +1,5 @@ -import Foundation import Combine +import Foundation class RestoreCloudService { private let cloudAccountBackupManager: CloudBackupManager @@ -8,19 +8,27 @@ class RestoreCloudService { private var cancellables = Set() private let deleteItemCompletedSubject = PassthroughSubject() - @Published var items = [Item]() + @Published var oneWalletItems = [Item]() + @Published var fullBackupItems = [Item]() init(cloudAccountBackupManager: CloudBackupManager, accountManager: AccountManager) { self.cloudAccountBackupManager = cloudAccountBackupManager self.accountManager = accountManager cloudAccountBackupManager.$oneWalletItems - .sink { [weak self] in - self?.sync(backups: $0) - } - .store(in: &cancellables) + .sink { [weak self] in + self?.sync(backups: $0) + } + .store(in: &cancellables) + + cloudAccountBackupManager.$fullBackupItems + .sink { [weak self] in + self?.sync(fullBackupItems: $0) + } + .store(in: &cancellables) sync(backups: cloudAccountBackupManager.oneWalletItems) + sync(fullBackupItems: cloudAccountBackupManager.fullBackupItems) } private func sync(backups: [String: WalletBackup]) { @@ -28,28 +36,43 @@ class RestoreCloudService { let items = backups.map { backup in Item( - name: withoutExtension(backup.key), - backup: backup.value, - imported: accountUniqueIds.contains(backup.value.id) + name: withoutExtension(backup.key), + source: .wallet(backup.value), + imported: accountUniqueIds.contains(backup.value.id) ) } - self.items = items.sorted { (item1: Item, item2: Item) in - if item1.backup.timestamp == nil && item2.backup.timestamp == nil { + oneWalletItems = items.sorted { (item1: Item, item2: Item) in + if item1.source.timestamp == nil, item2.source.timestamp == nil { return item1.name > item2.name } - return (item1.backup.timestamp ?? 0) > (item2.backup.timestamp ?? 0) + return (item1.source.timestamp ?? 0) > (item2.source.timestamp ?? 0) + } + } + + private func sync(fullBackupItems: [String: FullBackup]) { + let items = fullBackupItems.map { backup in + Item( + name: withoutExtension(backup.key), + source: .full(backup.value), + imported: false + ) + } + + self.fullBackupItems = items.sorted { (item1: Item, item2: Item) in + if item1.source.timestamp == nil, item2.source == nil { + return item1.name > item2.name + } + return (item1.source.timestamp ?? 0) > (item2.source.timestamp ?? 0) } } private func withoutExtension(_ name: String) -> String { (name as NSString).deletingPathExtension } - } extension RestoreCloudService { - func remove(id: String) { do { try cloudAccountBackupManager.delete(uniqueId: id) @@ -62,15 +85,12 @@ extension RestoreCloudService { var deleteItemCompletedPublisher: AnyPublisher { deleteItemCompletedSubject.eraseToAnyPublisher() } - } extension RestoreCloudService { - struct Item { let name: String - let backup: WalletBackup + let source: BackupModule.Source let imported: Bool } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift index 907d70cf59..e119ec7eb6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift @@ -14,13 +14,15 @@ class RestoreCloudViewController: ThemeViewController { private let emptyView = PlaceholderView() private let tableView = SectionsTableView(style: .grouped) - private var viewItem: RestoreCloudViewModel.ViewItem + private var walletViewItem: RestoreCloudViewModel.ViewItem + private var fullBackupViewItem: RestoreCloudViewModel.ViewItem init(viewModel: RestoreCloudViewModel, returnViewController: UIViewController?) { self.viewModel = viewModel self.returnViewController = returnViewController - viewItem = viewModel.viewItem + walletViewItem = viewModel.walletViewItem + fullBackupViewItem = viewModel.fullBackupViewItem super.init() } @@ -54,10 +56,17 @@ class RestoreCloudViewController: ThemeViewController { emptyView.image = UIImage(named: "no_internet_48") emptyView.text = "restore.cloud.empty".localized - viewModel.$viewItem + viewModel.$walletViewItem .receive(on: DispatchQueue.main) .sink { [weak self] viewItem in - self?.sync(viewItem: viewItem) + self?.sync(type: .wallet, viewItem: viewItem) + } + .store(in: &cancellables) + + viewModel.$fullBackupViewItem + .receive(on: DispatchQueue.main) + .sink { [weak self] viewItem in + self?.sync(type: .full, viewItem: viewItem) } .store(in: &cancellables) @@ -85,8 +94,8 @@ class RestoreCloudViewController: ThemeViewController { (returnViewController ?? self)?.dismiss(animated: true) } - private func restore(item: RestoreCloudModule.RestoredBackup) { - let viewController = RestoreCloudPassphraseModule.restorePassword(item: item, returnViewController: returnViewController) + private func restore(item: BackupModule.NamedSource) { + let viewController = RestorePassphraseModule.viewController(item: item, returnViewController: returnViewController) navigationController?.pushViewController(viewController, animated: true) } @@ -99,8 +108,13 @@ class RestoreCloudViewController: ThemeViewController { } } - private func sync(viewItem: RestoreCloudViewModel.ViewItem) { - emptyView.isHidden = !viewItem.isEmpty + private func sync(type: RestoreCloudViewModel.BackupType, viewItem: RestoreCloudViewModel.ViewItem) { + switch type { + case .wallet: walletViewItem = viewItem + case .full: fullBackupViewItem = viewItem + } + + emptyView.isHidden = !walletViewItem.isEmpty || !fullBackupViewItem.isEmpty tableView.reload() } @@ -160,22 +174,28 @@ class RestoreCloudViewController: ThemeViewController { extension RestoreCloudViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { - guard !viewItem.isEmpty else { + guard !walletViewItem.isEmpty else { return [] } var sections = [ descriptionSection, ] - if !viewItem.notImported.isEmpty { + if !walletViewItem.notImported.isEmpty { + sections.append( + section(id: "not_imported", headerTitle: "restore.cloud.wallets".localized, viewItems: viewModel.walletViewItem.notImported) + ) + } + + if !walletViewItem.imported.isEmpty { sections.append( - section(id: "not_imported", viewItems: viewModel.viewItem.notImported) + section(id: "imported", headerTitle: "restore.cloud.imported".localized, viewItems: viewModel.walletViewItem.imported) ) } - if !viewItem.imported.isEmpty { + if !fullBackupViewItem.notImported.isEmpty { sections.append( - section(id: "imported", headerTitle: "restore.cloud.imported".localized, viewItems: viewModel.viewItem.imported) + section(id: "app_backups", headerTitle: "restore.cloud.app_backups".localized, viewItems: viewModel.fullBackupViewItem.notImported) ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewModel.swift index 026487f953..252a418c9f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewModel.swift @@ -5,20 +5,26 @@ class RestoreCloudViewModel { private let service: RestoreCloudService private var cancellables = Set() - @Published private(set) var viewItem: ViewItem = .empty - private let restoreSubject = PassthroughSubject() + @Published private(set) var walletViewItem: ViewItem = .empty + @Published private(set) var fullBackupViewItem: ViewItem = .empty + private let restoreSubject = PassthroughSubject() init(service: RestoreCloudService) { self.service = service - service.$items - .sink { [weak self] in self?.sync(items: $0) } + service.$oneWalletItems + .sink { [weak self] in self?.sync(type: .wallet, items: $0) } .store(in: &cancellables) - sync(items: service.items) + service.$fullBackupItems + .sink { [weak self] in self?.sync(type: .full, items: $0) } + .store(in: &cancellables) + + sync(type: .wallet, items: service.oneWalletItems) + sync(type: .full, items: service.fullBackupItems) } - private func sync(items: [RestoreCloudService.Item]) { + private func sync(type: BackupModule.Source.Abstract, items: [RestoreCloudService.Item]) { var imported = [BackupViewItem]() var notImported = [BackupViewItem]() @@ -31,19 +37,22 @@ class RestoreCloudViewModel { } } - viewItem = ViewItem(notImported: notImported, imported: imported) + switch type { + case .wallet: walletViewItem = ViewItem(notImported: notImported, imported: imported) + case .full: fullBackupViewItem = ViewItem(notImported: notImported, imported: imported) + } } private func viewItem(item: RestoreCloudService.Item) -> BackupViewItem { - let description = item.backup.timestamp.map { DateHelper.instance.formatFullTime(from: Date(timeIntervalSince1970: $0)) } ?? "----" - return BackupViewItem(uniqueId: item.backup.id, name: item.name, description: description) + let description = item.source.timestamp.map { DateHelper.instance.formatFullTime(from: Date(timeIntervalSince1970: $0)) } ?? "----" + return BackupViewItem(uniqueId: item.source.id, name: item.name, description: description) } } extension RestoreCloudViewModel { - var restorePublisher: AnyPublisher { + var restorePublisher: AnyPublisher { restoreSubject.eraseToAnyPublisher() } @@ -56,16 +65,22 @@ extension RestoreCloudViewModel { } func didTap(id: String) { - guard let item = service.items.first(where: { item in item.backup.id == id }) else { - return + if let item = service.oneWalletItems.first(where: { item in item.source.id == id }) { + restoreSubject.send(BackupModule.NamedSource(name: item.name, source: item.source)) } - restoreSubject.send(RestoreCloudModule.RestoredBackup(name: item.name, walletBackup: item.backup)) + if let item = service.fullBackupItems.first(where: { item in item.source.id == id}) { + restoreSubject.send(BackupModule.NamedSource(name: item.name, source: item.source)) + } } } extension RestoreCloudViewModel { + enum BackupType { + case wallet + case full + } struct BackupViewItem { let uniqueId: String diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift new file mode 100644 index 0000000000..f965264f49 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationModule.swift @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000000..af02e1fa27 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift @@ -0,0 +1,48 @@ +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.encode(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 ?? 0 + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift new file mode 100644 index 0000000000..cfa9b242a5 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationViewModel.swift @@ -0,0 +1,4 @@ +import Foundation + +class BackupFileConfigurationViewModel { +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseModule.swift new file mode 100644 index 0000000000..37d4c5f554 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseModule.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit + +class RestorePassphraseModule { + + static func viewController(item: BackupModule.NamedSource, returnViewController: UIViewController?) -> UIViewController { + let service = RestorePassphraseService( + iCloudManager: App.shared.cloudBackupManager, + appBackupProvider: App.shared.appBackupProvider, + accountFactory: App.shared.accountFactory, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager, + restoreSettingsManager: App.shared.restoreSettingsManager, + restoredBackup: item + ) + let viewModel = RestorePassphraseViewModel(service: service) + let controller = RestorePassphraseViewController(viewModel: viewModel, returnViewController: returnViewController) + + return controller + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift new file mode 100644 index 0000000000..c8ad21d080 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift @@ -0,0 +1,64 @@ +import Foundation +import MarketKit + +class RestorePassphraseService { + private let iCloudManager: CloudBackupManager + private let appBackupProvider: AppBackupProvider + private let accountFactory: AccountFactory + private let accountManager: AccountManager + private let walletManager: WalletManager + private let restoreSettingsManager: RestoreSettingsManager + + let restoredBackup: BackupModule.NamedSource + var passphrase: String = "" + + init(iCloudManager: CloudBackupManager, appBackupProvider: AppBackupProvider, accountFactory: AccountFactory, accountManager: AccountManager, walletManager: WalletManager, restoreSettingsManager: RestoreSettingsManager, restoredBackup: BackupModule.NamedSource) { + self.iCloudManager = iCloudManager + self.appBackupProvider = appBackupProvider + self.accountFactory = accountFactory + self.accountManager = accountManager + self.walletManager = walletManager + self.restoreSettingsManager = restoreSettingsManager + self.restoredBackup = restoredBackup + } +} + +extension RestorePassphraseService { + func validate(text: String?) -> Bool { + PassphraseValidator.validate(text: text) + } + + func next() async throws -> RestoreResult { + switch restoredBackup.source { + case let .wallet(walletBackup): + let accountType = try walletBackup + .crypto + .accountType(type: walletBackup.type, passphrase: passphrase) + let restoredBackup = RestoreCloudModule.RestoredBackup(name: restoredBackup.name, walletBackup: walletBackup) + appBackupProvider.walletRestore(backup: restoredBackup, accountType: accountType) + switch accountType { + case .cex: + return .success + default: + return .restoredAccount(RestoreCloudModule.RestoredAccount( + name: restoredBackup.name, + accountType: accountType, + isManualBackedUp: walletBackup.isManualBackedUp, + isFileBackedUp: walletBackup.isFileBackedUp, + showSelectCoins: walletBackup.enabledWallets.isEmpty + )) + } + case .full: + print("Lets try!") + return .success + } + } +} + +extension RestorePassphraseService { + enum RestoreResult { + case restoredAccount(RestoreCloudModule.RestoredAccount) + case source(BackupModule.Source) + case success + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift similarity index 88% rename from UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift index 4ff179b41c..5a14b0d4b9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift @@ -6,8 +6,8 @@ import ThemeKit import UIExtensions import UIKit -class RestoreCloudPassphraseViewController: KeyboardAwareViewController { - private let viewModel: RestoreCloudPassphraseViewModel +class RestorePassphraseViewController: KeyboardAwareViewController { + private let viewModel: RestorePassphraseViewModel private var cancellables = Set() private weak var returnViewController: UIViewController? @@ -18,12 +18,12 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { private let passphraseCautionCell = FormCautionCell() private let gradientWrapperView = BottomGradientHolder() - private let importButton = PrimaryButton() + private let nextButton = PrimaryButton() private var keyboardShown = false private var isLoaded = false - init(viewModel: RestoreCloudPassphraseViewModel, returnViewController: UIViewController?) { + init(viewModel: RestorePassphraseViewModel, returnViewController: UIViewController?) { self.viewModel = viewModel self.returnViewController = returnViewController @@ -54,11 +54,11 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { tableView.sectionDataSource = self gradientWrapperView.add(to: self) - gradientWrapperView.addSubview(importButton) + gradientWrapperView.addSubview(nextButton) show(processing: false) - importButton.setTitle("button.import".localized, for: .normal) - importButton.addTarget(self, action: #selector(onTapCreate), for: .touchUpInside) + nextButton.setTitle(viewModel.buttonTitle, for: .normal) + nextButton.addTarget(self, action: #selector(onTapNext), for: .touchUpInside) passphraseCell.set(textSecure: true) passphraseCell.onTextSecurityChange = { [weak self] in self?.passphraseCell.set(textSecure: $0) } @@ -146,17 +146,17 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { dismiss(animated: true) } - @objc private func onTapCreate() { - viewModel.onTapImport() + @objc private func onTapNext() { + viewModel.onTapNext() } private func show(processing: Bool) { if processing { - importButton.set(style: .yellow, accessoryType: .spinner) - importButton.isEnabled = false + nextButton.set(style: .yellow, accessoryType: .spinner) + nextButton.isEnabled = false } else { - importButton.set(style: .yellow) - importButton.isEnabled = true + nextButton.set(style: .yellow) + nextButton.isEnabled = true } } @@ -176,7 +176,7 @@ class RestoreCloudPassphraseViewController: KeyboardAwareViewController { } } -extension RestoreCloudPassphraseViewController: SectionsDataSource { +extension RestorePassphraseViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { [ Section( @@ -215,7 +215,7 @@ extension RestoreCloudPassphraseViewController: SectionsDataSource { } } -extension RestoreCloudPassphraseViewController: IDynamicHeightCellDelegate { +extension RestorePassphraseViewController: IDynamicHeightCellDelegate { func onChangeHeight() { guard isLoaded else { return diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift similarity index 80% rename from UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift index bd7ef35223..c313b81d19 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudPassphrase/RestoreCloudPassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift @@ -2,10 +2,10 @@ import Combine import Foundation import HsExtensions -class RestoreCloudPassphraseViewModel { +class RestorePassphraseViewModel { private var cancellables = Set() - private let service: RestoreCloudPassphraseService + private let service: RestorePassphraseService @Published public var passphraseCaution: Caution? @Published public var processing: Bool = false @@ -14,7 +14,7 @@ class RestoreCloudPassphraseViewModel { private let openSelectCoinsSubject = PassthroughSubject() private let successSubject = PassthroughSubject() - init(service: RestoreCloudPassphraseService) { + init(service: RestorePassphraseService) { self.service = service } @@ -23,11 +23,9 @@ class RestoreCloudPassphraseViewModel { passphraseCaution = nil } } - } -extension RestoreCloudPassphraseViewModel { - +extension RestorePassphraseViewModel { var clearInputsPublisher: AnyPublisher { clearInputsSubject.eraseToAnyPublisher() } @@ -57,27 +55,29 @@ extension RestoreCloudPassphraseViewModel { return validated } - func onTapImport() { + func onTapNext() { passphraseCaution = nil processing = true Task { [weak self, service] in do { - let result = try await service.importWallet() + let result = try await service.next() self?.processing = false switch result { case .success: self?.successSubject.send() - case .restoredAccount(let account): + case let .restoredAccount(account): if account.showSelectCoins { self?.openSelectCoinsSubject.send(account) } else { self?.successSubject.send() } + case let .source(source): + print("Open next source list") } } catch { - switch (error as? RestoreCloudModule.RestoreError) { + switch error as? RestoreCloudModule.RestoreError { case .emptyPassphrase: self?.passphraseCaution = Caution(text: "backup.cloud.password.error.empty_passphrase".localized, type: .error) case .simplePassword: @@ -93,5 +93,13 @@ extension RestoreCloudPassphraseViewModel { } } } +} +extension RestorePassphraseViewModel { + var buttonTitle: String { + switch service.restoredBackup.source { + case .wallet: return "button.import".localized + case .full: return "button.continue".localized + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift index 1ee4f286d4..7ffbad74cb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeModule.swift @@ -1,10 +1,9 @@ -import UIKit import ThemeKit +import UIKit struct RestoreTypeModule { - - static func viewController(sourceViewController: UIViewController? = nil, returnViewController: UIViewController? = nil) -> UIViewController { - let viewModel = RestoreTypeViewModel(cloudAccountBackupManager: App.shared.cloudBackupManager) + static func viewController(type: BackupModule.Source.Abstract, sourceViewController: UIViewController? = nil, returnViewController: UIViewController? = nil) -> UIViewController { + let viewModel = RestoreTypeViewModel(cloudAccountBackupManager: App.shared.cloudBackupManager, sourceType: type) let viewController = RestoreTypeViewController(viewModel: viewModel, returnViewController: returnViewController) let module = ThemeNavigationController(rootViewController: viewController) @@ -15,4 +14,21 @@ struct RestoreTypeModule { } } + 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 { + enum RestoreType: CaseIterable { + case recoveryOrPrivateKey + case cloudRestore + case fileRestore + case cex + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift index 189c6aa574..e88eaeda4e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewController.swift @@ -26,7 +26,7 @@ class RestoreTypeViewController: ThemeViewController { override func viewDidLoad() { super.viewDidLoad() - title = "restore.title".localized + title = viewModel.title navigationItem.largeTitleDisplayMode = .never navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.cancel".localized, style: .plain, target: self, action: #selector(didTapCancel)) @@ -66,40 +66,44 @@ class RestoreTypeViewController: ThemeViewController { dismiss(animated: true) } - private func row(_ item: RestoreTypeViewModel.RestoreType) -> RowProtocol { + private func row(_ type: RestoreTypeModule.RestoreType) -> RowProtocol { let backgroundStyle: BaseThemeCell.BackgroundStyle = .lawrence let titleFont: UIFont = .headline2 let valueFont: UIFont = .subhead2 + let icon = viewModel.icon(type: type) + let title = viewModel.title(type: type) + let description = viewModel.description(type: type) + return CellBuilderNew.row( rootElement: .hStack([ .image24 { (component: ImageComponent) -> () in - component.imageView.image = UIImage(named: item.icon) + return component.imageView.image = UIImage(named: icon) }, .vStackCentered([ .text { (component: TextComponent) -> () in component.font = titleFont component.textColor = .themeLeah - component.text = item.title + component.text = title component.numberOfLines = 0 }, .margin4, .text { (component: TextComponent) -> () in component.font = valueFont component.textColor = .themeGray - component.text = item.description + component.text = description component.numberOfLines = 0 } ]) ]), tableView: tableView, - id: item.description, + id: description, autoDeselect: true, dynamicHeight: { containerWidth in let size = CellBuilderNew.height( containerWidth: containerWidth, backgroundStyle: backgroundStyle, - text: item.title, + text: title, font: titleFont, verticalPadding: .margin24, elements: [.fixed(width: .iconSize24), .multiline] @@ -107,7 +111,7 @@ class RestoreTypeViewController: ThemeViewController { CellBuilderNew.height( containerWidth: containerWidth, backgroundStyle: backgroundStyle, - text: item.description, + text: description, font: valueFont, verticalPadding: 0, elements: [.fixed(width: .iconSize24), .multiline] @@ -119,23 +123,19 @@ class RestoreTypeViewController: ThemeViewController { cell.set(backgroundStyle: backgroundStyle, isFirst: true, isLast: true) }, action: { [weak self] in - self?.viewModel.onTap(type: item) + self?.viewModel.onTap(type: type) } ) } - private func show(type: RestoreTypeViewModel.RestoreType) { - switch type { - case .recoveryOrPrivateKey: - let viewController = RestoreModule.viewController(sourceViewController: self, returnViewController: returnViewController) - navigationController?.pushViewController(viewController, animated: true) - case .cloudRestore: - let viewController = RestoreCloudModule.viewController(returnViewController: returnViewController) - navigationController?.pushViewController(viewController, animated: true) - case .cex: - let viewController = RestoreCexViewController(returnViewController: returnViewController) - navigationController?.pushViewController(viewController, animated: true) + private func show(type: RestoreTypeModule.RestoreType) { + guard let viewController = RestoreTypeModule.destination( + restoreType: type, + sourceViewController: self, + returnViewController: returnViewController) else { + return } + navigationController?.pushViewController(viewController, animated: true) } private func showNotCloudAvailable() { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift index 12a7dceb28..542ebbebce 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift @@ -1,33 +1,31 @@ -import UIKit import Combine +import UIKit class RestoreTypeViewModel { private let cloudAccountBackupManager: CloudBackupManager + let sourceType: BackupModule.Source.Abstract private let showCloudNotAvailableSubject = PassthroughSubject() - private let showModuleSubject = PassthroughSubject() + private let showModuleSubject = PassthroughSubject() - init(cloudAccountBackupManager: CloudBackupManager) { + init(cloudAccountBackupManager: CloudBackupManager, sourceType: BackupModule.Source.Abstract) { self.cloudAccountBackupManager = cloudAccountBackupManager + self.sourceType = sourceType } - } extension RestoreTypeViewModel { - - var items: [RestoreType] { RestoreType.allCases } - var showCloudNotAvailablePublisher: AnyPublisher { showCloudNotAvailableSubject.eraseToAnyPublisher() } - var showModulePublisher: AnyPublisher { + var showModulePublisher: AnyPublisher { showModuleSubject.eraseToAnyPublisher() } - func onTap(type: RestoreType) { + func onTap(type: RestoreTypeModule.RestoreType) { switch type { - case .recoveryOrPrivateKey, .cex: showModuleSubject.send(type) + case .recoveryOrPrivateKey, .cex, .fileRestore: showModuleSubject.send(type) case .cloudRestore: if cloudAccountBackupManager.isAvailable { showModuleSubject.send(type) @@ -36,43 +34,47 @@ extension RestoreTypeViewModel { } } } - } extension RestoreTypeViewModel { - - enum RestoreType: CaseIterable { - case recoveryOrPrivateKey - case cloudRestore - case cex + var items: [RestoreTypeModule.RestoreType] { + switch sourceType { + case .wallet: return [.recoveryOrPrivateKey, .cloudRestore, .fileRestore] + case .full: return [.cloudRestore, .fileRestore] + } } -} - -extension RestoreTypeViewModel.RestoreType { - var title: String { - switch self { + switch sourceType { + case .wallet: return "restore.title".localized + case .full: return "backup_app.restore_type.title".localized + } + } + + func title(type: RestoreTypeModule.RestoreType) -> String { + 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 .cex: return "restore_type.cex.title".localized } } - var description: String { - switch self { + func description(type: RestoreTypeModule.RestoreType) -> String { + switch type { case .recoveryOrPrivateKey: return "restore_type.recovery.description".localized case .cloudRestore: return "restore_type.cloud.description".localized + case .fileRestore: return "restore_type.file.description".localized case .cex: return "restore_type.cex.description".localized } } - var icon: String { - switch self { + func icon(type: RestoreTypeModule.RestoreType) -> String { + switch type { case .recoveryOrPrivateKey: return "edit_24" case .cloudRestore: return "icloud_24" + case .fileRestore: return "file_24" case .cex: return "link_24" } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift similarity index 80% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift index 64c4c72aa3..e0373a8722 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift @@ -31,8 +31,8 @@ class BackupAppViewModel: ObservableObject { // Configuration ViewModel @Published var selected: [String: Bool] = [:] - @Published var accountItems: [AccountItem] = [] - @Published var otherItems: [Item] = [] + @Published var accountItems: [BackupAppModule.AccountItem] = [] + @Published var otherItems: [BackupAppModule.Item] = [] @Published var disclaimerPushed = false { didSet { // need to reset future fields: @@ -129,7 +129,7 @@ extension BackupAppViewModel { .map { $0.id } } - private func item(account: Account) -> AccountItem { + private func item(account: Account) -> BackupAppModule.AccountItem { var alertSubtitle: String? let hasAlertDescription = !(account.backedUp || cloudBackupManager.backedUp(uniqueId: account.type.uniqueId())) if account.nonStandard { @@ -143,7 +143,7 @@ extension BackupAppViewModel { let cautionType: CautionType? = showAlert ? .error : .none let description = alertSubtitle ?? account.type.detailedDescription - return AccountItem( + return BackupAppModule.AccountItem( accountId: account.id, name: account.name, description: description, @@ -151,47 +151,16 @@ extension BackupAppViewModel { ) } - private func getOtherItems() -> [Item] { - var items = [Item]() - - let watchAccountCount = accounts(watch: true).count - if watchAccountCount != 0 { - items.append(Item( - title: "backup_list.other.watch_account.title".localized, - description: "backup_list.other.watch_account.description".localized(watchAccountCount) - )) - } - - let watchlistCount = favoritesManager.allCoinUids.count - if watchlistCount != 0 { - items.append(Item( - title: "backup_list.other.watchlist.title".localized, - description: "backup_list.other.watchlist.description".localized(watchlistCount) - )) - } - + private func getOtherItems() -> [BackupAppModule.Item] { let contacts = contactManager.all ?? [] let contactAddressCount = contacts.reduce(into: 0) { $0 += $1.addresses.count } - if contactAddressCount != 0 { - items.append(Item( - title: "backup_list.other.contacts.title".localized, - description: "backup_list.other.contacts.description".localized(contactAddressCount) - )) - } - - let blockchainSourcesCount = evmSyncSourceManager.customSyncSources(blockchainType: nil).count - if blockchainSourcesCount != 0 { - items.append(Item( - title: "backup_list.other.blockchain_settings.title".localized, - description: "backup_list.other.blockchain_settings.description".localized(blockchainSourcesCount) - )) - } - items.append(Item( - title: "backup_list.other.app_settings.title".localized, - description: "backup_list.other.app_settings.description".localized - )) - return items + return BackupAppModule.items( + watchAccountCount: accounts(watch: true).count, + watchlistCount: favoritesManager.allCoinUids.count, + contactAddressCount: contactAddressCount, + blockchainSourcesCount: evmSyncSourceManager.customSyncSources(blockchainType: nil).count + ) } var configuration: [AppBackupProvider.Field] { @@ -220,7 +189,7 @@ extension BackupAppViewModel { } extension BackupAppViewModel { - func toggle(item: AccountItem) { + func toggle(item: BackupAppModule.AccountItem) { selected[item.accountId]?.toggle() } } @@ -345,34 +314,6 @@ extension BackupAppViewModel { } extension BackupAppViewModel { - struct AccountItem: Comparable, Identifiable { - let accountId: String - let name: String - let description: String - let cautionType: CautionType? - - static func < (lhs: AccountItem, rhs: AccountItem) -> Bool { - lhs.name < rhs.name - } - - static func == (lhs: AccountItem, rhs: AccountItem) -> Bool { - lhs.accountId == rhs.accountId - } - - var id: String { - accountId - } - } - - struct Item: Identifiable { - let title: String - let description: String - - var id: String { - title - } - } - enum NameError: Error, LocalizedError { case empty case alreadyExist diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupDisclaimer/BackupDisclaimerView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift similarity index 89% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift index 7c5f512838..d058571ca2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupList/BackupListView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift @@ -11,10 +11,10 @@ struct BackupListView: View { BottomGradientWrapper { VStack(spacing: .margin24) { VStack(spacing: 0) { - ListSectionHeader(text: "backup_list.header.wallets".localized) + ListSectionHeader(text: "backup_app.backup_list.header.wallets".localized) ListSection { - ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppViewModel.AccountItem) in + ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppModule.AccountItem) in if viewModel.selected[item.id] != nil { let selected = binding(for: item.accountId) @@ -39,10 +39,10 @@ struct BackupListView: View { } VStack(spacing: 0) { - ListSectionHeader(text: "backup_list.header.other".localized) + ListSectionHeader(text: "backup_app.backup_list.header.other".localized) ListSection { - ForEach(viewModel.otherItems) { (item: BackupAppViewModel.Item) in + ForEach(viewModel.otherItems) { (item: BackupAppModule.Item) in ListRow { VStack(spacing: 1) { Text(item.title).themeBody() @@ -68,7 +68,7 @@ struct BackupListView: View { .buttonStyle(PrimaryButtonStyle(style: .yellow)) } } - .navigationTitle("backup_list.title".localized) + .navigationTitle("backup_app.backup_list.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { @@ -88,7 +88,7 @@ struct BackupListView: View { extension BackupListView { struct AccountView: View { - var item: BackupAppViewModel.AccountItem + var item: BackupAppModule.AccountItem var body: some View { let color: Color? = item.cautionType.map { $0 == .error ? .themeLucian : .themeJacob } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift similarity index 93% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift index 5a32ef42e3..56b9843663 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupName/BackupNameView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift @@ -9,7 +9,7 @@ struct BackupNameView: View { ThemeView { BottomGradientWrapper { VStack(spacing: .margin24) { - Text("backup.name.description".localized) + Text("backup_app.backup.name.description".localized) .themeSubhead2() .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) @@ -40,7 +40,7 @@ struct BackupNameView: View { .disabled(viewModel.nameCautionState != .none) } } - .navigationBarTitle("backup.name.title".localized) + .navigationBarTitle("backup_app.backup.name.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift similarity index 94% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift index 1f7c6b2526..0f15dbe967 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupPassword/BackupPasswordView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift @@ -12,7 +12,7 @@ struct BackupPasswordView: View { ThemeView { BottomGradientWrapper { VStack(spacing: .margin32) { - Text("backup.password.description".localized) + Text("backup_app.backup.password.description".localized) .themeSubhead2() .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) @@ -45,7 +45,7 @@ struct BackupPasswordView: View { } .animation(.default, value: secureLock) - HighlightedTextView(text: "backup.password.highlighted_description".localized, style: .warning) + HighlightedTextView(text: "backup_app.backup.password.highlighted_description".localized, style: .warning) } .animation(.default, value: viewModel.passwordCautionState) .animation(.default, value: viewModel.confirmCautionState) @@ -85,7 +85,7 @@ struct BackupPasswordView: View { .onReceive(viewModel.dismissPublisher) { backupPresented = false } - .navigationBarTitle("backup.password.title".localized) + .navigationBarTitle("backup_app.backup.password.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift similarity index 86% rename from UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift index 07a2ed5232..61d9e77c6f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupType/BackupTypeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift @@ -12,16 +12,16 @@ struct BackupTypeView: View { var body: some View { ScrollableThemeView { VStack(spacing: .margin24) { - Text("backup_type.description".localized) + Text("backup_app.backup_type.description".localized) .themeSubhead2() .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) ListSection { - navigation(image: "icloud_24", text: "backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable, isActive: $cloudNavigationPushed) { + navigation(image: "icloud_24", text: "backup_app.backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable, isActive: $cloudNavigationPushed) { if viewModel.cloudAvailable { viewModel.destination = .cloud } else { cloudAlertPresented = true } } - navigation(image: "file_24", text: "backup_type.file".localized, isActive: $localNavigationPushed) { + navigation(image: "file_24", text: "backup_app.backup_type.file".localized, isActive: $localNavigationPushed) { viewModel.destination = .local } } @@ -33,7 +33,7 @@ struct BackupTypeView: View { } } } - .navigationBarTitle("backup_type.title".localized) + .navigationBarTitle("backup_app.backup_type.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift index 1ed8a0a95b..2285de7138 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift @@ -27,16 +27,16 @@ extension BackupAppModule { switch self { case .cloud: return BackupDestinationDisclaimer( - title: "backup.disclaimer.cloud.title".localized, - highlightedDescription: "backup.disclaimer.cloud.description".localized, - selectedCheckboxText: "backup.disclaimer.cloud.checkbox_label".localized, + title: "backup_app.backup.disclaimer.cloud.title".localized, + highlightedDescription: "backup_app.backup.disclaimer.cloud.description".localized, + selectedCheckboxText: "backup_app.backup.disclaimer.cloud.checkbox_label".localized, buttonTitle: "button.next".localized ) case .local: return BackupDestinationDisclaimer( - title: "backup.disclaimer.file.title".localized, - highlightedDescription: "backup.disclaimer.file.description".localized, - selectedCheckboxText: "backup.disclaimer.file.checkbox_label".localized, + title: "backup_app.backup.disclaimer.file.title".localized, + highlightedDescription: "backup_app.backup.disclaimer.file.description".localized, + selectedCheckboxText: "backup_app.backup.disclaimer.file.checkbox_label".localized, buttonTitle: "button.next".localized ) } @@ -50,3 +50,73 @@ extension BackupAppModule { let buttonTitle: String } } + +extension BackupAppModule { + static func items(watchAccountCount: Int, watchlistCount: Int, contactAddressCount: Int, blockchainSourcesCount: Int) -> [BackupAppModule.Item] { + var items = [Item]() + + if watchAccountCount != 0 { + items.append(BackupAppModule.Item( + title: "backup_app.backup_list.other.watch_account.title".localized, + description: "backup_app.backup_list.other.watch_account.description".localized(watchAccountCount) + )) + } + + if watchlistCount != 0 { + items.append(BackupAppModule.Item( + title: "backup_app.backup_list.other.watchlist.title".localized, + description: "backup_app.backup_list.other.watchlist.description".localized(watchlistCount) + )) + } + + if contactAddressCount != 0 { + items.append(BackupAppModule.Item( + title: "backup_app.backup_list.other.contacts.title".localized, + description: "backup_app.backup_list.other.contacts.description".localized(contactAddressCount) + )) + } + + if blockchainSourcesCount != 0 { + items.append(BackupAppModule.Item( + title: "backup_app.backup_list.other.blockchain_settings.title".localized, + description: "backup_app.backup_list.other.blockchain_settings.description".localized(blockchainSourcesCount) + )) + } + items.append(BackupAppModule.Item( + title: "backup_app.backup_list.other.app_settings.title".localized, + description: "backup_app.backup_list.other.app_settings.description".localized + )) + + return items + } + +} +extension BackupAppModule { + struct AccountItem: Comparable, Identifiable { + let accountId: String + let name: String + let description: String + let cautionType: CautionType? + + static func < (lhs: AccountItem, rhs: AccountItem) -> Bool { + lhs.name < rhs.name + } + + static func == (lhs: AccountItem, rhs: AccountItem) -> Bool { + lhs.accountId == rhs.accountId + } + + var id: String { + accountId + } + } + + struct Item: Identifiable { + let title: String + let description: String + + var id: String { + title + } + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift index 93c71f72c7..6c919ec1e1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift @@ -13,13 +13,13 @@ struct BackupManagerView: View { restorePresented = true }) { Image("download_24").themeIcon(color: .themeJacob) - Text("backup_manager.restore".localized).themeBody(color: .themeJacob) + Text("backup_app.backup_manager.restore".localized).themeBody(color: .themeJacob) } ClickableRow(action: { backupPresented = true }) { Image("plus_24").themeIcon(color: .themeJacob) - Text("backup_manager.create".localized).themeBody(color: .themeJacob) + Text("backup_app.backup_manager.create".localized).themeBody(color: .themeJacob) } } .sheet(isPresented: $restorePresented) { @@ -28,7 +28,7 @@ struct BackupManagerView: View { .sheet(isPresented: $backupPresented) { ThemeNavigationView { BackupAppModule.view(backupPresented: $backupPresented) } } - .navigationBarTitle("backup_manager.title".localized) + .navigationBarTitle("backup_app.backup_manager.title".localized) .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Restore/RestoreAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Restore/RestoreAppViewModel.swift new file mode 100644 index 0000000000..765ce98adc --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Restore/RestoreAppViewModel.swift @@ -0,0 +1,13 @@ +import Combine + +class RestoreAppViewModel { + let cloudBackupManager: CloudBackupManager + + init(cloudBackupManager: CloudBackupManager) { + self.cloudBackupManager = cloudBackupManager + } +} + +extension RestoreAppViewModel { + +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift index 0807c05231..65371796d9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewController.swift @@ -235,7 +235,7 @@ class WalletViewController: ThemeViewController { } @objc func onTapRestore() { - let viewController = RestoreTypeModule.viewController(sourceViewController: self) + let viewController = RestoreTypeModule.viewController(type: .wallet, sourceViewController: self) present(viewController, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 04e2f6fbcb..bbf3f7aefb 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -139,18 +139,22 @@ Go to Settings - > %@ and allow access to the camera."; "restore_type.recovery.title" = "from Recovery Phrase"; "restore_type.cloud.title" = "from iCloud"; +"restore_type.file.title" = "from Files"; "restore_type.cex.title" = "from Exchange Wallet"; "restore_type.recovery.description" = "Import using recovery phrase or private key."; "restore_type.cloud.description" = "Import from a backup file in your keychain."; +"restore_type.file.description" = "Import a backup file from your local folder."; "restore_type.cex.description" = "Connect to a wallet on centralized exchange."; // Restore Cloud "restore.cloud.title" = "Select Backup"; -"restore.cloud.description" = "Select the backup copy of the wallet you want to restore."; +"restore.cloud.description" = "Select the backup file that you want to restore."; "restore.cloud.empty" = "No backups found."; +"restore.cloud.wallets" = "Wallet backups"; "restore.cloud.imported" = "Imported wallets"; +"restore.cloud.app_backups" = "App backups"; "restore.cloud.password.title" = "Enter Password"; "restore.cloud.password.placeholder" = "Backup Password"; @@ -1087,43 +1091,43 @@ Go to Settings - > %@ and allow access to the camera."; // Settings -> Backup Manager -"backup_manager.title" = "Backup Manager"; -"backup_manager.restore" = "Restore Backup"; -"backup_manager.create" = "Create New Backup"; - -"backup_type.title" = "Backup Type"; -"backup_type.description" = "Select where you want to save the backup file."; -"backup_type.cloud" = "Backup to iCloud"; -"backup_type.file" = "Backup to Files"; - -"backup_list.title" = "Backup File"; -"backup_list.description.restore" = "List of contents in the backup file."; -"backup_list.header.wallets" = "Wallets"; -"backup_list.header.other" = "Other"; -"backup_list.other.watch_account.title" = "Watch Wallets"; -"backup_list.other.watch_account.description" = "Addresses: %d"; -"backup_list.other.watchlist.title" = "Watchlist"; -"backup_list.other.watchlist.description" = "Coins: %d"; -"backup_list.other.contacts.title" = "Contacts"; -"backup_list.other.contacts.description" = "Addresses: %d"; -"backup_list.other.blockchain_settings.title" = "Blockchain Settings"; -"backup_list.other.blockchain_settings.description" = "Custom RPCs: %d"; -"backup_list.other.app_settings.title" = "App Settings"; -"backup_list.other.app_settings.description" = "Language, Currency, Appearance ..."; - -"backup.disclaimer.cloud.title" = "Backup to iCloud"; -"backup.disclaimer.cloud.description" = "iCloud is a cloud storage service provided by Apple. It's important to know that your backup data will be stored on Apple's servers."; -"backup.disclaimer.cloud.checkbox_label" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; -"backup.disclaimer.file.title" = "Backup to File"; -"backup.disclaimer.file.description" = "Storage devices i.e. hard drives, USB drives , storage on smartphone etc. are all vulnerable to loss due to physical damage, theft or other unforeseen circumstances."; -"backup.disclaimer.file.checkbox_label" = "I understand that theft or damage of a backup device will result in loss of a backup to a respective wallet."; - -"backup.name.title" = "Backup Name"; -"backup.name.description" = "Enter name for the backup file."; - -"backup.password.title" = "Backup Password"; -"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.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.backup_manager.title" = "Backup Manager"; +"backup_app.backup_manager.restore" = "Restore Backup"; +"backup_app.backup_manager.create" = "Create New Backup"; + +"backup_app.backup_type.title" = "Backup Type"; +"backup_app.backup_type.description" = "Select where you want to save the backup file."; +"backup_app.backup_type.cloud" = "Backup to iCloud"; +"backup_app.backup_type.file" = "Backup to Files"; + +"backup_app.backup_list.title" = "Backup File"; +"backup_app.backup_list.description.restore" = "List of contents in the backup file."; +"backup_app.backup_list.header.wallets" = "Wallets"; +"backup_app.backup_list.header.other" = "Other"; +"backup_app.backup_list.other.watch_account.title" = "Watch Wallets"; +"backup_app.backup_list.other.watch_account.description" = "Addresses: %d"; +"backup_app.backup_list.other.watchlist.title" = "Watchlist"; +"backup_app.backup_list.other.watchlist.description" = "Coins: %d"; +"backup_app.backup_list.other.contacts.title" = "Contacts"; +"backup_app.backup_list.other.contacts.description" = "Addresses: %d"; +"backup_app.backup_list.other.blockchain_settings.title" = "Blockchain Settings"; +"backup_app.backup_list.other.blockchain_settings.description" = "Custom RPCs: %d"; +"backup_app.backup_list.other.app_settings.title" = "App Settings"; +"backup_app.backup_list.other.app_settings.description" = "Language, Currency, Appearance ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Backup to iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud is a cloud storage service provided by Apple. It's important to know that your backup data will be stored on Apple's servers."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; +"backup_app.backup.disclaimer.file.title" = "Backup to File"; +"backup_app.backup.disclaimer.file.description" = "Storage devices i.e. hard drives, USB drives , storage on smartphone etc. are all vulnerable to loss due to physical damage, theft or other unforeseen circumstances."; +"backup_app.backup.disclaimer.file.checkbox_label" = "I understand that theft or damage of a backup device will result in loss of a backup to a respective wallet."; + +"backup_app.backup.name.title" = "Backup Name"; +"backup_app.backup.name.description" = "Enter name for the backup file."; + +"backup_app.backup.password.title" = "Backup Password"; +"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."; // Settings -> Security From db3495dac6578bd2ddd671aec202d8bc533d2301 Mon Sep 17 00:00:00 2001 From: ant013 Date: Sun, 8 Oct 2023 17:43:48 +0600 Subject: [PATCH 46/63] Refactor backup and crypto structures. - Extract many small functions from encrypt/decrypt, refactor names and logic for compose full backups --- .../project.pbxproj | 6 + .../Core/Crypto/BackupCrypto.swift | 14 +- .../Core/Crypto/FullBackup.swift | 6 +- .../Core/Crypto/RawFullBackup.swift | 14 + .../Core/Crypto/SettingsBackup.swift | 2 +- .../Core/Managers/CloudBackupManager.swift | 21 +- .../Core/Managers/EvmBlockchainManager.swift | 4 +- .../Core/Managers/EvmSyncSourceManager.swift | 70 ++-- .../Core/Storage/ContactBookManager.swift | 10 +- .../Models/AccountType.swift | 65 ++-- .../Backup/ICloud/AppBackupProvider.swift | 299 ++++++++---------- .../RestoreFileConfigurationService.swift | 4 +- .../RestorePassphraseService.swift | 27 +- .../RestorePassphraseViewController.swift | 10 +- .../RestorePassphraseViewModel.swift | 14 +- .../BackupApp/Backup/BackupAppViewModel.swift | 28 +- .../Modules/Swap/SwapProviderManager.swift | 21 +- 17 files changed, 296 insertions(+), 319 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Core/Crypto/RawFullBackup.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index acfcf7af1f..b16b709e92 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2080,6 +2080,7 @@ ABC9A36297D869E49C152CAB /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; 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 */; }; @@ -2403,6 +2404,7 @@ ABC9AEF62C857F322FFA87E4 /* ContactBookAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */; }; ABC9AF04946C86FA6DBD4225 /* RestorePassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */; }; ABC9AF1729BA19223BB39E06 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; }; + ABC9AF309AAE5C54D2020B23 /* RawFullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A819E6708797C571CA0B /* RawFullBackup.swift */; }; ABC9AF371FBB4BEA654A78B6 /* MetadataMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2491ADC4E5E089CD42 /* MetadataMonitor.swift */; }; ABC9AF4D82ACDFBFBDC2D23C /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; }; ABC9AF5B0B1D5FE002288AE1 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */; }; @@ -3882,6 +3884,7 @@ ABC9A80143F95E28346C81FE /* SendMemoInputService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMemoInputService.swift; sourceTree = ""; }; ABC9A806FD17A129212E3F7C /* NftAssetOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetOverviewViewController.swift; sourceTree = ""; }; ABC9A8080797194017F736AB /* ContactBookContactViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactViewModel.swift; sourceTree = ""; }; + ABC9A819E6708797C571CA0B /* RawFullBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawFullBackup.swift; sourceTree = ""; }; ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendNftModule.swift; sourceTree = ""; }; ABC9A830FE79DBF62FD63CC4 /* ThemeMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeMode.swift; sourceTree = ""; }; ABC9A845B2969166028BA5F0 /* WalletConnectAppShowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowView.swift; sourceTree = ""; }; @@ -7230,6 +7233,7 @@ ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */, ABC9A41F6AA0B65FDA91EB68 /* FullBackup.swift */, ABC9AA7FC181E0E0FB74BEF5 /* SettingsBackup.swift */, + ABC9A819E6708797C571CA0B /* RawFullBackup.swift */, ); path = Crypto; sourceTree = ""; @@ -9459,6 +9463,7 @@ ABC9A904FCE6BFE793C944AE /* RestoreFileConfigurationModule.swift in Sources */, ABC9A394D1D0D165160F5F43 /* RestoreFileConfigurationService.swift in Sources */, ABC9A481F1C13DBAAD3F632B /* RestoreFileConfigurationViewModel.swift in Sources */, + ABC9AF309AAE5C54D2020B23 /* RawFullBackup.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10785,6 +10790,7 @@ ABC9A372F53F1F1D59BF8969 /* RestoreFileConfigurationModule.swift in Sources */, ABC9A2A327AF3D72F9842DCA /* RestoreFileConfigurationService.swift in Sources */, ABC9A2F6D2A2AAFA31C64BAB /* RestoreFileConfigurationViewModel.swift in Sources */, + ABC9A37FB71FA7DA14553EFC /* RawFullBackup.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift index e3fb6d3577..4f670d4b3a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/BackupCrypto.swift @@ -50,7 +50,7 @@ class BackupCrypto: Codable { } extension BackupCrypto { - func data(passphrase: String) throws -> Data { + func decrypt(passphrase: String) throws -> Data { try Self.validate(passphrase: passphrase) // Validation data guard let data = Data(base64Encoded: cipherText) else { @@ -76,16 +76,6 @@ extension BackupCrypto { kdf: kdfParams ) } - - func accountType(type: AccountType.Abstract, passphrase: String) throws -> AccountType { - let data = try data(passphrase: passphrase) - - guard let accountType = AccountType.decode(uniqueId: data, type: type) else { - throw RestoreCloudModule.RestoreError.invalidBackup - } - - return accountType - } } extension BackupCrypto { @@ -104,7 +94,7 @@ extension BackupCrypto { } } - static func instance(data: Data, passphrase: String, kdf: KdfParams = .defaultBackup) throws -> BackupCrypto { + static func encrypt(data: Data, passphrase: String, kdf: KdfParams = .defaultBackup) throws -> BackupCrypto { let iv = BackupCryptoHelper.generateInitialVector().hs.hex let cipherText = try BackupCryptoHelper.AES128( diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift index e22ba17c5f..628a5f3d2a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -5,7 +5,7 @@ struct FullBackup { let wallets: [RestoreCloudModule.RestoredBackup] let watchlistIds: [String] let contacts: BackupCrypto? - let settings: SettingsBackup? + let settings: SettingsBackup let version: Int let timestamp: TimeInterval? } @@ -27,7 +27,7 @@ extension FullBackup: Codable { wallets = (try? container.decode([RestoreCloudModule.RestoredBackup].self, forKey: .wallets)) ?? [] watchlistIds = (try? container.decode([String].self, forKey: .watchlistIds)) ?? [] contacts = try? container.decode(BackupCrypto.self, forKey: .contacts) - settings = try? container.decode(SettingsBackup.self, forKey: .settings) + settings = try container.decode(SettingsBackup.self, forKey: .settings) version = try container.decode(Int.self, forKey: .version) timestamp = try? container.decode(TimeInterval.self, forKey: .timestamp) } @@ -38,7 +38,7 @@ extension FullBackup: Codable { if !wallets.isEmpty { try container.encode(wallets, forKey: .wallets) } if !watchlistIds.isEmpty { try container.encode(watchlistIds, forKey: .watchlistIds) } if let contacts { try container.encode(contacts, forKey: .contacts) } - if let settings { try container.encode(settings, forKey: .settings) } + try container.encode(settings, forKey: .settings) try container.encode(version, forKey: .version) try? container.encode(timestamp, forKey: .timestamp) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/RawFullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/RawFullBackup.swift new file mode 100644 index 0000000000..19a02a468f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/RawFullBackup.swift @@ -0,0 +1,14 @@ +import Foundation + +struct RawFullBackup { + var accounts: [RawWalletBackup] + let watchlistIds: [String] + let contacts: [BackupContact] + let settings: SettingsBackup + let customSyncSources: [EvmSyncSourceRecord] +} + +struct RawWalletBackup { + let account: Account + let enabledWallets: [WalletBackup.EnabledWallet] +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift index 2bea154ba2..696c6502ce 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift @@ -4,7 +4,7 @@ import CurrencyKit import ThemeKit struct SettingsBackup: Codable { - let evmSyncSources: EvmSyncSourceManager.SyncSourceBackup + var evmSyncSources: EvmSyncSourceManager.SyncSourceBackup let btcModes: [BtcBlockchainManager.BtcRestoreModeBackup] let lockTimeEnabled: Bool diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift index 4d9c105b0e..52dd8e128d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/CloudBackupManager.swift @@ -76,7 +76,7 @@ class CloudBackupManager { let oneWalletItemsV2: [String: RestoreCloudModule.RestoredBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) let mapped = oneWalletItemsV2.reduce(into: [:]) { $0[$1.value.name] = $1.value.walletBackup } - oneWalletItems.merge(mapped) { backup, backup2 in backup2 } + oneWalletItems.merge(mapped) { _, backup2 in backup2 } let fullBackupItems: [String: FullBackup] = try Self.downloadItems(url: url, fileStorage: fileStorage, logger: logger) state = .success @@ -159,8 +159,9 @@ extension CloudBackupManager { } func save(account: Account, passphrase: String, name: String) throws { - let backup = try appBackupProvider.walletBackup( + let backup = try AppBackupProvider.encrypt( account: account, + wallets: appBackupProvider.enabledWallets(account: account), passphrase: passphrase ) @@ -173,16 +174,14 @@ extension CloudBackupManager { } } - private func data(fields: [AppBackupProvider.Field], passphrase: String) throws -> Data { - let backup = try appBackupProvider.fullBackup( - fields: fields, - passphrase: passphrase - ) + private func data(accountIds: [String], passphrase: String) throws -> Data { + let rawBackup = appBackupProvider.fullBackup(accountIds: accountIds) + let backup = try appBackupProvider.encrypt(raw: rawBackup, passphrase: passphrase) return try JSONEncoder().encode(backup) } - func file(fields: [AppBackupProvider.Field], passphrase: String, name: String) throws -> URL { - let data = try data(fields: fields, passphrase: passphrase) + func file(accountIds: [String], passphrase: String, name: String) throws -> URL { + let data = try data(accountIds: accountIds, passphrase: passphrase) // save book to temporary file guard let temporaryFileUrl = ContactBookManager.localUrl?.appendingPathComponent(name + ".json") else { @@ -193,9 +192,9 @@ extension CloudBackupManager { return temporaryFileUrl } - func save(fields: [AppBackupProvider.Field], passphrase: String, name: String) throws { + func save(accountIds: [String], passphrase: String, name: String) throws { do { - let encoded = try data(fields: fields, passphrase: passphrase) + let encoded = try data(accountIds: accountIds, passphrase: passphrase) try save(encoded: encoded, name: name) } catch { logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)") diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift index fc851d6ac0..010a5bb92f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift @@ -3,7 +3,7 @@ import MarketKit import HsToolKit class EvmBlockchainManager { - private let blockchainTypes: [BlockchainType] = [ + static let blockchainTypes: [BlockchainType] = [ .ethereum, .binanceSmartChain, .polygon, @@ -24,7 +24,7 @@ class EvmBlockchainManager { var allBlockchains: [Blockchain] { do { - return try marketKit.blockchains(uids: blockchainTypes.map { $0.uid }) + return try marketKit.blockchains(uids: EvmBlockchainManager.blockchainTypes.map { $0.uid }) } catch { return [] } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift index 953b4e6283..316a3911ba 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmSyncSourceManager.swift @@ -286,54 +286,66 @@ extension EvmSyncSourceManager { } extension EvmSyncSourceManager { - func backup(passphrase: String) -> SyncSourceBackup { - let customSources = ((try? evmSyncSourceStorage.getAll()) ?? []) - .map { record in - let crypto = record.auth - .flatMap { $0.isEmpty ? nil : $0 } - .flatMap { $0.data(using: .utf8) } - .flatMap { try? BackupCrypto.instance(data: $0, passphrase: passphrase) } - - return CustomSyncSource( - blockchainTypeUid: record.blockchainTypeUid, - url: record.url, - auth: crypto - ) - } - let selected = BlockchainType - .supported - .filter { !$0.allowedProviders.isEmpty } + var customSources: [EvmSyncSourceRecord] { + (try? evmSyncSourceStorage.getAll()) ?? [] + } + + var selectedSources: [SelectedSource] { + EvmBlockchainManager + .blockchainTypes .map { type in SelectedSource( blockchainTypeUid: type.uid, url: syncSource(blockchainType: type).rpcSource.url.absoluteString ) } - return .init(selected: selected, custom: customSources) } +} - func restore(backup: SyncSourceBackup, passphrase: String? = nil) { - var blockchainTypes = Set() - backup.custom.forEach { source in - let auth: String? = source.auth.flatMap { - guard let passphrase else { return nil } - return try? $0.data(passphrase: passphrase) - } +extension EvmSyncSourceManager { + func decrypt(sources: [CustomSyncSource], passphrase: String) throws -> [EvmSyncSourceRecord] { + try sources.map { source in + let auth = try source.auth + .flatMap { try $0.decrypt(passphrase: passphrase) } .flatMap { String(data: $0, encoding: .utf8) } - let record = EvmSyncSourceRecord( + + return EvmSyncSourceRecord( blockchainTypeUid: source.blockchainTypeUid, url: source.url, auth: auth ) + } + } + func encrypt(sources: [EvmSyncSourceRecord], passphrase: String) throws -> [CustomSyncSource] { + try sources.map { source in + let crypto = try source.auth + .flatMap { $0.isEmpty ? nil : $0 } + .flatMap { $0.data(using: .utf8) } + .flatMap { try BackupCrypto.encrypt(data: $0, passphrase: passphrase) } + + return CustomSyncSource( + blockchainTypeUid: source.blockchainTypeUid, + url: source.url, + auth: crypto + ) + } + } +} + +extension EvmSyncSourceManager { + func restore(selected: [SelectedSource], custom: [EvmSyncSourceRecord]) { + var blockchainTypes = Set() + custom.forEach { source in blockchainTypes.insert(BlockchainType(uid: source.blockchainTypeUid)) - try? evmSyncSourceStorage.save(record: record) + try? evmSyncSourceStorage.save(record: source) } - backup.selected.forEach { source in + selected.forEach { source in let blockchainType = BlockchainType(uid: source.blockchainTypeUid) if let syncSource = allSyncSources(blockchainType: blockchainType) - .first(where: { $0.rpcSource.url.absoluteString == source.url }) { + .first(where: { $0.rpcSource.url.absoluteString == source.url }) + { saveCurrent(syncSource: syncSource, blockchainType: blockchainType) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift index ef32071e96..26cb76fe8c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift @@ -431,8 +431,14 @@ extension ContactBookManager { } extension ContactBookManager { - static func encode(crypto: BackupCrypto, passphrase: String) throws -> [BackupContact] { - let data = try crypto.data(passphrase: passphrase) + static func encrypt(contacts: [BackupContact], passphrase: String) throws -> BackupCrypto { + let encoder = JSONEncoder() + let data = try encoder.encode(contacts) + return try BackupCrypto.encrypt(data: data, passphrase: passphrase) + } + + static func decrypt(crypto: BackupCrypto, passphrase: String) throws -> [BackupContact] { + let data = try crypto.decrypt(passphrase: passphrase) let decoder = JSONDecoder() let contacts = try decoder.decode([BackupContact].self, from: data) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index 4151add3f7..457064d8ff 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -1,10 +1,10 @@ +import BitcoinCore +import Crypto +import EvmKit import Foundation import HdWalletKit -import EvmKit -import TronKit -import BitcoinCore import MarketKit -import Crypto +import TronKit enum AccountType { case mnemonic(words: [String], salt: String, bip39Compliant: Bool) @@ -18,8 +18,8 @@ enum AccountType { switch self { case let .mnemonic(words, salt, bip39Compliant): return bip39Compliant - ? Mnemonic.seed(mnemonic: words, passphrase: salt) - : Mnemonic.seedNonStandard(mnemonic: words, passphrase: salt) + ? Mnemonic.seed(mnemonic: words, passphrase: salt) + : Mnemonic.seedNonStandard(mnemonic: words, passphrase: salt) default: return nil } @@ -79,7 +79,7 @@ enum AccountType { case (.tron, .native), (.tron, .eip20): return true default: return false } - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch token.blockchainType { case .bitcoin, .litecoin: guard let derivation = token.type.derivation, key.purposes.contains(where: { $0.mnemonicDerivation == derivation }) else { @@ -141,7 +141,7 @@ enum AccountType { var withdrawalAllowed: Bool { switch self { - case .cex(cexAccount: let account): return account.cex.withdrawalAllowed + case let .cex(cexAccount: account): return account.cex.withdrawalAllowed default: return true } } @@ -155,7 +155,7 @@ enum AccountType { var description: String { switch self { - case .mnemonic(let words, let salt, _): + case let .mnemonic(words, salt, _): let count = "\(words.count)" return salt.isEmpty ? "manage_accounts.n_words".localized(count) : "manage_accounts.n_words_with_passphrase".localized(count) case .evmPrivateKey: @@ -164,7 +164,7 @@ enum AccountType { return "EVM Address" case .tronAddress: return "TRON Address" - case .hdExtendedKey(let key): + case let .hdExtendedKey(key): switch key { case .private: switch key.derivedType { @@ -178,16 +178,16 @@ enum AccountType { default: return "" } } - case .cex(let cexAccount): + case let .cex(cexAccount): return cexAccount.cex.title } } var detailedDescription: String { switch self { - case .evmAddress(let address): + case let .evmAddress(address): return address.eip55.shortened - case .tronAddress(let address): + case let .tronAddress(address): return address.base58.shortened default: return description } @@ -201,7 +201,7 @@ enum AccountType { } return try? Signer.address(seed: mnemonicSeed, chain: chain) - case .evmPrivateKey(let data): + case let .evmPrivateKey(data): return Signer.address(privateKey: data) default: return nil @@ -220,17 +220,15 @@ enum AccountType { } return try? EvmKit.Kit.sign(message: message, privateKey: privateKey, isLegacy: isLegacy) - case .evmPrivateKey(let data): + case let .evmPrivateKey(data): return try? EvmKit.Kit.sign(message: message, privateKey: data, isLegacy: isLegacy) default: return nil } } - } extension AccountType { - private static func split(_ string: String, separator: String) -> (String, String) { if let index = string.firstIndex(of: Character(separator)) { let left = String(string.prefix(upTo: index)) @@ -256,7 +254,7 @@ extension AccountType { return AccountType.evmPrivateKey(data: uniqueId) case .hdExtendedKey: do { - return AccountType.hdExtendedKey(key: try HDExtendedKey(data: uniqueId)) + return try AccountType.hdExtendedKey(key: HDExtendedKey(data: uniqueId)) } catch { return nil } @@ -264,7 +262,7 @@ extension AccountType { return AccountType.evmAddress(address: EvmKit.Address(raw: uniqueId)) case .tronAddress: do { - return AccountType.tronAddress(address: try TronKit.Address(raw: uniqueId)) + return try AccountType.tronAddress(address: TronKit.Address(raw: uniqueId)) } catch { return nil } @@ -278,12 +276,12 @@ extension AccountType { } enum Abstract: String, Codable { - case mnemonic = "mnemonic" + case mnemonic case evmPrivateKey = "private_key" case evmAddress = "evm_address" case tronAddress = "tron_address" case hdExtendedKey = "hd_extended_key" - case cex = "cex" + case cex init(_ type: AccountType) { switch type { @@ -303,24 +301,22 @@ extension AccountType { } } } - } extension AccountType: Hashable { - - public static func ==(lhs: AccountType, rhs: AccountType) -> Bool { + public static func == (lhs: AccountType, rhs: AccountType) -> Bool { switch (lhs, rhs) { case (let .mnemonic(lhsWords, lhsSalt, lhsBip39Compliant), let .mnemonic(rhsWords, rhsSalt, rhsBip39Compliant)): return lhsWords == rhsWords && lhsSalt == rhsSalt && lhsBip39Compliant == rhsBip39Compliant - case (let .evmPrivateKey(lhsData), let .evmPrivateKey(rhsData)): + case let (.evmPrivateKey(lhsData), .evmPrivateKey(rhsData)): return lhsData == rhsData - case (let .evmAddress(lhsAddress), let .evmAddress(rhsAddress)): + case let (.evmAddress(lhsAddress), .evmAddress(rhsAddress)): return lhsAddress == rhsAddress - case (let .tronAddress(lhsAddress), let .tronAddress(rhsAddress)): + case let (.tronAddress(lhsAddress), .tronAddress(rhsAddress)): return lhsAddress == rhsAddress - case (let .hdExtendedKey(lhsKey), let .hdExtendedKey(rhsKey)): + case let (.hdExtendedKey(lhsKey), .hdExtendedKey(rhsKey)): return lhsKey == rhsKey - case (let .cex(lhsCexAccount), let .cex(rhsCexAccount)): + case let (.cex(lhsCexAccount), .cex(rhsCexAccount)): return lhsCexAccount == rhsCexAccount default: return false } @@ -350,5 +346,16 @@ extension AccountType: Hashable { hasher.combine(cexAccount) } } +} + +extension AccountType { + static func decrypt(crypto: BackupCrypto, type: AccountType.Abstract, passphrase: String) throws -> AccountType { + let data = try crypto.decrypt(passphrase: passphrase) + guard let accountType = AccountType.decode(uniqueId: data, type: type) else { + throw RestoreCloudModule.RestoreError.invalidBackup + } + + return accountType + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index fbf37d1d14..14ff7a2f2c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -65,19 +65,9 @@ class AppBackupProvider { self.contactManager = contactManager } - private func walletBackups(ids: [String], passphrase: String) -> [RestoreCloudModule.RestoredBackup] { - ids.compactMap { - accountManager.account(id: $0) - }.compactMap { - try? walletBackup(account: $0, passphrase: passphrase) - } - } -} - -extension AppBackupProvider { - func walletBackup(account: Account, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { - let wallets = App.shared - .walletManager + // Parts of backups + func enabledWallets(account: Account) -> [WalletBackup.EnabledWallet] { + walletManager .wallets(account: account).map { let settings = restoreSettingsManager .settings(accountId: account.id, blockchainType: $0.token.blockchainType) @@ -85,108 +75,75 @@ extension AppBackupProvider { return WalletBackup.EnabledWallet($0, settings: settings) } - return try AppBackupProvider.walletBackup( - accountType: account.type, - wallets: wallets, - isManualBackedUp: account.backedUp, - isFileBackedUp: account.fileBackedUp, - name: account.name, - passphrase: passphrase - ) } - func fullBackup(fields: [Field], passphrase: String) throws -> FullBackup { - var wallets = [RestoreCloudModule.RestoredBackup]() - var watchlistIds = [String]() - var contacts = [BackupContact]() - var settings: SettingsBackup? - for field in fields { - switch field { - case let .accounts(ids): - wallets.append(contentsOf: walletBackups(ids: ids, passphrase: passphrase)) - case .watchlist: - watchlistIds = favoritesManager.allCoinUids - case .contacts: - contacts = contactManager.backupContactBook?.contacts ?? [] - case .settings: - let providers: [SettingsBackup.DefaultProvider] = BlockchainType - .supported - .filter { !$0.allowedProviders.isEmpty } - .map { - SettingsBackup.DefaultProvider( - blockchainTypeId: $0.uid, - provider: localStorage.defaultProvider(blockchainType: $0).id - ) - } - settings = SettingsBackup( - evmSyncSources: evmSyncSourceManager.backup(passphrase: passphrase), - btcModes: btcBlockchainManager.backup, - lockTimeEnabled: localStorage.lockTimeEnabled, - remoteContactsSync: localStorage.remoteContactsSync, - swapProviders: providers, - chartIndicators: chartRepository.backup, - indicatorsShown: localStorage.indicatorsShown, - currentLanguage: languageManager.currentLanguage, - baseCurrency: currencyKit.baseCurrency.code, - mode: themeManager.themeMode, - showMarketTab: launchScreenManager.showMarket, - launchScreen: launchScreenManager.launchScreen, - conversionTokenQueryId: balanceConversionManager.conversionToken?.tokenQuery.id, - balancePrimaryValue: balancePrimaryValueManager.balancePrimaryValue, - balanceAutoHide: balanceHiddenManager.balanceAutoHide, - appIcon: appIconManager.appIcon.title + private var swapProviders: [SettingsBackup.DefaultProvider] { + EvmBlockchainManager + .blockchainTypes + .map { + SettingsBackup.DefaultProvider( + blockchainTypeId: $0.uid, + provider: localStorage.defaultProvider(blockchainType: $0).id ) } - } + } - guard !wallets.isEmpty || - !watchlistIds.isEmpty || - !contacts.isEmpty || - settings != nil - else { - throw CodingError.emptyParameters - } + private func settings(evmSyncSources: EvmSyncSourceManager.SyncSourceBackup) -> SettingsBackup { + SettingsBackup( + evmSyncSources: evmSyncSources, + btcModes: btcBlockchainManager.backup, + lockTimeEnabled: localStorage.lockTimeEnabled, + remoteContactsSync: localStorage.remoteContactsSync, + swapProviders: swapProviders, + chartIndicators: chartRepository.backup, + indicatorsShown: localStorage.indicatorsShown, + currentLanguage: languageManager.currentLanguage, + baseCurrency: currencyKit.baseCurrency.code, + mode: themeManager.themeMode, + showMarketTab: launchScreenManager.showMarket, + launchScreen: launchScreenManager.launchScreen, + conversionTokenQueryId: balanceConversionManager.conversionToken?.tokenQuery.id, + balancePrimaryValue: balancePrimaryValueManager.balancePrimaryValue, + balanceAutoHide: balanceHiddenManager.balanceAutoHide, + appIcon: appIconManager.appIcon.title + ) + } - var contactCrypto: BackupCrypto? - if !contacts.isEmpty { - let encoder = JSONEncoder() - let data = try encoder.encode(contacts) - contactCrypto = try BackupCrypto.instance(data: data, passphrase: passphrase) + func encrypt(accountIds: [String], passphrase: String) throws -> [RestoreCloudModule.RestoredBackup] { + try accountIds.compactMap { + accountManager.account(id: $0) + }.compactMap { + try Self.encrypt(account: $0, wallets: enabledWallets(account: $0), passphrase: passphrase) } + } - return FullBackup( - id: UUID().uuidString, - wallets: wallets, - watchlistIds: watchlistIds, - contacts: contactCrypto, - settings: settings, - version: AppBackupProvider.version, - timestamp: Date().timeIntervalSince1970.rounded() + func fullBackup(accountIds: [String]) -> RawFullBackup { + let accounts = accountIds + .compactMap { accountManager.account(id: $0) } + .compactMap { RawWalletBackup(account: $0, enabledWallets: enabledWallets(account: $0)) } + + let custom = evmSyncSourceManager.customSources + let selected = evmSyncSourceManager.selectedSources + let syncSources = EvmSyncSourceManager.SyncSourceBackup(selected: selected, custom: []) + return RawFullBackup( + accounts: accounts, + watchlistIds: favoritesManager.allCoinUids, + contacts: contactManager.backupContactBook?.contacts ?? [], + settings: settings(evmSyncSources: syncSources), + customSyncSources: custom ) } +} - func walletRestore(backup: RestoreCloudModule.RestoredBackup, accountType: AccountType) { - switch accountType { +extension AppBackupProvider { + func restore(raw: RawWalletBackup) { + switch raw.account.type { case .cex: - let account = accountFactory.account( - type: accountType, - origin: .restored, - backedUp: backup.walletBackup.isManualBackedUp, - fileBackedUp: backup.walletBackup.isFileBackedUp, - name: backup.name - ) - accountManager.save(account: account) + accountManager.save(account: raw.account) default: - let account = accountFactory.account( - type: accountType, - origin: .restored, - backedUp: backup.walletBackup.isManualBackedUp, - fileBackedUp: backup.walletBackup.isFileBackedUp, - name: backup.name - ) - accountManager.save(account: account) + accountManager.save(account: raw.account) - let wallets = backup.walletBackup.enabledWallets.map { + let wallets = raw.enabledWallets.map { if !$0.settings.isEmpty { var restoreSettings = [RestoreSettingType: String]() $0.settings.forEach { key, value in @@ -195,12 +152,12 @@ extension AppBackupProvider { } } if let tokenQuery = TokenQuery(id: $0.tokenQueryId) { - restoreSettingsManager.save(settings: restoreSettings, account: account, blockchainType: tokenQuery.blockchainType) + restoreSettingsManager.save(settings: restoreSettings, account: raw.account, blockchainType: tokenQuery.blockchainType) } } return EnabledWallet( tokenQueryId: $0.tokenQueryId, - accountId: account.id, + accountId: raw.account.id, coinName: $0.coinName, coinCode: $0.coinCode, tokenDecimals: $0.tokenDecimals @@ -210,83 +167,107 @@ extension AppBackupProvider { } } - func fullRestore(backup: FullBackup, passphrase: String) throws { - var encryptionError: Error? - var encodedWallets = [(RestoreCloudModule.RestoredBackup, AccountType)]() - backup.wallets.forEach { wallet in - do { - let accountType = try wallet - .walletBackup - .crypto - .accountType(type: wallet.walletBackup.type, passphrase: passphrase) - encodedWallets.append((wallet, accountType)) - } catch { - encryptionError = error - } + func restore(raw: RawFullBackup) { + raw.accounts.forEach { wallet in + restore(raw: wallet) } + favoritesManager.add(coinUids: raw.watchlistIds) - if encodedWallets.count != backup.wallets.count { - encryptionError = CodingError.invalidPassword + if !raw.contacts.isEmpty { + try? contactManager.restore(contacts: raw.contacts) } - if let encryptionError { - throw encryptionError + evmSyncSourceManager.restore(selected: raw.settings.evmSyncSources.selected, custom: raw.customSyncSources) + btcBlockchainManager.restore(backup: raw.settings.btcModes) + chartRepository.restore(backup: raw.settings.chartIndicators) + localStorage.restore(backup: raw.settings) + languageManager.currentLanguage = raw.settings.currentLanguage + if let currency = currencyKit.currencies.first(where: { $0.code == raw.settings.baseCurrency }) { + currencyKit.baseCurrency = currency } - // restore only if all wallet was encrypted with password - encodedWallets.forEach { wallet in - walletRestore( - backup: wallet.0, - accountType: wallet.1 - ) + themeManager.themeMode = raw.settings.mode + launchScreenManager.showMarket = raw.settings.showMarketTab + launchScreenManager.launchScreen = raw.settings.launchScreen + + balanceConversionManager.set(tokenQueryId: raw.settings.conversionTokenQueryId) + balanceHiddenManager.set(balanceAutoHide: raw.settings.balanceAutoHide) + let appIcon = AppIconManager.allAppIcons.first { $0.title == raw.settings.appIcon } ?? .main + if appIconManager.appIcon != appIcon { + appIconManager.appIcon = appIcon } + } +} - favoritesManager.add(coinUids: backup.watchlistIds) +extension AppBackupProvider { + func decrypt(walletBackup: WalletBackup, name: String, passphrase: String) throws -> RawWalletBackup { + let accountType = try AccountType.decrypt( + crypto: walletBackup.crypto, + type: walletBackup.type, + passphrase: passphrase + ) + let account = accountFactory.account( + type: accountType, + origin: .restored, + backedUp: walletBackup.isManualBackedUp, + fileBackedUp: walletBackup.isFileBackedUp, + name: name + ) - if let contacts = backup.contacts { - let contacts = try ContactBookManager.encode(crypto: contacts, passphrase: passphrase) - try contactManager.restore(contacts: contacts) - } + return RawWalletBackup(account: account, enabledWallets: walletBackup.enabledWallets) + } - if let settings = backup.settings { - evmSyncSourceManager.restore(backup: settings.evmSyncSources) - btcBlockchainManager.restore(backup: settings.btcModes) - chartRepository.restore(backup: settings.chartIndicators) - localStorage.restore(backup: settings) - languageManager.currentLanguage = settings.currentLanguage - if let currency = currencyKit.currencies.first(where: { $0.code == settings.baseCurrency }) { - currencyKit.baseCurrency = currency - } - themeManager.themeMode = settings.mode - launchScreenManager.showMarket = settings.showMarketTab - launchScreenManager.launchScreen = settings.launchScreen + func decrypt(fullBackup: FullBackup, passphrase: String) throws -> RawFullBackup { + let wallets = try fullBackup.wallets + .map { try decrypt(walletBackup: $0.walletBackup, name: $0.name, passphrase: passphrase) } - balanceConversionManager.set(tokenQueryId: settings.conversionTokenQueryId) - balanceHiddenManager.set(balanceAutoHide: settings.balanceAutoHide) - let appIcon = AppIconManager.allAppIcons.first { $0.title == settings.appIcon } ?? .main - if appIconManager.appIcon != appIcon { - appIconManager.appIcon = appIcon - } + let contacts = try fullBackup.contacts.map { try ContactBookManager.decrypt(crypto: $0, passphrase: passphrase) } + + let customSources = try evmSyncSourceManager.decrypt(sources: fullBackup.settings.evmSyncSources.custom, passphrase: passphrase) + + return RawFullBackup( + accounts: wallets, + watchlistIds: fullBackup.watchlistIds, + contacts: contacts ?? [], + settings: fullBackup.settings, + customSyncSources: customSources + ) + } + + func encrypt(raw: RawFullBackup, passphrase: String) throws -> FullBackup { + let wallets = try raw.accounts.map { + try Self.encrypt(account: $0.account, wallets: $0.enabledWallets, passphrase: passphrase) } + + let contacts = try ContactBookManager.encrypt(contacts: raw.contacts, passphrase: passphrase) + let custom = try evmSyncSourceManager.encrypt(sources: raw.customSyncSources, passphrase: passphrase) + + return FullBackup( + id: UUID().uuidString, + wallets: wallets, + watchlistIds: raw.watchlistIds, + contacts: contacts, + settings: settings(evmSyncSources: .init(selected: raw.settings.evmSyncSources.selected, custom: custom)), + version: AppBackupProvider.version, + timestamp: Date().timeIntervalSince1970.rounded() + ) } -} -extension AppBackupProvider { - static func walletBackup(accountType: AccountType, wallets: [WalletBackup.EnabledWallet], isManualBackedUp: Bool, isFileBackedUp: Bool, name: String, passphrase: String) throws -> RestoreCloudModule.RestoredBackup { - let message = accountType.uniqueId(hashed: false) - let crypto = try BackupCrypto.instance(data: message, passphrase: passphrase) + static func encrypt(account: Account, wallets: [WalletBackup.EnabledWallet], passphrase: String) throws -> RestoreCloudModule.RestoredBackup { + let message = account.type.uniqueId(hashed: false) + let crypto = try BackupCrypto.encrypt(data: message, passphrase: passphrase) let walletBackup = WalletBackup( crypto: crypto, enabledWallets: wallets, - id: accountType.uniqueId().hs.hex, - type: AccountType.Abstract(accountType), - isManualBackedUp: isManualBackedUp, - isFileBackedUp: isFileBackedUp, + id: account.type.uniqueId().hs.hex, + type: AccountType.Abstract(account.type), + isManualBackedUp: account.backedUp, + isFileBackedUp: account.fileBackedUp, version: Self.version, timestamp: Date().timeIntervalSince1970.rounded() ) - return .init(name: name, walletBackup: walletBackup) + return .init(name: account.name, walletBackup: walletBackup) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift index af02e1fa27..3eef61fed6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/BakcupFileConfiguration/RestoreFileConfigurationService.swift @@ -35,14 +35,14 @@ extension RestoreFileConfigurationService { .filter { $0.walletBackup.type.isWatch } .count - let contacts = fullBackup.contacts.flatMap { try? ContactBookManager.encode(crypto: $0, passphrase: passphrase) } + 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 ?? 0 + blockchainSourcesCount: fullBackup.settings.evmSyncSources.custom.count ) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift index c8ad21d080..40ff63bced 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift @@ -31,34 +31,25 @@ extension RestorePassphraseService { func next() async throws -> RestoreResult { switch restoredBackup.source { case let .wallet(walletBackup): - let accountType = try walletBackup - .crypto - .accountType(type: walletBackup.type, passphrase: passphrase) - let restoredBackup = RestoreCloudModule.RestoredBackup(name: restoredBackup.name, walletBackup: walletBackup) - appBackupProvider.walletRestore(backup: restoredBackup, accountType: accountType) - switch accountType { + let rawBackup = try appBackupProvider.decrypt(walletBackup: walletBackup, name: restoredBackup.name, passphrase: passphrase) + appBackupProvider.restore(raw: rawBackup) + switch rawBackup.account.type { case .cex: return .success default: - return .restoredAccount(RestoreCloudModule.RestoredAccount( - name: restoredBackup.name, - accountType: accountType, - isManualBackedUp: walletBackup.isManualBackedUp, - isFileBackedUp: walletBackup.isFileBackedUp, - showSelectCoins: walletBackup.enabledWallets.isEmpty - )) + return .restoredAccount(rawBackup) } - case .full: - print("Lets try!") - return .success + case let .full(fullBackup): + let rawBackup = try appBackupProvider.decrypt(fullBackup: fullBackup, passphrase: passphrase) + return .restoredFullBackup(rawBackup) } } } extension RestorePassphraseService { enum RestoreResult { - case restoredAccount(RestoreCloudModule.RestoredAccount) - case source(BackupModule.Source) + case restoredAccount(RawWalletBackup) + case restoredFullBackup(RawFullBackup) case success } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift index 5a14b0d4b9..cffa763a92 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewController.swift @@ -99,12 +99,12 @@ class RestorePassphraseViewController: KeyboardAwareViewController { viewModel.openSelectCoinsPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] backupAccount in + .sink { [weak self] account in self?.openSelectCoins( - accountName: backupAccount.name, - accountType: backupAccount.accountType, - isManualBackedUp: backupAccount.isManualBackedUp, - isFileBackedUp: backupAccount.isFileBackedUp + accountName: account.name, + accountType: account.type, + isManualBackedUp: account.backedUp, + isFileBackedUp: account.fileBackedUp ) } .store(in: &cancellables) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift index c313b81d19..374adadebf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseViewModel.swift @@ -11,7 +11,7 @@ class RestorePassphraseViewModel { private let clearInputsSubject = PassthroughSubject() private let showErrorSubject = PassthroughSubject() - private let openSelectCoinsSubject = PassthroughSubject() + private let openSelectCoinsSubject = PassthroughSubject() private let successSubject = PassthroughSubject() init(service: RestorePassphraseService) { @@ -34,7 +34,7 @@ extension RestorePassphraseViewModel { showErrorSubject.eraseToAnyPublisher() } - var openSelectCoinsPublisher: AnyPublisher { + var openSelectCoinsPublisher: AnyPublisher { openSelectCoinsSubject.eraseToAnyPublisher() } @@ -67,14 +67,14 @@ extension RestorePassphraseViewModel { switch result { case .success: self?.successSubject.send() - case let .restoredAccount(account): - if account.showSelectCoins { - self?.openSelectCoinsSubject.send(account) + case let .restoredAccount(rawBackup): + if rawBackup.enabledWallets.isEmpty { + self?.openSelectCoinsSubject.send(rawBackup.account) } else { self?.successSubject.send() } - case let .source(source): - print("Open next source list") + case let .restoredFullBackup(rawBackup): + print("Result: \(rawBackup)") } } catch { switch error as? RestoreCloudModule.RestoreError { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift index e0373a8722..8e52d037ae 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift @@ -162,30 +162,6 @@ extension BackupAppViewModel { blockchainSourcesCount: evmSyncSourceManager.customSyncSources(blockchainType: nil).count ) } - - var configuration: [AppBackupProvider.Field] { - var fields = [AppBackupProvider.Field.settings] - - var accountIds = accounts(watch: true).map { $0.id } - selected.forEach { id, selected in - if selected { - accountIds.append(id) - } - } - - fields.append(.accounts(ids: accountIds)) - - let contacts = contactManager.all ?? [] - if contacts.count != 0 { - fields.append(.contacts) - } - - if !favoritesManager.allCoinUids.isEmpty { - fields.append(.watchlist) - } - - return fields - } } extension BackupAppViewModel { @@ -285,7 +261,7 @@ extension BackupAppViewModel { case .none: () case .cloud: do { - try cloudBackupManager.save(fields: configuration, passphrase: password, name: name) + try cloudBackupManager.save(accountIds: accountIds, passphrase: password, name: name) passwordButtonProcessing = false await showSuccess() dismissSubject.send() @@ -295,7 +271,7 @@ extension BackupAppViewModel { } case .local: do { - let url = try cloudBackupManager.file(fields: configuration, passphrase: password, name: name) + let url = try cloudBackupManager.file(accountIds: accountIds, passphrase: password, name: name) sharePresented = url passwordButtonProcessing = false } catch { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapProviderManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapProviderManager.swift index fa1a0b7e5b..969b379d1c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapProviderManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/SwapProviderManager.swift @@ -1,23 +1,23 @@ -import UIKit import MarketKit -import SectionsTableView -import RxSwift +import OneInchKit import RxCocoa +import RxSwift +import SectionsTableView +import UIKit import UniswapKit -import OneInchKit class SwapProviderManager { private let localStorage: LocalStorage private let evmBlockchainManager: EvmBlockchainManager - private let dataSourceUpdatedRelay = PublishRelay<()>() + private let dataSourceUpdatedRelay = PublishRelay() private(set) var dataSourceProvider: ISwapProvider? { didSet { dataSourceUpdatedRelay.accept(()) } } - private let dexUpdatedRelay = PublishRelay<()>() + private let dexUpdatedRelay = PublishRelay() var dex: SwapModule.Dex? { didSet { dexUpdatedRelay.accept(()) @@ -63,11 +63,9 @@ class SwapProviderManager { return OneInchModule(dex: dex, dataSourceState: state) } } - } extension SwapProviderManager: ISwapDexManager { - func set(provider: SwapModule.Dex.Provider) { guard provider != dex?.provider else { return @@ -88,14 +86,12 @@ extension SwapProviderManager: ISwapDexManager { dataSourceProvider = self.provider(dex: dex) } - var dexUpdated: Signal<()> { + var dexUpdated: Signal { dexUpdatedRelay.asSignal() } - } extension SwapProviderManager: ISwapDataSourceManager { - var dataSource: ISwapDataSource? { dataSourceProvider?.dataSource } @@ -104,8 +100,7 @@ extension SwapProviderManager: ISwapDataSourceManager { dataSourceProvider?.settingsDataSource } - var dataSourceUpdated: Signal<()> { + var dataSourceUpdated: Signal { dataSourceUpdatedRelay.asSignal() } - } From b78981de9615b04e155d8faeeb99e1d685f829f8 Mon Sep 17 00:00:00 2001 From: ant013 Date: Mon, 9 Oct 2023 16:57:34 +0600 Subject: [PATCH 47/63] 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"; From 633386ad5e32d21376bd9d77f07747b851094104 Mon Sep 17 00:00:00 2001 From: Ermat Date: Mon, 9 Oct 2023 14:42:45 +0600 Subject: [PATCH 48/63] Extract chart into ChartUiView --- .../project.pbxproj | 6 + .../Modules/Chart/ChartCell.swift | 389 +---------------- .../Modules/Chart/ChartUiView.swift | 403 ++++++++++++++++++ 3 files changed, 420 insertions(+), 378 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 07fb6f7fff..52bc5525c9 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -443,6 +443,7 @@ 11B3553109794AE192BF7591 /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; 11B35531B3F80D06EF040301 /* CoinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B2D07C69579BAEC997 /* CoinType.swift */; }; 11B355342F86DF79AE7000B9 /* Cex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D6718A1DEB73A0CEC02 /* Cex.swift */; }; + 11B35538EF749777CF7B2E8B /* ChartUiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */; }; 11B3553AD73FD1179249F277 /* SecondaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */; }; 11B3553D2E9EEC05401B724A /* CexDepositViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0A0EC524FBC663BEA5 /* CexDepositViewItemFactory.swift */; }; 11B3553D410457FB50DEFAE4 /* ManageAccountsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A8342513D5834B2145A /* ManageAccountsViewModel.swift */; }; @@ -567,6 +568,7 @@ 11B356D9DACA3E988BEDB3B4 /* ActivateSubscriptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DFBFBF34277E7FC3325 /* ActivateSubscriptionViewModel.swift */; }; 11B356DA0B90ECAB25C520B7 /* CoinTreasury.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35185ECC372A193D00A00 /* CoinTreasury.swift */; }; 11B356DC58C5312D5034D30E /* RecoveryPhraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BCBAD15E32459826712 /* RecoveryPhraseViewModel.swift */; }; + 11B356DF455592656B742485 /* ChartUiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */; }; 11B356E555F3ACDA6324FA77 /* AlertItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35163E7C4454BBA9E2E9E /* AlertItemCell.swift */; }; 11B356E87B4A9AACBFEC7506 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D393EDFE4F015B0DEA /* Address.swift */; }; 11B356ECE0D41F928FCED96C /* PrimaryButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354E55E901615862E7CD4 /* PrimaryButtonCell.swift */; }; @@ -3363,6 +3365,7 @@ 11B35DDED1BC5B541DB6B4B3 /* CexDepositNetworkSelectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectViewModel.swift; sourceTree = ""; }; 11B35DE604E9725EB8B67A69 /* RestoreSelectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSelectViewController.swift; sourceTree = ""; }; 11B35DE76BBABD8F0914A0D2 /* NftActivityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityViewModel.swift; sourceTree = ""; }; + 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartUiView.swift; sourceTree = ""; }; 11B35DF08505C3A7CB1BBBB4 /* MarkdownViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownViewController.swift; sourceTree = ""; }; 11B35DFA83DA24A00D73EA7D /* RestoreSettingRecord_v_0_25.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingRecord_v_0_25.swift; sourceTree = ""; }; 11B35DFBFBF34277E7FC3325 /* ActivateSubscriptionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionViewModel.swift; sourceTree = ""; }; @@ -7556,6 +7559,7 @@ ABC9A9F6635146BEBFB432D1 /* ChartCell.swift */, ABC9A76ACF7C7D6D7D3FA323 /* Components */, ABC9A1CF38A26663C93F47B4 /* MarketCards */, + 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */, ); path = Chart; sourceTree = ""; @@ -9469,6 +9473,7 @@ ABC9AF309AAE5C54D2020B23 /* RawFullBackup.swift in Sources */, ABC9A67C2D782AD0DFDF0C3C /* RestoreFileConfigurationViewController.swift in Sources */, ABC9ACEB81BCB00435B35F64 /* RestoreFileHelper.swift in Sources */, + 11B35538EF749777CF7B2E8B /* ChartUiView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10797,6 +10802,7 @@ ABC9A37FB71FA7DA14553EFC /* RawFullBackup.swift in Sources */, ABC9A0C5DE01B3C50D4C7FF2 /* RestoreFileConfigurationViewController.swift in Sources */, ABC9A3231731F39ECA5B90ED /* RestoreFileHelper.swift in Sources */, + 11B356DF455592656B742485 /* ChartUiView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift index 074df7bda6..58ec929218 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift @@ -1,58 +1,12 @@ -import UIKit -import SnapKit -import RxSwift -import ThemeKit -import ComponentKit import Chart -import HUD +import SnapKit +import UIKit class ChartCell: UITableViewCell { - private let viewModel: IChartViewModel & IChartViewTouchDelegate - private let configuration: ChartConfiguration - private let disposeBag = DisposeBag() + private let chartView: ChartUiView - private let currentValueWrapper = UIView() - private let currentValueStackView = UIStackView() - private let currentValueLabel = UILabel() - private let currentValueDescriptionLabel = DiffLabel() - private let currentDiffLabel = DiffLabel() - private let currentSecondaryTitleLabel = UILabel() - private let currentSecondaryValueLabel = UILabel() - private let currentSecondaryDiffLabel = DiffLabel() - - private let chartInfoWrapper = UIStackView() - private let chartValueLabel = UILabel() - private let chartDiffLabel = DiffLabel() - private let chartTimeLabel = UILabel() - private let chartSecondaryTitleLabel = UILabel() - private let chartSecondaryValueLabel = UILabel() - private let chartSecondaryDiffLabel = DiffLabel() - - private let chartView: RateChartView - private let timePeriodView = FilterView(buttonStyle: .transparent, bottomSeparator: false) - private let loadingView = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderView() - - private var viewItem: ChartModule.ViewItem? - private var showIndicators: Bool = true { - didSet { - syncChart(viewItem: viewItem) - } - } - - private static let percentFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .percent - formatter.roundingMode = .halfEven - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 4 - return formatter - }() - - init(viewModel: IChartViewModel & IChartViewTouchDelegate, configuration: ChartConfiguration, isLast: Bool = false) { - self.viewModel = viewModel - self.configuration = configuration - chartView = RateChartView(configuration: configuration) + init(viewModel: IChartViewModel & IChartViewTouchDelegate, configuration: ChartConfiguration) { + chartView = ChartUiView(viewModel: viewModel, configuration: configuration) super.init(style: .default, reuseIdentifier: nil) @@ -60,345 +14,24 @@ class ChartCell: UITableViewCell { contentView.backgroundColor = .clear selectionStyle = .none - contentView.addSubview(currentValueWrapper) - currentValueWrapper.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - make.height.equalTo(CGFloat.heightDoubleLineCell) - } - - currentValueWrapper.addSubview(currentValueStackView) - currentValueStackView.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(CGFloat.margin16) - make.centerY.equalToSuperview() - } - - currentValueStackView.alignment = .firstBaseline - currentValueStackView.spacing = .margin4 - - currentValueStackView.addArrangedSubview(currentValueLabel) - currentValueLabel.font = .title3 - currentValueLabel.textColor = .themeLeah - - currentValueStackView.addArrangedSubview(currentValueDescriptionLabel) - currentValueDescriptionLabel.font = .subhead1 - currentValueDescriptionLabel.textColor = .themeGray - - currentValueStackView.addArrangedSubview(currentDiffLabel) - currentDiffLabel.font = .subhead1 - - let currentSecondaryStackView = UIStackView() - - currentValueWrapper.addSubview(currentSecondaryStackView) - currentSecondaryStackView.snp.makeConstraints { make in - make.leading.equalTo(currentValueStackView.snp.trailing).offset(CGFloat.margin12) - make.trailing.equalToSuperview().inset(CGFloat.margin16) - make.centerY.equalToSuperview() - } - - currentSecondaryStackView.axis = .vertical - currentSecondaryStackView.alignment = .trailing - currentSecondaryStackView.spacing = .margin4 - - currentSecondaryStackView.addArrangedSubview(currentSecondaryTitleLabel) - currentSecondaryTitleLabel.font = .subhead2 - currentSecondaryTitleLabel.textColor = .themeGray - - let currentSecondaryValueStackView = UIStackView() - - currentSecondaryStackView.addArrangedSubview(currentSecondaryValueStackView) - currentSecondaryValueStackView.spacing = .margin4 - - currentSecondaryValueStackView.addArrangedSubview(currentSecondaryValueLabel) - currentSecondaryValueLabel.font = .subhead2 - currentSecondaryValueLabel.textColor = .themeJacob - - currentSecondaryValueStackView.addArrangedSubview(currentSecondaryDiffLabel) - currentSecondaryDiffLabel.font = .subhead2 - - contentView.addSubview(chartInfoWrapper) - chartInfoWrapper.snp.makeConstraints { make in - make.edges.equalTo(currentValueWrapper) - } - - let chartInfoStackView = UIStackView() - - chartInfoWrapper.addSubview(chartInfoStackView) - chartInfoStackView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(CGFloat.margin16) - make.centerY.equalToSuperview() - } - - chartInfoStackView.axis = .vertical - chartInfoStackView.spacing = 1 - - let chartInfoTopStackView = UIStackView() - - chartInfoStackView.addArrangedSubview(chartInfoTopStackView) - chartInfoTopStackView.spacing = .margin12 - chartInfoTopStackView.alignment = .leading - - let chartInfoValueStackView = UIStackView() - - chartInfoTopStackView.addArrangedSubview(chartInfoValueStackView) - chartInfoValueStackView.spacing = .margin4 - chartInfoValueStackView.alignment = .center - - chartInfoValueStackView.addArrangedSubview(chartValueLabel) - chartValueLabel.setContentHuggingPriority(.required, for: .horizontal) - chartValueLabel.font = .headline2 - chartValueLabel.textColor = .themeLeah - - chartInfoValueStackView.addArrangedSubview(chartDiffLabel) - chartDiffLabel.font = .subhead1 - - chartInfoTopStackView.addArrangedSubview(chartSecondaryTitleLabel) - chartSecondaryTitleLabel.textAlignment = .right - chartSecondaryTitleLabel.adjustsFontSizeToFitWidth = true - chartSecondaryTitleLabel.minimumScaleFactor = 0.5 - chartSecondaryTitleLabel.font = .subhead2 - chartSecondaryTitleLabel.textColor = .themeGray - - let chartInfoBottomStackView = UIStackView() - - chartInfoStackView.addArrangedSubview(chartInfoBottomStackView) - chartInfoBottomStackView.spacing = .margin12 - chartInfoBottomStackView.alignment = .trailing - - chartInfoBottomStackView.addArrangedSubview(chartTimeLabel) - chartTimeLabel.font = .subhead2 - chartTimeLabel.textColor = .themeGray - - let chartInfoSecondaryValueStackView = UIStackView() - - chartInfoBottomStackView.addArrangedSubview(chartInfoSecondaryValueStackView) - chartInfoSecondaryValueStackView.spacing = .margin4 - - chartInfoSecondaryValueStackView.addArrangedSubview(chartSecondaryValueLabel) - chartSecondaryValueLabel.font = .subhead2 - chartSecondaryValueLabel.adjustsFontSizeToFitWidth = true - chartSecondaryValueLabel.minimumScaleFactor = 0.5 - - chartInfoSecondaryValueStackView.addArrangedSubview(chartSecondaryDiffLabel) - chartSecondaryDiffLabel.font = .subhead2 - contentView.addSubview(chartView) - chartView.snp.makeConstraints { maker in - maker.top.equalTo(currentValueWrapper.snp.bottom) - maker.leading.trailing.equalToSuperview() - maker.height.equalTo(configuration.mainHeight + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0)) - } - - chartView.delegate = viewModel - chartView.setVolumes(hidden: !configuration.showIndicatorArea) - - contentView.addSubview(timePeriodView) - timePeriodView.snp.makeConstraints { maker in - maker.top.equalTo(chartView.snp.bottom).offset(CGFloat.margin8) - maker.leading.trailing.equalToSuperview() - maker.height.equalTo(CGFloat.heightCell48) - } - - timePeriodView.backgroundColor = .clear - timePeriodView.reload(filters: viewModel.intervals.map { .item(title: $0) }) - - contentView.addSubview(loadingView) - loadingView.snp.makeConstraints { maker in - maker.center.equalTo(chartView) + chartView.snp.makeConstraints { make in + make.edges.equalToSuperview() } - - contentView.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.leading.top.trailing.equalToSuperview() - maker.bottom.equalTo(chartView) - } - - errorView.image = UIImage(named: "sync_error_48") - errorView.text = "sync_error".localized } - required init?(coder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } var cellHeight: CGFloat { - .heightDoubleLineCell + configuration.mainHeight + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0) + .margin8 + .heightCell48 + .margin8 - } - - private func syncChart(viewItem: ChartModule.ViewItem?) { - self.viewItem = viewItem - if let viewItem { - currentValueWrapper.isHidden = false - chartView.isHidden = false - - if let value = viewItem.value { - currentValueLabel.isHidden = false - currentValueLabel.text = value - } else { - currentValueLabel.isHidden = true - } - - if let valueDescription = viewItem.valueDescription { - currentValueDescriptionLabel.isHidden = false - currentValueDescriptionLabel.text = valueDescription - } else { - currentValueDescriptionLabel.isHidden = true - } - - if let value = viewItem.chartDiff { - currentDiffLabel.isHidden = false - currentDiffLabel.set(value: value) - } else { - currentDiffLabel.isHidden = true - } - - switch viewItem.rightSideMode { - case .none, .volume, .indicators: - currentSecondaryTitleLabel.isHidden = true - currentSecondaryValueLabel.isHidden = true - currentSecondaryDiffLabel.isHidden = true - case .dominance(let value, let diff): - currentSecondaryTitleLabel.isHidden = false - currentSecondaryValueLabel.isHidden = false - - currentSecondaryTitleLabel.text = "BTC Dominance" - currentSecondaryValueLabel.text = value.flatMap { Self.percentFormatter.string(from: ($0 / 100) as NSNumber) } - currentSecondaryValueLabel.textColor = .themeJacob - - if let diff { - currentSecondaryDiffLabel.isHidden = false - currentSecondaryDiffLabel.set(value: diff) - } else { - currentSecondaryDiffLabel.isHidden = true - } - } - - if !chartView.isPressed { - chartView.setCurve(colorType: viewItem.chartTrend.chartColorType) - } - chartView.set(chartData: viewItem.chartData, indicators: viewItem.indicators, showIndicators: showIndicators, animated: true) - chartView.set(highLimitText: viewItem.maxValue, lowLimitText: viewItem.minValue) - } else { - currentValueWrapper.isHidden = true - chartView.isHidden = true - } - } - - private func syncChart(selectedViewItem: ChartModule.SelectedPointViewItem?) { - guard let viewItem = selectedViewItem else { - UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn]) { [weak self] in - self?.currentValueWrapper.alpha = 1 - } - UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut]) { [weak self] in - self?.chartInfoWrapper.alpha = 0 - } - return - } - - chartValueLabel.text = viewItem.value - chartTimeLabel.text = viewItem.date - - if let diff = viewItem.diff { - chartDiffLabel.isHidden = false - chartDiffLabel.set(value: diff) - } else { - chartDiffLabel.isHidden = true - } - - switch viewItem.rightSideMode { - case .none: - chartSecondaryTitleLabel.isHidden = true - chartSecondaryValueLabel.isHidden = true - chartSecondaryDiffLabel.isHidden = true - case .volume(let value): - if let value = value { - chartSecondaryTitleLabel.isHidden = true - chartSecondaryValueLabel.isHidden = false - - chartSecondaryValueLabel.text = value - chartSecondaryValueLabel.textColor = .themeGray - } else { - chartSecondaryTitleLabel.isHidden = true - chartSecondaryValueLabel.isHidden = true - } - - chartSecondaryDiffLabel.isHidden = true - case .dominance(let value, let diff): - chartSecondaryTitleLabel.isHidden = false - chartSecondaryValueLabel.isHidden = false - - chartSecondaryTitleLabel.text = "BTC Dominance" - chartSecondaryValueLabel.text = value.flatMap { Self.percentFormatter.string(from: ($0 / 100) as NSNumber) } - chartSecondaryValueLabel.textColor = .themeJacob - - if let diff { - chartSecondaryDiffLabel.isHidden = false - chartSecondaryDiffLabel.set(value: diff) - } else { - chartSecondaryDiffLabel.isHidden = true - } - case .indicators(let top, let bottom): - chartSecondaryTitleLabel.isHidden = false - chartSecondaryValueLabel.isHidden = false - chartSecondaryTitleLabel.attributedText = top - chartSecondaryTitleLabel.lineBreakMode = .byTruncatingTail - chartSecondaryValueLabel.attributedText = bottom - chartSecondaryValueLabel.lineBreakMode = .byTruncatingTail - chartSecondaryDiffLabel.isHidden = true - } - - UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut]) { [weak self] in - self?.currentValueWrapper.alpha = 0 - } - UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn]) { [weak self] in - self?.chartInfoWrapper.alpha = 1 - } - } - - private func syncChart(typeIndex: Int) { - timePeriodView.select(index: typeIndex) - } - - private func syncIntervals(typeIndex: Int) { - timePeriodView.reload(filters: viewModel.intervals.map { .item(title: $0) }) - syncChart(typeIndex: typeIndex) - } - - private func syncChart(showIndicators: Bool) { - self.showIndicators = showIndicators + chartView.totalHeight } - - private func syncChart(loading: Bool) { - chartView.isUserInteractionEnabled = !loading - loadingView.isHidden = !loading - - if loading { - loadingView.startAnimating() - } else { - loadingView.stopAnimating() - } - } - - private func syncChart(error: Bool) { - errorView.isHidden = !error - } - } extension ChartCell { - func onLoad() { - subscribe(disposeBag, viewModel.pointSelectedItemDriver) { [weak self] in self?.syncChart(selectedViewItem: $0) } - subscribe(disposeBag, viewModel.intervalIndexDriver) { [weak self] in self?.syncChart(typeIndex: $0) } - subscribe(disposeBag, viewModel.intervalsUpdatedWithCurrentIndexDriver) { [weak self] in self?.syncIntervals(typeIndex: $0) } - - subscribe(disposeBag, viewModel.indicatorsShownDriver) { [weak self] in self?.syncChart(showIndicators: $0) } - subscribe(disposeBag, viewModel.loadingDriver) { [weak self] in self?.syncChart(loading: $0) } - subscribe(disposeBag, viewModel.errorDriver) { [weak self] in self?.syncChart(error: $0) } - subscribe(disposeBag, viewModel.chartInfoDriver) { [weak self] in self?.syncChart(viewItem: $0) } - - timePeriodView.onSelect = { [weak self] index in - self?.viewModel.onSelectInterval(at: index) - } + chartView.onLoad() } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift new file mode 100644 index 0000000000..01ac58e93f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift @@ -0,0 +1,403 @@ +import Chart +import ComponentKit +import HUD +import RxSwift +import SnapKit +import ThemeKit +import UIKit + +class ChartUiView: UIView { + private let viewModel: IChartViewModel & IChartViewTouchDelegate + private let configuration: ChartConfiguration + private let disposeBag = DisposeBag() + + private let currentValueWrapper = UIView() + private let currentValueStackView = UIStackView() + private let currentValueLabel = UILabel() + private let currentValueDescriptionLabel = DiffLabel() + private let currentDiffLabel = DiffLabel() + private let currentSecondaryTitleLabel = UILabel() + private let currentSecondaryValueLabel = UILabel() + private let currentSecondaryDiffLabel = DiffLabel() + + private let chartInfoWrapper = UIStackView() + private let chartValueLabel = UILabel() + private let chartDiffLabel = DiffLabel() + private let chartTimeLabel = UILabel() + private let chartSecondaryTitleLabel = UILabel() + private let chartSecondaryValueLabel = UILabel() + private let chartSecondaryDiffLabel = DiffLabel() + + private let chartView: RateChartView + private let timePeriodView = FilterView(buttonStyle: .transparent, bottomSeparator: false) + private let loadingView = HUDActivityView.create(with: .medium24) + private let errorView = PlaceholderView() + + private var viewItem: ChartModule.ViewItem? + private var showIndicators: Bool = true { + didSet { + syncChart(viewItem: viewItem) + } + } + + private static let percentFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.roundingMode = .halfEven + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 4 + return formatter + }() + + init(viewModel: IChartViewModel & IChartViewTouchDelegate, configuration: ChartConfiguration) { + self.viewModel = viewModel + self.configuration = configuration + chartView = RateChartView(configuration: configuration) + + super.init(frame: .zero) + + backgroundColor = .clear + + addSubview(currentValueWrapper) + currentValueWrapper.snp.makeConstraints { make in + make.leading.top.trailing.equalToSuperview() + make.height.equalTo(CGFloat.heightDoubleLineCell) + } + + currentValueWrapper.addSubview(currentValueStackView) + currentValueStackView.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(CGFloat.margin16) + make.centerY.equalToSuperview() + } + + currentValueStackView.alignment = .firstBaseline + currentValueStackView.spacing = .margin4 + + currentValueStackView.addArrangedSubview(currentValueLabel) + currentValueLabel.font = .title3 + currentValueLabel.textColor = .themeLeah + + currentValueStackView.addArrangedSubview(currentValueDescriptionLabel) + currentValueDescriptionLabel.font = .subhead1 + currentValueDescriptionLabel.textColor = .themeGray + + currentValueStackView.addArrangedSubview(currentDiffLabel) + currentDiffLabel.font = .subhead1 + + let currentSecondaryStackView = UIStackView() + + currentValueWrapper.addSubview(currentSecondaryStackView) + currentSecondaryStackView.snp.makeConstraints { make in + make.leading.equalTo(currentValueStackView.snp.trailing).offset(CGFloat.margin12) + make.trailing.equalToSuperview().inset(CGFloat.margin16) + make.centerY.equalToSuperview() + } + + currentSecondaryStackView.axis = .vertical + currentSecondaryStackView.alignment = .trailing + currentSecondaryStackView.spacing = .margin4 + + currentSecondaryStackView.addArrangedSubview(currentSecondaryTitleLabel) + currentSecondaryTitleLabel.font = .subhead2 + currentSecondaryTitleLabel.textColor = .themeGray + + let currentSecondaryValueStackView = UIStackView() + + currentSecondaryStackView.addArrangedSubview(currentSecondaryValueStackView) + currentSecondaryValueStackView.spacing = .margin4 + + currentSecondaryValueStackView.addArrangedSubview(currentSecondaryValueLabel) + currentSecondaryValueLabel.font = .subhead2 + currentSecondaryValueLabel.textColor = .themeJacob + + currentSecondaryValueStackView.addArrangedSubview(currentSecondaryDiffLabel) + currentSecondaryDiffLabel.font = .subhead2 + + addSubview(chartInfoWrapper) + chartInfoWrapper.snp.makeConstraints { make in + make.edges.equalTo(currentValueWrapper) + } + + let chartInfoStackView = UIStackView() + + chartInfoWrapper.addSubview(chartInfoStackView) + chartInfoStackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(CGFloat.margin16) + make.centerY.equalToSuperview() + } + + chartInfoStackView.axis = .vertical + chartInfoStackView.spacing = 1 + + let chartInfoTopStackView = UIStackView() + + chartInfoStackView.addArrangedSubview(chartInfoTopStackView) + chartInfoTopStackView.spacing = .margin12 + chartInfoTopStackView.alignment = .leading + + let chartInfoValueStackView = UIStackView() + + chartInfoTopStackView.addArrangedSubview(chartInfoValueStackView) + chartInfoValueStackView.spacing = .margin4 + chartInfoValueStackView.alignment = .center + + chartInfoValueStackView.addArrangedSubview(chartValueLabel) + chartValueLabel.setContentHuggingPriority(.required, for: .horizontal) + chartValueLabel.font = .headline2 + chartValueLabel.textColor = .themeLeah + + chartInfoValueStackView.addArrangedSubview(chartDiffLabel) + chartDiffLabel.font = .subhead1 + + chartInfoTopStackView.addArrangedSubview(chartSecondaryTitleLabel) + chartSecondaryTitleLabel.textAlignment = .right + chartSecondaryTitleLabel.adjustsFontSizeToFitWidth = true + chartSecondaryTitleLabel.minimumScaleFactor = 0.5 + chartSecondaryTitleLabel.font = .subhead2 + chartSecondaryTitleLabel.textColor = .themeGray + + let chartInfoBottomStackView = UIStackView() + + chartInfoStackView.addArrangedSubview(chartInfoBottomStackView) + chartInfoBottomStackView.spacing = .margin12 + chartInfoBottomStackView.alignment = .trailing + + chartInfoBottomStackView.addArrangedSubview(chartTimeLabel) + chartTimeLabel.font = .subhead2 + chartTimeLabel.textColor = .themeGray + + let chartInfoSecondaryValueStackView = UIStackView() + + chartInfoBottomStackView.addArrangedSubview(chartInfoSecondaryValueStackView) + chartInfoSecondaryValueStackView.spacing = .margin4 + + chartInfoSecondaryValueStackView.addArrangedSubview(chartSecondaryValueLabel) + chartSecondaryValueLabel.font = .subhead2 + chartSecondaryValueLabel.adjustsFontSizeToFitWidth = true + chartSecondaryValueLabel.minimumScaleFactor = 0.5 + + chartInfoSecondaryValueStackView.addArrangedSubview(chartSecondaryDiffLabel) + chartSecondaryDiffLabel.font = .subhead2 + + addSubview(chartView) + chartView.snp.makeConstraints { maker in + maker.top.equalTo(currentValueWrapper.snp.bottom) + maker.leading.trailing.equalToSuperview() + maker.height.equalTo(configuration.mainHeight + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0)) + } + + chartView.delegate = viewModel + chartView.setVolumes(hidden: !configuration.showIndicatorArea) + + addSubview(timePeriodView) + timePeriodView.snp.makeConstraints { maker in + maker.top.equalTo(chartView.snp.bottom).offset(CGFloat.margin8) + maker.leading.trailing.equalToSuperview() + maker.height.equalTo(CGFloat.heightCell48) + } + + timePeriodView.backgroundColor = .clear + timePeriodView.reload(filters: viewModel.intervals.map { .item(title: $0) }) + + addSubview(loadingView) + loadingView.snp.makeConstraints { maker in + maker.center.equalTo(chartView) + } + + addSubview(errorView) + errorView.snp.makeConstraints { maker in + maker.leading.top.trailing.equalToSuperview() + maker.bottom.equalTo(chartView) + } + + errorView.image = UIImage(named: "sync_error_48") + errorView.text = "sync_error".localized + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var totalHeight: CGFloat { + .heightDoubleLineCell + + configuration.mainHeight + + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0) + + .margin8 + .heightCell48 + .margin8 + } + + private func syncChart(viewItem: ChartModule.ViewItem?) { + self.viewItem = viewItem + if let viewItem { + currentValueWrapper.isHidden = false + chartView.isHidden = false + + if let value = viewItem.value { + currentValueLabel.isHidden = false + currentValueLabel.text = value + } else { + currentValueLabel.isHidden = true + } + + if let valueDescription = viewItem.valueDescription { + currentValueDescriptionLabel.isHidden = false + currentValueDescriptionLabel.text = valueDescription + } else { + currentValueDescriptionLabel.isHidden = true + } + + if let value = viewItem.chartDiff { + currentDiffLabel.isHidden = false + currentDiffLabel.set(value: value) + } else { + currentDiffLabel.isHidden = true + } + + switch viewItem.rightSideMode { + case .none, .volume, .indicators: + currentSecondaryTitleLabel.isHidden = true + currentSecondaryValueLabel.isHidden = true + currentSecondaryDiffLabel.isHidden = true + case let .dominance(value, diff): + currentSecondaryTitleLabel.isHidden = false + currentSecondaryValueLabel.isHidden = false + + currentSecondaryTitleLabel.text = "BTC Dominance" + currentSecondaryValueLabel.text = value.flatMap { Self.percentFormatter.string(from: ($0 / 100) as NSNumber) } + currentSecondaryValueLabel.textColor = .themeJacob + + if let diff { + currentSecondaryDiffLabel.isHidden = false + currentSecondaryDiffLabel.set(value: diff) + } else { + currentSecondaryDiffLabel.isHidden = true + } + } + + if !chartView.isPressed { + chartView.setCurve(colorType: viewItem.chartTrend.chartColorType) + } + chartView.set(chartData: viewItem.chartData, indicators: viewItem.indicators, showIndicators: showIndicators, animated: true) + chartView.set(highLimitText: viewItem.maxValue, lowLimitText: viewItem.minValue) + } else { + currentValueWrapper.isHidden = true + chartView.isHidden = true + } + } + + private func syncChart(selectedViewItem: ChartModule.SelectedPointViewItem?) { + guard let viewItem = selectedViewItem else { + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn]) { [weak self] in + self?.currentValueWrapper.alpha = 1 + } + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut]) { [weak self] in + self?.chartInfoWrapper.alpha = 0 + } + return + } + + chartValueLabel.text = viewItem.value + chartTimeLabel.text = viewItem.date + + if let diff = viewItem.diff { + chartDiffLabel.isHidden = false + chartDiffLabel.set(value: diff) + } else { + chartDiffLabel.isHidden = true + } + + switch viewItem.rightSideMode { + case .none: + chartSecondaryTitleLabel.isHidden = true + chartSecondaryValueLabel.isHidden = true + chartSecondaryDiffLabel.isHidden = true + case let .volume(value): + if let value = value { + chartSecondaryTitleLabel.isHidden = true + chartSecondaryValueLabel.isHidden = false + + chartSecondaryValueLabel.text = value + chartSecondaryValueLabel.textColor = .themeGray + } else { + chartSecondaryTitleLabel.isHidden = true + chartSecondaryValueLabel.isHidden = true + } + + chartSecondaryDiffLabel.isHidden = true + case let .dominance(value, diff): + chartSecondaryTitleLabel.isHidden = false + chartSecondaryValueLabel.isHidden = false + + chartSecondaryTitleLabel.text = "BTC Dominance" + chartSecondaryValueLabel.text = value.flatMap { Self.percentFormatter.string(from: ($0 / 100) as NSNumber) } + chartSecondaryValueLabel.textColor = .themeJacob + + if let diff { + chartSecondaryDiffLabel.isHidden = false + chartSecondaryDiffLabel.set(value: diff) + } else { + chartSecondaryDiffLabel.isHidden = true + } + case let .indicators(top, bottom): + chartSecondaryTitleLabel.isHidden = false + chartSecondaryValueLabel.isHidden = false + chartSecondaryTitleLabel.attributedText = top + chartSecondaryTitleLabel.lineBreakMode = .byTruncatingTail + chartSecondaryValueLabel.attributedText = bottom + chartSecondaryValueLabel.lineBreakMode = .byTruncatingTail + chartSecondaryDiffLabel.isHidden = true + } + + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut]) { [weak self] in + self?.currentValueWrapper.alpha = 0 + } + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn]) { [weak self] in + self?.chartInfoWrapper.alpha = 1 + } + } + + private func syncChart(typeIndex: Int) { + timePeriodView.select(index: typeIndex) + } + + private func syncIntervals(typeIndex: Int) { + timePeriodView.reload(filters: viewModel.intervals.map { .item(title: $0) }) + syncChart(typeIndex: typeIndex) + } + + private func syncChart(showIndicators: Bool) { + self.showIndicators = showIndicators + } + + private func syncChart(loading: Bool) { + chartView.isUserInteractionEnabled = !loading + loadingView.isHidden = !loading + + if loading { + loadingView.startAnimating() + } else { + loadingView.stopAnimating() + } + } + + private func syncChart(error: Bool) { + errorView.isHidden = !error + } +} + +extension ChartUiView { + func onLoad() { + subscribe(disposeBag, viewModel.pointSelectedItemDriver) { [weak self] in self?.syncChart(selectedViewItem: $0) } + subscribe(disposeBag, viewModel.intervalIndexDriver) { [weak self] in self?.syncChart(typeIndex: $0) } + subscribe(disposeBag, viewModel.intervalsUpdatedWithCurrentIndexDriver) { [weak self] in self?.syncIntervals(typeIndex: $0) } + + subscribe(disposeBag, viewModel.indicatorsShownDriver) { [weak self] in self?.syncChart(showIndicators: $0) } + subscribe(disposeBag, viewModel.loadingDriver) { [weak self] in self?.syncChart(loading: $0) } + subscribe(disposeBag, viewModel.errorDriver) { [weak self] in self?.syncChart(error: $0) } + subscribe(disposeBag, viewModel.chartInfoDriver) { [weak self] in self?.syncChart(viewItem: $0) } + + timePeriodView.onSelect = { [weak self] index in + self?.viewModel.onSelectInterval(at: index) + } + } +} From 51822e04f9caa09f264fb9619f096b72c1ae5829 Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 10 Oct 2023 14:17:55 +0600 Subject: [PATCH 49/63] Initial (not complete) implementation of CoinOverview module in SwiftUI --- .../project.pbxproj | 18 +++ .../Modules/Chart/ChartUiView.swift | 4 + .../Modules/Chart/ChartView.swift | 19 +++ .../Coin/CoinChart/CoinChartViewModel.swift | 8 +- .../CoinOverview/CoinOverviewModule.swift | 73 ++++++--- .../Coin/CoinOverview/CoinOverviewView.swift | 135 ++++++++++++++++ .../CoinOverviewViewModelNew.swift | 152 ++++++++++++++++++ .../Main/ChartIndicatorsModule.swift | 23 ++- .../SwiftUI/SecondaryCircleButtonStyle.swift | 2 +- 9 files changed, 410 insertions(+), 24 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 52bc5525c9..4a0d21ac3c 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -338,6 +338,7 @@ 11B353D3A4F2305366835086 /* NftActivityHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EC6F1B4D72D52B4D16 /* NftActivityHeaderView.swift */; }; 11B353DE48A4B088210D927D /* CoinAnalyticsRatingScaleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FDC67CE58FBE44A4107 /* CoinAnalyticsRatingScaleViewController.swift */; }; 11B353E15F4A208D393C7262 /* MarketCategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */; }; + 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B353E61A5496074178741C /* SendAvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */; }; 11B353E7A2462E19D946E723 /* CellComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355436F62829DBE3C92B4 /* CellComponent.swift */; }; 11B353EAF32244B06E44FAD1 /* PrivateKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A9DB4112F41D7FCAC12 /* PrivateKeysViewModel.swift */; }; @@ -624,6 +625,7 @@ 11B357A9F8949912C12A17D7 /* NftCollectionOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351436E090F4C05243103 /* NftCollectionOverviewViewModel.swift */; }; 11B357ADA154348A3C1A987B /* CoinTreasuriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */; }; 11B357AE8B51E09D0EB60D87 /* NftPriceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B451378835F7F060012 /* NftPriceRecord.swift */; }; + 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */; }; 11B357BADA228BE93B8451E7 /* AddEvmSyncSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29037572DDAAF9E16 /* AddEvmSyncSourceViewModel.swift */; }; 11B357BD9D9681D0D79DDEBE /* UITabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350369A891BEA3A525E5B /* UITabBarItem.swift */; }; 11B357BF378060E7E35F7052 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; @@ -783,6 +785,7 @@ 11B35967B7F22E0C689C5220 /* CoinMarketsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E4058159A4FE60A3F53 /* CoinMarketsService.swift */; }; 11B35968A3A43727ED6FB0B7 /* FavoriteCoinRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */; }; 11B35968D5BDA7A46C900548 /* AddressInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A382720D6531AE92F72 /* AddressInputView.swift */; }; + 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B3596F09D52300F7F0067D /* NftCollectionOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */; }; 11B35970257A865B76C0BBB9 /* NftEventMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BACB38FE566F6F575B /* NftEventMetadata.swift */; }; 11B35972FDF15D690466B792 /* SendAvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */; }; @@ -1302,12 +1305,14 @@ 11B35F9CC94DB2BC7B43BB59 /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35F9E1AF528B31C6F383C /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B35F9F489F4B358FCCE893 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543968337A40168D3EB0 /* MarkdownParser.swift */; }; + 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AFE2C95FF73F75652D8 /* ChartView.swift */; }; 11B35FA3A00690573A482BAC /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35FA6F9EE876BD65E9AD6 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */; }; 11B35FAB3263E489CB9017FC /* AddTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */; }; 11B35FB1B7B34756830942DC /* LaunchErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4096D259C9B1540D10 /* LaunchErrorViewController.swift */; }; 11B35FB28152F8881369DD9D /* AdapterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4E49ED2D2BF8E60863 /* AdapterManager.swift */; }; + 11B35FB362526C723329C9ED /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AFE2C95FF73F75652D8 /* ChartView.swift */; }; 11B35FB3A17F76325C98C2AB /* UnlinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A464627DCBABD1AC17 /* UnlinkService.swift */; }; 11B35FB4B6E5E6B442ADE3B2 /* BinanceCexProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E9CE0702287077F975 /* BinanceCexProvider.swift */; }; 11B35FB74A0FAB9385945628 /* CoinReportsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35469D625FF263504536F /* CoinReportsModule.swift */; }; @@ -1319,6 +1324,7 @@ 11B35FC6DE83EE46FB361756 /* CexWithdrawModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507A989EA73EE5E8EA8 /* CexWithdrawModule.swift */; }; 11B35FD18C255E2C6D75F38A /* RestoreMnemonicHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB288AF5A54B99A51E4 /* RestoreMnemonicHintView.swift */; }; 11B35FD73BCF3DD557FD9783 /* RecipientAddressInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DBDADDA8D4F9D88C7AA /* RecipientAddressInputCell.swift */; }; + 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */; }; 11B35FE0809AC8A716C41427 /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */; }; 11B35FE7DA00590FF95854FF /* WatchPublicKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350D2A21FC1BE1F457B41 /* WatchPublicKeyViewModel.swift */; }; 11B35FE8D60BFF31C3104484 /* SwitchAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BC3E707879846AC0AA /* SwitchAccountViewModel.swift */; }; @@ -2773,6 +2779,7 @@ 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopViewController.swift; sourceTree = ""; }; 11B350669B3E9E6155F33F23 /* BaseCurrencySettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsViewModel.swift; sourceTree = ""; }; 11B3506758F70E9014947BB3 /* CexWithdrawConfirmViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawConfirmViewModel.swift; sourceTree = ""; }; + 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewView.swift; sourceTree = ""; }; 11B3506CB3D780A00F4BBBBE /* AccountRecord_v_0_20.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_20.swift; sourceTree = ""; }; 11B35071F0BD63CCE6417ADC /* CexAmountInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAmountInputViewModel.swift; sourceTree = ""; }; 11B3507B0AFFDF51A528A6EE /* RestoreSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsView.swift; sourceTree = ""; }; @@ -2920,6 +2927,7 @@ 11B3534997B5CD413DBDB7C7 /* CoinProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinProvider.swift; sourceTree = ""; }; 11B3534E81EFE21D1F84C130 /* WatchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchViewModel.swift; sourceTree = ""; }; 11B3535FC407BA20765EBCF4 /* KeyboardObservingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObservingViewController.swift; sourceTree = ""; }; + 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewViewModelNew.swift; sourceTree = ""; }; 11B353684493AFDF3711DF2B /* TokenQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenQuery.swift; sourceTree = ""; }; 11B35368FF9DD8600557BF07 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = ""; }; 11B3536CE69BFC7513A9DFDF /* GuidesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidesService.swift; sourceTree = ""; }; @@ -3241,6 +3249,7 @@ 11B35ADF518A2F98FF673B4B /* CoinAuditsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAuditsViewModel.swift; sourceTree = ""; }; 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketFilteredListService.swift; sourceTree = ""; }; 11B35AE5785634316A1A5DA8 /* WalletBlockchainElementService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBlockchainElementService.swift; sourceTree = ""; }; + 11B35AFE2C95FF73F75652D8 /* ChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 11B35B0A0EC524FBC663BEA5 /* CexDepositViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositViewItemFactory.swift; sourceTree = ""; }; 11B35B106BD8E4DBD67B7700 /* BaseTransactionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsService.swift; sourceTree = ""; }; 11B35B109B4F60753BEC5078 /* ReceiveAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressService.swift; sourceTree = ""; }; @@ -7560,6 +7569,7 @@ ABC9A76ACF7C7D6D7D3FA323 /* Components */, ABC9A1CF38A26663C93F47B4 /* MarketCards */, 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */, + 11B35AFE2C95FF73F75652D8 /* ChartView.swift */, ); path = Chart; sourceTree = ""; @@ -7817,6 +7827,8 @@ 2FA5D4E16E60866549E0CD48 /* CoinOverviewViewModel.swift */, 2FA5DA1F5A41E633A244DAD1 /* CoinOverviewViewController.swift */, 58AAA0B8ECE5854FAB9362AC /* CoinOverviewViewItemFactory.swift */, + 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */, + 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */, ); path = CoinOverview; sourceTree = ""; @@ -9474,6 +9486,9 @@ ABC9A67C2D782AD0DFDF0C3C /* RestoreFileConfigurationViewController.swift in Sources */, ABC9ACEB81BCB00435B35F64 /* RestoreFileHelper.swift in Sources */, 11B35538EF749777CF7B2E8B /* ChartUiView.swift in Sources */, + 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */, + 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */, + 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10803,6 +10818,9 @@ ABC9A0C5DE01B3C50D4C7FF2 /* RestoreFileConfigurationViewController.swift in Sources */, ABC9A3231731F39ECA5B90ED /* RestoreFileHelper.swift in Sources */, 11B356DF455592656B742485 /* ChartUiView.swift in Sources */, + 11B35FB362526C723329C9ED /* ChartView.swift in Sources */, + 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */, + 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift index 01ac58e93f..f981478619 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift @@ -219,6 +219,10 @@ class ChartUiView: UIView { fatalError("init(coder:) has not been implemented") } + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: totalHeight) + } + var totalHeight: CGFloat { .heightDoubleLineCell + configuration.mainHeight diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift new file mode 100644 index 0000000000..908ead06a3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift @@ -0,0 +1,19 @@ +import Chart +import SwiftUI +import UIKit + +struct ChartView: UIViewRepresentable { + typealias UIViewType = UIView + + let viewModel: IChartViewModel & IChartViewTouchDelegate + let configuration: ChartConfiguration + + func makeUIView(context _: Context) -> UIView { + let chartView = ChartUiView(viewModel: viewModel, configuration: configuration) + chartView.setContentHuggingPriority(.required, for: .vertical) + chartView.onLoad() + return chartView + } + + func updateUIView(_: UIView, context _: Context) {} +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift index bf888fb398..5692f14032 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift @@ -6,8 +6,9 @@ import MarketKit import Chart import CurrencyKit import HUD +import Combine -class CoinChartViewModel { +class CoinChartViewModel: ObservableObject { private let service: CoinChartService private let factory: CoinChartFactory private let disposeBag = DisposeBag() @@ -24,6 +25,8 @@ class CoinChartViewModel { private let indicatorsShownRelay = BehaviorRelay(value: true) private let openSettingsRelay = PublishRelay<()>() + @Published private(set) var indicatorsShown: Bool + var intervals: [String] { service.validIntervals.map { $0.title } + ["chart.time_duration.all".localized] } @@ -32,6 +35,8 @@ class CoinChartViewModel { self.service = service self.factory = factory + indicatorsShown = service.indicatorsShown + subscribe(scheduler, disposeBag, service.intervalsUpdatedObservable) { [weak self] in self?.syncIntervalsUpdate() } subscribe(scheduler, disposeBag, service.periodTypeObservable) { [weak self] in self?.sync(periodType: $0) } subscribe(scheduler, disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } @@ -48,6 +53,7 @@ class CoinChartViewModel { private func updateIndicatorsShown() { indicatorsShownRelay.accept(service.indicatorsShown) + indicatorsShown = service.indicatorsShown } private func index(periodType: HsPeriodType) -> Int { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift index 41e7b62c8e..c67c3eacf5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift @@ -1,30 +1,62 @@ -import MarketKit -import LanguageKit import Chart +import LanguageKit +import MarketKit +import SwiftUI struct CoinOverviewModule { + static func view(coinUid: String) -> some View { + let repository = ChartIndicatorsRepository( + localStorage: App.shared.localStorage, + subscriptionManager: App.shared.subscriptionManager + ) + let chartService = CoinChartService( + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + localStorage: App.shared.localStorage, + indicatorRepository: repository, + coinUid: coinUid + ) + let chartFactory = CoinChartFactory(currentLocale: LanguageManager.shared.currentLocale) + let chartViewModel = CoinChartViewModel(service: chartService, factory: chartFactory) + + let viewModel = CoinOverviewViewModelNew( + coinUid: coinUid, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + languageManager: LanguageManager.shared, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager + ) + + return CoinOverviewView( + viewModel: viewModel, + chartViewModel: chartViewModel, + chartIndicatorRepository: repository, + chartPointFetcher: chartService + ) + } static func viewController(coinUid: String) -> CoinOverviewViewController { let service = CoinOverviewService( - coinUid: coinUid, - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit, - languageManager: LanguageManager.shared, - accountManager: App.shared.accountManager, - walletManager: App.shared.walletManager + coinUid: coinUid, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + languageManager: LanguageManager.shared, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager ) let repository = ChartIndicatorsRepository( - localStorage: App.shared.localStorage, - subscriptionManager: App.shared.subscriptionManager + localStorage: App.shared.localStorage, + subscriptionManager: App.shared.subscriptionManager ) let chartService = CoinChartService( - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit, - localStorage: App.shared.localStorage, - indicatorRepository: repository, - coinUid: coinUid + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + localStorage: App.shared.localStorage, + indicatorRepository: repository, + coinUid: coinUid ) let router = ChartIndicatorRouter(repository: repository, fetcher: chartService) @@ -34,12 +66,11 @@ struct CoinOverviewModule { let chartViewModel = CoinChartViewModel(service: chartService, factory: chartFactory) return CoinOverviewViewController( - viewModel: viewModel, - chartViewModel: chartViewModel, - chartRouter: router, - markdownParser: CoinPageMarkdownParser(), - urlManager: UrlManager(inApp: true) + viewModel: viewModel, + chartViewModel: chartViewModel, + chartRouter: router, + markdownParser: CoinPageMarkdownParser(), + urlManager: UrlManager(inApp: true) ) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift new file mode 100644 index 0000000000..661b157722 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift @@ -0,0 +1,135 @@ +import CurrencyKit +import SDWebImageSwiftUI +import SwiftUI + +struct CoinOverviewView: View { + @ObservedObject var viewModel: CoinOverviewViewModelNew + @ObservedObject var chartViewModel: CoinChartViewModel + let chartIndicatorRepository: IChartIndicatorsRepository + let chartPointFetcher: IChartPointFetcher + + @State private var chartIndicatorsShown = false + + var body: some View { + ThemeView { + ZStack { + switch viewModel.state { + case .loading: + ProgressView() + case let .failed(error): + Text(error.localizedDescription) + case let .completed(item): + let info = item.info + let coin = item.info.fullCoin.coin + let coinCode = coin.code + let rank = info.marketCapRank.map { "#\($0)" } + + ScrollView { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + WebImage(url: URL(string: coin.imageUrl)) + .placeholder(Image("placeholder_circle_32")) + .resizable() + .scaledToFit() + .frame(width: .iconSize32, height: .iconSize32) + + Text(coin.name).themeBody() + + if let rank { + Text(rank).themeSubhead1(alignment: .trailing) + } + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin12) + + ChartView(viewModel: chartViewModel, configuration: .coinChart) + .frame(maxWidth: .infinity) + .onAppear { + chartViewModel.start() + } + + VStack { + ListSection { + ListRow { + Text("coin_overview.indicators".localized).themeSubhead2() + + Button(action: { + chartViewModel.onToggleIndicators() + }) { + Text(chartViewModel.indicatorsShown ? "coin_overview.indicators.hide".localized : "coin_overview.indicators.show".localized) + .animation(.none) + } + .buttonStyle(SecondaryButtonStyle(style: .default)) + + Button(action: { + chartIndicatorsShown = true + }) { + Image("setting_20").renderingMode(.template) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default)) + } + } + + let infoItems = [ + format(value: info.marketCap, currency: viewModel.currency).map { + (title: "coin_overview.market_cap".localized, badge: rank, text: $0) + }, + format(value: info.totalSupply, coinCode: coinCode).map { + (title: "coin_overview.total_supply".localized, badge: nil, text: $0) + }, + format(value: info.circulatingSupply, coinCode: coinCode).map { + (title: "coin_overview.circulating_supply".localized, badge: nil, text: $0) + }, + format(value: info.volume24h, currency: viewModel.currency).map { + (title: "coin_overview.trading_volume".localized, badge: nil, text: $0) + }, + format(value: info.dilutedMarketCap, currency: viewModel.currency).map { + (title: "coin_overview.diluted_market_cap".localized, badge: nil, text: $0) + }, + info.genesisDate.map { + (title: "coin_overview.genesis_date".localized, badge: nil, text: DateHelper.instance.formatFullDateOnly(from: $0)) + }, + ].compactMap { $0 } + + if !infoItems.isEmpty { + ListSection { + ForEach(infoItems, id: \.title) { infoItem in + ListRow { + Text(infoItem.title).themeSubhead2() + Text(infoItem.text).themeSubhead1(color: .themeLeah, alignment: .trailing) + } + } + } + } + } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + } + } + } + } + } + .onAppear { + viewModel.sync() + } + .sheet(isPresented: $chartIndicatorsShown) { + ChartIndicatorsModule.view(repository: chartIndicatorRepository, fetcher: chartPointFetcher) + .ignoresSafeArea() + } + } + + private func format(value: Decimal?, coinCode: String) -> String? { + guard let value = value, !value.isZero else { + return nil + } + + return ValueFormatter.instance.formatShort(value: value, decimalCount: 0, symbol: coinCode) + } + + private func format(value: Decimal?, currency: Currency) -> String? { + guard let value = value, !value.isZero else { + return nil + } + + return ValueFormatter.instance.formatShort(currency: currency, value: value) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift new file mode 100644 index 0000000000..fafc1a6fdb --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift @@ -0,0 +1,152 @@ +import Combine +import CurrencyKit +import Foundation +import HsExtensions +import LanguageKit +import MarketKit + +class CoinOverviewViewModelNew: ObservableObject { + private var tasks = Set() + + private let coinUid: String + private let marketKit: MarketKit.Kit + private let currencyKit: CurrencyKit.Kit + private let languageManager: LanguageManager + private let accountManager: AccountManager + private let walletManager: WalletManager + private let viewItemFactory = CoinOverviewViewItemFactory() + + let currency: Currency + + @Published private(set) var state: DataStatus = .loading + + init(coinUid: String, marketKit: MarketKit.Kit, currencyKit: CurrencyKit.Kit, languageManager: LanguageManager, accountManager: AccountManager, walletManager: WalletManager) { + self.coinUid = coinUid + self.marketKit = marketKit + self.currencyKit = currencyKit + self.languageManager = languageManager + self.accountManager = accountManager + self.walletManager = walletManager + + currency = currencyKit.baseCurrency + } + + private func handleSuccess(info: MarketInfoOverview) { + let account = accountManager.activeAccount + + let tokens = info.fullCoin.tokens + .filter { + switch $0.type { + case let .unsupported(_, reference): return reference != nil + default: return true + } + } + + let walletTokens = walletManager.activeWallets.map { + $0.token + } + + let tokenItems = tokens + .sorted { lhsToken, rhsToken in + let lhsTypeOrder = lhsToken.type.order + let rhsTypeOrder = rhsToken.type.order + + guard lhsTypeOrder == rhsTypeOrder else { + return lhsTypeOrder < rhsTypeOrder + } + + return lhsToken.blockchainType.order < rhsToken.blockchainType.order + } + .map { token in + let state: TokenItemState + + if let account = account, !account.watchAccount, account.type.supports(token: token) { + if walletTokens.contains(token) { + state = .alreadyAdded + } else { + state = .canBeAdded + } + } else { + state = .cannotBeAdded + } + + return TokenItem( + token: token, + state: state + ) + } + + DispatchQueue.main.async { + self.state = .completed(Item(info: info, tokens: tokenItems, guideUrl: self.guideUrl)) + } + } + + private func handleFailure(error: Error) { + DispatchQueue.main.async { + self.state = .failed(error) + } + } + + private var guideUrl: URL? { + guard let guideFileUrl = guideFileUrl else { + return nil + } + + return URL(string: guideFileUrl, relativeTo: AppConfig.guidesIndexUrl) + } + + private var guideFileUrl: String? { + switch coinUid { + case "bitcoin": return "guides/token_guides/en/bitcoin.md" + case "ethereum": return "guides/token_guides/en/ethereum.md" + case "bitcoin-cash": return "guides/token_guides/en/bitcoin-cash.md" + case "zcash": return "guides/token_guides/en/zcash.md" + case "uniswap": return "guides/token_guides/en/uniswap.md" + case "curve-dao-token": return "guides/token_guides/en/curve-finance.md" + case "balancer": return "guides/token_guides/en/balancer-dex.md" + case "synthetix-network-token": return "guides/token_guides/en/synthetix.md" + case "tether": return "guides/token_guides/en/tether.md" + case "maker": return "guides/token_guides/en/makerdao.md" + case "dai": return "guides/token_guides/en/makerdao.md" + case "aave": return "guides/token_guides/en/aave.md" + case "compound": return "guides/token_guides/en/compound.md" + default: return nil + } + } +} + +extension CoinOverviewViewModelNew { + func sync() { + tasks = Set() + + state = .loading + + Task { [weak self, marketKit, coinUid, currencyKit, languageManager] in + do { + let info = try await marketKit.marketInfoOverview(coinUid: coinUid, currencyCode: currencyKit.baseCurrency.code, languageCode: languageManager.currentLanguage) + self?.handleSuccess(info: info) + } catch { + self?.handleFailure(error: error) + } + }.store(in: &tasks) + } +} + +extension CoinOverviewViewModelNew { + struct Item { + let info: MarketInfoOverview + let tokens: [TokenItem] + let guideUrl: URL? + } + + struct TokenItem { + let token: Token + let state: TokenItemState + } + + enum TokenItemState { + case canBeAdded + case alreadyAdded + case cannotBeAdded + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift index 411bfd1b1e..8ececd3e13 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift @@ -1,6 +1,7 @@ import Foundation -import UIKit +import SwiftUI import ThemeKit +import UIKit class ChartIndicatorRouter { private let repository: IChartIndicatorsRepository @@ -17,5 +18,25 @@ class ChartIndicatorRouter { return ThemeNavigationController(rootViewController: ChartIndicatorsViewController(viewModel: viewModel)) } +} + +enum ChartIndicatorsModule { + static func view(repository: IChartIndicatorsRepository, fetcher: IChartPointFetcher) -> some View { + let service = ChartIndicatorsService(repository: repository, chartPointFetcher: fetcher, subscriptionManager: App.shared.subscriptionManager) + let viewModel = ChartIndicatorsViewModel(service: service) + + return ChartIndicatorsView(viewModel: viewModel) + } +} + +struct ChartIndicatorsView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let viewModel: ChartIndicatorsViewModel + + func makeUIViewController(context _: Context) -> UIViewController { + ThemeNavigationController(rootViewController: ChartIndicatorsViewController(viewModel: viewModel)) + } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift index 8442269d98..0960a67cc0 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift @@ -3,7 +3,7 @@ import SwiftUI struct SecondaryCircleButtonStyle: ButtonStyle { let style: Style - @Environment(\.isEnabled) var isEnabled + @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label From 163280a99ba3d89888271cd2387e16a3af8fb271 Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 10 Oct 2023 15:31:13 +0600 Subject: [PATCH 50/63] Update layout for description section in CoinOverview module --- .../CoinOverviewViewController.swift | 517 +++++++++--------- .../CoinOverviewViewItemFactory.swift | 86 ++- .../CoinOverview/CoinOverviewViewModel.swift | 24 +- .../Modules/Coin/CoinPageMarkdownParser.swift | 28 +- .../en.lproj/Localizable.strings | 5 +- 5 files changed, 318 insertions(+), 342 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift index d82123d097..05bea222cf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift @@ -1,12 +1,12 @@ -import UIKit -import RxSwift -import ThemeKit -import SectionsTableView -import SnapKit -import HUD import Chart import ComponentKit import Down +import HUD +import RxSwift +import SectionsTableView +import SnapKit +import ThemeKit +import UIKit class CoinOverviewViewController: ThemeViewController { private let viewModel: CoinOverviewViewModel @@ -44,15 +44,15 @@ class CoinOverviewViewController: ThemeViewController { chartCell = ChartCell(viewModel: chartViewModel, configuration: .coinChart) chartRow = StaticRow( - cell: chartCell, - id: "chartView", - height: chartCell.cellHeight + cell: chartCell, + id: "chartView", + height: chartCell.cellHeight ) chartConfigurationRow = StaticRow( - cell: chartConfigurationCell, - id: "chartConfiguration", - height: .heightCell48 + cell: chartConfigurationCell, + id: "chartConfiguration", + height: .heightCell48 ) super.init() @@ -60,7 +60,8 @@ class CoinOverviewViewController: ThemeViewController { hidesBottomBarWhenPushed = true } - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -161,32 +162,32 @@ class CoinOverviewViewController: ThemeViewController { private func syncChartConfigurationCell() { CellBuilderNew.buildStatic( - cell: chartConfigurationCell, - rootElement: .hStack([ - .textElement(text: .body("coin_overview.indicators".localized)), - .secondaryButton { [weak self] component in - component.isHidden = false - component.button.set(style: .default) - let title = (self?.chartIndicatorShown ?? false) ? "coin_overview.indicators.hide".localized : "coin_overview.indicators.show".localized - component.button.setTitle(title, for: .normal) - component.onTap = { - self?.chartViewModel.onToggleIndicators() - } - }, - .margin(8), - .secondaryCircleButton { component in - component.isHidden = false - component.button.set(image: UIImage(named: "setting_20")) - component.button.isEnabled = true - component.onTap = { [weak self] in - self?.chartViewModel.onTapChartSettings() - } - }, - .image24 { component in - component.isHidden = true - component.imageView.image = UIImage(named: "lock_24") + cell: chartConfigurationCell, + rootElement: .hStack([ + .textElement(text: .body("coin_overview.indicators".localized)), + .secondaryButton { [weak self] component in + component.isHidden = false + component.button.set(style: .default) + let title = (self?.chartIndicatorShown ?? false) ? "coin_overview.indicators.hide".localized : "coin_overview.indicators.show".localized + component.button.setTitle(title, for: .normal) + component.onTap = { + self?.chartViewModel.onToggleIndicators() } - ]) + }, + .margin(8), + .secondaryCircleButton { component in + component.isHidden = false + component.button.set(image: UIImage(named: "setting_20")) + component.button.isEnabled = true + component.onTap = { [weak self] in + self?.chartViewModel.onTapChartSettings() + } + }, + .image24 { component in + component.isHidden = true + component.imageView.image = UIImage(named: "lock_24") + }, + ]) ) } @@ -200,67 +201,92 @@ class CoinOverviewViewController: ThemeViewController { private func openChartSettings() { parentNavigationController?.present(chartRouter.viewController(), animated: true) } - } extension CoinOverviewViewController { - - private func linkRow(id: String, image: String, title: String, isFirst: Bool, isLast: Bool, action: @escaping () -> ()) -> RowProtocol { + private func linkRow(id: String, image: String, title: String, isFirst: Bool, isLast: Bool, action: @escaping () -> Void) -> RowProtocol { tableView.universalRow48( - id: id, - image: .local(UIImage(named: image)?.withTintColor(.themeGray)), - title: .body(title), - accessoryType: .disclosure, - autoDeselect: true, - isFirst: isFirst, - isLast: isLast, - action: action + id: id, + image: .local(UIImage(named: image)?.withTintColor(.themeGray)), + title: .body(title), + accessoryType: .disclosure, + autoDeselect: true, + isFirst: isFirst, + isLast: isLast, + action: action ) } private func coinInfoSection(viewItem: CoinOverviewViewModel.CoinViewItem) -> SectionProtocol { Section( - id: "coin-info", - rows: [ - tableView.universalRow56( - id: "coin-info", - image: .url(viewItem.imageUrl, placeholder: viewItem.imagePlaceholderName), - title: .body(viewItem.name, color: .themeGray), - value: .subhead1(viewItem.marketCapRank, color: .themeGray), - backgroundStyle: .transparent, - isFirst: true, - isLast: false - ) - ] + id: "coin-info", + rows: [ + tableView.universalRow56( + id: "coin-info", + image: .url(viewItem.imageUrl, placeholder: viewItem.imagePlaceholderName), + title: .body(viewItem.name, color: .themeGray), + value: .subhead1(viewItem.marketCapRank, color: .themeGray), + backgroundStyle: .transparent, + isFirst: true, + isLast: false + ), + ] ) } private var chartSection: SectionProtocol { Section( - id: "chart", - rows: [chartRow] + id: "chart", + rows: [chartRow] ) } - private func descriptionSection(description: String) -> SectionProtocol { - var rows: [RowProtocol] = [ - tableView.subtitleRow(text: "chart.about.header".localized) - ] + private func descriptionSection(description: String) -> SectionProtocol? { + guard let attributedText = try? markdownParser.attributedString(from: description) else { + return nil + } - descriptionTextCell.contentText = try? markdownParser.attributedString(from: description) - rows.append( - StaticRow( - cell: descriptionTextCell, - id: "about_cell", - dynamicHeight: { [weak self] containerWidth in - self?.descriptionTextCell.cellHeight(containerWidth: containerWidth) ?? 0 - } - )) + let backgroundStyle: BaseThemeCell.BackgroundStyle = .lawrence + let layoutMargins = UIEdgeInsets(top: .margin12, left: .margin16, bottom: .margin12, right: .margin16) + + let descriptionWarning = "coin_overview.description_warning".localized + let descriptionWarningFont: UIFont = .subhead2 + let descriptionWarningPadding: CGFloat = .margin24 return Section( - id: "description", - headerState: .margin(height: .margin12), - rows: rows + id: "description", + headerState: .margin(height: .margin12), + rows: [ + tableView.subtitleRow(text: "coin_overview.overview".localized), + CellBuilderNew.row( + rootElement: .vStack([ + .text { component in + component.attributedText = attributedText + component.numberOfLines = 0 + }, + .margin(descriptionWarningPadding), + .text { component in + component.font = descriptionWarningFont + component.textColor = .themeJacob + component.numberOfLines = 0 + component.text = descriptionWarning + }, + ]), + layoutMargins: layoutMargins, + tableView: tableView, + id: "description", + dynamicHeight: { containerWidth in + let textWidth = containerWidth - BaseThemeCell.margin(backgroundStyle: backgroundStyle).width - layoutMargins.width + return attributedText.height(containerWidth: textWidth) + + descriptionWarningPadding + + descriptionWarning.height(forContainerWidth: textWidth, font: descriptionWarningFont) + + layoutMargins.height + }, + bind: { cell in + cell.set(backgroundStyle: backgroundStyle, isFirst: true, isLast: true) + } + ), + ] ) } @@ -271,35 +297,35 @@ extension CoinOverviewViewController { let isLast = links.isEmpty let guideRow = linkRow( - id: "guide", - image: "academy_1_24", - title: "coin_overview.guide".localized, - isFirst: true, - isLast: isLast, - action: { [weak self] in - let module = MarkdownModule.viewController(url: guideUrl) - self?.parentNavigationController?.pushViewController(module, animated: true) - } + id: "guide", + image: "academy_1_24", + title: "coin_overview.guide".localized, + isFirst: true, + isLast: isLast, + action: { [weak self] in + let module = MarkdownModule.viewController(url: guideUrl) + self?.parentNavigationController?.pushViewController(module, animated: true) + } ) guideRows.append(guideRow) } return Section( - id: "links", - headerState: .margin(height: .margin12), - rows: [tableView.subtitleRow(text: "coin_overview.links".localized)] + guideRows + links.enumerated().map { index, link in - linkRow( - id: link.title, - image: link.iconName, - title: link.title, - isFirst: guideRows.isEmpty && index == 0, - isLast: index == links.count - 1, - action: { [weak self] in - self?.openLink(url: link.url) - } - ) - } + id: "links", + headerState: .margin(height: .margin12), + rows: [tableView.subtitleRow(text: "coin_overview.links".localized)] + guideRows + links.enumerated().map { index, link in + linkRow( + id: link.title, + image: link.iconName, + title: link.title, + isFirst: guideRows.isEmpty && index == 0, + isLast: index == links.count - 1, + action: { [weak self] in + self?.openLink(url: link.url) + } + ) + } ) } @@ -318,63 +344,42 @@ extension CoinOverviewViewController { private func poweredBySection(text: String) -> SectionProtocol { Section( - id: "powered-by", - headerState: .margin(height: .margin32), - rows: [ - Row( - id: "powered-by", - dynamicHeight: { containerWidth in - BrandFooterCell.height(containerWidth: containerWidth, title: text) - }, - bind: { cell, _ in - cell.title = text - } - ) - ] + id: "powered-by", + headerState: .margin(height: .margin32), + rows: [ + Row( + id: "powered-by", + dynamicHeight: { containerWidth in + BrandFooterCell.height(containerWidth: containerWidth, title: text) + }, + bind: { cell, _ in + cell.title = text + } + ), + ] ) } private func performanceSection(viewItems: [[CoinOverviewViewModel.PerformanceViewItem]]) -> SectionProtocol { Section( - id: "return_of_investments_section", - headerState: .margin(height: .margin12), - rows: [ - Row( - id: "return_of_investments_cell", - dynamicHeight: { _ in - PerformanceTableViewCell.height(viewItems: viewItems) - }, - bind: { cell, _ in - cell.bind(viewItems: viewItems) - } - ) - ] - ) - } - - private func categoriesSection(categories: [String]) -> SectionProtocol { - let text = categories.joined(separator: ", ") - - return Section( - id: "categories", - headerState: .margin(height: .margin12), - rows: [ - tableView.subtitleRow(text: "coin_overview.category".localized), - Row( - id: "categories", - dynamicHeight: { width in - TextCell.height(containerWidth: width, text: text) - }, - bind: { cell, _ in - cell.contentText = text - } - ) - ] + id: "return_of_investments_section", + headerState: .margin(height: .margin12), + rows: [ + Row( + id: "return_of_investments_cell", + dynamicHeight: { _ in + PerformanceTableViewCell.height(viewItems: viewItems) + }, + bind: { cell, _ in + cell.bind(viewItems: viewItems) + } + ), + ] ) } private func typeRow(viewItem: CoinOverviewViewModel.TypeViewItem, index: Int, isFirst: Bool, isLast: Bool) -> RowProtocol { - var action: (() -> ())? + var action: (() -> Void)? if let reference = viewItem.reference { action = { @@ -383,116 +388,116 @@ extension CoinOverviewViewController { } return CellBuilderNew.row( - rootElement: .hStack([ - .imageElement(image: .url(viewItem.iconUrl, placeholder: "placeholder_rectangle_32"), size: .image32), - .vStackCentered([ - .textElement(text: .body(viewItem.title)), - .margin(1), - .textElement(text: .subhead2(viewItem.subtitle), parameters: .truncatingMiddle) - ]), - .secondaryCircleButton { [weak self] component in - component.isHidden = !viewItem.showAdd - component.button.set(image: UIImage(named: "add_to_wallet_2_20")) - component.onTap = { - self?.viewModel.onTapAddToWallet(index: index) - } - }, - .secondaryCircleButton { [weak self] component in - component.isHidden = !viewItem.showAdded - component.button.set(image: UIImage(named: "filled_wallet_20")) - component.button.isSelected = true + rootElement: .hStack([ + .imageElement(image: .url(viewItem.iconUrl, placeholder: "placeholder_rectangle_32"), size: .image32), + .vStackCentered([ + .textElement(text: .body(viewItem.title)), + .margin(1), + .textElement(text: .subhead2(viewItem.subtitle), parameters: .truncatingMiddle), + ]), + .secondaryCircleButton { [weak self] component in + component.isHidden = !viewItem.showAdd + component.button.set(image: UIImage(named: "add_to_wallet_2_20")) + component.onTap = { + self?.viewModel.onTapAddToWallet(index: index) + } + }, + .secondaryCircleButton { [weak self] component in + component.isHidden = !viewItem.showAdded + component.button.set(image: UIImage(named: "filled_wallet_20")) + component.button.isSelected = true + component.onTap = { + self?.viewModel.onTapAddedToWallet(index: index) + } + }, + .secondaryCircleButton { [weak self] component in + if let explorerUrl = viewItem.explorerUrl { + component.isHidden = false + component.button.set(image: UIImage(named: "globe_20")) component.onTap = { - self?.viewModel.onTapAddedToWallet(index: index) - } - }, - .secondaryCircleButton { [weak self] component in - if let explorerUrl = viewItem.explorerUrl { - component.isHidden = false - component.button.set(image: UIImage(named: "globe_20")) - component.onTap = { - self?.urlManager.open(url: explorerUrl, from: self?.parentNavigationController) - } - } else { - component.isHidden = true + self?.urlManager.open(url: explorerUrl, from: self?.parentNavigationController) } + } else { + component.isHidden = true } - ]), - tableView: tableView, - id: "type-\(index)", - height: .heightDoubleLineCell, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) }, - action: action + ]), + tableView: tableView, + id: "type-\(index)", + height: .heightDoubleLineCell, + autoDeselect: true, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + }, + action: action ) } private func typesSection(typesViewItem: CoinOverviewViewModel.TypesViewItem) -> SectionProtocol { var rows: [RowProtocol] = [ - tableView.subtitleRow(text: typesViewItem.title) + tableView.subtitleRow(text: typesViewItem.title), ] for (index, viewItem) in typesViewItem.viewItems.enumerated() { rows.append( - typeRow( - viewItem: viewItem, - index: index, - isFirst: index == 0, - isLast: typesViewItem.action != nil ? false : index == typesViewItem.viewItems.count - 1 - ) + typeRow( + viewItem: viewItem, + index: index, + isFirst: index == 0, + isLast: typesViewItem.action != nil ? false : index == typesViewItem.viewItems.count - 1 + ) ) } if let action = typesViewItem.action { rows.append( - CellBuilderNew.row( - rootElement: .textElement(text: .body(action.title), parameters: .centerAlignment), - tableView: tableView, - id: "action", - hash: "\(action.rawValue)", - height: .heightCell48, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isLast: true) - }, - action: { [weak self] in - self?.viewModel.onTap(typesAction: action) - } - ) + CellBuilderNew.row( + rootElement: .textElement(text: .body(action.title), parameters: .centerAlignment), + tableView: tableView, + id: "action", + hash: "\(action.rawValue)", + height: .heightCell48, + autoDeselect: true, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isLast: true) + }, + action: { [weak self] in + self?.viewModel.onTap(typesAction: action) + } + ) ) } return Section( - id: "types", - headerState: .margin(height: .margin12), - rows: rows + id: "types", + headerState: .margin(height: .margin12), + rows: rows ) } private func marketRow(id: String, title: String, badge: String?, text: String, isFirst: Bool, isLast: Bool) -> RowProtocol { CellBuilderNew.row( - rootElement: .hStack([ - .textElement(text: .subhead2(title), parameters: .highHugging), - .margin8, - .badge { (component: BadgeComponent) in - component.badgeView.set(style: .small) - - if let badge = badge { - component.badgeView.text = badge - component.isHidden = false - } else { - component.isHidden = true - } - }, - .textElement(text: .subhead1(text), parameters: .rightAlignment) - ]), - tableView: tableView, - id: id, - height: .heightCell48, - bind: { cell in - cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) - } + rootElement: .hStack([ + .textElement(text: .subhead2(title), parameters: .highHugging), + .margin8, + .badge { (component: BadgeComponent) in + component.badgeView.set(style: .small) + + if let badge = badge { + component.badgeView.text = badge + component.isHidden = false + } else { + component.isHidden = true + } + }, + .textElement(text: .subhead1(text), parameters: .rightAlignment), + ]), + tableView: tableView, + id: id, + height: .heightCell48, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + } ) } @@ -515,7 +520,7 @@ extension CoinOverviewViewController { }, viewItem.genesisDate.map { (id: "genesis_date", title: "coin_overview.genesis_date".localized, badge: nil, text: $0) - } + }, ].compactMap { $0 } @@ -526,27 +531,24 @@ extension CoinOverviewViewController { let rows = datas.enumerated().map { index, tuple in marketRow( - id: tuple.id, - title: tuple.title, - badge: tuple.badge, - text: tuple.text, - isFirst: index == 0, - isLast: index == datas.count - 1 + id: tuple.id, + title: tuple.title, + badge: tuple.badge, + text: tuple.text, + isFirst: index == 0, + isLast: index == datas.count - 1 ) } - return Section( - id: "market_info_section", - headerState: .margin(height: .margin12), - rows: rows + id: "market_info_section", + headerState: .margin(height: .margin12), + rows: rows ) } - } extension CoinOverviewViewController: SectionsDataSource { - public func buildSections() -> [SectionProtocol] { var sections = [SectionProtocol]() @@ -554,13 +556,13 @@ extension CoinOverviewViewController: SectionsDataSource { sections.append(coinInfoSection(viewItem: viewItem.coinViewItem)) sections.append(chartSection) sections.append( - Section( - id: "chart-configuration", - headerState: .margin(height: .margin12), - rows: [ - chartConfigurationRow - ] - ) + Section( + id: "chart-configuration", + headerState: .margin(height: .margin12), + rows: [ + chartConfigurationRow, + ] + ) ) if let marketInfoSection = marketInfoSection(viewItem: viewItem) { @@ -573,12 +575,8 @@ extension CoinOverviewViewController: SectionsDataSource { sections.append(typesSection(typesViewItem: types)) } - if let categories = viewItem.categories { - sections.append(categoriesSection(categories: categories)) - } - - if !viewItem.description.isEmpty { - sections.append(descriptionSection(description: viewItem.description)) + if !viewItem.description.isEmpty, let descriptionSection = descriptionSection(description: viewItem.description) { + sections.append(descriptionSection) } if viewItem.guideUrl != nil || !viewItem.links.isEmpty { @@ -590,5 +588,4 @@ extension CoinOverviewViewController: SectionsDataSource { return sections } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift index 5f167eb4aa..68468ea63c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift @@ -1,9 +1,8 @@ -import Foundation import CurrencyKit +import Foundation import MarketKit class CoinOverviewViewItemFactory { - private func roundedFormat(coinCode: String, value: Decimal?) -> String? { guard let value = value, !value.isZero, let formattedValue = ValueFormatter.instance.formatShort(value: value, decimalCount: 0, symbol: coinCode) else { return nil @@ -57,11 +56,6 @@ class CoinOverviewViewItemFactory { return viewItems } - private func categories(info: MarketInfoOverview) -> [String]? { - let categories = info.categories - return categories.isEmpty ? nil : categories.map { $0.name } - } - private func typesTitle(coinUid: String) -> String { switch coinUid { case "bitcoin", "litecoin": return "coin_overview.bips".localized @@ -85,23 +79,23 @@ class CoinOverviewViewItemFactory { case .native: title = blockchain.name subtitle = "coin_platforms.native".localized - case .derived(let derivation): + case let .derived(derivation): title = derivation.mnemonicDerivation.title subtitle = derivation.mnemonicDerivation.addressType + derivation.mnemonicDerivation.recommended - case .addressType(let type): + case let .addressType(type): title = type.bitcoinCashCoinType.title subtitle = type.bitcoinCashCoinType.description + type.bitcoinCashCoinType.recommended - case .eip20(let address): + case let .eip20(address): title = blockchain.name subtitle = address.shortened reference = address url = blockchain.explorerUrl(reference: address) - case .bep2(let symbol): + case let .bep2(symbol): title = blockchain.name subtitle = symbol reference = symbol url = blockchain.explorerUrl(reference: symbol) - case .spl(let address): + case let .spl(address): title = blockchain.name subtitle = address.shortened reference = address @@ -120,13 +114,13 @@ class CoinOverviewViewItemFactory { } return CoinOverviewViewModel.TypeViewItem( - iconUrl: blockchain.type.imageUrl, - title: title, - subtitle: subtitle, - reference: reference, - explorerUrl: url, - showAdd: showAdd, - showAdded: showAdded + iconUrl: blockchain.type.imageUrl, + title: title, + subtitle: subtitle, + reference: reference, + explorerUrl: url, + showAdd: showAdd, + showAdded: showAdded ) } } @@ -190,17 +184,15 @@ class CoinOverviewViewItemFactory { } return CoinOverviewViewModel.LinkViewItem( - title: linkTitle(type: linkType, url: url), - iconName: linkIconName(type: linkType), - url: linkUrl(type: linkType, url: url) + title: linkTitle(type: linkType, url: url), + iconName: linkIconName(type: linkType), + url: linkUrl(type: linkType, url: url) ) } } - } extension CoinOverviewViewItemFactory { - func viewItem(item: CoinOverviewService.Item, currency: Currency, typesShown: Bool) -> CoinOverviewViewModel.ViewItem { let info = item.info let coin = info.fullCoin.coin @@ -211,35 +203,33 @@ extension CoinOverviewViewItemFactory { if !item.tokens.isEmpty { types = CoinOverviewViewModel.TypesViewItem( - title: typesTitle(coinUid: coin.uid), - viewItems: typeViewItems(tokenItems: item.tokens.count > 4 && !typesShown ? Array(item.tokens.prefix(3)) : item.tokens), - action: item.tokens.count > 4 ? (typesShown ? .showLess : .showMore) : nil + title: typesTitle(coinUid: coin.uid), + viewItems: typeViewItems(tokenItems: item.tokens.count > 4 && !typesShown ? Array(item.tokens.prefix(3)) : item.tokens), + action: item.tokens.count > 4 ? (typesShown ? .showLess : .showMore) : nil ) } return CoinOverviewViewModel.ViewItem( - coinViewItem: CoinOverviewViewModel.CoinViewItem( - name: coin.name, - marketCapRank: marketCapRank, - imageUrl: coin.imageUrl, - imagePlaceholderName: "placeholder_circle_32" - ), - + coinViewItem: CoinOverviewViewModel.CoinViewItem( + name: coin.name, marketCapRank: marketCapRank, - marketCap: info.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, - totalSupply: roundedFormat(coinCode: coinCode, value: info.totalSupply), - circulatingSupply: roundedFormat(coinCode: coinCode, value: info.circulatingSupply), - volume24h: info.volume24h.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, - dilutedMarketCap: info.dilutedMarketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, - genesisDate: info.genesisDate.map { DateHelper.instance.formatFullDateOnly(from: $0) }, - - performance: performanceViewItems(info: info), - categories: categories(info: info), - types: types, - description: info.description, - guideUrl: item.guideUrl, - links: links(info: info) + imageUrl: coin.imageUrl, + imagePlaceholderName: "placeholder_circle_32" + ), + + marketCapRank: marketCapRank, + marketCap: info.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, + totalSupply: roundedFormat(coinCode: coinCode, value: info.totalSupply), + circulatingSupply: roundedFormat(coinCode: coinCode, value: info.circulatingSupply), + volume24h: info.volume24h.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, + dilutedMarketCap: info.dilutedMarketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) }, + genesisDate: info.genesisDate.map { DateHelper.instance.formatFullDateOnly(from: $0) }, + + performance: performanceViewItems(info: info), + types: types, + description: info.description, + guideUrl: item.guideUrl, + links: links(info: info) ) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift index 0dfd4e4318..4f795834fd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift @@ -1,10 +1,10 @@ +import ComponentKit import Foundation -import RxSwift -import RxRelay -import RxCocoa import MarketKit +import RxCocoa +import RxRelay +import RxSwift import UIKit -import ComponentKit class CoinOverviewViewModel { private let service: CoinOverviewService @@ -32,7 +32,7 @@ class CoinOverviewViewModel { viewItemRelay.accept(nil) loadingRelay.accept(true) syncErrorRelay.accept(false) - case .completed(let item): + case let .completed(item): viewItemRelay.accept(viewItemFactory.viewItem(item: item, currency: service.currency, typesShown: typesShown)) loadingRelay.accept(false) syncErrorRelay.accept(false) @@ -42,11 +42,9 @@ class CoinOverviewViewModel { syncErrorRelay.accept(true) } } - } extension CoinOverviewViewModel { - var viewItemDriver: Driver { viewItemRelay.asDriver() } @@ -75,20 +73,18 @@ extension CoinOverviewViewModel { do { try service.editWallet(index: index, add: true) hudRelay.accept(.addedToWallet) - } catch { - } + } catch {} } func onTapAddedToWallet(index: Int) { do { try service.editWallet(index: index, add: false) hudRelay.accept(.removedFromWallet) - } catch { - } + } catch {} } func onTap(typesAction: TypesAction) { - guard case .completed(let item) = service.state else { + guard case let .completed(item) = service.state else { return } @@ -99,11 +95,9 @@ extension CoinOverviewViewModel { viewItemRelay.accept(viewItemFactory.viewItem(item: item, currency: service.currency, typesShown: typesShown)) } - } extension CoinOverviewViewModel { - struct CoinViewItem { let name: String let marketCapRank: String? @@ -123,7 +117,6 @@ extension CoinOverviewViewModel { let genesisDate: String? let performance: [[PerformanceViewItem]] - let categories: [String]? let types: TypesViewItem? let description: String let guideUrl: URL? @@ -170,5 +163,4 @@ extension CoinOverviewViewModel { let iconName: String let url: String } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageMarkdownParser.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageMarkdownParser.swift index e65b9f633a..f0dcad03a9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageMarkdownParser.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageMarkdownParser.swift @@ -1,20 +1,19 @@ -import UIKit import Down +import UIKit class CoinPageMarkdownParser { - let fonts = StaticFontCollection( - heading1: .title2, - heading2: .title3, - heading3: .subhead1, - body: .subhead2 + heading1: .title3, + heading2: .headline2, + heading3: .subhead1, + body: .subhead2 ) let colors = StaticColorCollection( - heading1: .themeLeah, - heading2: .themeJacob, - heading3: .themeBran, - body: .themeGray + heading1: .themeLeah, + heading2: .themeLeah, + heading3: .themeBran, + body: .themeGray ) let paragraphStyles: StaticParagraphStyleCollection = { @@ -42,14 +41,13 @@ class CoinPageMarkdownParser { let listItemOptions = ListItemOptions(maxPrefixDigits: 1, spacingAfterPrefix: .margin8, spacingAbove: .margin12, spacingBelow: .margin12) let configuration = DownStylerConfiguration( - fonts: fonts, - colors: colors, - paragraphStyles: paragraphStyles, - listItemOptions: listItemOptions + fonts: fonts, + colors: colors, + paragraphStyles: paragraphStyles, + listItemOptions: listItemOptions ) let styler = DownStyler(configuration: configuration) return try down.toAttributedString(styler: styler) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 3193f2733b..516ad7941f 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -740,8 +740,8 @@ Go to Settings - > %@ and allow access to the camera."; "coin_overview.roi.day200" = "6 Month"; "coin_overview.roi.year1" = "1 Year"; -"coin_overview.category" = "Category"; - +"coin_overview.overview" = "Overview"; +"coin_overview.description_warning" = "This is an AI generated description based on the provided reference material for the given cryptocurrency. It may contain errors."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Coin Types"; @@ -1372,7 +1372,6 @@ Go to Settings - > %@ and allow access to the camera."; "chart.performance.week_changes" = "Changes (1W)"; "chart.performance.month_changes" = "Changes (1M)"; -"chart.about.header" = "About"; "chart.about.read_more" = "Read More"; "chart.about.read_less" = "Read Less"; From ed9b449eeece620a91d6e8aaf35e89cba933597f Mon Sep 17 00:00:00 2001 From: _imadia Date: Tue, 10 Oct 2023 16:05:44 +0600 Subject: [PATCH 51/63] Apply text changes --- .../de.lproj/Localizable.strings | 2 +- .../en.lproj/Localizable.strings | 79 +++++++++++-------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index 3c7a7278db..bc54de2545 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -1666,7 +1666,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Launch -"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting app or report the error to our support team."; +"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting the app or report the error to our support team."; "launch.failed_to_launch.report" = "Report"; // Tron diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 516ad7941f..5825ab80c3 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -147,7 +147,7 @@ Go to Settings - > %@ and allow access to the camera."; "restore_type.recovery.description" = "Import using recovery phrase or private key."; "restore_type.cloud.description" = "Import from a backup file in your keychain."; "restore_type.file.description" = "Import a backup file from your local folder."; -"restore_type.cex.description" = "Connect to a wallet on centralized exchange."; +"restore_type.cex.description" = "Connect to a wallet on a centralized exchange."; // Restore Cloud @@ -262,11 +262,11 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.name.description" = "Enter name for the backup file."; "backup.cloud.name.empty" = "Backup name can't be empty!"; "backup.cloud.name.error.empty" = "Backup name must be not empty"; -"backup.cloud.name.error.already_exist" = "Backup name already exist!"; +"backup.cloud.name.error.already_exist" = "Backup name already exists!"; "backup.cloud.name.placeholder" = "Name"; "backup.cloud.password.title" = "Set Password"; -"backup.cloud.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.cloud.password.description" = "Set the 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.cloud.password.highlighted_description" = "Don't forget this password! It is separate from your Apple iCloud password, and it cannot be recovered or reset."; "backup.cloud.password.placeholder" = "Password"; "backup.cloud.password.confirm.placeholder" = "Confirm"; @@ -336,7 +336,7 @@ Go to Settings - > %@ and allow access to the camera."; "balance.token.locked.info.description" = "The sender sent these funds with a spending lock that will expire on the shown date. \n\nNo worries, the received Bitcoins are already yours, but until the lock period expires you cannot spend them on the Bitcoin network."; "balance.token.processing" = "Processing"; "balance.token.processing.info.title" = "Processing amount"; -"balance.token.processing.info.description" = "Transactions with this amount still syncing. And when they will be confirmed, this tokens will be available for spending"; +"balance.token.processing.info.description" = "Transactions with this amount still syncing. And when they are confirmed, these tokens will be available for spending"; "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; @@ -417,7 +417,7 @@ Go to Settings - > %@ and allow access to the camera."; "send.hodler_locktime_off" = "Off"; "send.hodler_error.unsupported_address" = "Time locking works only when sending to payment addresses starting with 1... (aka BIP44 addresses)"; "send.fee_info.title" = "Fee Rate"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within a reasonable amount of time.\n\nThe recommended fee rate is shown as the amount of satoshi the user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting a higher fee rate."; "send.transaction_inputs_outputs_info.title" = "Transaction Inputs / Outputs"; "send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; @@ -438,9 +438,9 @@ Go to Settings - > %@ and allow access to the camera."; "send.confirmation.time_lock" = "Time Lock"; "send.confirmation.slide_to_send" = "Slide to Send"; "send.confirmation.sending" = "Sending"; -"send.confirmation.resend_description" = "This action will attempt to invalidate the previous transaction by resending it with a higher fee. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; +"send.confirmation.resend_description" = "This action will attempt to invalidate the previous transaction by resending it with a higher fee. If the original transaction remains pending when a new one is sent there is a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; "send.confirmation.resend" = "Resend"; -"send.confirmation.cancel_description" = "This action will attempt to invalidate previous transaction by resending as a new 0 amount transaction to self. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; +"send.confirmation.cancel_description" = "This action will attempt to invalidate the previous transaction by resending it as a new 0 amount transaction to self. If the original transaction remains pending when a new one is sent there is a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; "send.confirmation.cancel" = "Cancel Transaction"; "send.confirmation.nonce" = "Nonce"; "send.confirmation.method" = "Method"; @@ -784,7 +784,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.cex_volume_rank.description" = "Tokens ranked by trading volume for the token on centralized exchanges."; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; -"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over 30-day period."; +"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info4" = "List of all tokens ranked based on trading volume on centralized exchanges over 24H / 7D / 1M intervals."; "coin_analytics.dex_volume" = "DEX Volume"; @@ -814,16 +814,16 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.active_addresses_rank.description" = "Tokens ranked by number of unique addresses transacting with the token."; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over a 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; -"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; -"coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; +"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over a 30-day period."; +"coin_analytics.active_addresses.info4" = "Token's rank is based on the number of active wallets transacting with the token 30-day period."; "coin_analytics.active_addresses.info5" = "List of all tokens ranked based on the number of daily active addresses transacting with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count" = "Transaction Count"; "coin_analytics.transaction_count_rank" = "Tx Count Rank"; -"coin_analytics.transaction_count_rank.description" = "Tokens ranked by number of transactions on a blockchain."; -"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; +"coin_analytics.transaction_count_rank.description" = "Tokens are ranked by a number of transactions on a blockchain."; +"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with tokens over a 30-day period."; "coin_analytics.transaction_count.info2" = "Chart showing variation in transaction count over 1 year period."; -"coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; +"coin_analytics.transaction_count.info3" = "Token's rank is based on the number of transactions within the token 30-day period."; "coin_analytics.transaction_count.info4" = "List of all tokens ranked based on the number of transactions with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count.info5" = "The total number of tokens transferred over the blockchain over the 30 day period."; @@ -840,19 +840,19 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.project_tvl" = "Project TVL"; "coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; "coin_analytics.project_tvl.info_title" = "Project TVL (Total Value Locked)"; -"coin_analytics.project_tvl.info1" = "Total-Value-Locked (or Assets Under Management) in the project's smart contracts."; +"coin_analytics.project_tvl.info1" = "Total-value-locked (or Assets Under Management) in the project's smart contracts."; "coin_analytics.project_tvl.info2" = "Chart showing variation Total-Value-Locked in project's smart contracts over 1 year period."; -"coin_analytics.project_tvl.info3" = "Token's rank based on current Total-Value-Locked."; +"coin_analytics.project_tvl.info3" = "Token's rank is based on current Total-Value-Locked."; "coin_analytics.project_tvl.info4" = "List of all tokens ranked based on current Total-Value-Locked."; "coin_analytics.project_tvl.info5" = "Market Cap / TVL ratio for the project."; "coin_analytics.project_fee" = "Project Fee"; "coin_analytics.project_fee_rank" = "Project Fee Rank"; -"coin_analytics.project_fee_rank.description" = "Tokens ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; +"coin_analytics.project_fee_rank.description" = "Tokens are ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; "coin_analytics.project_revenue" = "Project Revenue"; "coin_analytics.project_revenue_rank" = "Project Revenue Rank"; -"coin_analytics.project_revenue_rank.description" = "Tokens ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.description" = "Tokens are ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; "coin_analytics.other_data" = "Other Data"; @@ -882,11 +882,11 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.overall_score.poor" = "Poor"; "coin_analytics.overall_score.cex_volume" = "The overall score is based on the average daily trading volume on centralized exchanges over the last 7 days."; "coin_analytics.overall_score.dex_volume" = "The overall score is based on the average daily trading volume on decentralized exchanges over the last 7 days."; -"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total avilable liquidity on decentralized exchanges."; +"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total available liquidity on decentralized exchanges."; "coin_analytics.overall_score.active_addresses" = "The overall score is based on the average daily active addresses over the last 7 days."; "coin_analytics.overall_score.project_tvl" = "The overall score is based on the total value locked (assets under management) on the project represented by the given token."; "coin_analytics.overall_score.transaction_count" = "The overall score is based on the average daily transaction count over the last 7 days."; -"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding respective token."; +"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding the respective token."; "coin_analytics.rank" = "Rank"; "coin_analytics.30_day_rank" = "30-Day Rank"; @@ -1035,7 +1035,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings.base_currency.title" = "Base Currency"; "settings.base_currency.other" = "Other"; "settings.base_currency.disclaimer" = "Disclaimer"; -"settings.base_currency.disclaimer.description" = "The exchange rate data is provided by a third party service - Coingecko.com. \n\nThe %@ wallet app doesn't guarantee these values are always correct and matches market data. The chance for inconsistency is higher if you select any base currency other than %@."; +"settings.base_currency.disclaimer.description" = "The exchange rate data is provided by a third-party service - Coingecko.com. \n\nThe %@ wallet app doesn't guarantee these values are always correct and match market data. The chance for inconsistency is higher if you select any base currency other than %@."; "settings.base_currency.disclaimer.set" = "Set"; // Settings -> Manage Wallet @@ -1045,7 +1045,7 @@ Go to Settings - > %@ and allow access to the camera."; "manage_wallets.search_placeholder" = "Name, code or contract address"; "manage_wallets.contract_address" = "Contract Address"; "manage_wallets.derivation_description" = "There are 4 common address formats %@ wallets can use to receive incoming payments:\n\n- BIP44 (oldest)\n- BIP49\n- BIP84 (recommended)\n- BIP86 (newest)\n\nWhile %@ wallet supports all 4, it recommends to use a %@ wallet operating in BIP84 format."; -"manage_wallets.bitcoin_cash_coin_type_description" = "There are 2 address formats Bitcoin Cash wallets can use to receive incoming payments:\n\n- TYPE 0 (older)\n- TYPE 145 (newer)\n\nWhile %@ wallet supports both of them it recommends to use a Bitcoin Cash wallet operating in TYPE 145 format."; +"manage_wallets.bitcoin_cash_coin_type_description" = "There are 2 address formats Bitcoin Cash wallets can use to receive incoming payments:\n\n- TYPE 0 (older)\n- TYPE 145 (newer)\n\nWhile %@ wallet supports both of them it recommends using a Bitcoin Cash wallet operating in TYPE 145 format."; // Settings -> Personal Support @@ -1124,6 +1124,12 @@ Go to Settings - > %@ and allow access to the camera."; "backup_app.backup.disclaimer.file.description" = "Storage devices i.e. hard drives, USB drives , storage on smartphone etc. are all vulnerable to loss due to physical damage, theft or other unforeseen circumstances."; "backup_app.backup.disclaimer.file.checkbox_label" = "I understand that theft or damage of a backup device will result in loss of a backup to a respective wallet."; +"backup.disclaimer.cloud.title" = "Backup to iCloud"; +"backup.disclaimer.cloud.description" = "iCloud is a cloud storage service provided by Apple. It's important to know that your backup data will be stored on Apple's servers."; +"backup.disclaimer.cloud.checkbox_label" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; +"backup.disclaimer.file.title" = "Backup to File"; +"backup.disclaimer.file.description" = "Storage devices i.e. hard drives, USB drives, storage on smartphones, etc. are all vulnerable to loss due to physical damage, theft, or other unforeseen circumstances."; +"backup.disclaimer.file.checkbox_label" = "I understand that theft or damage of a backup device will result in the loss of a backup to a respective wallet."; "backup_app.backup.name.title" = "Backup Name"; "backup_app.backup.name.description" = "Enter name for the backup file."; @@ -1134,6 +1140,9 @@ Go to Settings - > %@ and allow access to the camera."; "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"; +"backup.password.title" = "Backup Password"; +"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.password.highlighted_description" = "This password is used to encrypt the backup file of your wallet. It can't be recovered or reset if lost or forgotten."; // Settings -> Security @@ -1160,7 +1169,7 @@ Go to Settings - > %@ and allow access to the camera."; // Enable Duress Mode "enable_duress_mode.intro.title" = "Duress Mode"; -"enable_duress_mode.intro.description" = "This mode allows user to setup multiple unlock app passcodes where a desired passcode shows only specified wallets. Designed to keep selected wallets safe under coercion or threats."; +"enable_duress_mode.intro.description" = "This mode allows users to set up multiple unlock app passcodes where a desired passcode shows only specified wallets. Designed to keep selected wallets safe under coercion or threats."; "enable_duress_mode.intro.notes" = "Notes"; "enable_duress_mode.intro.biometrics.description" = "The %@ feature will work to unlock the Duress Mode. You can disable %@ for convenience."; "enable_duress_mode.intro.passcode_disabling" = "Passcode Disabling"; @@ -1467,7 +1476,7 @@ Go to Settings - > %@ and allow access to the camera."; "ethereum_transaction.error.insufficient_balance_with_fee" = "The current %@ balance is below the amount required to process this transaction, including the transaction fee."; "ethereum_transaction.error.lower_than_base_gas_limit" = "The selected fee value is too low and will be rejected!"; "ethereum_transaction.error.nonce_already_in_block" = "The transaction is already in block!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Fee not enough to replace the transaction"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Fee is not enough to replace the transaction"; "ethereum_transaction.error.transaction_underpriced" = "Fee not enough to send the transaction"; "ethereum_transaction.error.tips_higher_than_max_fee" = "Max fee cannot be lower than the tips, because Max fee includes the tips."; "ethereum_transaction.error.reverted" = "The transaction cannot be executed: %@"; @@ -1497,7 +1506,7 @@ Go to Settings - > %@ and allow access to the camera."; "wallet_connect.paired_dapps.title" = "Paired dApps"; "wallet_connect.paired_dapps.cant_disconnect" = "Can't Disconnect"; "wallet_connect.paired_dapps.disconnect_all" = "Delete All"; -"wallet_connect.pending_requests.nonactive_footer" = "To open an request you must activate the desired wallet"; +"wallet_connect.pending_requests.nonactive_footer" = "To open a request you must activate the desired wallet"; // App Status @@ -1515,7 +1524,7 @@ Go to Settings - > %@ and allow access to the camera."; "status_info.title" = "Status"; "status_info.pending.title" = "Pending"; -"status_info.pending.content" = "The transaction has not been confirmed on the blockchain yet. Transactions sent with a recommended or higher fee setting are generally processed within a few minutes. Transactions sent with a low fee may remain pending for a few hours or even days, and can even be rejected. Note that status of an individual transaction in %@ wallet interface typically updated with a short delay."; +"status_info.pending.content" = "The transaction has not been confirmed on the blockchain yet. Transactions sent with a recommended or higher fee setting are generally processed within a few minutes. Transactions sent with a low fee may remain pending for a few hours or even days, and can even be rejected. Note that the status of an individual transaction in %@ wallet interface is typically updated with a short delay."; "status_info.processing.title" = "Processing"; "status_info.processing.content" = "The transaction has been already included in the blockchain but has not reached permanent finality. At this point, it's safe to consider the transaction as completed for smaller payments. For larger payments, it's recommended to wait until the transaction status changes to completed."; "status_info.completed.title" = "Completed"; @@ -1555,19 +1564,19 @@ Go to Settings - > %@ and allow access to the camera."; "public_keys.title" = "Public Keys"; "public_keys.evm_address" = "EVM Address"; -"public_keys.evm_address.description" = "Allows read-only monitoring of wallets holding assets on Ethereum, Binance Smart Chain and other EVM based blockchains."; +"public_keys.evm_address.description" = "Allows read-only monitoring of wallets holding assets on Ethereum, Binance Smart Chain and other EVM-based blockchains."; "public_keys.account_extended_public_key" = "Account Extended Public Key"; -"public_keys.account_extended_public_key.description" = "Allows read-only monitoring of wallets holding Bitcoin and other UTXO based crypto (i.e. Litecoin, Bitcoin Cash, Dash, etc.)."; +"public_keys.account_extended_public_key.description" = "Allows read-only monitoring of wallets holding Bitcoin and other UTXO-based crypto (i.e. Litecoin, Bitcoin Cash, Dash, etc.)."; // Manage Account -> Private Keys "private_keys.title" = "Private Keys"; "private_keys.evm_private_key" = "EVM Private Key"; -"private_keys.evm_private_key.description" = "Grants full control over EVM based crypto i.e. Ethereum, Binance Smart Chain etc within respective wallet."; +"private_keys.evm_private_key.description" = "Grants full control over EVM-based crypto i.e. Ethereum, Binance Smart Chain, etc within the respective wallet."; "private_keys.bip32_root_key" = "BIP32 Root Key"; "private_keys.bip32_root_key.description" = "Grants full control over the assets on the respective wallet."; "private_keys.account_extended_private_key" = "Account Extended Private Key"; -"private_keys.account_extended_private_key.description" = "Grants full control over Bitcoin and other UTXO based crypto i.e. Litecoin, Bitcoin Cash, Dash, etc. within respective wallet."; +"private_keys.account_extended_private_key.description" = "Grants full control over Bitcoin and other UTXO-based crypto i.e. Litecoin, Bitcoin Cash, Dash, etc. within the respective wallet."; // Manage Account -> EVM Address @@ -1602,8 +1611,8 @@ Go to Settings - > %@ and allow access to the camera."; "add_evm_sync_source.name" = "Name"; "add_evm_sync_source.rpc_url" = "RPC URL"; "add_evm_sync_source.basic_auth" = "Basic Auth (optional)"; -"add_evm_sync_source.warning.url_exists" = "RPC Source with this url already exists"; -"add_evm_sync_source.error.invalid_url" = "Entered url is invalid. Valid url must have one of the following schemes: https, wss"; +"add_evm_sync_source.warning.url_exists" = "RPC Source with this URL already exists"; +"add_evm_sync_source.error.invalid_url" = "The entered URL is invalid. Valid url must have one of the following schemes: https, wss"; // Send Settings @@ -1632,11 +1641,11 @@ Go to Settings - > %@ and allow access to the camera."; "fee_settings.gas_price.info" = "The fee for transacting on the network is measured in gas units. Gas Price is the amount a user is willing to spend per unit of gas. When the network is busy, gas prices are high, and low when it's idle. An insufficient gas price is often a reason for a transaction to remain pending for an extended period."; "fee_settings.base_fee" = "Base Fee"; -"fee_settings.base_fee.info" = "The network protocol determines the base price per gas for each block, called base fee rate. It varies according to the network utilization level from block to block. It can increase or decrease by no more than 12.5% in the next block, making fees more predictable. The value shown here is the current block's base fee rate."; +"fee_settings.base_fee.info" = "The network protocol determines the base price per gas for each block, called the base fee rate. It varies according to the network utilization level from block to block. It can increase or decrease by no more than 12.5% in the next block, making fees more predictable. The value shown here is the current block's base fee rate."; "fee_settings.max_fee_rate" = "Max Fee Rate"; "fee_settings.max_fee_rate.info" = "This is the maximum total price per gas the user is willing to pay. It must cover the network's base fee rate and max priority fee rate. The value shown here is suggested based on an estimate of the next block's base fee rate plus the max priority fee rate chosen by the user. The actual fee rate paid will normally be lower. Setting this lower than the current base fee rate will limit the fee paid, but will result in longer waiting times for the transaction to be confirmed, or even in a stuck transaction."; "fee_settings.tips" = "Max Priority Fee"; -"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; +"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for the transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; "fee_settings.errors.insufficient_balance" = "Insufficient balance"; "fee_settings.errors.unexpected_error" = "Unexpected Error"; @@ -1768,7 +1777,7 @@ Go to Settings - > %@ and allow access to the camera."; // Launch -"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting app or report the error to our support team."; +"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting the app or report the error to our support team."; "launch.failed_to_launch.report" = "Report"; // Tron @@ -1777,7 +1786,7 @@ Go to Settings - > %@ and allow access to the camera."; "tron.send.resources_consumed" = "Resources Consumed"; "tron.send.bandwidth" = "Bandwidth"; "tron.send.energy" = "Energy"; -"tron.send.fee.info" = "The estimated cost of sending given transaction on the network. (Without excluding Energy, Bandwidth and Activating Fee)"; +"tron.send.fee.info" = "The estimated cost of sending a given transaction on the network. (Without excluding Energy, Bandwidth, and Activating Fee)"; "tron.send.resources_consumed.info" = "Bandwidth is the unit that measures the size of the transaction bytes stored in the blockchain database. The larger the transaction, the more bandwidth resources will be consumed.\n\nEnergy is the unit that measures the amount of computation required by the TRON virtual machine to perform specific operations on the TRON network.\n\nSince smart contract transactions require computing resources to execute, each smart contract transaction requires to pay for the energy fee."; "tron.send.activation_fee.info" = "Transferring TRX or TRC-10 tokens to an inactive account address will activate the account."; "tron.send.inactive_address" = "This address is not active"; From e5a0366e3f9f4491dd170551449e9d4882482946 Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 11 Oct 2023 10:30:32 +0600 Subject: [PATCH 52/63] Update full backup logic - Fix contacts replace on restore - Add restore screens - Refactor navigation for screens --- .../project.pbxproj | 40 +++---- .../Core/Storage/ContactBookManager.swift | 19 +++- .../UnstoppableWallet/Models/Contact.swift | 40 +++---- .../Backup/ICloud/AppBackupProvider.swift | 2 +- .../ContactBook/ContactBookHelper.swift | 72 +++++++++---- .../ContactBookSettingsService.swift | 2 +- ...storeFileConfigurationViewController.swift | 26 +++-- .../RestoreFileConfigurationViewModel.swift | 2 +- .../RestoreFile/RestoreFileHelper.swift | 19 +++- .../BackupApp/Backup/BackupAppViewModel.swift | 100 ++++++++---------- .../BackupDisclaimerView.swift | 11 +- .../Backup/BackupList/BackupListView.swift | 18 +++- .../Backup/BackupName/BackupNameView.swift | 8 +- .../BackupPassword/BackupPasswordView.swift | 16 ++- .../Backup/BackupType/BackupTypeView.swift | 62 ++++++----- .../Settings/BackupApp/BackupAppModule.swift | 21 ++-- .../BackupManager/BackupManagerModule.swift | 7 -- .../BackupManager/BackupManagerView.swift | 35 ------ .../BackupManagerModule.swift | 7 ++ .../BackupManagerViewController.swift | 94 ++++++++++++++++ .../Main/MainSettingsViewController.swift | 2 +- .../en.lproj/Localizable.strings | 20 ++-- 22 files changed, 376 insertions(+), 247 deletions(-) delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift delete mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerModule.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerViewController.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 4a0d21ac3c..7a9cae0963 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2089,6 +2089,7 @@ ABC9A36297D869E49C152CAB /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.swift */; }; ABC9A372F53F1F1D59BF8969 /* RestoreFileConfigurationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */; }; + ABC9A37B5FAB65E7AB66547E /* BackupManagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEA4B072067A9F10BE36 /* BackupManagerViewController.swift */; }; ABC9A37FB71FA7DA14553EFC /* RawFullBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A819E6708797C571CA0B /* RawFullBackup.swift */; }; ABC9A395A96C1F7C30F21940 /* WalletConnectPendingRequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */; }; ABC9A3B155B3F6E7E0F2CB07 /* HudHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A381CB4C09FF7CB62A94 /* HudHelper.swift */; }; @@ -2129,7 +2130,6 @@ ABC9A4E323FF7AAD86FA8E75 /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; }; ABC9A4F4B7F17169DC240A98 /* WalletConnectUriHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1C31F5343EB2BEA4540 /* WalletConnectUriHandler.swift */; }; ABC9A4FF1E1964FB77700C4E /* ChartIndicatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6DE5C760A5D0C90B70E /* ChartIndicatorFactory.swift */; }; - ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; ABC9A51979D2047BEF45A2AE /* TokenSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A791A47F4F1E71B51B3B /* TokenSelectView.swift */; }; ABC9A51E36466E414AF24C67 /* WalletConnectMainPendingRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D8072033A5AC7E4897 /* WalletConnectMainPendingRequestViewModel.swift */; }; ABC9A51F2E7EB0B477EBE708 /* SendZcashService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE6D2CD14194802E7976 /* SendZcashService.swift */; }; @@ -2197,7 +2197,6 @@ ABC9A7655AE66379E42FE2A4 /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; ABC9A767A49B686B3A3AC154 /* UniswapV3Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6363DB5DAE5B58AFDC0 /* UniswapV3Module.swift */; }; ABC9A774500F8D8D3D9E04DD /* SendBitcoinAdapterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1BD3B1B53C72DDF923A /* SendBitcoinAdapterService.swift */; }; - ABC9A77A41517173B610E0FC /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */; }; ABC9A78CFF8B232D330EC7B5 /* DiffLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A916C64B5EA9D96B8FDA /* DiffLabel.swift */; }; ABC9A78D3A4267CAC0F5D0E8 /* SendConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFF7119B9AC0E32B2060 /* SendConfirmationModule.swift */; }; ABC9A794E47FC07ABFC32BBD /* FeePriceScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF8E8DE67732371A00E0 /* FeePriceScale.swift */; }; @@ -2215,7 +2214,6 @@ ABC9A802418438F6BD1FC1E3 /* WalletTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A64A66778C137FA9642C /* WalletTokenViewController.swift */; }; ABC9A80BCDA72347C6619E6C /* SendTimeLockErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADF114FCFABEA148AF04 /* SendTimeLockErrorService.swift */; }; ABC9A819DDAEE683FCCA02EF /* NftAssetCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A30A8F78E9C9AEE861F1 /* NftAssetCellFactory.swift */; }; - ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */; }; ABC9A82D771D920162551294 /* WalletConnectPendingRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE15C187118DE6F0CE7B /* WalletConnectPendingRequestsViewModel.swift */; }; ABC9A83233DA4C83AF83E483 /* WalletConnectPendingRequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */; }; ABC9A8451CEF02EA0A94CEAA /* ProFeaturesAuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9628A708749A31EEA70 /* ProFeaturesAuthorizationManager.swift */; }; @@ -2234,7 +2232,6 @@ ABC9A8A74C527C4E01EBB8A5 /* RestoreCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A45E29D1773EF27A0074 /* RestoreCloudModule.swift */; }; ABC9A8AC5E635D9CB1704568 /* BackupDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFFD435E0C9FBE0E5E7C /* BackupDisclaimerView.swift */; }; ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB61EA3B39D8BDB1EEDE /* AppBackupProvider.swift */; }; - ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */; }; ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */; }; ABC9A8D215CC5D6A70736E84 /* SendBaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A48552CF0C90E22686A9 /* SendBaseService.swift */; }; ABC9A8D8709EC2B40D74A97A /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; }; @@ -2313,6 +2310,7 @@ ABC9ABFA7299ADDDFEE918F7 /* WalletConnectPairingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE88105815BFC477D71 /* WalletConnectPairingViewController.swift */; }; ABC9AC011EA9D58866999D88 /* UniswapV3DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE97D361FBF43F46F016 /* UniswapV3DataSource.swift */; }; ABC9AC10D815702B812CFFB7 /* NftAssetOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ABE473B354836327B3AC /* NftAssetOverviewService.swift */; }; + ABC9AC170807B409634706E6 /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD42C324F58B5EE00610 /* BackupManagerModule.swift */; }; ABC9AC1BD5C95957726F8AE8 /* MarketCardValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4FCDC5085002DF35C17 /* MarketCardValueView.swift */; }; ABC9AC50E2E966F009D78FD5 /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6B2EF46FF7EDA4728D3 /* CheckboxView.swift */; }; ABC9AC5552508D091D622027 /* SendZcashFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6F55A2C6777D25F57D5 /* SendZcashFactory.swift */; }; @@ -2347,6 +2345,7 @@ 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 */; }; + ABC9ACFCC63CDB6C7712E512 /* BackupManagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEA4B072067A9F10BE36 /* BackupManagerViewController.swift */; }; ABC9AD05E7B986179310D6D7 /* SwapInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A56611CF5E7B3F25CD5C /* SwapInputAccessoryView.swift */; }; ABC9AD1C8D0CE88A604D5250 /* SendBinanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0DD32AB4B9BAB79F11 /* SendBinanceFactory.swift */; }; ABC9AD1F6A6A7C97E4120F2F /* BackupAppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */; }; @@ -2384,6 +2383,7 @@ ABC9AE1E60CABA0101D62738 /* FullCoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9B35C58F6525F3B2D5C /* FullCoin.swift */; }; ABC9AE2131780654A7139081 /* RestorePassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0547CBE2B5A3E38891E /* RestorePassphraseViewController.swift */; }; ABC9AE223619E13A296BED51 /* MarketCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2B7FBA735A76083990C /* MarketCardCell.swift */; }; + ABC9AE262936C29D89DC61C8 /* BackupManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD42C324F58B5EE00610 /* BackupManagerModule.swift */; }; ABC9AE2C026D04B679644279 /* IntegerAmountInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA527E63E18179CB689A /* IntegerAmountInputCell.swift */; }; ABC9AE3D64AF3981A68D9913 /* SendConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3AB799024C8FC2C7DD8 /* SendConfirmationViewModel.swift */; }; ABC9AE3F4B99ABE949945A3A /* ChartIndicatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0C131342CC764890C2B /* ChartIndicatorsViewController.swift */; }; @@ -3808,7 +3808,6 @@ ABC9A1057AD189DA1CE31BF5 /* PredefinedBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredefinedBlockchainService.swift; sourceTree = ""; }; ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinDetailAdviceViewController.swift; sourceTree = ""; }; ABC9A1136889E6976E17B347 /* WalletConnectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectService.swift; sourceTree = ""; }; - ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManagerView.swift; sourceTree = ""; }; ABC9A12529DC8DE5D46D9776 /* ContactBookAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressViewModel.swift; sourceTree = ""; }; ABC9A12E4155640075755699 /* IndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorDataSource.swift; sourceTree = ""; }; ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapRevokeConfirmationViewController.swift; sourceTree = ""; }; @@ -3982,9 +3981,9 @@ ABC9AD2E1F25A5CED10DB81F /* MaIndicatorDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaIndicatorDataSource.swift; sourceTree = ""; }; ABC9AD35D41AEEBD38AA08B5 /* NftAssetModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetModule.swift; sourceTree = ""; }; ABC9AD3F677671FB57CCD886 /* WalletConnectPendingRequestsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPendingRequestsService.swift; sourceTree = ""; }; + ABC9AD42C324F58B5EE00610 /* BackupManagerModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManagerModule.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -4003,6 +4002,7 @@ ABC9AE8D5944EB202A471C80 /* RestorePassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePassphraseViewModel.swift; sourceTree = ""; }; ABC9AE97D361FBF43F46F016 /* UniswapV3DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3DataSource.swift; sourceTree = ""; }; ABC9AEA1D717D8CED8462AB0 /* WalletConnectMainViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectMainViewModel.swift; sourceTree = ""; }; + ABC9AEA4B072067A9F10BE36 /* BackupManagerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManagerViewController.swift; sourceTree = ""; }; ABC9AEAD18F73D4FBE05783D /* Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; ABC9AEC034DE5784F55BD5F3 /* PseudoAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PseudoAccessoryView.swift; sourceTree = ""; }; ABC9AECEEB35D57CB0965E79 /* WalletBackup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackup.swift; sourceTree = ""; }; @@ -7356,10 +7356,10 @@ ABC9A9C9D65EAD890AF617A4 /* BackupApp */ = { isa = PBXGroup; children = ( - ABC9AD5DC41717F92AC2151C /* BackupManager */, ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */, ABC9A386552F4E2372850DBB /* Backup */, ABC9A1A8B6E734D38D55E1D2 /* Restore */, + ABC9AE7C2B9E59FFF7663BCD /* BackupManagerLegacy */, ); path = BackupApp; sourceTree = ""; @@ -7489,15 +7489,6 @@ path = Platforms; sourceTree = ""; }; - ABC9AD5DC41717F92AC2151C /* BackupManager */ = { - isa = PBXGroup; - children = ( - ABC9A121EDBB1FF651D514D1 /* BackupManagerView.swift */, - ABC9AD8AEA1E3B1198DDECCE /* BackupManagerModule.swift */, - ); - path = BackupManager; - sourceTree = ""; - }; ABC9ADC8373B45DF33E35DCA /* Binance */ = { isa = PBXGroup; children = ( @@ -7539,6 +7530,15 @@ path = PriceView; sourceTree = ""; }; + ABC9AE7C2B9E59FFF7663BCD /* BackupManagerLegacy */ = { + isa = PBXGroup; + children = ( + ABC9AD42C324F58B5EE00610 /* BackupManagerModule.swift */, + ABC9AEA4B072067A9F10BE36 /* BackupManagerViewController.swift */, + ); + path = BackupManagerLegacy; + sourceTree = ""; + }; ABC9AE7E6FB8D22E8697DBA9 /* Eip1155 */ = { isa = PBXGroup; children = ( @@ -9456,8 +9456,6 @@ 11B356A35A5981DD231E580C /* ListStyle.swift in Sources */, 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */, 11B358D35D2270FD78C6EF82 /* AutoLockPeriod.swift in Sources */, - ABC9A50F1811EEABE61913F7 /* BackupManagerView.swift in Sources */, - ABC9A81D09F820C68D4F75FB /* BackupManagerModule.swift in Sources */, ABC9AF95141EA649524FBF88 /* CheckboxStyle.swift in Sources */, ABC9A79CFCEBAC442A1B791D /* BackupAppModule.swift in Sources */, ABC9A09E0B614E5B4E32B7F9 /* InputTextView.swift in Sources */, @@ -9489,6 +9487,8 @@ 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */, 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */, 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */, + ABC9AE262936C29D89DC61C8 /* BackupManagerModule.swift in Sources */, + ABC9ACFCC63CDB6C7712E512 /* BackupManagerViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10788,8 +10788,6 @@ 11B353577381981235B90A82 /* ListStyle.swift in Sources */, 11B354DC983042AD922339A6 /* DuressModeSelectView.swift in Sources */, 11B3511DAD3881FDE2419A64 /* AutoLockPeriod.swift in Sources */, - ABC9A8CAD0A3AAB7398C2A30 /* BackupManagerView.swift in Sources */, - ABC9A77A41517173B610E0FC /* BackupManagerModule.swift in Sources */, ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */, ABC9A99A45187C36D48840F8 /* BackupAppModule.swift in Sources */, ABC9AE51262C09EABF5CCEEE /* InputTextView.swift in Sources */, @@ -10821,6 +10819,8 @@ 11B35FB362526C723329C9ED /* ChartView.swift in Sources */, 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */, 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */, + ABC9AC170807B409634706E6 /* BackupManagerModule.swift in Sources */, + ABC9A37B5FAB65E7AB66547E /* BackupManagerViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift index 26cb76fe8c..fb81da12d7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/ContactBookManager.swift @@ -411,17 +411,24 @@ extension ContactBookManager { return contacts } - func restore(contacts: [BackupContact]) throws { + func restore(contacts: [BackupContact], mergePolitics: MergePolitics) throws { guard let localUrl else { state = .failed(ContactBookManager.StorageError.localUrlNotAvailable) return } - let newContactBook = helper.contactBook(contacts: contacts, lastVersion: state.data?.version) + let resolved: ContactBook - try save(url: localUrl, newContactBook) + switch mergePolitics { + case .replace: + resolved = helper.contactBook(contacts: contacts, lastVersion: state.data?.version) + case .insert: + resolved = helper.insert(contacts: contacts, book: state.data) + } + + try save(url: localUrl, resolved) if remoteSync { - try? saveToICloud(book: newContactBook) + try? saveToICloud(book: resolved) } } @@ -447,6 +454,10 @@ extension ContactBookManager { } extension ContactBookManager { + enum MergePolitics { + case replace, insert + } + enum StorageError: Error { case cloudUrlNotAvailable case localUrlNotAvailable diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift b/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift index 19710a987e..89f6e0b1d6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Contact.swift @@ -7,7 +7,7 @@ class ContactAddress: Codable, ImmutableMappable, Hashable, Equatable { enum CodingKeys: String, CodingKey { case blockchainUid = "blockchain_uid" - case address = "address" + case address } init(blockchainUid: String, address: String) { @@ -22,7 +22,7 @@ class ContactAddress: Codable, ImmutableMappable, Hashable, Equatable { func mapping(map: Map) { blockchainUid >>> map[CodingKeys.blockchainUid.rawValue] - address >>> map[CodingKeys.address.rawValue] + address >>> map[CodingKeys.address.rawValue] } func hash(into hasher: inout Hasher) { @@ -30,19 +30,16 @@ class ContactAddress: Codable, ImmutableMappable, Hashable, Equatable { hasher.combine(address.lowercased()) } - static func ==(lhs: ContactAddress, rhs: ContactAddress) -> Bool { + static func == (lhs: ContactAddress, rhs: ContactAddress) -> Bool { lhs.address.lowercased() == rhs.address.lowercased() && - lhs.blockchainUid == rhs.blockchainUid + lhs.blockchainUid == rhs.blockchainUid } - } extension Array where Element == ContactAddress { - - static func ==(lhs: [ContactAddress], rhs: [ContactAddress]) -> Bool { + static func == (lhs: [ContactAddress], rhs: [ContactAddress]) -> Bool { Set(lhs) == Set(rhs) } - } class Contact: Codable, ImmutableMappable, Hashable, Equatable { @@ -66,13 +63,13 @@ class Contact: Codable, ImmutableMappable, Hashable, Equatable { } func mapping(map: Map) { - uid >>> map["uid"] - modifiedAt >>> map["modified_at"] - name >>> map["name"] - addresses >>> map["addresses"] + uid >>> map["uid"] + modifiedAt >>> map["modified_at"] + name >>> map["name"] + addresses >>> map["addresses"] } - static func ==(lhs: Contact, rhs: Contact) -> Bool { + static func == (lhs: Contact, rhs: Contact) -> Bool { lhs.uid == rhs.uid } @@ -81,9 +78,8 @@ class Contact: Codable, ImmutableMappable, Hashable, Equatable { } func address(blockchainUid: String) -> ContactAddress? { - addresses.first { $0.blockchainUid == blockchainUid } + addresses.first { $0.blockchainUid == blockchainUid } } - } class DeletedContact: Codable, ImmutableMappable, Hashable, Equatable { @@ -101,18 +97,17 @@ class DeletedContact: Codable, ImmutableMappable, Hashable, Equatable { } func mapping(map: Map) { - uid >>> map["uid"] - deletedAt >>> map["deleted_at"] + uid >>> map["uid"] + deletedAt >>> map["deleted_at"] } - static func ==(lhs: DeletedContact, rhs: DeletedContact) -> Bool { + static func == (lhs: DeletedContact, rhs: DeletedContact) -> Bool { lhs.uid == rhs.uid } func hash(into hasher: inout Hasher) { hasher.combine(uid) } - } class ContactBook: Codable, ImmutableMappable { @@ -134,9 +129,8 @@ class ContactBook: Codable, ImmutableMappable { } func mapping(map: Map) { - version >>> map["version"] - contacts >>> map["contacts"] - deleted >>> map["deleted"] + version >>> map["version"] + contacts >>> map["contacts"] + deleted >>> map["deleted"] } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index ace803fe26..63a8bf4f64 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -186,7 +186,7 @@ extension AppBackupProvider { favoritesManager.add(coinUids: raw.watchlistIds) if !raw.contacts.isEmpty { - try? contactManager.restore(contacts: raw.contacts) + try? contactManager.restore(contacts: raw.contacts, mergePolitics: .replace) } evmSyncSourceManager.restore(selected: raw.settings.evmSyncSources.selected, custom: raw.customSyncSources) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookHelper.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookHelper.swift index 42d65284f2..e8bc5abb42 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookHelper.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookHelper.swift @@ -1,7 +1,6 @@ import Foundation class ContactBookHelper { - // Merge contacts with replace older contacts by newer func mergeNewer(lhs: [Contact], rhs: [Contact]) -> [Contact] { let left = Set(lhs) @@ -12,8 +11,8 @@ class ContactBookHelper { let mergedNewer = intersectionLeft.map { lContact in if let rContact = right.first(where: { $0.uid == lContact.uid }), - rContact.modifiedAt > lContact.modifiedAt { - + rContact.modifiedAt > lContact.modifiedAt + { return rContact } return lContact @@ -32,8 +31,8 @@ class ContactBookHelper { let mergedNewer = intersectionLeft.map { lContact in if let rContact = right.first(where: { $0.uid == lContact.uid }), - rContact.deletedAt > lContact.deletedAt { - + rContact.deletedAt > lContact.deletedAt + { return rContact } return lContact @@ -48,7 +47,6 @@ class ContactBookHelper { let contacts = contacts.filter { contact in if let deletedIndex = deleted.firstIndex(where: { contact.uid == $0.uid }) { - if deleted[deletedIndex].deletedAt > contact.modifiedAt { return false } else { @@ -71,10 +69,41 @@ class ContactBookHelper { return Set(lhsContacts) == Set(rhsContacts) && Set(lhsDeleted) == Set(rhsDeleted) } - } extension ContactBookHelper { + func insert(contacts: [BackupContact], book: ContactBook?) -> ContactBook { + var updatedContacts = book?.contacts ?? [] + + for contact in contacts { + var name = contact.name + if let index = updatedContacts.firstIndex(where: { $0.name == contact.name }) { + if updatedContacts[index].addresses == contact.addresses { + continue + } + + name = RestoreFileHelper.resolve( + name: contact.name, + elements: updatedContacts.map { $0.name }, + style: "(%d)" + ) + } + updatedContacts.append( + Contact( + uid: UUID().uuidString, + modifiedAt: Date().timeIntervalSince1970, + name: name, + addresses: contact.addresses + ) + ) + } + + return ContactBook( + version: (book?.version ?? 0) + 1, + contacts: updatedContacts, + deletedContacts: book?.deleted ?? [] + ) + } func update(contact: Contact, book: ContactBook) -> ContactBook { var contacts = book.contacts @@ -104,7 +133,6 @@ extension ContactBookHelper { // try resolve contact book. If one of them not changed - return it, else return new one func resolved(lhs: ContactBook, rhs: ContactBook) -> ResolveResult { - if lhs.version > rhs.version { return .left } @@ -138,34 +166,33 @@ extension ContactBookHelper { func backupContactBook(contactBook: ContactBook) -> BackupContactBook { BackupContactBook( - contacts: contactBook - .contacts - .map { BackupContact(uid: $0.uid, name: $0.name, addresses: $0.addresses) } + contacts: contactBook + .contacts + .map { BackupContact(uid: $0.uid, name: $0.name, addresses: $0.addresses) } ) } func contactBook(contacts: [BackupContact], lastVersion: Int?) -> ContactBook { // we need increase version and create new book with latest timestamps for all contacts ContactBook( - version: (lastVersion ?? 0) + 1, - contacts: contacts - .map { Contact(uid: $0.uid, - modifiedAt: Date().timeIntervalSince1970, - name: $0.name, - addresses: $0.addresses) - }, - deletedContacts: []) + version: (lastVersion ?? 0) + 1, + contacts: contacts + .map { Contact(uid: $0.uid, + modifiedAt: Date().timeIntervalSince1970, + name: $0.name, + addresses: $0.addresses) + }, + deletedContacts: [] + ) } - } extension ContactBookHelper { - private struct EqualContactData: Equatable, Hashable { let uid: String let timestamp: TimeInterval - static func ==(lhs: EqualContactData, rhs: EqualContactData) -> Bool { + static func == (lhs: EqualContactData, rhs: EqualContactData) -> Bool { lhs.uid == rhs.uid && lhs.timestamp == rhs.timestamp } @@ -181,5 +208,4 @@ extension ContactBookHelper { case right case merged(ContactBook) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookSettings/ContactBookSettingsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookSettings/ContactBookSettingsService.swift index 7e542c535a..e13ca5057a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookSettings/ContactBookSettingsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ContactBook/ContactBookSettings/ContactBookSettingsService.swift @@ -97,7 +97,7 @@ extension ContactBookSettingsService { } func replace(contacts: [BackupContact]) throws { - try contactManager.restore(contacts: contacts) + try contactManager.restore(contacts: contacts, mergePolitics: .replace) } func createBackupFile() throws -> URL { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift index 993903037a..c99e24a21e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift @@ -82,7 +82,7 @@ class RestoreFileConfigurationViewController: KeyboardAwareViewController { .highlightedDescription(text: "backup_app.restore.notice.description".localized), ], buttons: [ - .init(style: .yellow, title: "backup_app.restore.notice.merge".localized, actionType: .afterClose) { [weak self] in + .init(style: .red, title: "backup_app.restore.notice.merge".localized, actionType: .afterClose) { [weak self] in self?.viewModel.onTapRestore() }, .init(style: .transparent, title: "button.cancel".localized, actionType: .afterClose), @@ -105,13 +105,23 @@ class RestoreFileConfigurationViewController: KeyboardAwareViewController { } 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 - ) + if let description = item.description { + return tableView.universalRow62( + id: item.title, + title: .body(item.title), + description: .subhead2(description), + isFirst: rowInfo.isFirst, + isLast: rowInfo.isLast + ) + } else { + return tableView.universalRow48( + id: item.title, + title: .body(item.title), + value: .subhead1(item.value, color: .themeGray), + isFirst: rowInfo.isFirst, + isLast: rowInfo.isLast + ) + } } private var descriptionSection: SectionProtocol { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift index 1686634c30..abe5a7b13f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift @@ -46,7 +46,7 @@ extension RestoreFileConfigurationViewModel { } var otherItems: [BackupAppModule.Item] { - let contactAddressCount = rawBackup.contacts.reduce(into: 0) { $0 += $1.addresses.count } + let contactAddressCount = rawBackup.contacts.count let watchAccounts = rawBackup .accounts .filter { $0.account.watchAccount } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift index 8f3ea1cc79..dd7fe31cb6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileHelper.swift @@ -6,18 +6,33 @@ struct RestoreFileHelper { 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 } + static func resolve(name: String, elements: [String], checkRaw: Bool = false, style: String = "%d") -> String { + let name: (String?) -> String = { [name, $0].compactMap { $0 }.joined(separator: " ") } + + if checkRaw { + if !elements.contains(where: { $0.lowercased() == name(nil).lowercased() }) { + return name(nil) + } + } + + for i in 1 ..< elements.count + 1 { + let newName = name(style.localized(i)) + if !elements.contains(where: { $0.lowercased() == newName.lowercased() }) { + return newName + } + } + return name(style.localized(elements.count + 1)) + } } extension RestoreFileHelper { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift index 8e52d037ae..609f85b564 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift @@ -71,18 +71,17 @@ class BackupAppViewModel: ObservableObject { @Published var passwordCautionState: CautionState = .none @Published var password: String = AppConfig.defaultPassphrase { didSet { - validatePasswords() + clearCautions() } } @Published var confirmCautionState: CautionState = .none @Published var confirm: String = AppConfig.defaultPassphrase { didSet { - validatePasswords() + clearCautions() } } - @Published var passwordButtonDisabled = true @Published var passwordButtonProcessing = false private var dismissSubject = PassthroughSubject() @@ -111,8 +110,6 @@ class BackupAppViewModel: ObservableObject { otherItems = getOtherItems() selected = accountIds.reduce(into: [:]) { $0[$1] = true } name = nextName - - validatePasswords() } } @@ -153,15 +150,24 @@ extension BackupAppViewModel { private func getOtherItems() -> [BackupAppModule.Item] { let contacts = contactManager.all ?? [] - let contactAddressCount = contacts.reduce(into: 0) { $0 += $1.addresses.count } return BackupAppModule.items( watchAccountCount: accounts(watch: true).count, watchlistCount: favoritesManager.allCoinUids.count, - contactAddressCount: contactAddressCount, + contactAddressCount: contacts.count, blockchainSourcesCount: evmSyncSourceManager.customSyncSources(blockchainType: nil).count ) } + + private func clearCautions() { + if passwordCautionState != .none { + passwordCautionState = .none + } + + if confirmCautionState != .none { + confirmCautionState = .none + } + } } extension BackupAppViewModel { @@ -170,25 +176,17 @@ extension BackupAppViewModel { } } -// Backup Name VieeModel +// Backup Name ViewModel extension BackupAppViewModel { var nextName: String { - let name = { [Self.backupNamePrefix, $0].joined(separator: " ") } switch destination { case .cloud: - let exists = cloudBackupManager - .existFilenames - .filter { $0.hasPrefix(Self.backupNamePrefix) } - .sorted() - for i in 1 ..< exists.count + 1 { - let newName = name(i.description) - if !exists.contains(where: { $0.lowercased() == newName.lowercased() }) { - return newName - } - } - return name((exists.count + 1).description) + return RestoreFileHelper.resolve( + name: Self.backupNamePrefix, + elements: cloudBackupManager.existFilenames + ) default: - return name("1") + return Self.backupNamePrefix } } @@ -204,43 +202,30 @@ extension BackupAppViewModel { func validatePasswords() { var buttonDisabled = false - if password.isEmpty { - buttonDisabled = true - confirmCautionState = .none - } else { - do { - try BackupCrypto.validate(passphrase: password) - passwordCautionState = .none - } catch { - passwordCautionState = .caution(.init(text: error.localizedDescription, type: .error)) - buttonDisabled = true - } + clearCautions() + + do { + try BackupCrypto.validate(passphrase: password) + passwordCautionState = .none + } catch { + passwordCautionState = .caution(.init(text: error.localizedDescription, type: .error)) } - if confirm.isEmpty { - buttonDisabled = true - confirmCautionState = .none - } else { - do { - try BackupCrypto.validate(passphrase: confirm) - if password != confirm { - buttonDisabled = true - confirmCautionState = .caution( - .init( - text: "backup.cloud.password.confirm.error.doesnt_match".localized, - type: .error - ) + do { + try BackupCrypto.validate(passphrase: confirm) + if password != confirm { + confirmCautionState = .caution( + .init( + text: "backup.cloud.password.confirm.error.doesnt_match".localized, + type: .error ) - } else { - confirmCautionState = .none - } - } catch { - confirmCautionState = .caution(.init(text: error.localizedDescription, type: .error)) - buttonDisabled = true + ) + } else { + confirmCautionState = .none } + } catch { + confirmCautionState = .caution(.init(text: error.localizedDescription, type: .error)) } - - passwordButtonDisabled = buttonDisabled } @MainActor @@ -254,14 +239,19 @@ extension BackupAppViewModel { } func onTapSave() { + validatePasswords() + guard passwordCautionState == .none && confirmCautionState == .none else { + return + } passwordButtonProcessing = true + let selectedIds = accountIds.filter { (selected[$0] ?? false) } Task { switch destination { case .none: () case .cloud: do { - try cloudBackupManager.save(accountIds: accountIds, passphrase: password, name: name) + try cloudBackupManager.save(accountIds: selectedIds, passphrase: password, name: name) passwordButtonProcessing = false await showSuccess() dismissSubject.send() @@ -271,7 +261,7 @@ extension BackupAppViewModel { } case .local: do { - let url = try cloudBackupManager.file(accountIds: accountIds, passphrase: password, name: name) + let url = try cloudBackupManager.file(accountIds: selectedIds, passphrase: password, name: name) sharePresented = url passwordButtonProcessing = false } catch { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift index 73125c8f6c..29e4c4e26b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift @@ -4,7 +4,7 @@ import ThemeKit struct BackupDisclaimerView: View { @ObservedObject var viewModel: BackupAppViewModel - @Binding var backupPresented: Bool + var onDismiss: (() -> Void)? @State var isOn: Bool = true @@ -30,22 +30,23 @@ struct BackupDisclaimerView: View { .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( - destination: BackupNameView(viewModel: viewModel, backupPresented: $backupPresented), + destination: BackupNameView(viewModel: viewModel, onDismiss: onDismiss), isActive: $viewModel.namePushed ) { Button(action: { viewModel.namePushed = true }) { Text("button.next".localized) } - .buttonStyle(PrimaryButtonStyle(style: .yellow)) - .disabled(!isOn) + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(!isOn) } } } .navigationBarTitle(backupDisclaimer.title) + .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("button.cancel".localized) { - backupPresented = false + onDismiss?() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift index d058571ca2..f4e194cbb7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift @@ -4,7 +4,7 @@ import ThemeKit struct BackupListView: View { @ObservedObject var viewModel: BackupAppViewModel - @Binding var backupPresented: Bool + var onDismiss: (() -> Void)? var body: some View { ThemeView { @@ -45,9 +45,17 @@ struct BackupListView: View { ForEach(viewModel.otherItems) { (item: BackupAppModule.Item) in ListRow { VStack(spacing: 1) { - Text(item.title).themeBody() + HStack { + Text(item.title).themeBody() + + if let value = item.value { + Text(value).themeSubhead1(alignment: .trailing) + } + } - Text(item.description).themeSubhead2() + if let description = item.description { + Text(description).themeSubhead2() + } } } } @@ -57,7 +65,7 @@ struct BackupListView: View { .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( - destination: BackupDisclaimerView(viewModel: viewModel, backupPresented: $backupPresented), + destination: BackupDisclaimerView(viewModel: viewModel, onDismiss: onDismiss), isActive: $viewModel.disclaimerPushed ) { Button(action: { @@ -72,7 +80,7 @@ struct BackupListView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { - backupPresented = false + onDismiss?() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift index 56b9843663..dfeb9b3835 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift @@ -3,7 +3,7 @@ import ThemeKit struct BackupNameView: View { @ObservedObject var viewModel: BackupAppViewModel - @Binding var backupPresented: Bool + var onDismiss: (() -> Void)? var body: some View { ThemeView { @@ -28,7 +28,7 @@ struct BackupNameView: View { .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( - destination: BackupPasswordView(viewModel: viewModel, backupPresented: $backupPresented), + destination: BackupPasswordView(viewModel: viewModel, onDismiss: onDismiss), isActive: $viewModel.passwordPushed ) { Button(action: { @@ -40,11 +40,11 @@ struct BackupNameView: View { .disabled(viewModel.nameCautionState != .none) } } - .navigationBarTitle("backup_app.backup.name.title".localized) + .navigationTitle("backup_app.backup.name.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { - backupPresented = false + onDismiss?() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift index 0f15dbe967..3c585d3e44 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift @@ -4,7 +4,7 @@ import ThemeKit struct BackupPasswordView: View { @ObservedObject var viewModel: BackupAppViewModel - @Binding var backupPresented: Bool + var onDismiss: (() -> ())? @State var secureLock = true @@ -63,14 +63,14 @@ struct BackupPasswordView: View { } } .buttonStyle(PrimaryButtonStyle(style: .yellow)) - .disabled(viewModel.passwordButtonDisabled || viewModel.passwordButtonProcessing) + .disabled(viewModel.passwordButtonProcessing) .animation(.default, value: viewModel.passwordButtonProcessing) } .sheet(item: $viewModel.sharePresented) { url in let completion: UIActivityViewController.CompletionWithItemsHandler = { _, success, _, error in if success { + onDismiss?() showDone() - backupPresented = false } if let error { show(error: error) @@ -83,23 +83,19 @@ struct BackupPasswordView: View { } } .onReceive(viewModel.dismissPublisher) { - backupPresented = false + onDismiss?() } .navigationBarTitle("backup_app.backup.password.title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { Button("button.cancel".localized) { - backupPresented = false + onDismiss?() } + .disabled(viewModel.passwordButtonProcessing) } } } - @MainActor - private func showSuccess() { - HudHelper.instance.show(banner: .savedToCloud) - } - @MainActor private func show(error: Error) { HudHelper.instance.show(banner: .error(string: error.localizedDescription)) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift index 61d9e77c6f..2f05789cea 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift @@ -3,7 +3,7 @@ import ThemeKit struct BackupTypeView: View { @ObservedObject var viewModel: BackupAppViewModel - @Binding var backupPresented: Bool + var onDismiss: (() -> Void)? @State var cloudNavigationPushed = false @State var localNavigationPushed = false @@ -11,19 +11,18 @@ struct BackupTypeView: View { var body: some View { ScrollableThemeView { - VStack(spacing: .margin24) { - Text("backup_app.backup_type.description".localized) - .themeSubhead2() - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) - + VStack(spacing: .margin12) { ListSection { - navigation(image: "icloud_24", text: "backup_app.backup_type.cloud".localized, isAvailable: $viewModel.cloudAvailable, isActive: $cloudNavigationPushed) { + navigation( + image: "icloud_24", + text: "backup_app.backup_type.cloud".localized, + description: "backup_app.backup_type.cloud.description".localized, + isAvailable: $viewModel.cloudAvailable, + isActive: $cloudNavigationPushed + ) { if viewModel.cloudAvailable { viewModel.destination = .cloud } else { cloudAlertPresented = true } } - - navigation(image: "file_24", text: "backup_app.backup_type.file".localized, isActive: $localNavigationPushed) { - viewModel.destination = .local - } + .frame(minHeight: 106) } .sheet(isPresented: $cloudAlertPresented) { if #available(iOS 16, *) { @@ -32,38 +31,53 @@ struct BackupTypeView: View { ViewWrapper(BottomSheetModule.cloudNotAvailableController()) } } - } - .navigationBarTitle("backup_app.backup_type.title".localized) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - Button("button.cancel".localized) { - backupPresented = false + + ListSection { + navigation( + image: "file_24", + text: "backup_app.backup_type.file".localized, + description: "backup_app.backup_type.file.description".localized, + isActive: $localNavigationPushed + ) { + viewModel.destination = .local + } + .frame(minHeight: 106) } } .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } + .navigationTitle("backup_app.backup_type.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("button.cancel".localized) { + onDismiss?() + } + } } - @ViewBuilder func row(image: String, text: String) -> some View { + @ViewBuilder func row(image: String, text: String, description: String) -> some View { HStack(spacing: .margin16) { Image(image).themeIcon() - Text(text).themeBody() - Image.disclosureIcon + VStack(spacing: .margin4) { + Text(text).themeBody() + Text(description).themeSubhead2() + } } + .padding(EdgeInsets(top: .margin12, leading: 0, bottom: .margin12, trailing: 0)) } - @ViewBuilder func navigation(image: String, text: String, isAvailable: Binding = .constant(true), isActive: Binding, action: @escaping () -> Void = {}) -> some View { + @ViewBuilder func navigation(image: String, text: String, description: String, isAvailable: Binding = .constant(true), isActive: Binding, action: @escaping () -> Void = {}) -> some View { if isAvailable.wrappedValue { NavigationRow( - destination: { BackupListView(viewModel: viewModel, backupPresented: $backupPresented) }, + destination: { BackupListView(viewModel: viewModel, onDismiss: onDismiss) }, isActive: isActive ) { - row(image: image, text: text.localized) + row(image: image, text: text.localized, description: description) } .onChange(of: isActive.wrappedValue) { _ in action() } } else { ClickableRow(action: action) { - row(image: image, text: text.localized) + row(image: image, text: text.localized, description: description) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift index 2285de7138..d474139e49 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupAppModule.swift @@ -1,7 +1,7 @@ import SwiftUI struct BackupAppModule { - static func view(backupPresented: Binding) -> some View { + static func view(onDismiss: (() -> ())?) -> some View { let viewModel = BackupAppViewModel( accountManager: App.shared.accountManager, contactManager: App.shared.contactManager, @@ -10,7 +10,7 @@ struct BackupAppModule { evmSyncSourceManager: App.shared.evmSyncSourceManager ) - return BackupTypeView(viewModel: viewModel, backupPresented: backupPresented) + return BackupTypeView(viewModel: viewModel, onDismiss: onDismiss) } } @@ -58,28 +58,28 @@ extension BackupAppModule { if watchAccountCount != 0 { items.append(BackupAppModule.Item( title: "backup_app.backup_list.other.watch_account.title".localized, - description: "backup_app.backup_list.other.watch_account.description".localized(watchAccountCount) + value: watchAccountCount.description )) } if watchlistCount != 0 { items.append(BackupAppModule.Item( title: "backup_app.backup_list.other.watchlist.title".localized, - description: "backup_app.backup_list.other.watchlist.description".localized(watchlistCount) + value: watchlistCount.description )) } if contactAddressCount != 0 { items.append(BackupAppModule.Item( title: "backup_app.backup_list.other.contacts.title".localized, - description: "backup_app.backup_list.other.contacts.description".localized(contactAddressCount) + value: contactAddressCount.description )) } if blockchainSourcesCount != 0 { items.append(BackupAppModule.Item( title: "backup_app.backup_list.other.blockchain_settings.title".localized, - description: "backup_app.backup_list.other.blockchain_settings.description".localized(blockchainSourcesCount) + value: blockchainSourcesCount.description )) } items.append(BackupAppModule.Item( @@ -113,7 +113,14 @@ extension BackupAppModule { struct Item: Identifiable { let title: String - let description: String + let value: String? + let description: String? + + init(title: String, value: String? = nil, description: String? = nil) { + self.title = title + self.value = value + self.description = description + } var id: String { title diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift deleted file mode 100644 index 785b0ea0be..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerModule.swift +++ /dev/null @@ -1,7 +0,0 @@ -import SwiftUI - -struct BackupManagerModule { - static func view() -> some View { - BackupManagerView() - } -} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift deleted file mode 100644 index 6c919ec1e1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManager/BackupManagerView.swift +++ /dev/null @@ -1,35 +0,0 @@ -import SDWebImageSwiftUI -import SwiftUI -import ThemeKit - -struct BackupManagerView: View { - @State var restorePresented: Bool = false - @State var backupPresented: Bool = false - - var body: some View { - ScrollableThemeView { - ListSection { - ClickableRow(action: { - restorePresented = true - }) { - Image("download_24").themeIcon(color: .themeJacob) - Text("backup_app.backup_manager.restore".localized).themeBody(color: .themeJacob) - } - ClickableRow(action: { - backupPresented = true - }) { - Image("plus_24").themeIcon(color: .themeJacob) - Text("backup_app.backup_manager.create".localized).themeBody(color: .themeJacob) - } - } - .sheet(isPresented: $restorePresented) { - InfoModule.restoreSourceInfo - } - .sheet(isPresented: $backupPresented) { - ThemeNavigationView { BackupAppModule.view(backupPresented: $backupPresented) } - } - .navigationBarTitle("backup_app.backup_manager.title".localized) - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerModule.swift new file mode 100644 index 0000000000..1134aa2fb2 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerModule.swift @@ -0,0 +1,7 @@ +import UIKit + +class BackupManagerModule { + static func viewController() -> UIViewController { + BackupManagerViewController() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerViewController.swift new file mode 100644 index 0000000000..4854618775 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/BackupManagerLegacy/BackupManagerViewController.swift @@ -0,0 +1,94 @@ +import Combine +import SectionsTableView +import SwiftUI +import ThemeKit +import UIKit + +class BackupManagerViewController: ThemeViewController { + private let tableView = SectionsTableView(style: .grouped) + + override init() { + super.init() + + hidesBottomBarWhenPushed = true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "backup_app.backup_manager.title".localized + navigationItem.largeTitleDisplayMode = .never + + view.addSubview(tableView) + tableView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + + tableView.sectionDataSource = self + + tableView.buildSections() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.deselectCell(withCoordinator: transitionCoordinator, animated: animated) + } + + private func onRestore() { + let viewController = RestoreTypeModule.viewController(type: .full, sourceViewController: self) + present(viewController, animated: true) + } + + private func onCreate() { + let viewController = BackupAppModule + .view { [weak self] in + self?.presentedViewController?.dismiss(animated: true) + }.toNavigationViewController() + + present(viewController, animated: true) + } +} + +extension BackupManagerViewController: SectionsDataSource { + func buildSections() -> [SectionProtocol] { + [ + Section( + id: "type-section", + headerState: .margin(height: .margin12), + footerState: .margin(height: .margin32), + rows: [ + tableView.universalRow48( + id: "restore", + image: .local(UIImage(named: "download_24")?.withTintColor(.themeJacob)), + title: .body("backup_app.backup_manager.restore".localized, color: .themeJacob), + backgroundStyle: .lawrence, + autoDeselect: true, + isFirst: true, + isLast: false + ) { [weak self] in + self?.onRestore() + }, + tableView.universalRow48( + id: "create", + image: .local(UIImage(named: "plus_24")?.withTintColor(.themeJacob)), + title: .body("backup_app.backup_manager.create".localized, color: .themeJacob), + backgroundStyle: .lawrence, + autoDeselect: true, + isFirst: false, + isLast: true + ) { [weak self] in + self?.onCreate() + }, + ] + ), + ] + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index f300cafcd3..519d98edd2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -234,7 +234,7 @@ class MainSettingsViewController: ThemeViewController { accessoryType: .disclosure, isLast: true, action: { [weak self] in - let viewController = BackupManagerModule.view().toViewController(title: "backup_manager.title".localized) + let viewController = BackupManagerModule.viewController() self?.navigationController?.pushViewController(viewController, animated: true) } ), diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 5825ab80c3..3bb301c758 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -138,7 +138,6 @@ Go to Settings - > %@ and allow access to the camera."; // Restore Type "restore_type.title" = "Import Wallet"; - "restore_type.recovery.title" = "from Recovery Phrase"; "restore_type.cloud.title" = "from iCloud"; "restore_type.file.title" = "from Files"; @@ -1097,23 +1096,20 @@ Go to Settings - > %@ and allow access to the camera."; "backup_app.backup_manager.restore" = "Restore Backup"; "backup_app.backup_manager.create" = "Create New Backup"; -"backup_app.backup_type.title" = "Backup Type"; -"backup_app.backup_type.description" = "Select where you want to save the backup file."; -"backup_app.backup_type.cloud" = "Backup to iCloud"; -"backup_app.backup_type.file" = "Backup to Files"; +"backup_app.backup_type.title" = "Save Backup"; +"backup_app.backup_type.cloud" = "to iCloud"; +"backup_app.backup_type.cloud.description" = "Saving a backup copy file in the your keychain."; +"backup_app.backup_type.file" = "to Files"; +"backup_app.backup_type.file.description" = "Saving a backup copy file to your local folder."; "backup_app.backup_list.title" = "Backup File"; "backup_app.backup_list.description.restore" = "List of contents in the backup file."; "backup_app.backup_list.header.wallets" = "Wallets"; "backup_app.backup_list.header.other" = "Other"; "backup_app.backup_list.other.watch_account.title" = "Watch Wallets"; -"backup_app.backup_list.other.watch_account.description" = "Addresses: %d"; "backup_app.backup_list.other.watchlist.title" = "Watchlist"; -"backup_app.backup_list.other.watchlist.description" = "Coins: %d"; "backup_app.backup_list.other.contacts.title" = "Contacts"; -"backup_app.backup_list.other.contacts.description" = "Addresses: %d"; "backup_app.backup_list.other.blockchain_settings.title" = "Blockchain Settings"; -"backup_app.backup_list.other.blockchain_settings.description" = "Custom RPCs: %d"; "backup_app.backup_list.other.app_settings.title" = "App Settings"; "backup_app.backup_list.other.app_settings.description" = "Language, Currency, Appearance ..."; @@ -1137,8 +1133,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"; +"backup_app.restore_type.title" = "Restore"; + +"backup_app.restore.notice.description" = "This action will overwrite your local payment contacts as well as its iCloud copy (if there is one)"; +"backup_app.restore.notice.merge" = "Replace"; "backup.password.title" = "Backup Password"; "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."; From 607aa1f9cc2ece1e37bd97cfc8ef12fc4169c235 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 11 Oct 2023 17:32:31 +0600 Subject: [PATCH 53/63] Initial (not completed) implementation of CoinPage module in SwiftUI --- .../project.pbxproj | 24 +++++++ .../Coin/Analytics/CoinAnalyticsModule.swift | 43 ++++++++---- .../Coin/CoinMarkets/CoinMarketsModule.swift | 23 +++++-- .../Coin/CoinOverview/CoinOverviewView.swift | 4 ++ .../Modules/Coin/CoinPageModule.swift | 34 +++++++--- .../Modules/Coin/CoinPageView.swift | 68 +++++++++++++++++++ .../Modules/Coin/CoinPageViewModelNew.swift | 24 +++++++ .../UserInterface/TabButtonStyle.swift | 13 ++++ .../UserInterface/TabHeaderView.swift | 48 +++++++++++++ 9 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/TabButtonStyle.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 7a9cae0963..b162652128 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ 11B352184FCE4B2B3E68E459 /* CurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */; }; 11B3521C81ACF7BFC8875A61 /* TransactionsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DA1121283E64C139183 /* TransactionsHeaderView.swift */; }; 11B352210BEEE91481291D4C /* FormCautionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3568F6FAF721301DEC188 /* FormCautionView.swift */; }; + 11B3522207EA307D94070776 /* CoinPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3553967AFF40F6A9A611A /* CoinPageView.swift */; }; 11B35224D7A5A864C1C6F167 /* SecondaryCircleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */; }; 11B35228CD314AB9DC68DDAA /* EnabledWalletCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566B18FBFBA85D98D824 /* EnabledWalletCacheManager.swift */; }; 11B352309B81355B88BF6B66 /* OneInchKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3514BC3FF2FAC76ADF9F7 /* OneInchKit.swift */; }; @@ -365,6 +366,7 @@ 11B35421DB6DADDF59764A46 /* SimpleActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A3B86D99FBB036C74C7 /* SimpleActivateView.swift */; }; 11B35424EC5CBD0653F2A742 /* RestoreBinanceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35995E0D358AC4DA2FA74 /* RestoreBinanceModule.swift */; }; 11B35425857F772B06E7805D /* CexWithdrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ABC3E6C990E3BFA0A7B /* CexWithdrawViewController.swift */; }; + 11B3542694E183882F9BEBEC /* CoinPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3553967AFF40F6A9A611A /* CoinPageView.swift */; }; 11B3542831EAA647A1D16E8A /* MarkdownVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DC72F0D8DBBCCE2F988 /* MarkdownVisitor.swift */; }; 11B354283B8AC609B65AADDF /* FavoriteCoinRecord_v_0_22.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */; }; 11B3542A74C72C4CE03C727B /* CoinToggleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A3B8FB90E561DE3F22B /* CoinToggleViewController.swift */; }; @@ -433,12 +435,14 @@ 11B355028142F82D805752AF /* NftDoubleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FEC3027F45085959FBB /* NftDoubleCell.swift */; }; 11B35503B3E84FEFCDF1AFED /* ThemeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E7E7A5DBB09A2A5197D /* ThemeView.swift */; }; 11B3550424326606B055D7E5 /* AboutModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E80D544DAF20B12B56 /* AboutModule.swift */; }; + 11B3550A6826CF513B1A77F0 /* TabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */; }; 11B3550B0E0438427CBE72A3 /* NftCollectionOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507299A9DA6CF3C626A /* NftCollectionOverviewModule.swift */; }; 11B3550C46C5811A7540A934 /* ReceiveAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B109B4F60753BEC5078 /* ReceiveAddressService.swift */; }; 11B3550E1FD46B02F0154CBC /* PublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5A3CC8C229C0849756 /* PublicKeysViewController.swift */; }; 11B35516B09BF11EDC33482F /* TransactionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352C2F20DB6266112BE68 /* TransactionsService.swift */; }; 11B35518B24EDB088463A7A4 /* ManageAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E9E1D262629CD843F7E /* ManageAccountModule.swift */; }; 11B3551E5E9A6D167F7BA078 /* LaunchScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEC0AB0B09C7E4209A /* LaunchScreenManager.swift */; }; + 11B3551F51D987A150C3BC26 /* TabButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */; }; 11B35521FDF21F9B1667AF72 /* Eip20Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3524B273DD5AB2FF5C7A6 /* Eip20Kit.swift */; }; 11B35525C4BAEF22CF73F261 /* CreateAccountSimpleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351628BA5984C6EBB412E /* CreateAccountSimpleViewController.swift */; }; 11B3553109794AE192BF7591 /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; @@ -508,6 +512,7 @@ 11B3560586CBAB617211F003 /* Caution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D96CF03878016FC38FD /* Caution.swift */; }; 11B35608F7D19B3E6318CB22 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352972B14FA6EBEFD6904 /* Text.swift */; }; 11B3560E158C55624C466E27 /* GuidesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E511F9D2B6C65792324 /* GuidesViewController.swift */; }; + 11B3560F69D84432665A2BAA /* CoinPageViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */; }; 11B3561679C05C31F16EDC77 /* BaseUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */; }; 11B3561A469C906B67F24459 /* FeeRateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359BBFCD82C3C6DC06F96 /* FeeRateProvider.swift */; }; 11B3561E7DF566A274210E01 /* EvmSyncSourceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */; }; @@ -753,6 +758,7 @@ 11B3590E40C88ADD16DEEABB /* BackupMnemonicWordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35736BA15E54066036D54 /* BackupMnemonicWordCell.swift */; }; 11B3591204E6399DA9AA203E /* WatchEvmAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A8370C726989F4F456E /* WatchEvmAddressViewModel.swift */; }; 11B359131D838F3191A8C520 /* BtcBlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358830357DB1F87FCA006 /* BtcBlockchainSettingsViewModel.swift */; }; + 11B35916211F5D5EA0DBD207 /* CoinPageViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */; }; 11B3591854D77701EB7218BC /* CoinInvestorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */; }; 11B3591C77EE71054BF819D0 /* SetPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */; }; 11B3591DF0CC1D367C1241AF /* ExtendedKeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F1248EDA20F7141AB8 /* ExtendedKeyModule.swift */; }; @@ -947,6 +953,7 @@ 11B35B3F5DE4F73225EBFF36 /* CoinRankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */; }; 11B35B501A30615698B04C96 /* AddEvmTokenBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352ABFDEAEEA84D3FDD8B /* AddEvmTokenBlockchainService.swift */; }; 11B35B507F2F843A5B3E4C7C /* EvmNetworkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351895EE2816DE7BBC767 /* EvmNetworkViewModel.swift */; }; + 11B35B5451BA0A3C825809A2 /* TabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */; }; 11B35B5B8F3FEED445647E56 /* EvmCoinServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35410733A35D1558E55B2 /* EvmCoinServiceFactory.swift */; }; 11B35B5FA3177BC9ED21B929 /* SwapApproveConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356671FA76C7DEDA50B94 /* SwapApproveConfirmationModule.swift */; }; 11B35B6586B14C6A9F35E39D /* MarketAdvancedSearchResultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358C7505D0DE60CD03B22 /* MarketAdvancedSearchResultService.swift */; }; @@ -1149,6 +1156,7 @@ 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */; }; 11B35DDC98FFF447333278FF /* MarketAdvancedSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */; }; 11B35DDD77B56489D1EB72C5 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3569F2E6BD5E9CBCFCA1F /* Token.swift */; }; + 11B35DDE363387B6E7A1D3B9 /* TabButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */; }; 11B35DDFAF0532881A4F68B0 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; 11B35DE3E7E6EB3CFAB81329 /* UnlinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */; }; 11B35DE5BD5716307300AD2F /* OneInchKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3514BC3FF2FAC76ADF9F7 /* OneInchKit.swift */; }; @@ -2849,6 +2857,7 @@ 11B351CD91AE01747F66E746 /* SubscriptionInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoViewController.swift; sourceTree = ""; }; 11B351CEB402BC8F806365D9 /* MarketOverviewNftCollectionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewNftCollectionsService.swift; sourceTree = ""; }; 11B351DAF31FBE0834EBC066 /* TermsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsService.swift; sourceTree = ""; }; + 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabButtonStyle.swift; sourceTree = ""; }; 11B351E034126F57DB7B4263 /* NftActivityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityService.swift; sourceTree = ""; }; 11B351E1107158B6A2BF2149 /* ActivateSubscriptionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionService.swift; sourceTree = ""; }; 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesViewController.swift; sourceTree = ""; }; @@ -2888,6 +2897,7 @@ 11B3529B6C7C426755CE9E14 /* ManageWalletsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageWalletsService.swift; sourceTree = ""; }; 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditPasscodeModule.swift; sourceTree = ""; }; 11B3529D276325D741CAEEF5 /* UnlinkModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkModule.swift; sourceTree = ""; }; + 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageViewModelNew.swift; sourceTree = ""; }; 11B352A41EC99ADCC8F3E3E9 /* FormAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormAmountInputView.swift; sourceTree = ""; }; 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkViewController.swift; sourceTree = ""; }; 11B352ABFDEAEEA84D3FDD8B /* AddEvmTokenBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEvmTokenBlockchainService.swift; sourceTree = ""; }; @@ -2999,6 +3009,7 @@ 11B355129D9F61172FCAB8C0 /* NftService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftService.swift; sourceTree = ""; }; 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTokenModule.swift; sourceTree = ""; }; 11B3552D3F84BA594EFE964C /* MarkdownBlockQuoteCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownBlockQuoteCell.swift; sourceTree = ""; }; + 11B3553967AFF40F6A9A611A /* CoinPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageView.swift; sourceTree = ""; }; 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewService.swift; sourceTree = ""; }; 11B35542A7D7FE1BDC2E73E2 /* AccountType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountType.swift; sourceTree = ""; }; 11B355436F62829DBE3C92B4 /* CellComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellComponent.swift; sourceTree = ""; }; @@ -3103,6 +3114,7 @@ 11B357C3907AC1134C7A95DB /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 11B357C67623035CDF98B540 /* CexAssetResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAssetResponse.swift; sourceTree = ""; }; 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewController.swift; sourceTree = ""; }; + 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabHeaderView.swift; sourceTree = ""; }; 11B357D89546EBA13B01A1ED /* TransactionsViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsViewItemFactory.swift; sourceTree = ""; }; 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformHeaderCell.swift; sourceTree = ""; }; 11B357E9508BF369BDFF7753 /* MarkdownListItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownListItemCell.swift; sourceTree = ""; }; @@ -4610,6 +4622,8 @@ ABC9A4C18F27446916AD53E0 /* KeyboardTracker */, ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */, 11B3546A6E6F3CC013C9FF44 /* SwiftUI */, + 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */, + 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */, ); path = UserInterface; sourceTree = ""; @@ -6811,6 +6825,8 @@ 58AAA444C885BCC354F1B7B3 /* CoinPageMarkdownParser.swift */, 11B354B9064677BDA5946B48 /* Ranks */, ABC9A04E5F5F2817B1E287A2 /* Indicators */, + 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */, + 11B3553967AFF40F6A9A611A /* CoinPageView.swift */, ); path = Coin; sourceTree = ""; @@ -9489,6 +9505,10 @@ 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */, ABC9AE262936C29D89DC61C8 /* BackupManagerModule.swift in Sources */, ABC9ACFCC63CDB6C7712E512 /* BackupManagerViewController.swift in Sources */, + 11B3560F69D84432665A2BAA /* CoinPageViewModelNew.swift in Sources */, + 11B3542694E183882F9BEBEC /* CoinPageView.swift in Sources */, + 11B35B5451BA0A3C825809A2 /* TabHeaderView.swift in Sources */, + 11B35DDE363387B6E7A1D3B9 /* TabButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10821,6 +10841,10 @@ 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */, ABC9AC170807B409634706E6 /* BackupManagerModule.swift in Sources */, ABC9A37B5FAB65E7AB66547E /* BackupManagerViewController.swift in Sources */, + 11B35916211F5D5EA0DBD207 /* CoinPageViewModelNew.swift in Sources */, + 11B3522207EA307D94070776 /* CoinPageView.swift in Sources */, + 11B3550A6826CF513B1A77F0 /* TabHeaderView.swift in Sources */, + 11B3551F51D987A150C3BC26 /* TabButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift index b556cea1d3..f22656c8cf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift @@ -1,35 +1,37 @@ -import UIKit -import ThemeKit import MarketKit +import SwiftUI +import ThemeKit +import UIKit struct CoinAnalyticsModule { + static func view(fullCoin: FullCoin) -> some View { + CoinAnalyticsView(fullCoin: fullCoin) + } static func viewController(fullCoin: FullCoin) -> CoinAnalyticsViewController { let service = CoinAnalyticsService( - fullCoin: fullCoin, - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit, - subscriptionManager: App.shared.subscriptionManager + fullCoin: fullCoin, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + subscriptionManager: App.shared.subscriptionManager ) let technicalIndicatorService = TechnicalIndicatorService( - coinUid: fullCoin.coin.uid, - currencyKit: App.shared.currencyKit, - marketKit: App.shared.marketKit + coinUid: fullCoin.coin.uid, + currencyKit: App.shared.currencyKit, + marketKit: App.shared.marketKit ) let coinIndicatorViewItemFactory = CoinIndicatorViewItemFactory() let viewModel = CoinAnalyticsViewModel( - service: service, - technicalIndicatorService: technicalIndicatorService, - coinIndicatorViewItemFactory: coinIndicatorViewItemFactory + service: service, + technicalIndicatorService: technicalIndicatorService, + coinIndicatorViewItemFactory: coinIndicatorViewItemFactory ) return CoinAnalyticsViewController(viewModel: viewModel) } - } extension CoinAnalyticsModule { - enum Rating: String, CaseIterable { case excellent case good @@ -48,10 +50,21 @@ extension CoinAnalyticsModule { switch self { case .excellent: return .themeGreenD case .good: return .themeYellowD - case .fair: return UIColor(hex: 0xff7a00) + case .fair: return UIColor(hex: 0xFF7A00) case .poor: return .themeRedD } } } +} + +struct CoinAnalyticsView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let fullCoin: FullCoin + + func makeUIViewController(context _: Context) -> UIViewController { + CoinAnalyticsModule.viewController(fullCoin: fullCoin) + } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsModule.swift index 6a2734fb04..d2b7d23358 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsModule.swift @@ -1,13 +1,17 @@ -import UIKit import MarketKit +import SwiftUI +import UIKit struct CoinMarketsModule { + static func view(coin: Coin) -> some View { + CoinMarketsView(coin: coin) + } static func viewController(coin: Coin) -> CoinMarketsViewController { let service = CoinMarketsService( - coin: coin, - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit + coin: coin, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit ) let viewModel = CoinMarketsViewModel(service: service) @@ -15,5 +19,16 @@ struct CoinMarketsModule { return CoinMarketsViewController(viewModel: viewModel, headerViewModel: headerViewModel, urlManager: UrlManager(inApp: false)) } +} + +struct CoinMarketsView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let coin: Coin + + func makeUIViewController(context _: Context) -> UIViewController { + CoinMarketsModule.viewController(coin: coin) + } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift index 661b157722..210181e12c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift @@ -8,6 +8,7 @@ struct CoinOverviewView: View { let chartIndicatorRepository: IChartIndicatorsRepository let chartPointFetcher: IChartPointFetcher + @State private var hasAppeared = false @State private var chartIndicatorsShown = false var body: some View { @@ -109,6 +110,9 @@ struct CoinOverviewView: View { } } .onAppear { + guard !hasAppeared else { return } + hasAppeared = true + viewModel.sync() } .sheet(isPresented: $chartIndicatorsShown) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift index 7b89dd7065..9cb981eba5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift @@ -1,9 +1,24 @@ -import UIKit import LanguageKit -import ThemeKit import MarketKit +import SwiftUI +import ThemeKit +import UIKit struct CoinPageModule { + static func view(fullCoin: FullCoin) -> some View { + let viewModel = CoinPageViewModelNew(fullCoin: fullCoin, favoritesManager: App.shared.favoritesManager) + + let overviewView = CoinOverviewModule.view(coinUid: fullCoin.coin.uid) + let analyticsView = CoinAnalyticsModule.view(fullCoin: fullCoin) + let marketsView = CoinMarketsModule.view(coin: fullCoin.coin) + + return CoinPageView( + viewModel: viewModel, + overviewView: { overviewView }, + analyticsView: { analyticsView.ignoresSafeArea(edges: .bottom) }, + marketsView: { marketsView.ignoresSafeArea(edges: .bottom) } + ) + } static func viewController(coinUid: String) -> UIViewController? { guard let fullCoin = try? App.shared.marketKit.fullCoins(coinUids: [coinUid]).first else { @@ -11,8 +26,8 @@ struct CoinPageModule { } let service = CoinPageService( - fullCoin: fullCoin, - favoritesManager: App.shared.favoritesManager + fullCoin: fullCoin, + favoritesManager: App.shared.favoritesManager ) let viewModel = CoinPageViewModel(service: service) @@ -23,19 +38,17 @@ struct CoinPageModule { // let tweetsController = CoinTweetsModule.viewController(fullCoin: fullCoin) let viewController = CoinPageViewController( - viewModel: viewModel, - overviewController: overviewController, - analyticsController: analyticsController, - marketsController: marketsController + viewModel: viewModel, + overviewController: overviewController, + analyticsController: analyticsController, + marketsController: marketsController ) return ThemeNavigationController(rootViewController: viewController) } - } extension CoinPageModule { - enum Tab: Int, CaseIterable { case overview case analytics @@ -51,5 +64,4 @@ extension CoinPageModule { } } } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageView.swift new file mode 100644 index 0000000000..b92a8b395e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageView.swift @@ -0,0 +1,68 @@ +import SwiftUI + +struct CoinPageView: View { + @ObservedObject var viewModel: CoinPageViewModelNew + + @ViewBuilder let overviewView: Overview + @ViewBuilder let analyticsView: Analytics + @ViewBuilder let marketsView: Markets + + @Environment(\.presentationMode) private var presentationMode + @State private var currentTabIndex: Int = Tab.overview.rawValue + + var body: some View { + ThemeNavigationView { + ThemeView { + VStack(spacing: 0) { + TabHeaderView( + tabs: Tab.allCases.map { $0.title }, + currentTabIndex: $currentTabIndex + ) + + TabView(selection: $currentTabIndex) { + overviewView.tag(Tab.overview.rawValue) + analyticsView.tag(Tab.analytics.rawValue) + marketsView.tag(Tab.markets.rawValue) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .ignoresSafeArea(edges: .bottom) + } + } + .navigationTitle(viewModel.fullCoin.coin.code) + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("button.close".localized) { + presentationMode.wrappedValue.dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + viewModel.isFavorite.toggle() + }) { + Image(viewModel.isFavorite ? "filled_star_24" : "star_24") + .renderingMode(.template) + .foregroundColor(viewModel.isFavorite ? .themeJacob : .themeGray) + } + } + } + } + } +} + +extension CoinPageView { + enum Tab: Int, CaseIterable { + case overview + case analytics + case markets + + var title: String { + switch self { + case .overview: return "coin_page.overview".localized + case .analytics: return "coin_page.analytics".localized + case .markets: return "coin_page.markets".localized + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift new file mode 100644 index 0000000000..efdbd0b49a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift @@ -0,0 +1,24 @@ +import Combine +import MarketKit + +class CoinPageViewModelNew: ObservableObject { + let fullCoin: FullCoin + private let favoritesManager: FavoritesManager + + @Published var isFavorite: Bool { + didSet { + if isFavorite { + favoritesManager.add(coinUid: fullCoin.coin.uid) + } else { + favoritesManager.remove(coinUid: fullCoin.coin.uid) + } + } + } + + init(fullCoin: FullCoin, favoritesManager: FavoritesManager) { + self.fullCoin = fullCoin + self.favoritesManager = favoritesManager + + isFavorite = favoritesManager.isFavorite(coinUid: fullCoin.coin.uid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/TabButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabButtonStyle.swift new file mode 100644 index 0000000000..2f8a2c233c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabButtonStyle.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct TabButtonStyle: ButtonStyle { + let isActive: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(maxWidth: .infinity, maxHeight: .infinity) + .font(.themeSubhead1) + .foregroundColor(isActive ? .themeLeah : .themeGray) + .contentShape(Rectangle()) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift new file mode 100644 index 0000000000..5c4f39dcf8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct TabHeaderView: View { + let tabs: [String] + + @Binding var currentTabIndex: Int + @State private var offset: CGFloat = 0 + + var body: some View { + ZStack(alignment: .bottom) { + Rectangle() + .fill(Color.themeSteel10) + .frame(maxWidth: .infinity) + .frame(height: 1) + + ZStack(alignment: .bottom) { + HStack(spacing: 0) { + ForEach(tabs.indices, id: \.self) { index in + Button(action: { + currentTabIndex = index + }) { + Text(tabs[index]) + } + .buttonStyle(TabButtonStyle(isActive: index == currentTabIndex)) + } + } + .frame(maxWidth: .infinity) + + GeometryReader { geo in + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(Color.themeJacob) + .frame(height: 4) + .frame(width: geo.size.width / CGFloat(tabs.count)) + .offset(x: offset, y: 0) + .onChange(of: currentTabIndex) { index in + withAnimation(.spring().speed(1.5)) { + offset = geo.size.width / CGFloat(tabs.count) * CGFloat(index) + } + } + } + .frame(height: 2) + } + .padding(.horizontal, .margin12) + } + .frame(height: 44) + .clipped() + } +} From bc4948b1774bf8cf74fa6fe2d24a301aa8256ef8 Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 12 Oct 2023 12:55:06 +0600 Subject: [PATCH 54/63] Fix some unexpected behaviour when restore files from Android --- .../Core/Crypto/FullBackup.swift | 17 ++++++++++- .../Core/Crypto/SettingsBackup.swift | 2 +- .../Core/Storage/LocalStorage.swift | 2 +- .../Models/AccountType.swift | 12 +++----- .../Backup/ICloud/AppBackupProvider.swift | 28 +++++++++++-------- .../RestoreFileConfigurationViewModel.swift | 1 + .../RestoreType/RestoreTypeViewModel.swift | 2 +- .../BackupApp/Backup/BackupAppViewModel.swift | 4 +-- .../BackupDisclaimerView.swift | 4 +-- .../Backup/BackupName/BackupNameView.swift | 4 +-- .../en.lproj/Localizable.strings | 2 +- 11 files changed, 48 insertions(+), 30 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift index 628a5f3d2a..5e23d8b583 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/FullBackup.swift @@ -24,7 +24,12 @@ extension FullBackup: Codable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) - wallets = (try? container.decode([RestoreCloudModule.RestoredBackup].self, forKey: .wallets)) ?? [] + do { + wallets = (try container.decode([FailableDecodable].self, forKey: .wallets)) + .compactMap { $0.base } + } catch { + wallets = [] + } watchlistIds = (try? container.decode([String].self, forKey: .watchlistIds)) ?? [] contacts = try? container.decode(BackupCrypto.self, forKey: .contacts) settings = try container.decode(SettingsBackup.self, forKey: .settings) @@ -43,3 +48,13 @@ extension FullBackup: Codable { try? container.encode(timestamp, forKey: .timestamp) } } + +struct FailableDecodable : Decodable { + + let base: Base? + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + base = try? container.decode(Base.self) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift index 696c6502ce..67387cf761 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift @@ -8,7 +8,7 @@ struct SettingsBackup: Codable { let btcModes: [BtcBlockchainManager.BtcRestoreModeBackup] let lockTimeEnabled: Bool - let remoteContactsSync: Bool + let remoteContactsSync: Bool? let swapProviders: [DefaultProvider] let chartIndicators: ChartIndicatorsRepository.BackupIndicators let indicatorsShown: Bool diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift index 3f4aa55bbe..a49356ca46 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift @@ -103,7 +103,7 @@ extension LocalStorage { extension LocalStorage { func restore(backup: SettingsBackup) { lockTimeEnabled = backup.lockTimeEnabled - remoteContactsSync = backup.remoteContactsSync + remoteContactsSync = backup.remoteContactsSync ?? false indicatorsShown = backup.indicatorsShown backup.swapProviders.forEach { provider in let blockchainType = BlockchainType(uid: provider.blockchainTypeId) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index 457064d8ff..2ecad54c61 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -41,9 +41,9 @@ enum AccountType { case let .evmPrivateKey(data): privateData = data case let .evmAddress(address): - privateData = address.raw + privateData = address.hex.hs.data case let .tronAddress(address): - privateData = address.raw + privateData = address.hex.hs.data case let .hdExtendedKey(key): privateData = key.serialized case let .cex(cexAccount): @@ -259,13 +259,9 @@ extension AccountType { return nil } case .evmAddress: - return AccountType.evmAddress(address: EvmKit.Address(raw: uniqueId)) + return (try? EvmKit.Address(hex: string)).map { AccountType.evmAddress(address: $0) } case .tronAddress: - do { - return try AccountType.tronAddress(address: TronKit.Address(raw: uniqueId)) - } catch { - return nil - } + return (try? TronKit.Address(address: string)).map { AccountType.tronAddress(address: $0) } case .cex: guard let cexAccount = CexAccount.decode(uniqueId: string) else { return nil diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index 63a8bf4f64..6d732c9bab 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -150,28 +150,32 @@ extension AppBackupProvider { accountManager.save(accounts: updated.map { $0.account }) - updated.forEach { raw in + updated.forEach { (raw: RawWalletBackup) in switch raw.account.type { case .cex: () default: - let wallets = raw.enabledWallets.map { - if !$0.settings.isEmpty { + let wallets = raw.enabledWallets.compactMap { (wallet: WalletBackup.EnabledWallet) -> EnabledWallet? in + guard let tokenQuery = TokenQuery(id: wallet.tokenQueryId), + BlockchainType.supported.contains(tokenQuery.blockchainType) else { + return nil + } + + if !wallet.settings.isEmpty { var restoreSettings = [RestoreSettingType: String]() - $0.settings.forEach { key, value in + wallet.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) - } + restoreSettingsManager.save(settings: restoreSettings, account: raw.account, blockchainType: tokenQuery.blockchainType) } + return EnabledWallet( - tokenQueryId: $0.tokenQueryId, + tokenQueryId: wallet.tokenQueryId, accountId: raw.account.id, - coinName: $0.coinName, - coinCode: $0.coinCode, - tokenDecimals: $0.tokenDecimals + coinName: wallet.coinName, + coinCode: wallet.coinCode, + tokenDecimals: wallet.tokenDecimals ) } walletManager.save(enabledWallets: wallets) @@ -197,9 +201,11 @@ extension AppBackupProvider { if let currency = currencyKit.currencies.first(where: { $0.code == raw.settings.baseCurrency }) { currencyKit.baseCurrency = currency } + themeManager.themeMode = raw.settings.mode launchScreenManager.showMarket = raw.settings.showMarketTab launchScreenManager.launchScreen = raw.settings.launchScreen + balancePrimaryValueManager.balancePrimaryValue = raw.settings.balancePrimaryValue balanceConversionManager.set(tokenQueryId: raw.settings.conversionTokenQueryId) balanceHiddenManager.set(balanceAutoHide: raw.settings.balanceAutoHide) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift index abe5a7b13f..10a56c7e9a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift @@ -42,6 +42,7 @@ extension RestoreFileConfigurationViewModel { rawBackup .accounts .filter { !$0.account.watchAccount } + .sorted { wallet, wallet2 in wallet.account.name.lowercased() < wallet2.account.name.lowercased() } .map { item(account: $0.account) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift index 534b321441..1df2f45337 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreType/RestoreTypeViewModel.swift @@ -58,7 +58,7 @@ extension RestoreTypeViewModel { extension RestoreTypeViewModel { var items: [RestoreTypeModule.RestoreType] { switch sourceType { - case .wallet: return [.recoveryOrPrivateKey, .cloudRestore, .fileRestore] + case .wallet: return [.recoveryOrPrivateKey, .cloudRestore, .fileRestore, .cex] case .full: return [.cloudRestore, .fileRestore] } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift index 609f85b564..05656a3542 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift @@ -119,6 +119,7 @@ extension BackupAppViewModel { accountManager .accounts .filter { $0.watchAccount == watch } + .sorted { account, account2 in account.name.lowercased() < account2.name.lowercased() } } private var accountIds: [String] { @@ -201,7 +202,6 @@ extension BackupAppViewModel { } func validatePasswords() { - var buttonDisabled = false clearCautions() do { @@ -245,7 +245,7 @@ extension BackupAppViewModel { } passwordButtonProcessing = true - let selectedIds = accountIds.filter { (selected[$0] ?? false) } + let selectedIds = accountIds.filter { (selected[$0] ?? false) } + accounts(watch: true).map { $0.id } Task { switch destination { case .none: () diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift index 29e4c4e26b..2193670173 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift @@ -36,9 +36,9 @@ struct BackupDisclaimerView: View { Button(action: { viewModel.namePushed = true }) { Text("button.next".localized) } - .buttonStyle(PrimaryButtonStyle(style: .yellow)) - .disabled(!isOn) } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(!isOn) } } .navigationBarTitle(backupDisclaimer.title) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift index dfeb9b3835..e46de5d6f6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift @@ -36,9 +36,9 @@ struct BackupNameView: View { }) { Text("button.next".localized) } - .buttonStyle(PrimaryButtonStyle(style: .yellow)) - .disabled(viewModel.nameCautionState != .none) } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(viewModel.nameCautionState != .none) } .navigationTitle("backup_app.backup.name.title".localized) .navigationBarTitleDisplayMode(.inline) diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 3bb301c758..b9242c198c 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1109,7 +1109,7 @@ Go to Settings - > %@ and allow access to the camera."; "backup_app.backup_list.other.watch_account.title" = "Watch Wallets"; "backup_app.backup_list.other.watchlist.title" = "Watchlist"; "backup_app.backup_list.other.contacts.title" = "Contacts"; -"backup_app.backup_list.other.blockchain_settings.title" = "Blockchain Settings"; +"backup_app.backup_list.other.blockchain_settings.title" = "Custom RPC"; "backup_app.backup_list.other.app_settings.title" = "App Settings"; "backup_app.backup_list.other.app_settings.description" = "Language, Currency, Appearance ..."; From 5f62ff1d6b721681d4038b4792897350f26774cf Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 12 Oct 2023 14:21:16 +0600 Subject: [PATCH 55/63] Add appStatus for Zcash adapter - Fix warnings in app --- .../Core/Adapters/ZcashAdapter.swift | 42 +++++++++++-------- .../Core/Managers/RateAppManager.swift | 8 +++- .../RestoreCloud/RestoreCloudService.swift | 2 +- .../Modules/SendTron/SendTronViewModel.swift | 1 - .../Main/WalletConnectMainViewModel.swift | 13 ++---- .../UserInterface/SwiftUI/InputTextView.swift | 2 +- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index bc591b6309..8c6f4f27d3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -606,7 +606,12 @@ extension ZcashAdapter: IAdapter { } var statusInfo: [(String, Any)] { - [] + [ + ("Last Block Info", lastBlockHeight), + ("Sync State", state.description), + ("Birthday Height", birthday.description), + ("Init Mode", initMode.description), + ] } var debugInfo: String { @@ -799,8 +804,6 @@ enum ZCashAdapterState: Equatable { case syncing(progress: Int?, lastBlockDate: Date?) case downloadingSapling(progress: Int) case downloadingBlocks(progress: Float, lastBlock: Int) - case scanningBlocks(number: Int, lastBlock: Int) - case enhancingTransactions(number: Int, count: Int) case notSynced(error: Error) public static func == (lhs: ZCashAdapterState, rhs: ZCashAdapterState) -> Bool { @@ -811,8 +814,6 @@ enum ZCashAdapterState: Equatable { case let (.syncing(lProgress, lLastBlockDate), .syncing(rProgress, rLastBlockDate)): return lProgress == rProgress && lLastBlockDate == rLastBlockDate case let (.downloadingSapling(lProgress), .downloadingSapling(rProgress)): return lProgress == rProgress case let (.downloadingBlocks(lNumber, lLast), .downloadingBlocks(rNumber, rLast)): return lNumber == rNumber && lLast == rLast - case let (.scanningBlocks(lNumber, lLast), .scanningBlocks(rNumber, rLast)): return lNumber == rNumber && lLast == rLast - case let (.enhancingTransactions(lNumber, lCount), .enhancingTransactions(rNumber, rCount)): return lNumber == rNumber && lCount == rCount case (.notSynced, .notSynced): return true default: return false } @@ -829,25 +830,27 @@ enum ZCashAdapterState: Equatable { case let .downloadingBlocks(progress, _): let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), showSign: false) return .customSyncing(main: "balance.downloading_blocks".localized, secondary: percentValue, progress: Int(progress * 100)) - case let .scanningBlocks(number, lastBlock): - return .customSyncing(main: "Scanning Blocks", secondary: "\(number)/\(lastBlock)", progress: nil) - case let .enhancingTransactions(number, count): - let progress: String? = count == 0 ? nil : "\(number)/\(count)" - return .customSyncing(main: "Enhancing Transactions", secondary: progress, progress: nil) case let .notSynced(error): return .notSynced(error: error) } } - var isDownloading: Bool { + var description: String { switch self { - case .downloadingBlocks: return true - default: return false + case .idle: return "Idle" + case .preparing: return "Preparing..." + case .synced: return "Synced" + case let .syncing(progress, lastDate): return "Syncing: progress = \(progress?.description ?? "N/A"), lastBlockDate: \(lastDate?.description ?? "N/A")" + case let .downloadingSapling(progress): return "downloadingSapling: progress = \(progress)" + case let .downloadingBlocks(progress, _): + let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), showSign: false) + return "Downloading Blocks: \(percentValue?.description ?? "N/A") : \(Int(progress * 100))" + case let .notSynced(error): return "Not synced \(error.localizedDescription)" } } - var isScanning: Bool { + var isDownloading: Bool { switch self { - case .scanningBlocks: return true + case .downloadingBlocks: return true default: return false } } @@ -858,11 +861,14 @@ enum ZCashAdapterState: Equatable { default: return false } } +} - var lastProcessedBlockHeight: Int? { +extension WalletInitMode { + var description: String { switch self { - case let .downloadingBlocks(_, last), let .scanningBlocks(_, last): return last - default: return nil + case .newWallet: return "New Wallet" + case .existingWallet: return "Existing Wallet" + case .restoreWallet: return "Restored Wallet" } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift index 3c54adbe2e..a4305fe451 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/RateAppManager.swift @@ -50,8 +50,12 @@ class RateAppManager { } private func show() { - SKStoreReviewController.requestReview() - + if let scene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + DispatchQueue.main.async { + SKStoreReviewController.requestReview(in: scene) + } + } localStorage.rateAppLastRequestDate = Date() isRequestAllowed = false } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift index 342e48807d..c2d1562546 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudService.swift @@ -60,7 +60,7 @@ class RestoreCloudService { } self.fullBackupItems = items.sorted { (item1: Item, item2: Item) in - if item1.source.timestamp == nil, item2.source == nil { + if item1.source.timestamp == nil, item2.source.timestamp == nil { return item1.name > item2.name } return (item1.source.timestamp ?? 0) > (item2.source.timestamp ?? 0) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift index 634cc8794c..a345eb239d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift @@ -119,7 +119,6 @@ extension SendTronService.AddressError: LocalizedError { var errorDescription: String? { switch self { case .ownAddress: return "send.address_error.own_address".localized - default: return "\(self)" } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewModel.swift index 30580818f1..aad9d35164 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewModel.swift @@ -77,12 +77,7 @@ class WalletConnectMainViewModel { reconnectButtonRelay.accept(stateForReconnectButton ? (connectionState == .disconnected ? .enabled : .hidden) : .hidden) closeVisibleRelay.accept(state == .ready) - var address: String? - var network: String? - var networkEditable = false - var blockchains: [BlockchainViewItem]? - - blockchains = allowedBlockchains + let blockchains = allowedBlockchains .map { item in BlockchainViewItem( chainId: item.chainId, @@ -96,9 +91,9 @@ class WalletConnectMainViewModel { dAppMeta: service.appMetaItem.map { dAppMetaViewItem(appMetaItem: $0) }, status: status(connectionState: connectionState), activeAccountName: service.activeAccountName, - address: address, - network: network, - networkEditable: networkEditable, + address: nil, + network: nil, + networkEditable: false, blockchains: blockchains, hint: service.hint ) diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift index 77e076ea1d..2a7cdfeea7 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift @@ -75,7 +75,7 @@ struct InputTextView: View { } extension InputTextView { - @ViewBuilder func secure(_ secured: Binding) -> some View { + func secure(_ secured: Binding) -> some View { var selfView = self selfView._secured = secured From dec041e09030c4488e93ae867548597e3edc6c3d Mon Sep 17 00:00:00 2001 From: ant013 Date: Thu, 12 Oct 2023 16:53:12 +0600 Subject: [PATCH 56/63] Fix check version app --- .../Core/Managers/AppVersionManager.swift | 31 +++++++--------- .../UnstoppableWallet/Models/AppVersion.swift | 37 +++++++------------ .../Modules/Main/MainViewController.swift | 1 - .../Modules/Main/MainViewModel.swift | 4 -- .../Modules/Main/ReleaseNotesService.swift | 6 +-- 5 files changed, 29 insertions(+), 50 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppVersionManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppVersionManager.swift index b90263d4e7..1f37fd9c1b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppVersionManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppVersionManager.swift @@ -1,45 +1,42 @@ import Foundation -import RxSwift import RxRelay +import RxSwift class AppVersionManager { private let systemInfoManager: SystemInfoManager private let storage: AppVersionStorage - var newVersion: AppVersion? { - let currentVersion = systemInfoManager.appVersion - - guard let lastVersion = storage.appVersions.last, currentVersion > lastVersion else { - return nil - } - - return currentVersion - } - - func updateStoredVersion() { + func checkVersionUpdate() -> AppVersion? { let currentVersion = systemInfoManager.appVersion + // first start guard let lastVersion = storage.appVersions.last else { storage.save(appVersions: [currentVersion]) - return + return nil } - if lastVersion.version != currentVersion.version || lastVersion.build != currentVersion.build { + switch currentVersion.change(lastVersion) { + // show release + case .version: storage.save(appVersions: [currentVersion]) + return currentVersion + // just update db + case .build, .downgrade: + storage.save(appVersions: [currentVersion]) + case .none: () } + + return nil } init(systemInfoManager: SystemInfoManager, storage: AppVersionStorage) { self.systemInfoManager = systemInfoManager self.storage = storage } - } extension AppVersionManager { - var currentVersion: AppVersion { systemInfoManager.appVersion } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AppVersion.swift b/UnstoppableWallet/UnstoppableWallet/Models/AppVersion.swift index 24d1f363d0..1be834bf41 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AppVersion.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AppVersion.swift @@ -13,30 +13,12 @@ struct AppVersion: Codable { Int(version.components(separatedBy: ".")[1]) ?? 0 } - private var patch: Int? { - Int(version.components(separatedBy: ".")[2]) + func change(_ old: AppVersion) -> Change { + if version == old.version, build == old.build { return .none } + if major > old.major || (major == old.major && minor > old.minor) { return .version } + if version == old.version, build ?? "0" > old.build ?? "0" { return .build } + return .downgrade } - -} - -extension AppVersion: Comparable { - - public static func <(lhs: AppVersion, rhs: AppVersion) -> Bool { - if lhs.major < rhs.major { - return true - } - - if lhs.major == rhs.major && lhs.minor < rhs.minor { - return true - } - - return false - } - - public static func ==(lhs: AppVersion, rhs: AppVersion) -> Bool { - lhs.version == rhs.version - } - } extension AppVersion: CustomStringConvertible { @@ -56,3 +38,12 @@ extension AppVersion: CustomStringConvertible { } } + +extension AppVersion { + enum Change { + case none + case version + case build + case downgrade + } +} \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift index 05b9bbd603..2e9fad859b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift @@ -109,7 +109,6 @@ class MainViewController: ThemeTabBarController { return } - viewModel.onReleaseNotesShown() let module = MarkdownModule.gitReleaseNotesMarkdownViewController(url: url, presented: true, closeHandler: { [weak self] in self?.viewModel.handleNextAlert() }) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift index 89c771689c..b1a44d68c4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift @@ -120,10 +120,6 @@ extension MainViewModel { service.setMainShownOnce() } - func onReleaseNotesShown() { - releaseNotesService.updateStoredVersion() - } - func onSuccessJailbreakAlert() { jailbreakService.setAlertShown() } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesService.swift index 72f4d2578d..02d35513d5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesService.swift @@ -12,7 +12,7 @@ class ReleaseNotesService { } var releaseNotesUrl: URL? { - let version = appVersionManager.newVersion?.releaseNotesVersion + let version = appVersionManager.checkVersionUpdate()?.releaseNotesVersion if let version { return URL(string: Self.releaseUrl + version) @@ -20,10 +20,6 @@ class ReleaseNotesService { return nil } - func updateStoredVersion() { - appVersionManager.updateStoredVersion() - } - var lastVersionUrl: URL? { URL(string: Self.releaseUrl + appVersionManager.currentVersion.releaseNotesVersion) } From 50b12dd386c84e247f0b1fe3e0902a7768abdfdc Mon Sep 17 00:00:00 2001 From: imadia Date: Thu, 12 Oct 2023 17:40:09 +0600 Subject: [PATCH 57/63] New Crowdin updates --- .../de.lproj/Localizable.strings | 2022 +++++++++-------- .../es.lproj/Localizable.strings | 204 +- .../fr.lproj/Localizable.strings | 269 ++- .../ko.lproj/Localizable.strings | 208 +- .../pt-BR.lproj/Localizable.strings | 222 +- .../ru.lproj/Localizable.strings | 382 +++- .../tr.lproj/Localizable.strings | 216 +- .../zh.lproj/Localizable.strings | 216 +- 8 files changed, 2315 insertions(+), 1424 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index bc54de2545..c29930b622 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -1,90 +1,93 @@ "button.ok" = "OK"; -"button.apply" = "Apply"; -"button.cancel" = "Cancel"; -"button.close" = "Close"; -"button.continue" = "Continue"; -"button.done" = "Done"; -"button.reset" = "Reset"; -"button.import" = "Import"; -"button.next" = "Next"; -"button.delete" = "Delete"; -"button.share" = "Share"; -"button.paste" = "Paste"; -"button.resend" = "Resend"; -"button.backup" = "Backup"; -"button.copy" = "Copy"; -"button.retry" = "Retry"; -"button.report" = "Report"; -"button.add" = "Add"; -"button.approve" = "Approve"; -"button.revoke" = "Revoke"; -"button.reject" = "Reject"; -"button.connect" = "Connect"; -"button.save" = "Save"; -"button.send" = "Send"; -"button.sign" = "Sign"; -"button.more" = "Show More"; -"button.i_understand" = "I Understand"; +"button.apply" = "Übernehmen"; +"button.cancel" = "Abbrechen"; +"button.close" = "Schließen"; +"button.continue" = "Weiter"; +"button.done" = "Fertig"; +"button.reset" = "Zurücksetzen"; +"button.import" = "Wiederherstellen"; +"button.next" = "Weiter"; +"button.delete" = "Löschen"; +"button.share" = "Teilen"; +"button.paste" = "Einsetzen"; +"button.resend" = "Nochmal senden"; +"button.backup" = "Sicherung"; +"button.restore" = "Wiederherstellen"; +"button.copy" = "Kopieren"; +"button.retry" = "Wiederholen"; +"button.report" = "Melden"; +"button.add" = "Hinzufügen"; +"button.approve" = "Genehmigen"; +"button.revoke" = "Widerrufen"; +"button.reject" = "Verwerfen"; +"button.connect" = "Verbinden"; +"button.save" = "Speichern"; +"button.send" = "Senden"; +"button.sign" = "Signieren"; +"button.more" = "Mehr anzeigen"; +"button.i_understand" = "Ich stimme zu"; "button.learn_more" = "Learn More"; -"alert" = "Alert"; -"alert.copied" = "Copied"; -"alert.saved" = "Saved"; -"alert.saved_to_icloud" = "Saved to iCloud"; -"alert.no_internet" = "No Internet"; -"alert.wrong_amount" = "Wrong Amount"; -"alert.no_fee" = "Wrong Fee"; -"alert.warning" = "Warning"; -"alert.error" = "Error"; -"alert.unknown_error" = "Unknown Error"; -"alert.success_action" = "Done"; -"alert.success" = "Success"; - -"alert.added_to_watchlist" = "Added to Watchlist"; -"alert.removed_from_watchlist" = "Removed from Watchlist"; -"alert.added_to_wallet" = "Added to Wallet"; -"alert.removed_from_wallet" = "Removed from Wallet"; -"alert.already_added_to_wallet" = "Already added to Wallet"; -"alert.not_supported_yet" = "Not Supported Yet"; -"alert.created" = "Created"; -"alert.imported" = "Imported"; -"alert.wallet_added" = "Wallet Added"; -"alert.deleted" = "Deleted"; -"alert.waiting_for_session" = "Waiting for Session"; -"alert.try_again" = "Try Again"; -"alert.disconnecting" = "Disconnecting"; -"alert.disconnected" = "Disconnected"; -"alert.enabling" = "Enabling"; -"alert.enabled_coins" = "Enabled %@ more coins"; -"alert.sending" = "Sending"; -"alert.sent" = "Sent"; +"alert" = "Alarm"; +"alert.copied" = "Kopiert"; +"alert.saved" = "Gespeichert"; +"alert.saved_to_icloud" = "In iCloud gespeichert"; +"alert.no_internet" = "Kein Internet"; +"alert.wrong_amount" = "Falscher Betrag"; +"alert.no_fee" = "Falsche Gebühr"; +"alert.warning" = "Warnung"; +"alert.notice" = "Hinweis"; +"alert.error" = "Fehler"; +"alert.unknown_error" = "Unbekannter Fehler"; +"alert.success_action" = "Fertig"; +"alert.restored" = "Wiederhergestellt"; +"alert.success" = "Erfolgreich"; + +"alert.added_to_watchlist" = "Zur Merkliste hinzugefügt"; +"alert.removed_from_watchlist" = "Aus Merkliste entfernt"; +"alert.added_to_wallet" = "Zum Wallet hinzugefügt"; +"alert.removed_from_wallet" = "Aus Wallet entfernt"; +"alert.already_added_to_wallet" = "Bereits im Wallet enthalten"; +"alert.not_supported_yet" = "Wird noch nicht unterstützt"; +"alert.created" = "Erstellt"; +"alert.imported" = "Wiederherstellen"; +"alert.wallet_added" = "Wallet hinzugefügt"; +"alert.deleted" = "Gelöscht"; +"alert.waiting_for_session" = "Warte auf Sitzung"; +"alert.try_again" = "Nochmals versuchen"; +"alert.disconnecting" = "Verbindung wird getrennt"; +"alert.disconnected" = "Verbindung getrennt"; +"alert.enabling" = "Aktivieren"; +"alert.enabled_coins" = "%@ weitere Coins aktiviert"; +"alert.sending" = "Wird gesendet"; +"alert.sent" = "Gesendet"; "alert.swapping" = "Swapping"; "alert.swapped" = "Swapped"; -"alert.approving" = "Approving"; -"alert.approved" = "Approved"; -"alert.revoking" = "Revoking"; -"alert.revoked" = "Revoked"; -"alert.cant_recognize" = "Can't Recognize"; +"alert.approving" = "Wird bestätigt"; +"alert.approved" = "Bestätigt"; +"alert.revoking" = "Widerrufen"; +"alert.revoked" = "Widerrufen"; +"alert.cant_recognize" = "Kann nicht erkennen"; -"no_results_found" = "No results found"; -"action.loading" = "loading..."; -"placeholder.search" = "Search"; +"no_results_found" = "Keine Ergebnisse gefunden"; +"action.loading" = "wird geladen ..."; +"placeholder.search" = "Suche"; "status" = "Status"; -"connecting" = "Connecting..."; +"connecting" = "Verbinde..."; "online" = "Online"; "offline" = "Offline"; -"confirm" = "Confirm"; -"connect" = "Connect"; -"price" = "Price"; -"value" = "Value"; -"note" = "Note"; +"confirm" = "Bestätigen"; +"connect" = "Verbinden"; +"price" = "Preis"; +"value" = "Wert"; +"note" = "Hinweis"; "version" = "Version %@"; "n/a" = "N/A"; -"sync_error" = "Sync error. Try again."; +"sync_error" = "Sync-Fehler. Versuchen Sie es erneut."; "number.thousand" = "%@K"; "number.million" = "%@M"; @@ -92,436 +95,455 @@ "number.trillion" = "%@T"; "number.quadrillion" = "%@Q"; -"selector.any" = "Any"; +"selector.any" = "Beliebig"; -// Access Camera +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Sofort"; +"auto_lock.minute1" = "1 Minute"; +"auto_lock.minute5" = "5 Minuten"; +"auto_lock.minute15" = "15 Minuten"; +"auto_lock.minute30" = "30 Minuten"; +"auto_lock.hour1" = "1 Stunde"; +// Access Camera -"access_camera.message" = "Yo, %@ wants a lil' peek with that camera to catch that flashy QR code. +"access_camera.message" = "%@ benötigt Zugriff auf Ihre Kamera, um den QR-Code zu scannen. -Swag your way to Settings -> %@ and let it shine!"; -"access_camera.settings" = "Settings"; +Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; +"access_camera.settings" = "Einstellungen"; // Restore -"restore.title" = "Import Wallet"; -"restore.advanced" = "Advanced"; -"restore.import_by" = "Import By"; -"restore.restore_type.mnemonic" = "Recovery Phrase"; -"restore.restore_type.private_key" = "Private Key"; -"restore.mnemonic.placeholder" = "Enter Recovery Phrase"; -"restore.private_key.placeholder" = "Enter EVM Private Key, BIP32 Root Key or Account Extended Private Key"; -"restore.private_key.invalid_key" = "Invalid Key"; -"restore_error.mnemonic_word_count" = "Hold up, fam! We need it between 12 and 24 words. You droppin' %@ on me? Let's keep it wavy"; -"restore.checksum_error" = "Invalid checksum"; -"restore.passphrase" = "Passphrase"; -"restore.input.passphrase" = "Passphrase"; -"restore.passphrase_description" = "This is the password that locks up your wallet backup, like a secret track. Ain't no iCloud vibes here. Lose it and it's gone, like old Kanye mixtapes."; -"restore.error.empty_passphrase" = "The passphrase cannot be empty"; -"restore.non_standard_import" = "Non-Standard Import"; -"restore.non_standard_import.description" = "Yo, %@ fam, this page is for y'all with that unique Yeezy-style wallet. Maybe you were vibing in the %@ 0.27 - 0.28 era with some international lyrics or fancy symbols in your wallet. Seeing a big zero on that balance after the 0.29 drop? We got you. Dive in here to get back to the beat. And after? Consider rocking a brand-new, standard-compliant wallet and make those money moves."; -"restore.warning.non_recommended.description" = "Hold up! Your wallet's got that avant-garde character style. Not seeing the cash or the flow? Dive deep for the deets below. 🎤\n\nTAP IN FOR THE SCOOP"; -"restore.error.non_standard.description" = "Yo, this wallet's got its own wave. It's non-standard. 🕶️\n\nTAP TO UNRAVEL THE MYSTERY"; -// Restore Type +"restore.title" = "Wallet importieren"; +"restore.advanced" = "Erweitert"; +"restore.import_by" = "Importieren von"; +"restore.restore_type.mnemonic" = "Wiederherstellungsphrase"; +"restore.restore_type.private_key" = "Privater Schlüssel"; +"restore.mnemonic.placeholder" = "12, 15, 18, 21 oder 24 Wörter eingeben"; +"restore.private_key.placeholder" = "Geben Sie EVM privaten Schlüssel, BIP32 Root-Schlüssel oder Account Extended Private Key ein"; +"restore.private_key.invalid_key" = "Ungültiger Schlüssel"; +"restore_error.mnemonic_word_count" = "Falsche Wortzahl. Sie muss zwischen 12 und 24 Wörtern liegen. Sie haben eingegeben: %@"; +"restore.checksum_error" = "Ungültige Prüfsumme"; +"restore.passphrase" = "Passwort"; +"restore.input.passphrase" = "Passwort"; +"restore.passphrase_description" = "Dieses Passwort wird verwendet, um die Sicherungsdatei Ihrer Wallet zu verschlüsseln. Es hat nichts mit Ihrem Apple iCloud-Passwort zu tun. Wenn Sie es vergessen oder verlieren, kann es nicht wiederhergestellt oder zurückgesetzt werden."; +"restore.error.empty_passphrase" = "Die Passphrase darf nicht leer sein"; +"restore.non_standard_import" = "Nicht standardmäßige Wiederherstellung"; +"restore.non_standard_import.description" = "Diese Seite bietet einen speziellen Wallet-Importmechanismus für Benutzer von %@ die eine nicht standardmäßige Wallet haben. Solche Brieftaschen könnten in %@ Version 0 erstellt worden sein. 7 - 0.28 unter Verwendung einer nicht-englischen Wiederherstellungsphrase und/oder eines Sonderzeichens in der Brieftasche Passphrase (d.h. diakritisches Symbol).\n\nWenn Sie ein betroffener Benutzer sind, wird Ihr Wallet-Guthaben nach dem Import in Version 0.29 oder höher 0 angezeigt. Auf dieser Seite können Sie den Zugriff auf Ihre nicht standardmäßige Brieftasche wiederherstellen. Sobald Sie importiert haben, empfehlen wir Ihnen eine neue Wallet zu erstellen (die standardkonform ist) und Geld dorthin zu verschieben."; +"restore.warning.non_recommended.description" = "Diese Brieftasche scheint ein nicht standardmäßiges Zeichen in ihrer geheimen Wortliste und/oder einer Passphrase zu verwenden. Wenn Sie kein Guthaben oder Transaktionen sehen, lesen Sie bitte unten für Details.\n\nKLICKEN, UM WEITERE INFORMATIONEN ZU ERHALTEN"; +"restore.error.non_standard.description" = "Dies ist eine nicht standardmäßige Wallet. \n\nKLICKEN SIE MEHR INFO"; -"restore_type.title" = "Import Wallet"; +// Restore Type -"restore_type.recovery.title" = "from Recovery Phrase"; -"restore_type.cloud.title" = "from iCloud"; -"restore_type.cex.title" = "from Exchange Wallet"; +"restore_type.title" = "Wallet importieren"; +"restore_type.recovery.title" = "von Wiederherstellungsphrase"; +"restore_type.cloud.title" = "von iCloud"; +"restore_type.file.title" = "aus Dateien"; +"restore_type.cex.title" = "von Exchange Wallet"; -"restore_type.recovery.description" = "Bring it back with that secret lyric or the key to the studio. "; -"restore_type.cloud.description" = "Pull that backup track from your keychain playlist."; -"restore_type.cex.description" = "Link up with the main stage – the centralized exchange. Let's make those money moves!"; +"restore_type.recovery.description" = "Importieren Sie mithilfe der Wiederherstellungsphrase oder des privaten Schlüssels."; +"restore_type.cloud.description" = "Importieren Sie aus einer Sicherungsdatei in Ihrem Schlüsselbund."; +"restore_type.file.description" = "Importieren Sie eine Sicherungsdatei aus Ihrem lokalen Ordner."; +"restore_type.cex.description" = "Verbinden Sie sich mit einer Brieftasche bei zentralisiertem Austausch."; // Restore Cloud -"restore.cloud.title" = "Select Backup"; -"restore.cloud.description" = "Pick the mixtape (backup) of the wallet you wanna bring back to the spotlight."; -"restore.cloud.empty" = "No backups found."; -"restore.cloud.imported" = "Imported wallets"; +"restore.cloud.title" = "Sicherung auswählen"; +"restore.cloud.description" = "Wählen Sie die Sicherungsdatei, die Sie wiederherstellen möchten."; +"restore.cloud.empty" = "Keine Sicherungen gefunden."; +"restore.cloud.wallets" = "Wallet-Sicherungen"; +"restore.cloud.imported" = "Importierte Wallet"; +"restore.cloud.app_backups" = "App Sicherungen"; -"restore.cloud.password.title" = "Enter Password"; -"restore.cloud.password.placeholder" = "Backup Password"; -"restore.cloud.password.description" = "Drop that secret beat (backup password) to get your wallet vibes from the iCloud stage."; +"restore.cloud.password.title" = "Passwort eingeben"; +"restore.cloud.password.placeholder" = "Passwort sichern"; +"restore.cloud.password.description" = "Geben Sie das Backup-Passwort ein, um Ihre Brieftasche aus iCloud zu importieren."; // Restore Cex -"restore.cex.title" = "Select CEX"; -"restore.cex.description" = "Pick the main stage (centralized exchange) you wanna rock with."; +"restore.cex.title" = "CEX auswählen"; +"restore.cex.description" = "Wählen Sie den zentralen Austausch aus, zu dem Sie sich verbinden möchten."; // Restore Binance -"restore.binance.description" = "Drop those VIP passes (API Keys and API Secret) to get backstage with your exchange."; -"restore.binance.api_key" = "API Key"; -"restore.binance.secret_key" = "Secret Key"; -"restore.binance.connect" = "Connect"; -"restore.binance.connecting" = "Connecting..."; -"restore.binance.get_api_keys" = "Get API Keys"; -"restore.binance.failed_to_connect" = "Failed to connect your API Key"; -"restore.binance.invalid_qr_code" = "Invalid QR Code"; +"restore.binance.description" = "Bitte geben Sie den API-Schlüssel und das API-Geheimnis an, um Ihren Austausch zu verknüpfen."; +"restore.binance.api_key" = "API-Schlüssel"; +"restore.binance.secret_key" = "Geheimer Schlüssel"; +"restore.binance.connect" = "Verbinden"; +"restore.binance.connecting" = "Verbinde..."; +"restore.binance.get_api_keys" = "API-Schlüssel erhalten"; +"restore.binance.failed_to_connect" = "Verbindung zum API-Schlüssel fehlgeschlagen"; +"restore.binance.invalid_qr_code" = "Ungültiger QR-Code"; // Coin Settings -"coin_settings.title" = "Blockchain Settings"; +"coin_settings.title" = "Blockchain Einstellungen"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; -"sync_mode.from_blockchain" = "From Blockchain"; -"blockchain_settings.description" = "Pick your style (address format) for catching those payments. When bringing back an old mix (restoring a wallet), make sure the beats match."; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (empfohlen)"; +"sync_mode.from_blockchain" = "Von Blockchain"; +"blockchain_settings.description" = "Wählen Sie das Adressformat für den Empfang von Zahlungen. Ein korrektes Format sollte bei der Wiederherstellung einer bestehenden Brieftasche gewählt werden."; + // Coin Platforms -"coin_platforms.native" = "Native"; +"coin_platforms.native" = "Nativ"; // Copy Warning -"copy_warning.title" = "Risk of Copying"; -"copy_warning.description" = "For safety, like rocking shades indoors, skip that copy action for this value, fam."; -"copy_warning.dont_copy" = "Don't Copy"; -"copy_warning.i_will_risk_it" = "I will risk it"; +"copy_warning.title" = "Kopierrisiko"; +"copy_warning.description" = "Als Sicherheitsmaßnahme empfehlen wir, die Kopierfunktion nicht für diesen Wert zu verwenden."; +"copy_warning.dont_copy" = "Nicht kopieren"; +"copy_warning.i_will_risk_it" = "Ich werde es riskieren"; // Recovery Phrase -"recovery_phrase.title" = "Recovery Phrase"; -"recovery_phrase.warning" = "Hold onto this key like a rare Yeezy drop. The %@ Wallet crew? We won't ever slide into your DMs for it."; -"recovery_phrase.tap_to_show" = "Tap to show recovery phrase"; -"recovery_phrase.passphrase" = "Passphrase"; -"recovery_phrase.copy_warning.title" = "Risk of Recovery Phrase copy"; -"recovery_phrase.copy_warning.description" = "As a security measure,we recommend not using copy action on recovery phrase."; +"recovery_phrase.title" = "Wiederherstellungsphrase"; +"recovery_phrase.warning" = "Teilen Sie diesen Schlüssel niemals mit niemandem. %@ Wallet-Support-Team wird ihn niemals anfragen."; +"recovery_phrase.tap_to_show" = "Tippen, um Wiederherstellungsphrase anzuzeigen"; +"recovery_phrase.passphrase" = "Passwort"; +"recovery_phrase.copy_warning.title" = "Risiko der Wiederherstellungs-Phrase-Kopie"; +"recovery_phrase.copy_warning.description" = "Als Sicherheitsmaßnahme empfehlen wir, Kopiermaßnahmen nicht auf Wiederherstellungs-Phrase zu verwenden."; + // EVM Private Key -"evm_private_key.title" = "EVM Private Key"; -"evm_private_key.tap_to_show" = "Tap to show private key"; +"evm_private_key.title" = "EVM privater Schlüssel"; +"evm_private_key.tap_to_show" = "Tippen um privaten Schlüssel anzuzeigen"; // Extended Key "extended_key.bip32_root_key" = "BIP32 Root Key"; "extended_key.account_extended_private_key" = "Account Extended Private Key"; "extended_key.account_extended_public_key" = "Account Extended Public Key"; -"extended_key.purpose" = "Purpose"; +"extended_key.purpose" = "Zweck"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; -"extended_key.tap_to_show" = "Tap to show extended private key"; +"extended_key.tap_to_show" = "Zum Anzeigen des erweiterten privaten Schlüssels tippen"; // Backup -"backup.title" = "Recovery Phrase"; -"backup.description" = "Jot these words down, in flow, and stash them somewhere no haters can find."; -"backup.tap_to_show" = "Tap to show recovery phrase"; -"backup.passphrase" = "Passphrase"; -"backup.verify" = "Verify"; -"backup.verified" = "Verified"; +"backup.title" = "Wiederherstellungsphrase"; +"backup.description" = "Schreibe diese Wörter in die richtige Reihenfolge und halte sie an einem sicheren Ort"; +"backup.tap_to_show" = "Tippen, um Wiederherstellungsphrase anzuzeigen"; +"backup.passphrase" = "Passwort"; +"backup.verify" = "Prüfen"; +"backup.verified" = "Verifiziert"; // Backup Verify Words -"backup_verify_words.title" = "Verify"; -"backup_verify_words.description" = "Pick out those two spotlight words from your wallet's lyrical recovery phrase."; -"backup_verify_words.incorrect_word" = "Incorrect Word"; +"backup_verify_words.title" = "Prüfen"; +"backup_verify_words.description" = "Wählen Sie zwei angeforderte Wörter aus Ihrer Wallet-Wiederherstellungsphrase"; +"backup_verify_words.incorrect_word" = "Falsches Wort"; // Backup Verify Passphrase -"backup_verify_passphrase.title" = "Verify"; -"backup_verify_passphrase.description" = "Enter the passphrase"; -"backup_verify_passphrase.incorrect_passphrase" = "Incorrect passphrase"; - -// Backup Required - -"backup_required.title" = "Backup Required"; +"backup_verify_passphrase.title" = "Prüfen"; +"backup_verify_passphrase.description" = "Geben Sie Ihre Passphrase ein"; +"backup_verify_passphrase.incorrect_passphrase" = "Falsches Passwort"; // Backup Prompt -"backup_prompt.title" = "Manual Backup"; -"backup_prompt.warning" = "Craft a backup mix of the recovery vibes and that secret code. It's your safety net if your phone goes MIA, gets jacked, or just breaks down."; -"backup_prompt.backup" = "Backup"; -"backup_prompt.backup_manual" = "Manual Backup"; -"backup_prompt.backup_cloud" = "Backup to iCloud"; -"backup_prompt.later" = "Later"; +"backup_prompt.backup_recovery_phrase" = "Wiederherstellungsphrase sichern"; +"backup_prompt.backup_required" = "Sicherung erforderlich"; +"backup_prompt.warning" = "Erstellen Sie eine Sicherungskopie der Wiederherstellungsphrase und des zugehörigen Passworts, mit dem Sie Ihre Wallet wiederherstellen können, wenn Ihr Telefon verloren geht, gestohlen, kaputt, etc."; +"backup_prompt.backup" = "Sicherung"; +"backup_prompt.backup_manual" = "Manuelle Sicherung"; +"backup_prompt.backup_cloud" = "Sicherung in iCloud"; +"backup_prompt.later" = "Später"; // Backup to iCloud -"backup.cloud.title" = "Backup to iCloud"; -"backup.cloud.description" = "Heads up: iCloud is Apple's stage, not yours. Your beats (data) are on their mics (servers), not your personal studio. So, you're passing the mic and trust to them."; -"backup.cloud.terms.item.1" = "I understand that losing access to my iCloud, will result in loosing access to the backup of a respective wallet."; +"backup.cloud.title" = "Sicherung in iCloud"; +"backup.cloud.description" = "iCloud-Speicher ist ein von Apple bereitgestellter Cloud-Speicherdienst. Es ist wichtig zu wissen, dass Ihre Daten auf Apple-Servern gespeichert werden und nicht auf Ihren persönlichen Geräten. Das bedeutet, dass Sie Ihre Daten anvertrauen und die Sicherheit Ihrer Daten einem Drittanbieter überlassen."; +"backup.cloud.terms.item.1" = "Ich verstehe, dass der Verlust des Zugriffs auf meine iCloud dazu führen wird, dass auch der Zugriff auf die Sicherung einer entsprechenden Brieftasche verloren geht."; -"backup.cloud.name.title" = "Backup Name"; -"backup.cloud.name.description" = "Enter name for the backup file."; -"backup.cloud.name.empty" = "Backup name can't be empty!"; -"backup.cloud.name.error.empty" = "Backup name must be not empty"; -"backup.cloud.name.error.already_exist" = "Backup name already exist!"; +"backup.cloud.name.title" = "Sicherungsname"; +"backup.cloud.name.description" = "Geben Sie den Namen für die Backup-Datei ein."; +"backup.cloud.name.empty" = "Sicherungsname darf nicht leer sein!"; +"backup.cloud.name.error.empty" = "Sicherungsname darf nicht leer sein"; +"backup.cloud.name.error.already_exist" = "Sicherungsname existiert bereits!"; "backup.cloud.name.placeholder" = "Name"; -"backup.cloud.password.title" = "Set Password"; -"backup.cloud.password.description" = "Craft a password that's as iconic as a Kanye track. Need 8 beats (symbols) with a mix of highs (uppercase), lows (lowercase), numbers, and some flair (special character)."; -"backup.cloud.password.highlighted_description" = "Don't forget this password! It is separate from your Apple iCloud password, and it cannot be recovered or reset."; -"backup.cloud.password.placeholder" = "Password"; -"backup.cloud.password.confirm.placeholder" = "Confirm"; -"backup.cloud.password.save" = "Save and Backup"; - -"backup.cloud.password.error.empty_passphrase" = "Passphrase cannot be empty"; -"backup.cloud.password.error.forbidden_symbols" = "Stick to the platinum hits: A-Z a-z 0-9 and these symbols:A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %. No remixes, please."; -"backup.cloud.password.error.minimum_requirement" = "Bring at least 8 characters to the stage, featuring a capital hit, a chill lowercase, a number, and some special effects."; -"backup.cloud.password.error.invalid_password" = "Incorrect Password"; -"backup.cloud.password.error.invalid_backup" = "Backup is corrupted"; -"backup.cloud.password.confirm.error.doesnt_match" = "Password doesn’t match"; -"backup.cloud.not_available" = "iCloud not Available"; -"backup.cloud.cant_create_file" = "Can't save File to iCloud"; -"backup.cloud.cant_delete_file" = "Can't delete from iCloud"; -"backup.cloud.no_access.title" = "Access iCloud"; -"backup.cloud.no_access.description" = "Wanna drop a backup? Gotta pass the mic to iCloud storage first."; +"backup.cloud.password.title" = "Passwort festlegen"; +"backup.cloud.password.description" = "Legen Sie ein Passwort fest, um das Entsperren zu ermöglichen. Das Passwort muss mindestens 8 Zeichen lang sein und einen Kleinbuchstaben, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten."; +"backup.cloud.password.highlighted_description" = "Vergessen Sie dieses Passwort nicht! Es ist getrennt von Ihrem Apple iCloud-Passwort und kann nicht wiederhergestellt oder zurückgesetzt werden."; +"backup.cloud.password.placeholder" = "Passwort"; +"backup.cloud.password.confirm.placeholder" = "Bestätigen"; +"backup.cloud.password.save" = "Speichern und sichern"; + +"backup.cloud.password.error.empty_passphrase" = "Passphrase darf nicht leer sein"; +"backup.cloud.password.error.forbidden_symbols" = "Bitte verwende nur unterstützte Symbole: \\ _ # @ | % ]]>"; +"backup.cloud.password.error.minimum_requirement" = "Das Passwort muss mindestens 8 Zeichen enthalten, darunter ein Großbuchstabe, ein Kleinbuchstabe, eine Zahl und ein Symbol."; +"backup.cloud.password.error.invalid_password" = "Falsches Passwort"; +"backup.cloud.password.error.invalid_backup" = "Backup ist beschädigt"; +"backup.cloud.password.confirm.error.doesnt_match" = "Passwort stimmt nicht überein"; +"backup.cloud.not_available" = "iCloud nicht verfügbar"; +"backup.cloud.cant_create_file" = "Die Datei kann nicht in iCloud gespeichert werden"; +"backup.cloud.cant_delete_file" = "Kann nicht von iCloud gelöscht werden"; +"backup.cloud.no_access.title" = "Zugriff auf iCloud"; +"backup.cloud.no_access.description" = "Um ein Backup zu erstellen, müssen Sie Zugriff auf iCloud-Speicher gewähren."; // Errors -"error.send.self_transfer" = "Sending to yourself is not supported"; -"error.send_binance.memo_required" = "Yo, the receiver's looking for a memo that's not silent. Drop some beats in that transfer note."; -"error.send_binance.only_digits_allowed" = "The memo's gotta be numbers only, like chart-topping digits."; -"error.send_z_cash.transparent_address" = "Hold up, %@ ain't vibing with payments to a see-through address."; +"error.send.self_transfer" = "Sich selbst zu senden wird nicht unterstützt"; +"error.send_binance.memo_required" = "Empfänger benötigt nicht-leeren Vermerk bei Transfer einer Transaktion"; +"error.send_binance.only_digits_allowed" = "Der Vermerk darf nur Zahlen enthalten"; +"error.send_z_cash.transparent_address" = "%@ unterstützt keine Zahlungen an eine transparente Adresse"; // Balance -"balance.title" = "Balance"; -"balance.tab_bar_item" = "Balance"; -"balance.send" = "Send"; -"balance.withdraw" = "Withdraw"; +"balance.title" = "Guthaben"; +"balance.tab_bar_item" = "Guthaben"; +"balance.send" = "Senden"; +"balance.withdraw" = "Abheben"; "balance.swap" = "Swap"; -"balance.receive" = "Receive"; -"balance.deposit" = "Deposit"; -"balance.address" = "Address"; -"balance.rate_per_coin" = "%@ per %@"; -"balance.syncing" = "Syncing..."; -"balance.searching" = "Searching transactions..."; -"balance.downloading_sapling" = "Downloading Sapling... %d%%"; -"balance.downloading_blocks" = "Downloading Blocks"; -"balance.scanning_blocks" = "Scanning Blocks"; -"balance.enhancing_transactions" = "Enhancing Transactions"; +"balance.receive" = "Empfangen"; +"balance.deposit" = "Anfordern"; +"balance.address" = "Adresse"; +"balance.rate_per_coin" = "%@ pro %@"; +"balance.syncing" = "Synchronisieren..."; +"balance.searching" = "Transaktionen werden gesucht..."; +"balance.stopped" = "Stoppen"; +"balance.downloading_sapling" = "Setze herunterladen... %d%%"; +"balance.downloading_blocks" = "Blöcke herunterladen"; +"balance.scanning_blocks" = "Scanne Blöcke"; +"balance.enhancing_transactions" = "Transaktionen verbessern"; +"wait_for_synchronization" = "Warten auf Synchronisation"; "balance.searching.count" = "%@ tx"; -"balance.syncing_percent" = "Syncing... %@"; -"balance.synced_through" = "until %@"; -"balance.add_coin" = "Add Coin"; -"balance.invalid_api_key" = "Invalid API Key"; -"balance.empty.add_coins" = "Add Coins"; -"balance.empty.description" = "Looks like your wallet's still waiting for its debut album. No coins dropped in yet."; -"balance.watch_empty.description" = "This wallet's address? Still waiting for its first hit single. No balance here."; -"balance.sort_by" = "Sort By"; -"balance.sort.header" = "Sort by"; -"balance.sort.valueHighToLow" = "Balance"; +"balance.syncing_percent" = "Synchronisieren... %@"; +"balance.synced_through" = "bis %@"; +"balance.add_coin" = "Coin hinzufügen"; +"balance.invalid_api_key" = "Ungültiger API-Schlüssel"; +"balance.empty.add_coins" = "Coins hinzufügen"; +"balance.empty.description" = "Sie haben keine Coins zu diesem Wallet hinzugefügt."; +"balance.watch_empty.description" = "Diese Wallet-Adresse hat keinen Kontostand"; +"balance.sort_by" = "Sortieren nach"; +"balance.sort.header" = "Sortieren nach"; +"balance.sort.valueHighToLow" = "Guthaben"; + "balance.sort.az" = "Name"; -"balance.sort.price_change" = "Price Change"; +"balance.sort.price_change" = "Preisänderung"; -"balance_error.change_source" = "Change Source"; -"balance_error.sync_error" = "Sync Error"; -"lost_accounts.warning_title" = "iOS Keychain Error"; -"lost_accounts.warning_message" = "The encrypted data holding your wallet was recently invalidated because your iOS lock screen was changed"; +"balance_error.change_source" = "Quelle ändern"; +"balance_error.sync_error" = "Sync-Fehler"; +"lost_accounts.warning_title" = "iOS-Keychain-Fehler"; +"lost_accounts.warning_message" = "Die verschlüsselten Daten Ihres Wallets wurden kürzlich ungültig, weil Ihr iOS-Sperrbildschirm geändert wurde"; // Token Balance Page "balance.token.locked" = "Locked"; "balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "The sender dropped these funds with a lil' hold - kinda like a track release date.\n\nChill, those Bitcoins are already in your studio, but you gotta wait for the drop before you can make it rain on the Bitcoin network."; -"balance.token.staked" = "Staked"; -"balance.token.staked.info.title" = "Staked title"; -"balance.token.staked.info.description" = "Staked Description Text"; +"balance.token.locked.info.description" = "Der Sender hat dieses Guthaben mit einer Ausgabesperre versehen, die zu dem angezeigten Datum abläuft.\n\nKeine Sorge, die erhaltenen Bitcoin sind bereits Ihre. Die Bitcoins sind nur zeitweise gesperrt, so dass Sie sie erst nach Ablauf der Sperrzeit im Bitcoin-Netzwerk ausgeben können."; +"balance.token.processing" = "Verarbeite"; +"balance.token.processing.info.title" = "Bearbeitungsbetrag"; +"balance.token.processing.info.description" = "Transaktionen mit diesem Betrag werden noch synchronisiert. Und wenn sie bestätigt werden, stehen diese Token für Ausgaben zur Verfügung"; +"balance.token.staked" = "Absteckung"; +"balance.token.staked.info.title" = "Absteckter Titel"; +"balance.token.staked.info.description" = "Absteckungstext"; + "balance.token.frozen" = "Frozen"; -"balance.token.frozen.info.title" = "Frozen title"; -"balance.token.frozen.info.description" = "Frozen Description Text"; +"balance.token.frozen.info.title" = "gefrorener Titel"; +"balance.token.frozen.info.description" = "gefrorener Beschreibungstext"; // Account switcher -"switch_account.title" = "Switch Wallet"; +"switch_account.title" = "Wallet wechseln"; "switch_account.wallets" = "Wallets"; -"switch_account.watch_wallets" = "Watch Wallets"; +"switch_account.watch_wallets" = "Watch-Adresse"; // Release notes -"release_notes.title" = "What's New"; -"release_notes.follow_us" = "Follow Us"; +"release_notes.title" = "Das ist neu"; +"release_notes.follow_us" = "Folgen Sie uns"; // Deposit -"deposit.receive_coin" = "Receive %@"; -"deposit.address" = "Address"; -"deposit.your_address" = "Your Address"; -"receive_alert.any_coins.not_backed_up_description" = "Before you collect those coins, make sure you've laid down a backup of %@. "; -"receive_alert.not_backed_up_description" = "Before you catch those %@ vibes, make sure you've saved %@ like a classic track."; -"deposit.no_adapter.error" = "Can't provide address"; +"deposit.receive_coin" = "%@ Anfordern "; +"deposit.address" = "Adresse"; +"deposit.your_address" = "Deine Adresse"; +"receive_alert.not_backed_up_description" = "Sie müssen %@ sichern, bevor Sie %@ empfangen können."; +"receive_alert.any_coins.not_backed_up_description" = "Du musst %@ sichern, bevor du Münzen erhalten kannst."; +"deposit.no_adapter.error" = "Kann keine Adresse angeben"; "deposit.address_format" = "Format"; -"deposit.address_network" = "Network"; -"deposit.qr_code_description" = "Your address for depositing %@"; -"deposit.qr_code_description.watch" = "Watch address of %@"; +"deposit.address_network" = "Netzwerk"; +"deposit.qr_code_description" = "Ihre Adresse für %@"; +"deposit.qr_code_description.watch" = "Beobachtungsadresse von %@"; "deposit.account" = "Account"; -"deposit.not_active" = "Not active"; -"deposit.not_active.title" = "Not Active Address"; -"deposit.not_active.tron_description" = "Newly created accounts on the TRON blockchain are inactive and cannot be queried or explored. They need to be activated.\n\nTransferring TRX or TRC-10 tokens to an inactive account address will activate the account. Activating a new account on the Tron chain requires a fee of 1 TRX"; +"deposit.not_active" = "nicht aktiv"; +"deposit.not_active.title" = "Keine aktive Adresse"; +"deposit.not_active.tron_description" = "Neu erstellte Konten in der TRON-Blockchain sind inaktiv und können nicht abgefragt oder untersucht werden. Sie müssen aktiviert werden.\n\nDie Übertragung von TRX- oder TRC-10-Token an eine inaktive Konto-Adresse wird das Konto aktivieren. Zur Aktivierung eines neuen Kontos auf der Tron-Kette ist eine Gebühr von 1 TRX erforderlich"; -"deposit.zcash.restore.description" = "Have you previously owned any ZEC coins?"; -"deposit.zcash.restore.already_own" = "Yes, I already own"; -"deposit.zcash.restore.dont_have" = "No, I don’t have"; +"deposit.zcash.restore.description" = "Hast du schon einmal im Besitz von ZEC-Münzen?"; +"deposit.zcash.restore.already_own" = "Ja, ich besitze bereits"; +"deposit.zcash.restore.dont_have" = "Nein, ich habe nicht"; -"deposit.warning" = "Send only %@ to this address. Sending other types of tokens to this address will result in their ultimate loss."; +"deposit.warning" = "Senden Sie nur %@ an diese Adresse. Das Senden anderer Arten von Tokens an diese Adresse führt zu ihrem ultimativen Verlust."; -"receive_network_select.title" = "Network"; -"receive_network_select.description" = "Choose a network and get an address to receive."; +"receive_network_select.title" = "Netzwerk"; +"receive_network_select.description" = "Wählen Sie ein Netzwerk aus und erhalten Sie eine Adresse."; -"receive_address_format_select.title" = "Address Format"; -"receive_address_format_select.description" = "Select an address format to receive your address."; -"receive_address_format_select.bitcoin.bottom_description" = "The Native SegWit format is preferred in Bitcoin for improved throughput and security. All address formats (Taproot, SegWit, Legacy) can be used interchangeably to receive BTC regardless of the sender's address format, enabling seamless transactions across different coin types."; -"receive_address_format_select.bitcoin_cash.bottom_description" = "The Cash Address format is preferred for receiving Bitcoin Cash (BCH) due to its improved user experience and compatibility. However, both address formats can be used interchangeably to receive BCH, regardless of the sender's address format."; +"receive_address_format_select.title" = "Adressformatierung"; +"receive_address_format_select.description" = "Wählen Sie ein Adressformat aus, um Ihre Adresse zu erhalten."; +"receive_address_format_select.bitcoin.bottom_description" = "Das Native SegWit Format wird in Bitcoin bevorzugt für einen verbesserten Durchsatz und mehr Sicherheit. Alle Adressformate (Taproot, SegWit, Legacy) können unabhängig vom Adressformat des Absenders zum Empfang von BTC verwendet werden ermöglicht nahtlose Transaktionen über verschiedene Münztypen."; +"receive_address_format_select.bitcoin_cash.bottom_description" = "Das Cash Address Format wird bevorzugt für den Erhalt von Bitcoin Cash (BCH) aufgrund seiner verbesserten Benutzerfreundlichkeit und Kompatibilität bevorzugt. Beide Adressformate können jedoch unabhängig vom Adressformat des Absenders austauschbar genutzt werden, um BCH zu empfangen."; -"blockchain_type.recommended" = " (recommended)"; +"blockchain_type.recommended" = " (empfohlen)"; // Send -"send.title" = "Send %@"; -"send.send" = "Send"; -"send.no_assets" = "You have no assets to send."; -"send.amount_placeholder" = "Amount"; -"send.address_placeholder" = "Address"; -"send.address_or_domain_placeholder" = "Address or Domain"; -"send.fee" = "Fee"; -"send.network_fee" = "Network Fee"; -"send.estimated_fee" = "Estimated Fee"; -"send.max_fee" = "Max Fee"; -"send.duration.hours" = "%d h."; -"send.duration.minutes" = "%d min."; -"send.available_balance" = "Available Balance"; -"send.max_button" = "Max"; -"send.next_button" = "Next"; -"send.error.invalid" = "Invalid"; -"send.error.address" = "Address"; +"send.title" = "Senden %@"; +"send.send" = "Senden"; +"send.no_assets" = "Sie haben keine Assets zu senden."; +"send.amount_placeholder" = "Betrag"; +"send.address_placeholder" = "Adresse"; +"send.address_or_domain_placeholder" = "Adresse oder Domäne"; +"send.fee" = "Gebühr"; +"send.network_fee" = "Netzwerkgebühr"; +"send.estimated_fee" = "Geschätzte Gebühren"; +"send.max_fee" = "Max Gebühr"; +"send.duration.hours" = "%d Std."; +"send.duration.minutes" = "%d Min."; +"send.available_balance" = "Verfügbares Guthaben"; +"send.max_button" = "Max."; +"send.next_button" = "Weiter"; +"send.error.invalid" = "Ungültig"; +"send.error.address" = "Adresse"; "send.hodler_locktime" = "TimeLock"; -"send.hodler_locktime_hour" = "1 hour"; -"send.hodler_locktime_month" = "1 month"; -"send.hodler_locktime_half_year" = "6 months"; -"send.hodler_locktime_year" = "1 year"; -"send.hodler_locktime_off" = "Off"; -"send.hodler_error.unsupported_address" = "Time locking works only when sending to payment addresses starting with 1... (aka BIP44 addresses)"; -"send.fee_info.title" = "Fee Rate"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.hodler_locktime_hour" = "1 Stunde"; +"send.hodler_locktime_month" = "1 Monat"; +"send.hodler_locktime_half_year" = "6 Monate"; +"send.hodler_locktime_year" = "1 Jahr"; +"send.hodler_locktime_off" = "Aus"; +"send.hodler_error.unsupported_address" = "Zeitsperre funktioniert nur mit P2PKH-Adressen (beginnend mit 1) "; +"send.fee_info.title" = "Gebührenrate"; +"send.fee_info.description" = "Blockchains verlangen, dass Nutzer Netzwerkgebühren beim Versenden von Transaktionen bezahlen. Die Gebühren sind höher, wenn viele Transaktionen im Netzwerk stattfinden.\n\nDie %@ Brieftasche schätzt die Gebühr basierend auf der aktuellen Blockchain Aktivität und empfiehlt den optimalen Wert, damit die Transaktion innerhalb einer angemessenen Zeit bearbeitet werden kann.\n\nDie empfohlene Gebührenrate, die als Betrag des satoshi Benutzers angezeigt wird, um für ein einziges Byte der Transaktion zu bezahlen. Daher hängt die Gesamtgebühr von der Gesamtgröße der Transaktion ab, die in Bytes gemessen wird.\n\nBenutzer können die zur Verfügung gestellten Steuerelemente verwenden, um den Gebührenwert zu erhöhen oder zu senken. Die Änderung des Gebührensatzes ändert die Gesamtgebühr für die Transaktion, die der Nutzer zu zahlen hat.\n\nSetzen der Gebühr unter dem empfohlenen Wert kann dazu führen, dass eine Transaktion stundenlang als ausstehend gehalten oder abgelehnt wird. Je niedriger der Wert, desto länger dauert die Bestätigung der Transaktion. Für Transaktionen, bei denen Priorität wichtig ist, empfehlen wir, höhere Gebührensätze festzulegen."; "send.transaction_inputs_outputs_info.title" = "Transaction Inputs / Outputs"; -"send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; +"send.transaction_inputs_outputs_info.description" = "Die meisten Bitcoin-Transaktionen sowie Transaktionen in Kryptowährungen wie Bitcoin-Cash, Dash, und Litecoin erzeugen zwei Ausgänge. Die eine Ausgabe ist der Betrag, der an den Empfänger geht, die andere ist die Umstellausgabe, die an den Absender zurückgegeben wird. Die Art und Weise, wie die meisten Brieftaschen Transaktionen erstellen, macht es für einen Dritten leicht zu verstehen, welche der Ausgaben an den Empfänger gingen und welcher der Änderungsbetrag an den Absender zurückgegeben wurde. Da die an den Absender zurückgegebene Ausgabe später bei zukünftigen Transaktionen verwendet wird, zeigt sich eine Verbindung zwischen diesen beiden Transaktionen.\n\nDie Brieftasche %@ implementiert Maßnahmen, um es jemandem zu erschweren, herauszufinden, welche Ausgabe wohin geht.\n\nEs gibt zwei Optionen für %@ Benutzer:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "The order of transaction outputs is randomized on every transaction. Sometimes change can be the first output, sometimes it can be the second. If a user trusts the developer of the app, then consider this a recommended option."; +"send.transaction_inputs_outputs_info.shuffle.description" = "Die Reihenfolge der Transaktionsausgänge wird bei jeder Transaktion zufällig. Manchmal können Änderungen die erste Ausgabe sein, manchmal kann es die zweite sein. Wenn ein Benutzer dem Entwickler der App vertraut, dann erwäge dies eine empfohlene Option."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; -"send.transaction_inputs_outputs_info.deterministic.description" = "There is a commonly agreed standard for ordering transaction outputs (known as BIP69). In open-source wallets, that standard ensures wallet users do not need to trust how developers of the app implement the ordering of the outputs. As this standard is new, not many wallets have implemented it yet. As a result, it's somewhat possible to see on the blockchain whether a transaction was sent from a wallet that uses that standard or not."; +"send.transaction_inputs_outputs_info.deterministic.description" = "Es gibt einen allgemein vereinbarten Standard für die Bestellung von Transaktionsausgängen (bekannt als BIP69). In Open-Source-Brieftaschen stellt dieser Standard sicher, dass Wallet-Benutzer nicht darauf vertrauen müssen, wie Entwickler der App die Reihenfolge der Ausgaben implementieren. Da dieser Standard neu ist, haben noch nicht viele Brieftaschen ihn implementiert. Infolgedessen ist es einigermaßen möglich, auf der Blockchain zu sehen, ob eine Transaktion von einer Brieftasche gesendet wurde, die diesen Standard verwendet hat oder nicht."; -"send.confirmation.you_send" = "You Send"; -"send.confirmation.to" = "To"; -"send.confirmation.contact_name" = "Contact Name"; -"send.confirmation.domain" = "Domain"; -"send.confirmation.address" = "Address"; +"send.confirmation.you_send" = "Sie Senden"; +"send.confirmation.to" = "An"; +"send.confirmation.contact_name" = "Kontaktname"; +"send.confirmation.domain" = "Domäne"; +"send.confirmation.address" = "Adresse"; "send.confirmation.account" = "Account"; "send.confirmation.memo" = "Memo"; "send.confirmation.memo_placeholder" = "Memo"; -"send.confirmation.total" = "Total"; -"send.confirmation.fee" = "Fee"; -"send.confirmation.time_lock" = "Time Lock"; -"send.confirmation.slide_to_send" = "Slide to Send"; -"send.confirmation.sending" = "Sending"; -"send.confirmation.resend_description" = "This action will attempt to invalidate the previous transaction by resending it with a higher fee. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; -"send.confirmation.resend" = "Resend"; -"send.confirmation.cancel_description" = "This action will attempt to invalidate previous transaction by resending as a new 0 amount transaction to self. If the original transaction remains pending when a new one sent there is s a high chance (not guaranteed) that it will be invalidated and replaced. Only one of these two transactions will be included in the blockchain."; -"send.confirmation.cancel" = "Cancel Transaction"; +"send.confirmation.total" = "Insgesamt"; +"send.confirmation.fee" = "Gebühr"; +"send.confirmation.time_lock" = "TimeLock"; +"send.confirmation.slide_to_send" = "Zum Senden wischen"; +"send.confirmation.sending" = "Wird gesendet"; +"send.confirmation.resend_description" = "Mit dieser Aktion wird versucht, die vorherige Transaktion durch erneutes Senden mit einer höheren Gebühr ungültig zu machen. Wenn die ursprüngliche Transaktion noch aussteht, während eine neue gesendet wird, besteht eine hohe Wahrscheinlichkeit (nicht garantiert), dass sie für ungültig erklärt und ersetzt wird. Nur eine dieser beiden Transaktionen wird in die Blockchain aufgenommen."; +"send.confirmation.resend" = "Nochmal senden"; +"send.confirmation.cancel_description" = "Mit dieser Aktion wird versucht, die vorherige Transaktion durch eine neue Transaktion mit einem Betrag von 0 an sich selbst ungültig zu machen. Wenn die ursprüngliche Transaktion noch aussteht, während eine neue Transaktion gesendet wird, besteht eine hohe Wahrscheinlichkeit (nicht garantiert), dass sie für ungültig erklärt und ersetzt wird. Nur eine dieser beiden Transaktionen wird in die Blockchain aufgenommen."; +"send.confirmation.cancel" = "Transaktion abbrechen"; "send.confirmation.nonce" = "Nonce"; -"send.confirmation.method" = "Method"; -"send.amount_error.balance" = "Not Enough Balance"; -"send.address_error.own_address" = "Cannot transfer TRX to yourself"; -"send.amount_error.maximum_amount" = "Maximum amount %@"; -"send.amount_error.minimum_amount" = "Minimum amount %@"; -"send.amount_error.min_required_balance" = "Min. required remainder %@"; -"send.amount_warning.coin_needed_for_fee" = "Consider leaving some %@ on balance to be able to pay for future transactions."; -"send.token.insufficient_fee_alert" = "Transaction fees for transferring %@ (on %@) paid in %@. You need %@."; - -"send.fee_settings.amount_error.balance.title" = "Insufficient Balance"; -"send.fee_settings.amount_error.balance" = "The current %@ balance is below the amount required to process theis transaction, including transaction fee."; - -"send.fee_settings.stuck_warning.title" = "Risk of getting stuck"; -"send.fee_settings.stuck_warning" = "The transaction may get stuck or fail."; -"send.fee_settings.fee_error.title" = "Fee Error"; -"send.fee_settings.too_low" = "Fee Rate is too low."; -"send.fee_settings.fee_rate_unavailable" = "Fee Rate unavailable. Please, check fee rates manually"; - -"send.stuck_warning" = "Warning! Risk of getting stuck"; +"send.confirmation.method" = "Methode"; +"send.amount_error.balance" = "Nicht genügend Guthaben"; +"send.address_error.own_address" = "TRX kann nicht auf sich selbst übertragen werden"; +"send.amount_error.maximum_amount" = "Maximaler Betrag %@"; +"send.amount_error.minimum_amount" = "Minimaler Betrag %@"; +"send.amount_error.min_required_balance" = "Mindestens erforderlicher Rest %@"; +"send.amount_warning.coin_needed_for_fee" = "Überlegen Sie, ob Sie einige %@ auf dem Guthaben belassen wollen, um für zukünftige Transaktionen bezahlen zu können."; +"send.token.insufficient_fee_alert" = "Transaktionsgebühren für %@ %@ in %@ bezahlt. Du brauchst %@."; + +"send.fee_settings.amount_error.balance.title" = "Unzureichendes Guthaben"; +"send.fee_settings.amount_error.balance" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; + +"send.fee_settings.stuck_warning.title" = "Gefahr des Festklemmens"; +"send.fee_settings.stuck_warning" = "Die Transaktion kann festklemmen oder fehlschlagen."; +"send.fee_settings.fee_error.title" = "Gebührenfehler"; +"send.fee_settings.too_low" = "Die Gebühr ist zu niedrig."; +"send.fee_settings.fee_rate_unavailable" = "Gebührensatz nicht verfügbar. Bitte überprüfen Sie die Gebührensätze manuell"; + +"send.stuck_warning" = "Warnung! Gefahr des Festklemmens"; "send.lock_time" = "TimeLock"; -"approve.confirmation.you_approve" = "You Approve"; -"approve.confirmation.you_revoke" = "You Revoke"; +"approve.confirmation.you_approve" = "Sie genehmigen"; +"approve.confirmation.you_revoke" = "Du widerrufen"; "approve.confirmation.spender" = "Spender"; // Donate -"donate.list.title" = "Donate with"; -"donate.list.get_address" = "Get Address"; -"donate.list.get_address.title" = "Addresses"; -"donate.title" = "Donate %@"; -"donate.no_assets" = "You have no assets to donate."; -"donate.support.description" = "Together, with your support, we can make this app even better!"; +"donate.list.title" = "Spenden mit"; +"donate.list.get_address" = "Adresse erhalten"; +"donate.list.get_address.title" = "Adresse"; +"donate.title" = "%@ Spenden"; +"donate.no_assets" = "Sie haben keine Assets zu spenden."; +"donate.support.description" = "Gemeinsam können wir mit Ihrer Unterstützung diese App noch besser machen!"; // CoinSelector -"choose_coin.title" = "Choose Coins"; +"choose_coin.title" = "Coins auswählen"; // Swap "swap.title" = "Swap"; -"swap.no_assets" = "You have no assets to swap."; -"swap.you_pay" = "You Pay"; -"swap.estimated" = "estimated"; -"swap.balance" = "Balance"; -"swap.allowance" = "Allowance"; -"swap.you_get" = "You Get"; -"swap.token" = "Select"; -"swap.advanced_settings" = "Swap Settings"; -"swap.proceed_button" = "Next"; -"swap.approve.title" = "Swap Approve"; -"swap.approve.description" = "You should grant permission to a smart contract to swap a given token on your behalf. This permission sets the amount that can be used by a smart contract. It doesn't affect your balance but requires a small fee to execute an approval transaction. \n\nWhile it may be done on-demand before each trade, it's cheaper to approve a higher amount in advance for future trades."; -"swap.approve.amount_error.already_approved" = "You already have an allowance for this amount"; -"swap.approving_button" = "Approving..."; -"swap.revoke_warning" = "You can exchange %@, or you need to revoke and approve the new amount"; -"swap.revoking_button" = "Revoking..."; -"swap.not_available_button" = "Balance N/A"; -"swap.trade_error.not_found" = "Can't swap these tokens"; -"swap.trade_error.wrap_unwrap_not_allowed" = "This service doesn't allow wrapping/unwrapping. Please, try another swap service. 1Inch recommended"; -"swap.button_error.insufficient_balance" = "Insufficient balance"; -"swap.switch_provider.title" = "Swap Service"; +"swap.no_assets" = "Du hast keine Vermögenswerte zum Tauschen."; +"swap.you_pay" = "Sie bezahlen"; +"swap.estimated" = "geschätzt"; +"swap.balance" = "Guthaben"; +"swap.allowance" = "Vergütung"; +"swap.you_get" = "Sie erhalten"; +"swap.token" = "Auswählen"; +"swap.advanced_settings" = "Swap-Einstellungen"; +"swap.proceed_button" = "Weiter"; +"swap.approve.title" = "Genehmigen tauschen"; +"swap.approve.description" = "Sie sollten einem intelligenten Vertrag die Erlaubnis erteilen, in Ihrem Namen gegebene Token einzutauschen. Diese Erlaubnis legt einen Betrag fest, der von einem intelligenten Vertrag verwendet werden kann. Sie wirkt sich nicht auf Ihr Guthaben aus, erfordert jedoch eine geringe Gebühr für die Ausführung der Genehmigungstransaktion.\n\nEs kann zwar vor jedem Handel auf Anfrage erfolgen, es ist jedoch günstiger, bei zukünftigen Handelsgeschäften einen höheren Betrag im Voraus für uns zu genehmigen."; +"swap.approve.amount_error.already_approved" = "Sie haben bereits eine Vergütung für diesen Betrag"; +"swap.approving_button" = "Wird bestätigt..."; +"swap.revoke_warning" = "Sie können %@tauschen, oder Sie müssen den neuen Betrag widerrufen und genehmigen"; +"swap.revoking_button" = "Widerrufen..."; +"swap.not_available_button" = "Saldo N/A"; +"swap.trade_error.not_found" = "Diese Token sind nicht tauschbar"; +"swap.trade_error.wrap_unwrap_not_allowed" = "Dieser Service erlaubt keine Verpackung/Auspackung. Bitte versuchen Sie einen anderen Swap-Service. 1Inch empfohlen"; +"swap.button_error.insufficient_balance" = "Unzureichendes Guthaben"; +"swap.switch_provider.title" = "Dienst wechseln"; "swap.amount_type.coin" = "Coin"; -"swap.price" = "Price"; -"swap.buy_price" = "Buy Price"; -"swap.sell_price" = "Sell Price"; -"swap.price_impact" = "Price Impact"; -"swap.maximum_paid" = "Maximum Amount"; -"swap.minimum_got" = "Guaranteed Amount"; -"swap.estimate_short" = "(est)"; +"swap.price" = "Preis"; +"swap.buy_price" = "Kaufpreis"; +"swap.sell_price" = "Verkaufspreis"; +"swap.price_impact" = "Preisauswirkungen"; +"swap.maximum_paid" = "Maximal bezahlt"; +"swap.minimum_got" = "Mindestens erhalten"; +"swap.estimate_short" = "(geschätzt)"; "swap.minimum_short" = "(min)"; "swap.maximum_short" = "(max)"; // Swap Advanced Settings -"swap.advanced_settings.slippage" = "Slippage Tolerance"; -"swap.advanced_settings.slippage.footer" = "Your transaction will revert if the price changes unfavorably by more than this percentage"; -"swap.advanced_settings.deadline" = "Transaction Deadline"; -"swap.advanced_settings.deadline.footer" = "Your transaction will revert if it is pending for more than this long."; -"swap.advanced_settings.recipient.footer" = "After the exchange operation, the amount will be transferred to the specified address"; -"swap.advanced_settings.deadline_minute" = "%@ min"; -"swap.advanced_settings.recipient_address" = "Recipient Address"; -"swap.advanced_settings.warning.unusual_slippage" = "Your transaction may be frontrun"; -"swap.advanced_settings.service_fee_description" = "A service fee for the swap action on the platform typicaly either 0.3% or 0.6%"; -"swap.advanced_settings.error.lower_slippage" = "Your transaction is likely to fail."; -"swap.advanced_settings.error.higher_slippage" = "Slippage Tolerance can’t be more than %@%%"; -"swap.advanced_settings.error.invalid_address" = "Invalid Address"; -"swap.advanced_settings.error.invalid_slippage" = "Invalid Slippage"; -"swap.advanced_settings.error.invalid_deadline" = "Invalid Deadline"; - -"swap.one_inch.error.cannot_estimate" = "Estimation Error"; -"swap.one_inch.error.cannot_estimate.info" = "Check the balance to ensure there is enough %@ to cover the fee. Or try increasing the price slippage amount and try again. Will auto-retry in 3 sec..."; -"swap.one_inch.error.insufficient_liquidity" = "Insufficient Liquidity"; -"swap.one_inch.error.insufficient_liquidity.info" = "Likely there is not enough liquidity available to process this trade. Try lowering the amount."; - -"swap.service" = "Service"; -"swap.service.title" = "Service"; +"swap.advanced_settings.slippage" = "Slippage-Toleranz"; +"swap.advanced_settings.slippage.footer" = "Ihre Transaktion wird zurückgesetzt, wenn sich der Preis um mehr als diesen Prozentsatz ungünstig ändert."; +"swap.advanced_settings.deadline" = "Transaktionsfrist"; +"swap.advanced_settings.deadline.footer" = "Ihre Transaktion wird rückgängig gemacht, wenn sie länger als diese aussteht."; +"swap.advanced_settings.recipient.footer" = "Nach dem Umtauschvorgang wird der Betrag an die angegebene Adresse überwiesen"; +"swap.advanced_settings.deadline_minute" = "%@ Min"; +"swap.advanced_settings.recipient_address" = "Empfängeradresse"; +"swap.advanced_settings.warning.unusual_slippage" = "Ihre Transaktion kann vorzeitig ausgeführt werden"; +"swap.advanced_settings.service_fee_description" = "Eine Service-Gebühr für die Swap-Aktion auf der Plattform typischerweise entweder 0,3% oder 0,6%"; +"swap.advanced_settings.error.lower_slippage" = "Ihre Transaktion wird wahrscheinlich fehlschlagen."; +"swap.advanced_settings.error.higher_slippage" = "Die Slippage-Toleranz darf nicht mehr als %@%% betragen"; +"swap.advanced_settings.error.invalid_address" = "Ungültige Adresse"; +"swap.advanced_settings.error.invalid_slippage" = "Ungültige Slippage"; +"swap.advanced_settings.error.invalid_deadline" = "Ungültige Frist"; + +"swap.one_inch.error.cannot_estimate" = "Schätzungsfehler"; +"swap.one_inch.error.cannot_estimate.info" = "Fehler! Überprüfen Sie Ihr Guthaben und vergewissern Sie sich, dass genügend %@ zur Deckung der Gebühr vorhanden sind. Oder versuchen Sie, die Kursdifferenz zu erhöhen und versuchen Sie es erneut. Wird in 3 Sekunden erneut versucht..."; +"swap.one_inch.error.insufficient_liquidity" = "Unzureichende Liquidität"; +"swap.one_inch.error.insufficient_liquidity.info" = "Wahrscheinlich ist nicht genügend Liquidität vorhanden, um diesen Handel abzuwickeln. Versuchen Sie einen niedrigeren Betrag."; + +"swap.service" = "Dienst"; +"swap.service.title" = "Dienst"; // Swap Approving @@ -529,34 +551,34 @@ Swag your way to Settings -> %@ and let it shine!"; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Slide to Swap"; +"swap.confirmation.slide_to_swap" = "Wischen zum Tauschen"; "swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.impact_too_high" = "%@ has disabled swap action for this trade because you're getting an extremely unfavorable price. This is due to extremely low liquidity.\nIf you still want to swap please use %@ website instead."; -"swap.confirmation.impact_warning" = "Important! You're getting an extremely unfavorable price. This is due to extremely low liquidity."; - -"swap.confirmation.minimum_received" = "Minimum Received"; -"swap.confirmation.maximum_sent" = "Maximum Sent"; - -"swap.dex_info.description" = "This exchange service is powered by %@, a decentralized token exchange protocol built on the %@ blockchain. \n\n%@ is fully automated and managed by smart contracts that facilitate token exchanges in a reliable manner without any means to cheat."; -"swap.dex_info.header_dex_related" = "%@ Related"; -"swap.dex_info.header_allowance" = "Allowance"; -"swap.dex_info.content_allowance" = "The amount an exchange can spend on a user’s behalf when executing token swaps. A proceeding transaction setting sufficient allowance is required before an actual swap transaction can take place."; -"swap.dex_info.header_price_impact" = "Price Impact"; -"swap.dex_info.content_price_impact" = "The expected price deviation from a shown price typically increases with the swap amount."; -"swap.dex_info.header_swap_fee" = "Swap Fee"; -"swap.dex_info.content_swap_fee" = "A service fee for the swap action on the platform, shown in the currency the user sells. For most orders, it should be either 0.3 % or 0.6 %."; -"swap.dex_info.header_guaranteed_amount" = "Guaranteed Amount"; -"swap.dex_info.content_guaranteed_amount" = "The minimum amount a user is going to receive as a result of a swap action."; -"swap.dex_info.header_maximum_spend" = "Maximum Spend"; -"swap.dex_info.content_maximum_spend" = "The maximum amount a user is going to spend as a result of a swap action."; - -"swap.dex_info.header_other" = "Other"; -"swap.dex_info.header_transaction_fee" = "Transaction Fee"; -"swap.dex_info.content_transaction_fee" = "The estimated processing cost of the given transaction on %@ blockchain. %@ related transactions will typically cost more than generic token transfer transactions."; -"swap.dex_info.header_transaction_speed" = "Transaction Speed"; -"swap.dex_info.content_transaction_speed" = "Transactions with a higher transaction fee will result in faster processing speeds. The opposite is true as well."; - -"swap.dex_info.link_button" = "%@ Site"; +"swap.confirmation.impact_too_high" = "%@ hat Swap-Aktion für diesen Handel deaktiviert, da du einen extrem ungünstigen Preis bekommst. Dies ist auf extrem niedrige Liquidität zurückzuführen.\nWenn Sie trotzdem wechseln möchten, verwenden Sie stattdessen %@ Webseite."; +"swap.confirmation.impact_warning" = "Wichtig! Du bekommst einen extrem ungünstigen Preis. Dies ist auf die extrem niedrige Liquidität zurückzuführen."; + +"swap.confirmation.minimum_received" = "Mindestens erhalten"; +"swap.confirmation.maximum_sent" = "Maximale Ausgaben"; + +"swap.dex_info.description" = "Dieser Tauschdienst wird von %@ betrieben, einem dezentralen Token-Tauschprotokoll, das auf der %@ Blockchain basiert.\n\n%@ ist vollständig automatisiert und wird von intelligenten Verträgen verwaltet, die den Token-Tausch in einer zuverlässigen Art und Weise ermöglichen, ohne dass Betrug betrieben werden kann."; +"swap.dex_info.header_dex_related" = "%@ zugehörige"; +"swap.dex_info.header_allowance" = "Vergütung"; +"swap.dex_info.content_allowance" = "Der Betrag, den ein Austausch im Namen des Benutzers ausgeben kann, wenn Token Swaps ausgeführt werden. Bevor eine tatsächliche Swap-Transaktion durchgeführt werden kann, ist eine vorherige Transaktion erforderlich, die ausreichende Zertifikate festlegt."; +"swap.dex_info.header_price_impact" = "Preisauswirkungen"; +"swap.dex_info.content_price_impact" = "Erwartete Preisabweichung von einem angezeigten Preis, steigt normalerweise mit dem Tauschbetrag."; +"swap.dex_info.header_swap_fee" = "Swap-Gebühr"; +"swap.dex_info.content_swap_fee" = "Eine Servicegebühr für die Tauschaktion auf der Plattform, angegeben in der Währung, die der Benutzer verkauft. Für die meisten Aufträge beträgt diese entweder 0,3 % oder 0,6 %."; +"swap.dex_info.header_guaranteed_amount" = "Mindestens erhalten"; +"swap.dex_info.content_guaranteed_amount" = "Der Mindestbetrag, den ein Benutzer als Ergebnis einer Tauschaktion erhalten wird."; +"swap.dex_info.header_maximum_spend" = "Maximale Ausgaben"; +"swap.dex_info.content_maximum_spend" = "Der Maximalbetrag, den ein Benutzer als Ergebnis einer Tauschaktion ausgeben wird."; + +"swap.dex_info.header_other" = "Andere"; +"swap.dex_info.header_transaction_fee" = "Transaktionsgebühr"; +"swap.dex_info.content_transaction_fee" = "Die geschätzten Verarbeitungskosten für die angegebene Transaktion auf der %@ Blockchain. Transaktionen, die sich auf %@ beziehen, kosten in der Regel mehr als allgemeine Token-Transaktionen."; +"swap.dex_info.header_transaction_speed" = "Transaktionsgeschwindigkeit"; +"swap.dex_info.content_transaction_speed" = "Transaktionen mit höheren Transaktionsgebühren führen zu schnelleren Bearbeitungsgeschwindigkeiten. Auch das Gegenteil ist der Fall."; + +"swap.dex_info.link_button" = "%@ Seite"; // Market @@ -564,7 +586,7 @@ Swag your way to Settings -> %@ and let it shine!"; "market.title" = "Markets"; "market.category.overview" = "Overview"; "market.category.posts" = "News"; -"market.category.watchlist" = "Watchlist"; +"market.category.watchlist" = "Merkliste"; "market.total_market_cap" = "Total M.Cap"; "market.24h_volume" = "24h Volume"; "market.defi_cap" = "DeFi Cap"; @@ -572,7 +594,7 @@ Swag your way to Settings -> %@ and let it shine!"; "market.project_has_no_coin" = "This project doesn’t have a coin"; -"market.top.section.header.see_all" = "See All"; +"market.top.section.header.see_all" = "Alle anzeigen"; "market.top.section.header.top_gainers" = "Top Gainers"; "market.top.section.header.top_losers" = "Top Losers"; "market.top.section.header.top_sectors" = "Top Sectors"; @@ -589,41 +611,41 @@ Swag your way to Settings -> %@ and let it shine!"; "market.tvl.platform_field.all" = "All"; -"market.sort_by" = "Sort By"; +"market.sort_by" = "Sortieren nach"; "market.top.title" = "Top Coins"; "market.top.description" = "Top Coins by market cap rank"; -"market.top.highest_cap" = "Highest Cap"; -"market.top.lowest_cap" = "Lowest Cap"; -"market.top.highest_volume" = "Highest Volume"; -"market.top.lowest_volume" = "Lowest Volume"; +"market.top.highest_cap" = "Höchste Obergrenze"; +"market.top.lowest_cap" = "Niedrigste Obergrenze"; +"market.top.highest_volume" = "Höchste Lautstärke"; +"market.top.lowest_volume" = "Geringste Lautstärke"; "market.top.top_gainers" = "Top Gainers"; "market.top.top_losers" = "Top Losers"; "market.top.top_collections" = "Top NFT Collections"; -"market.top.floor_price" = "Floor:"; +"market.top.floor_price" = "Stockwerk:"; "market.top.top_platforms" = "Top Platforms"; "market.top.protocols" = "Protocols"; -"top_platforms.title" = "Platforms Rank"; -"top_platforms.description" = "Leading blockchain platforms by cumulative market of projects built on top."; +"top_platforms.title" = "Plattform-Rang"; +"top_platforms.description" = "Leading blockchain platforms by the cumulative market of projects built on top."; "top_platform.title" = "%@ Ecosystem"; "top_platform.description" = "Market cap of all protocols on the %@ chain"; -"market_discovery.title" = "Discovery"; +"market_discovery.title" = "Discovey"; "market_discovery.filters" = "Filters"; "market_discovery.browse_categories" = "Browse Categories"; "market_discovery.top_coins" = "TOP Coins"; -"market_discovery.not_found" = "No results found"; +"market_discovery.not_found" = "Keine Ergebnisse gefunden"; "market_watchlist.empty.caption" = "Your watchlist is empty."; -"market.search.title" = "Search"; -"market.search.empty_text" = "No results found"; +"market.search.title" = "Suche"; +"market.search.empty_text" = "Keine Ergebnisse gefunden"; "market.advanced_search.title" = "Filters"; -"market.advanced_search.show_results" = "Show Results"; +"market.advanced_search.show_results" = "Ergebnisse anzeigen"; "market.advanced_search.empty_results" = "Empty Results"; "market.advanced_search.dex_description" = "This setting applies to tokens traded on Ethereum (Uniswap DEX) and Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24h"; @@ -637,16 +659,16 @@ Swag your way to Settings -> %@ and let it shine!"; "market.advanced_search.liquidity" = "DEX Liquidity"; "market.advanced_search.blockchains" = "Blockchains"; "market.advanced_search.price_period" = "Price Period"; -"market.advanced_search.price_change" = "Price Change"; +"market.advanced_search.price_change" = "Preisänderung"; -"market.advanced_search.outperformed_btc" = "Outperformed BTC"; -"market.advanced_search.outperformed_eth" = "Outperformed ETH"; -"market.advanced_search.outperformed_bnb" = "Outperformed BNB"; +"market.advanced_search.outperformed_btc" = "Übertraf BTC"; +"market.advanced_search.outperformed_eth" = "Übertraf ETH"; +"market.advanced_search.outperformed_bnb" = "Übertraf BNB"; "market.advanced_search.price_close_to_ath" = "Price Close To ATH"; "market.advanced_search.price_close_to_atl" = "Price Close To ATL"; "market.advanced_search.top" = "Top %d"; -"market.advanced_search.reset_all" = "Reset"; +"market.advanced_search.reset_all" = "Zurücksetzen"; "market.advanced_search.less_5_m" = "< 5M"; "market.advanced_search.less_10_m" = "< 10M"; @@ -673,9 +695,9 @@ Swag your way to Settings -> %@ and let it shine!"; "market.advanced_search.more_500_b" = "> 500B"; "market.advanced_search.day" = "1 Day"; -"market.advanced_search.week" = "1 Week"; +"market.advanced_search.week" = "1Week"; "market.advanced_search.week2" = "2 Weeks"; -"market.advanced_search.month" = "1 Month"; +"market.advanced_search.month" = "1 Monat"; "market.advanced_search.month6" = "6 Months"; "market.advanced_search.year" = "1 Year"; @@ -697,7 +719,7 @@ Swag your way to Settings -> %@ and let it shine!"; // Coin Page -"coin_page.overview" = "Price"; +"coin_page.overview" = "Preis"; "coin_page.analytics" = "Analytics"; "coin_page.markets" = "Markets"; "coin_page.tweets" = "Tweets"; @@ -715,18 +737,18 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_overview.trading_volume" = "Trading Volume"; "coin_overview.roi.hour24" = "1 Day"; -"coin_overview.roi.day7" = "1 Week"; +"coin_overview.roi.day7" = "1Week"; "coin_overview.roi.day14" = "2 Weeks"; -"coin_overview.roi.day30" = "1 Month"; +"coin_overview.roi.day30" = "1 Monat"; "coin_overview.roi.day200" = "6 Month"; "coin_overview.roi.year1" = "1 Year"; -"coin_overview.category" = "Category"; - +"coin_overview.overview" = "Overview"; +"coin_overview.description_warning" = "This is an AI generated description based on the provided reference material for the given cryptocurrency. It may contain errors."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; -"coin_overview.coin_types" = "Coin Types"; -"coin_overview.show_more" = "Show More"; +"coin_overview.coin_types" = "Coin Type"; +"coin_overview.show_more" = "Mehr anzeigen"; "coin_overview.show_less" = "Show Less"; "coin_overview.links" = "Links"; @@ -746,12 +768,12 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_analytics.indicators.strong_sell" = "Strong Sell"; "coin_analytics.period" = "Period"; "coin_analytics.period.select_title" = "Select Period"; -"coin_analytics.period.1h" = "1 hour"; +"coin_analytics.period.1h" = "1 Stunde"; "coin_analytics.period.4h" = "4 hours"; "coin_analytics.period.1d" = "1 day"; "coin_analytics.period.1w" = "1 week"; -"coin_analytics.details" = "Details"; +"coin_analytics.details" = "Info"; "coin_analytics.not_available" = "This project has no analytics data"; @@ -763,15 +785,15 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_analytics.cex_volume" = "CEX Volume"; "coin_analytics.cex_volume_rank" = "CEX Volume Rank"; "coin_analytics.cex_volume_rank.description" = "Tokens ranked by trading volume for the token on centralized exchanges."; -"coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; +"coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; -"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over 30-day period."; +"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info4" = "List of all tokens ranked based on trading volume on centralized exchanges over 24H / 7D / 1M intervals."; "coin_analytics.dex_volume" = "DEX Volume"; "coin_analytics.dex_volume_rank" = "DEX Volume Rank"; "coin_analytics.dex_volume_rank.description" = "Tokens ranked by trading volume for the token on decentralized exchanges."; -"coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading decentralized exchanges over a 30-day period."; +"coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.dex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading decentralized exchanges over 1 year period."; "coin_analytics.dex_volume.info3" = "Token's rank based on trading volume on leading decentralized exchanges over 30-day period."; "coin_analytics.dex_volume.info4" = "List of all tokens ranked based on trading volume on decentralized exchanges over 24H / 7D / 1M intervals."; @@ -793,18 +815,18 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_analytics.active_addresses.30_day_unique_addresses" = "30-Day Unique Addresses"; "coin_analytics.active_addresses_rank" = "Active Addresses Rank"; "coin_analytics.active_addresses_rank.description" = "Tokens ranked by number of unique addresses transacting with the token."; -"coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over a 24-hour period."; +"coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; -"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; -"coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; +"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over a 30-day period."; +"coin_analytics.active_addresses.info4" = "Token's rank is based on the number of active wallets transacting with the token 30-day period."; "coin_analytics.active_addresses.info5" = "List of all tokens ranked based on the number of daily active addresses transacting with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count" = "Transaction Count"; "coin_analytics.transaction_count_rank" = "Tx Count Rank"; -"coin_analytics.transaction_count_rank.description" = "Tokens ranked by number of transactions on a blockchain."; -"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; +"coin_analytics.transaction_count_rank.description" = "Tokens are ranked by a number of transactions on a blockchain."; +"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with tokens over a 30-day period."; "coin_analytics.transaction_count.info2" = "Chart showing variation in transaction count over 1 year period."; -"coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; +"coin_analytics.transaction_count.info3" = "Token's rank is based on the number of transactions within the token 30-day period."; "coin_analytics.transaction_count.info4" = "List of all tokens ranked based on the number of transactions with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count.info5" = "The total number of tokens transferred over the blockchain over the 30 day period."; @@ -816,24 +838,24 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_analytics.holders.tracked_blockchains" = "Tracked blockchains: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; "coin_analytics.holders.in_top_10_addresses" = "in top 10 holders"; "coin_analytics.holders.count" = "Total Holders: %@"; -"coin_analytics.holders.see_all" = "See All"; +"coin_analytics.holders.see_all" = "Alle anzeigen"; "coin_analytics.project_tvl" = "Project TVL"; "coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; "coin_analytics.project_tvl.info_title" = "Project TVL (Total Value Locked)"; -"coin_analytics.project_tvl.info1" = "Total-Value-Locked (or Assets Under Management) in the project's smart contracts."; +"coin_analytics.project_tvl.info1" = "Total-value-locked (or Assets Under Management) in the project's smart contracts."; "coin_analytics.project_tvl.info2" = "Chart showing variation Total-Value-Locked in project's smart contracts over 1 year period."; -"coin_analytics.project_tvl.info3" = "Token's rank based on current Total-Value-Locked."; +"coin_analytics.project_tvl.info3" = "Token's rank is based on current Total-Value-Locked."; "coin_analytics.project_tvl.info4" = "List of all tokens ranked based on current Total-Value-Locked."; "coin_analytics.project_tvl.info5" = "Market Cap / TVL ratio for the project."; "coin_analytics.project_fee" = "Project Fee"; "coin_analytics.project_fee_rank" = "Project Fee Rank"; -"coin_analytics.project_fee_rank.description" = "Tokens ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; +"coin_analytics.project_fee_rank.description" = "Tokens are ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; "coin_analytics.project_revenue" = "Project Revenue"; "coin_analytics.project_revenue_rank" = "Project Revenue Rank"; -"coin_analytics.project_revenue_rank.description" = "Tokens ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.description" = "Tokens are ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; "coin_analytics.other_data" = "Other Data"; @@ -863,11 +885,11 @@ Swag your way to Settings -> %@ and let it shine!"; "coin_analytics.overall_score.poor" = "Poor"; "coin_analytics.overall_score.cex_volume" = "The overall score is based on the average daily trading volume on centralized exchanges over the last 7 days."; "coin_analytics.overall_score.dex_volume" = "The overall score is based on the average daily trading volume on decentralized exchanges over the last 7 days."; -"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total avilable liquidity on decentralized exchanges."; +"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total available liquidity on decentralized exchanges."; "coin_analytics.overall_score.active_addresses" = "The overall score is based on the average daily active addresses over the last 7 days."; "coin_analytics.overall_score.project_tvl" = "The overall score is based on the total value locked (assets under management) on the project represented by the given token."; "coin_analytics.overall_score.transaction_count" = "The overall score is based on the average daily transaction count over the last 7 days."; -"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding respective token."; +"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding the respective token."; "coin_analytics.rank" = "Rank"; "coin_analytics.30_day_rank" = "30-Day Rank"; @@ -913,152 +935,156 @@ Swag your way to Settings -> %@ and let it shine!"; // Transactions -"transactions.title" = "Transactions"; -"transactions.tab_bar_item" = "Transactions"; +"transactions.title" = "Transaktionen"; +"transactions.tab_bar_item" = "Transaktionen"; "transactions.blockchain" = "Blockchain"; -"transactions.all_blockchains" = "All Blockchains"; -"transactions.all_coins" = "All Coins"; -"transactions.choose_coin" = "Choose Coin"; +"transactions.all_blockchains" = "Alle Blockchains"; +"transactions.all_coins" = "Alle Coins"; +"transactions.choose_coin" = "Coins auswählen"; "transactions.filter_all" = "All"; -"transactions.empty_text" = "You don't have any pending or past transactions yet"; -"transactions.pending" = "Pending"; -"transactions.processing" = "Processing"; -"transactions.completed" = "Completed"; -"transactions.failed" = "Failed"; - -"transactions.receive" = "Receive"; -"transactions.send" = "Send"; +"transactions.empty_text" = "Sie haben noch keine offenen oder früheren Transaktionen"; +"transactions.pending" = "In Bearbeitung"; +"transactions.processing" = "Verarbeite"; +"transactions.completed" = "Abgeschlossen"; +"transactions.failed" = "Fehlgeschlagen"; + +"transactions.receive" = "Empfangen"; +"transactions.send" = "Senden"; "transactions.burn" = "Burn"; "transactions.mint" = "Mint"; -"transactions.approve" = "Approve"; +"transactions.approve" = "Genehmigen"; "transactions.swap" = "Swap"; -"transactions.contract_call" = "Contract Call"; -"transactions.contract_creation" = "Contract Creation"; -"transactions.external_call" = "External Call"; +"transactions.contract_call" = "Vertragsaufruf"; +"transactions.contract_creation" = "Vertragserstellung"; +"transactions.external_call" = "Externe Anrufe"; -"transactions.to" = "To %@"; -"transactions.from" = "From %@"; +"transactions.to" = "An %@"; +"transactions.from" = "Von %@"; -"transactions.multiple" = "Multiple"; +"transactions.multiple" = "Mehrere"; -"transactions.value.unlimited" = "unlimited"; +"transactions.value.unlimited" = "unbegrenzt"; -"transactions.today" = "Today"; -"transactions.yesterday" = "Yesterday"; +"transactions.today" = "Heute"; +"transactions.yesterday" = "Gestern"; "transactions.types.all" = "All"; -"transactions.types.incoming" = "Received"; -"transactions.types.outgoing" = "Sent"; +"transactions.types.incoming" = "Erhalten"; +"transactions.types.outgoing" = "Gesendet"; "transactions.types.swap" = "Swaps"; -"transactions.types.approve" = "Approvals"; +"transactions.types.approve" = "Genehmigungen"; -"transactions.unknown_transaction.title" = "Unknown Transaction"; -"transactions.unknown_transaction.description" = "Transaction can not be parsed"; +"transactions.unknown_transaction.title" = "Unbekannte Transaktion"; +"transactions.unknown_transaction.description" = "Transaktion kann nicht analysiert werden"; // Transaction Info -"tx_info.title" = "Transaction Info"; -"tx_info.date" = "Date"; +"tx_info.title" = "Transaktionsdaten"; +"tx_info.date" = "Datum"; "tx_info.title_approval" = "Swap Approval"; -"tx_info.status.pending" = "Pending"; -"tx_info.status.completed" = "Completed"; -"tx_info.status.failed" = "Failed"; -"tx_info.from_hash" = "From"; -"tx_info.transaction_id" = "ID"; -"tx_info.to_hash" = "To"; +"tx_info.status.pending" = "In Bearbeitung"; +"tx_info.status.completed" = "Abgeschlossen"; +"tx_info.status.failed" = "Fehlgeschlagen"; +"tx_info.from_hash" = "Von"; +"tx_info.transaction_id" = "Transaktion ID"; +"tx_info.to_hash" = "An"; "tx_info.spender" = "Spender"; -"tx_info.contact_name" = "Contact Name"; -"tx_info.button_explorer" = "View on %@"; -"tx_info.rate" = "Historical Rate"; -"tx_info.options.speed_up" = "Speed Up"; -"tx_info.options.cancel" = "Cancel Transaction"; -"tx_info.transaction.already_in_block" = "Transaction already in block"; -"tx_info.fee" = "Fee"; -"tx_info.fee.estimated" = "Fee (est.)"; -"tx_info.to_self_note" = "This transaction is sent to own address"; -"tx_info.double_spent_note" = "Double Spend Risk!"; -"tx_info.locked_until" = "Locked until %@"; -"tx_info.unlocked_at" = "Unlocked at %@"; -"tx_info.recipient_hash" = "Recipient"; -"tx_info.raw_transaction" = "Raw Transaction"; +"tx_info.contact_name" = "Kontaktname"; +"tx_info.button_explorer" = "Auf %@ ansehen"; +"tx_info.rate" = "Historischer Rate"; +"tx_info.options.speed_up" = "Beschleunigen"; +"tx_info.options.cancel" = "Transaktion abbrechen"; +"tx_info.transaction.already_in_block" = "Transaktion bereits im Block"; +"tx_info.fee" = "Gebühr"; +"tx_info.fee.estimated" = "Gebühr (est.)"; +"tx_info.to_self_note" = "Diese Transaktion wurde an die eigene Adresse gesendet"; +"tx_info.double_spent_note" = "Double-Spend-Risiko!"; +"tx_info.locked_until" = "Gesperrt bis %@"; +"tx_info.unlocked_at" = "Freigegeben am %@"; +"tx_info.recipient_hash" = "Empfänger"; +"tx_info.raw_transaction" = "Rohdaten der Transaktion"; "tx_info.memo" = "Memo"; -"tx_info.service" = "Service"; -"tx_info.view_on" = "View on %@"; -"tx_info.you_pay" = "You Pay"; -"tx_info.you_get" = "You Get"; -"tx_info.you_paid" = "You Paid"; -"tx_info.you_got" = "You Got"; -"tx_info.price" = "Price"; +"tx_info.service" = "Dienst"; +"tx_info.view_on" = "Auf %@ ansehen"; +"tx_info.you_pay" = "Sie bezahlen"; +"tx_info.you_get" = "Sie erhalten"; +"tx_info.you_paid" = "Du hast gezahlt"; +"tx_info.you_got" = "Du bekommst"; +"tx_info.price" = "Preis"; // Settings -"settings.title" = "Settings"; -"settings.tab_bar_item" = "Settings"; -"settings.manage_accounts" = "Manage Wallets"; -"settings.blockchain_settings" = "Blockchain Settings"; -"settings.security" = "Security"; -"settings.experimental_features" = "Experimental"; -"settings.personal_support" = "Personal Support"; -"settings.base_currency" = "Base Currency"; -"settings.language" = "Language"; +"settings.title" = "Einstellungen"; +"settings.tab_bar_item" = "Einstellungen"; +"settings.manage_accounts" = "Wallets verwalten"; +"settings.blockchain_settings" = "Blockchain Einstellungen"; +"settings.backup_manager" = "Backup-Manager"; +"settings.security" = "Sicherheit"; +"settings.experimental_features" = "Experimentell"; +"settings.personal_support" = "Persönlicher Support"; +"settings.base_currency" = "Währung"; +"settings.language" = "Sprache"; "settings.faq" = "FAQ"; -"settings.theme" = "Theme"; -"settings.info_subtitle" = "decentralized app"; -"settings.donate.description" = "Together, with your support, we can make this app even better!"; -"settings.donate.title" = "Donate"; +"settings.theme" = "Design"; +"settings.info_subtitle" = "decentralized"; +"settings.donate.description" = "Gemeinsam können wir mit Ihrer Unterstützung diese App noch besser machen!"; +"settings.donate.title" = "Spenden"; +"settings.rate_us" = "Bewerten Sie uns"; +"settings.tell_friends" = "Freunden erzählen"; +"settings.contact_us" = "Kontaktiere uns"; // Settings -> Base Currency -"settings.base_currency.title" = "Base Currency"; -"settings.base_currency.other" = "Other"; -"settings.base_currency.disclaimer" = "Disclaimer"; -"settings.base_currency.disclaimer.description" = "The exchange rate data is provided by a third party service - Coingecko.com. \n\nThe %@ wallet app doesn't guarantee these values are always correct and matches market data. The chance for inconsistency is higher if you select any base currency other than %@."; -"settings.base_currency.disclaimer.set" = "Set"; +"settings.base_currency.title" = "Währung"; +"settings.base_currency.other" = "Andere"; +"settings.base_currency.disclaimer" = "Haftungsausschluss"; +"settings.base_currency.disclaimer.description" = "Die Wechselkursdaten werden von einem Drittanbieter-Service - Coingecko.com bereitgestellt. \n\nDie %@ Wallet-App garantiert nicht, dass diese Werte immer korrekt sind und den Marktdaten entsprechen. Die Wahrscheinlichkeit für Inkonsistenz ist höher, wenn du eine andere Basiswährung als %@ wählst."; +"settings.base_currency.disclaimer.set" = "Übernehmen"; // Settings -> Manage Wallet -"manage_wallets.title" = "Coin Manager"; -"manage_wallets.not_found" = "No results found. Try to add token manually."; -"manage_wallets.search_placeholder" = "Name, code or contract address"; -"manage_wallets.contract_address" = "Contract Address"; -"manage_wallets.derivation_description" = "There are 4 common address formats %@ wallets can use to receive incoming payments:\n\n- BIP44 (oldest)\n- BIP49\n- BIP84 (recommended)\n- BIP86 (newest)\n\nWhile %@ wallet supports all 4, it recommends to use a %@ wallet operating in BIP84 format."; -"manage_wallets.bitcoin_cash_coin_type_description" = "There are 2 address formats Bitcoin Cash wallets can use to receive incoming payments:\n\n- TYPE 0 (older)\n- TYPE 145 (newer)\n\nWhile %@ wallet supports both of them it recommends to use a Bitcoin Cash wallet operating in TYPE 145 format."; +"manage_wallets.title" = "Kryptowährung"; +"manage_wallets.not_found" = "Keine Ergebnisse gefunden. Versuchen Sie, Token manuell hinzuzufügen."; +"manage_wallets.search_placeholder" = "Name, Code oder Vertragsadresse"; +"manage_wallets.contract_address" = "Vertragsadresse"; +"manage_wallets.derivation_description" = "Es gibt 4 gängige Adressformate %@ Brieftaschen zum Empfang eingehender Zahlungen:\n\n- BIP44 (ältes)\n- BIP49\n- BIP84 (empfohlen)\n- BIP86 (neueste)\n\nWährend %@ alle 4 unterstützt empfiehlt es, eine %@ Brieftasche im BIP84-Format zu verwenden."; +"manage_wallets.bitcoin_cash_coin_type_description" = "Es gibt 2 Adressformate Bitcoin Cash Wallets zum Empfang eingehender Zahlungen:\n\n- TYPE 0 (älter)\n- TYPE 145 (neuer)\n\nWährend %@ Brieftasche beide unterstützt, empfiehlt es sich, ein Bitcoin Cash Wallet im TYPE 145 Format zu verwenden."; // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Account"; -"settings.personal_support.telegram_username.placeholder" = "@username"; -"settings.personal_support.description" = "Enter your Telegram account name to open a personal support chat and we'll send message to you."; -"settings.personal_support.request" = "Request"; -"settings.personal_support.requested" = "Requested"; -"settings.personal_support.failed" = "Request failed"; -"settings.personal_support.need_subscription" = "This feature only for %@ Wallet premium users. More info in our official site."; -"settings.personal_support.requested.description" = "You've already requested a private chat, find it on Telegram"; -"settings.personal_support.requested.open_telegram" = "Open Telegram"; -"settings.personal_support.requested.new_request" = "New Request"; +"settings.personal_support.telegram_username.placeholder" = "@benutzername"; +"settings.personal_support.description" = "Geben Sie Ihren Telegram-Kontonamen ein, um einen persönlichen Support-Chat zu eröffnen und wir senden Ihnen eine Nachricht."; +"settings.personal_support.request" = "Anforderung"; +"settings.personal_support.requested" = "Angefragt"; +"settings.personal_support.failed" = "Anfrage fehlgeschlagen"; +"settings.personal_support.need_subscription" = "This feature is only for %@ Wallet premium users. More info on our official site."; +"settings.personal_support.requested.description" = "Sie haben bereits einen privaten Chat angefordert, finden Sie ihn auf Telegram"; +"settings.personal_support.requested.open_telegram" = "Telegram öffnen"; +"settings.personal_support.requested.new_request" = "Neue Zahlungsaufforderung"; // Settings -> Experimental Features -"settings.experimental_features.title" = "Experimental"; -"settings.experimental_features.description" = "The features below are experimental and should be used with caution. While we have thoroughly tested these features using our own crypto funds, we cannot guarantee they will work as expected in all possible cases."; +"settings.experimental_features.title" = "Experimentell"; +"settings.experimental_features.description" = "Die unten aufgeführten Funktionen sind experimentell und sollten mit Vorsicht eingesetzt werden. Wir haben sie zwar ausführlich mit unserem eigenen Kryptoguthaben getestet, aber wir können noch nicht garantieren, dass sie in allen denkbaren Fällen funktionieren."; "settings.experimental_features.bitcoin_hodling" = "TimeLock"; // Settings -> Experimental Features -> Bitcoin HODLing "settings.bitcoin_hodling.title" = "TimeLock"; -"settings.bitcoin_hodling.lock_time" = "Activate"; -"settings.bitcoin_hodling.description" = "This enables you to send Bitcoins that cannot be spent until a specified date. \n\nThe receiver of such transactions should use the %@ wallet app version 0.10 or newer, with the BIP44 address format for Bitcoin. \n\nOnly the %@ wallet can correctly identify such transactions on the Bitcoin network, as well as enable the receiver to spend those Bitcoins after the lock period expires. \n\nIf you’re a HODLer, you may use this feature to force yourself into hodling your Bitcoins by sending such transactions to yourself."; +"settings.bitcoin_hodling.lock_time" = "Aktivieren"; +"settings.bitcoin_hodling.description" = "Dadurch können Sie Bitcoins senden, die bis zu einem bestimmten Datum nicht ausgegeben werden können. \n\nDer Empfänger solcher Transaktionen sollte die %@ Wallet-App Version 0 verwenden. 0 oder neuer, mit dem BIP44 Adressformat für Bitcoin. \n\nNur die %@ Brieftasche kann solche Transaktionen im Bitcoin-Netzwerk korrekt identifizieren sowie den Empfänger in die Lage zu versetzen, diese Bitcoins nach Ablauf der Sperrfrist auszugeben. \n\nWenn Sie ein HODLer sind Sie können diese Funktion nutzen, um sich selbst zu erzwingen, Ihre Bitcoins zu hodlen, indem Sie solche Transaktionen an sich senden."; // Settings -> Terms -"terms.title" = "Terms"; -"terms.i_agree" = "I Agree"; +"terms.title" = "Bedingungen"; +"terms.i_agree" = "Ich stimme zu"; -"terms.item.1" = "Securely backup recovery phrases for each wallet. It's the only way to regain access to funds if the app malfunctions."; -"terms.item.2" = "The wallet recovery phrases are randomly generated on the device during setup and are not stored elsewhere."; -"terms.item.3" = "Disabling unlock PIN (code) on the smartphone deletes all wallets from the app. Recovery phrases will be needed to restore access to funds."; -"terms.item.4" = "Jailbreaking (rooting), using outdated OS, and installing apps from unknown sources may endanger the safety of funds."; -"terms.item.5" = "There may be undiscovered software issues in the code powering this app which may cause the app to malfunction."; +"terms.item.1" = "Sicheres Backup der Wiederherstellungsausdrücke für jede Wallet. Dies ist die einzige Möglichkeit, den Zugriff auf Gelder zurückzugewinnen, wenn die App nicht funktioniert."; +"terms.item.2" = "Die Wallet-Wiederherstellungs-Phrasen werden während des Setups zufällig auf dem Gerät generiert und werden an anderer Stelle nicht gespeichert."; +"terms.item.3" = "Das Deaktivieren der PIN (Code) auf dem Smartphone löscht alle Brieftaschen aus der App. Wiederherstellungs-Phrasen werden benötigt, um den Zugriff auf Gelder wiederherzustellen."; +"terms.item.4" = "Jailbreaking (Rooting), Verwendung veralteter Betriebssysteme und das Installieren von Apps aus unbekannten Quellen könnten die Sicherheit des Geldes gefährden."; +"terms.item.5" = "Möglicherweise gibt es unentdeckte Softwarefehler in dem Code, der diese App aktiviert, was zu Fehlfunktionen der App führen kann."; // Settings -> Tell Friends @@ -1066,210 +1092,297 @@ Swag your way to Settings -> %@ and let it shine!"; // Settings -> Blockchain Settings -"blockchain_settings.title" = "Blockchain Settings"; +"blockchain_settings.title" = "Blockchain Einstellungen"; + +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Backup-Manager"; +"backup_app.backup_manager.restore" = "Backup wiederherstellen"; +"backup_app.backup_manager.create" = "Neue Sicherung erstellen"; + +"backup_app.backup_type.title" = "Backup speichern"; +"backup_app.backup_type.cloud" = "nach iCloud"; +"backup_app.backup_type.cloud.description" = "Speichern einer Sicherungskopierdatei in Ihrem Schlüsselbund."; +"backup_app.backup_type.file" = "in Dateien"; +"backup_app.backup_type.file.description" = "Speichern einer Sicherungskopierdatei in Ihrem lokalen Ordner."; + +"backup_app.backup_list.title" = "Backup-Datei"; +"backup_app.backup_list.description.restore" = "Liste der Inhalte in der Backup-Datei."; +"backup_app.backup_list.header.wallets" = "Wallets"; +"backup_app.backup_list.header.other" = "Andere"; +"backup_app.backup_list.other.watch_account.title" = "Watch-Adresse"; +"backup_app.backup_list.other.watchlist.title" = "Merkliste"; +"backup_app.backup_list.other.contacts.title" = "Kontakte"; +"backup_app.backup_list.other.blockchain_settings.title" = "Custom RPC"; +"backup_app.backup_list.other.app_settings.title" = "App-Einstellungen"; +"backup_app.backup_list.other.app_settings.description" = "Sprache, Währung, Erscheinung ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Sicherung in iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud ist ein von Apple bereitgestellter Cloud-Speicherdienst. Es ist wichtig zu wissen, dass Ihre Backup-Daten auf Apples Servern gespeichert werden."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "Ich verstehe, dass der Verlust des Zugriffs auf meine iCloud dazu führen wird, dass auch der Zugriff auf die Sicherung einer entsprechenden Brieftasche verloren geht."; +"backup_app.backup.disclaimer.file.title" = "In Datei sichern"; +"backup_app.backup.disclaimer.file.description" = "Speichergeräte wie Festplatten, USB-Laufwerke und Smartphone-Speicher sind alle anfällig für Datenverlust aufgrund von physischen Schäden, Diebstahl oder anderen unvorhergesehenen Umständen."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Ich verstehe, dass ein Diebstahl oder eine Beschädigung eines Sicherungsgeräts zum Verlust des Sicherungskopfes an eine entsprechende Brieftasche führen wird."; + +"backup.disclaimer.cloud.title" = "Sicherung in iCloud"; +"backup.disclaimer.cloud.description" = "iCloud ist ein von Apple bereitgestellter Cloud-Speicherdienst. Es ist wichtig zu wissen, dass Ihre Backup-Daten auf Apples Servern gespeichert werden."; +"backup.disclaimer.cloud.checkbox_label" = "Ich verstehe, dass der Verlust des Zugriffs auf meine iCloud dazu führen wird, dass auch der Zugriff auf die Sicherung einer entsprechenden Brieftasche verloren geht."; +"backup.disclaimer.file.title" = "In Datei sichern"; +"backup.disclaimer.file.description" = "Speichergeräte wie Festplatten, USB-Laufwerke und Smartphone-Speicher sind alle anfällig für Datenverlust aufgrund von physischen Schäden, Diebstahl oder anderen unvorhergesehenen Umständen."; +"backup.disclaimer.file.checkbox_label" = "Ich verstehe, dass ein Diebstahl oder eine Beschädigung eines Sicherungsgeräts zum Verlust des Sicherungskopfes an eine entsprechende Brieftasche führen wird."; +"backup_app.backup.name.title" = "Sicherungsname"; +"backup_app.backup.name.description" = "Geben Sie den Namen für die Backup-Datei ein."; + +"backup_app.backup.password.title" = "Passwort sichern"; +"backup_app.backup.password.description" = "Legen Sie ein Passwort fest, um das Entsperren zu ermöglichen. Das Passwort muss mindestens 8 Zeichen lang sein und einen Kleinbuchstaben, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten."; +"backup_app.backup.password.highlighted_description" = "Dieses Passwort wird verwendet, um die Sicherungsdatei Ihrer Brieftasche zu verschlüsseln. Wenn es verloren geht oder vergessen wird, kann es nicht wiederhergestellt oder zurückgesetzt werden."; + +"backup_app.restore_type.title" = "Wiederherstellen"; + +"backup_app.restore.notice.description" = "Diese Aktion wird Ihre lokalen Zahlungskontakte sowie die iCloud-Kopie (falls vorhanden) überschreiben."; +"backup_app.restore.notice.merge" = "Ersetzen"; + +"backup.password.title" = "Passwort sichern"; +"backup.password.description" = "Legen Sie ein Passwort fest, um das Entsperren zu ermöglichen. Das Passwort muss mindestens 8 Zeichen lang sein und einen Kleinbuchstaben, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten."; +"backup.password.highlighted_description" = "Dieses Passwort wird verwendet, um die Sicherungsdatei Ihrer Brieftasche zu verschlüsseln. Wenn es verloren geht oder vergessen wird, kann es nicht wiederhergestellt oder zurückgesetzt werden."; // Settings -> Security -"settings_security.title" = "Security"; -"settings_security.passcode" = "Passcode"; -"settings_security.change_pin" = "Edit Passcode"; -"settings_security.touch_id" = "Touch ID"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Blockchain Settings"; -"security_settings.delete_alert_button" = "Delete from Phone"; +"settings_security.title" = "Sicherheit"; +"settings_security.enable_passcode" = "Code aktivieren"; +"settings_security.edit_passcode" = "Code bearbeiten"; +"settings_security.disable_passcode" = "Code deaktivieren"; +"settings_security.auto_lock" = "Auto-Sperren"; +"settings_security.balance_auto_hide" = "Auto-Ausblenden ausgleichen"; +"settings_security.balance_auto_hide.description" = "Bei jedem Öffnen der App wird der Saldo automatisch ausgeblendet, unabhängig von den vorherigen Einstellungen."; +"settings_security.enable_duress_mode" = "Duress Modus setzen"; +"settings_security.edit_duress_passcode" = "Duress Code bearbeiten"; +"settings_security.disable_duress_mode" = "Duress Modus deaktivieren"; +"settings_security.duress_mode.description" = "Ein spezialisierter Modus, der entworfen wurde, um ausgewählte Brieftaschen unter Zwang zu schützen."; + +// Create Passcode + +"create_passcode.title" = "Code erstellen"; +"create_passcode.description" = "Dein Code wird verwendet werden, um deinen Wallet zu entsperren und Geld schicken"; +"create_passcode.description.biometry" = "Code für %@ aktivieren"; +"create_passcode.description.duress_mode" = "Code zum Aktivieren des Duress Modus festlegen"; +"create_passcode.confirm_passcode" = "Bestätigen"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Duress-Modus"; +"enable_duress_mode.intro.description" = "In diesem Modus können Benutzer mehrere Entsperr-App-Passcodes einrichten, bei denen ein gewünschter Passcode nur bestimmte Wallets anzeigt. Entwickelt um ausgewählte Brieftaschen vor Zwang oder Bedrohungen zu schützen."; +"enable_duress_mode.intro.notes" = "Notizen"; +"enable_duress_mode.intro.biometrics.description" = "Die %@ Funktion wird den Duress Modus entsperren. Sie können %@ für Bequemlichkeit deaktivieren."; +"enable_duress_mode.intro.passcode_disabling" = "Code deaktivieren"; +"enable_duress_mode.intro.passcode_disabling.description" = "Das Deaktivieren des Code im Hauptmodus wird den Duress Modus automatisch zurücksetzen."; +"enable_duress_mode.intro.passcode_change" = "Codeänderung"; +"enable_duress_mode.intro.passcode_change.description" = "Das Ändern des Code im Duress Modus ändert auch den aktuellen Code für diesen Modus."; + +"enable_duress_mode.select.title" = "Wallets auswählen"; +"enable_duress_mode.select.description" = "Wählen Sie die Wallets, die im Duress Modus angezeigt werden."; +"enable_duress_mode.select.wallets" = "Wallets"; +"enable_duress_mode.select.watch_wallets" = "Watch-Adresse"; + +"enable_duress_mode.passcode.title" = "Duress Code"; +"enable_duress_mode.passcode.description" = "Code für Duress Modus festlegen"; +"enable_duress_mode.passcode.confirm" = "Bestätigen"; -"btc_blockchain_settings.restore_source" = "Restore Source"; -"btc_blockchain_settings.restore_source.description" = "Select a data source for restoring a wallet with transactions."; -"btc_blockchain_settings.restore_source.alert" = "After changing Restore Source the wallet will have to resync itself with the %@ blockchain."; +// Edit Passcode -"btc_restore_mode.recommended" = "Recommended"; -"btc_restore_mode.more_private" = "More Private"; +"edit_passcode.title" = "Code bearbeiten"; +"edit_passcode.enter_new_passcode" = "Neues Code eingeben"; +"edit_passcode.confirm_new_passcode" = "Bestätigen"; -"btc_transaction_sort_mode.shuffle" = "Shuffle"; -"btc_transaction_sort_mode.shuffle.description" = "Random Indexing"; -"btc_transaction_sort_mode.bip69" = "Deterministic Bip69"; -"btc_transaction_sort_mode.bip69.description" = "Lexicographical Indexing"; +// Edit Duress Passcode -"blockchain_settings.info.restore_source" = "Restore Source"; -"blockchain_settings.info.restore_source.content" = "This setting is only relevant when restoring an existing wallet. It is a process of getting transaction history for a given cryptocurrency so the wallet app is able to display past transactions and calculate the user's balance. This needs to happen only once when the user restores previously created wallets.\n\nAt this point, there are two potential ways for a mobile wallet like %@ to do this:\n\n1. from the API Server: There is a third-party predefined server that hosts the entire blockchain and has all the data processed and optimized to provide that data in a fast manner. This method is fast but potentially (not necessarily) less private. It's also a centralized method to restore a wallet as it depends on the availability of a 3rd party server. This option is recommended due to its speed of getting data (5-10 minutes).\n\n2. from Blockchain: The app tries to restore directly from a network of blockchain nodes. This is a decentralized way to restore wallet balance and past transactions. The app pings many of the network nodes and requests data from them without addressing some nodes specifically. This option is slow and can easily take 2-3 hours, the app needs to be open while restoring is happening. This restore method doesn't depend on any entity and should work in all conditions."; -"blockchain_settings.info.rpc_source" = "RPC Source"; -"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer. %@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; +"edit_duress_passcode.title" = "Duress Code bearbeiten"; +"edit_duress_passcode.enter_new_passcode" = "Neues Code für Duress Modus eingeben"; +"edit_duress_passcode.confirm_new_passcode" = "Bestätigen"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Ungültige Bestätigung"; +"set_passcode.already_used" = "Dieser Code wird bereits verwendet"; + +// Unlock + +"unlock.title" = "Entsperren"; +"unlock.passcode" = "Code eingeben"; +"unlock.biometry_reason" = "Wallet entsperren"; +"unlock.attempts_left" = "Verbleibende Versuche: %@"; +"unlock.disabled_until" = "Deaktiviert bis: %@ "; +"unlock.random" = "Zufällig"; + +"security_settings.delete_alert_button" = "Von Telefon löschen"; + +"btc_blockchain_settings.restore_source" = "Parameter wiederherstellen"; +"btc_blockchain_settings.restore_source.description" = "Wählen Sie die Datenquelle für die Wiederherstellung des Wallets mit Transaktionen aus."; +"btc_blockchain_settings.restore_source.alert" = "Nach dem Ändern der Quelle wiederherstellen muss sich die Wallet mit der %@ Blockchain synchronisieren."; + +"btc_restore_mode.recommended" = "Empfohlen"; +"btc_restore_mode.more_private" = "Mehr Privat"; + +"btc_transaction_sort_mode.shuffle" = "Zufällig"; +"btc_transaction_sort_mode.shuffle.description" = "Zufällige Indizierung"; +"btc_transaction_sort_mode.bip69" = "BIP69"; +"btc_transaction_sort_mode.bip69.description" = "Lexikographische Indizierung"; + +"blockchain_settings.info.restore_source" = "Parameter wiederherstellen"; +"blockchain_settings.info.restore_source.content" = "Diese Einstellung ist nur relevant, wenn eine bestehende Brieftasche wiederhergestellt wird. Es ist ein Prozess, die Transaktionsgeschichte für eine bestimmte Kryptowährung abzurufen, so dass die Wallet-App in der Lage ist, vergangene Transaktionen anzuzeigen und den Kontostand des Benutzers zu berechnen. Dies muss nur einmal passieren, wenn der Benutzer die zuvor erstellten Wallets wiederherstellt.\n\nan diesem Punkt es gibt zwei mögliche Wege für eine mobile Brieftasche wie %@ , dies zu tun:\n\n1. vom API-Server: Es gibt einen vordefinierten Server von Dritten, der die gesamte Blockchain beherbergt und alle Daten verarbeitet und optimiert hat, um diese Daten schnell zur Verfügung zu stellen. Diese Methode ist schnell, aber möglicherweise (nicht notwendig) weniger privat. Es ist auch eine zentralisierte Methode, um eine Brieftasche wiederherzustellen, da sie von der Verfügbarkeit eines Drittanbieter-Servers abhängt. Diese Option wird aufgrund der Geschwindigkeit des Abrufs von Daten (5-10 Minuten) empfohlen.\n\n2. von Blockchain: Die App versucht direkt aus einem Netzwerk von Blockchain-Knoten wiederherzustellen. Dies ist ein dezentralisierter Weg, um das Gleichgewicht der Brieftasche und der vergangenen Transaktionen wiederherzustellen. Die App pingt viele Netzwerkknoten und fordert Daten von ihnen an, ohne einige Knoten speziell anzusprechen. Diese Option ist langsam und kann leicht 2-3 Stunden dauern. Die App muss während der Wiederherstellung geöffnet sein. Diese Wiederherstellungsmethode hängt nicht von einer Entität ab und sollte unter allen Bedingungen funktionieren."; +"blockchain_settings.info.rpc_source" = "RPC-Quelle"; +"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer.%@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; // Manage Accounts -"manage_accounts.migration_required" = "Migration Required"; -"manage_accounts.migration_recommended" = "Migration recommended"; -"manage_accounts.backup_required" = "Backup required"; +"manage_accounts.migration_required" = "Migration erforderlich"; +"manage_accounts.migration_recommended" = "Migration empfohlen"; +"manage_accounts.backup_required" = "Sicherung erforderlich"; // Manage Keys -"settings_manage_keys.title" = "Manage Wallets"; -"settings_manage_keys.delete" = "Delete"; -"settings_manage_keys.backup" = "Backup"; -"settings_manage_keys.delete.title" = "Delete Wallet"; -"settings_manage_keys.delete.confirmation_remove" = "The action will delete this wallet from the device."; -"settings_manage_keys.delete.confirmation_loose" = "If you didn't back up the private key for this wallet, you will lose access to your funds."; -"settings_manage_keys.delete.confirmation_watch" = "Do you want to stop watching this wallet address?"; -"settings_manage_keys.delete.confirmation_watch.button" = "Stop Watching"; +"settings_manage_keys.title" = "Wallets verwalten"; +"settings_manage_keys.delete" = "Löschen"; +"settings_manage_keys.backup" = "Sicherung"; +"settings_manage_keys.delete.title" = "Wallet löschen"; +"settings_manage_keys.delete.confirmation_remove" = "Damit wird das Wallet von diesem Gerät gelöscht."; +"settings_manage_keys.delete.confirmation_loose" = "Wenn Sie den Private Key für dieses Wallet nicht gesichert haben, verlieren Sie den Zugang zu Ihrem Guthaben."; +"settings_manage_keys.delete.confirmation_watch" = "Möchten Sie die Beobachtung dieser Wallet-Adresse beenden?"; +"settings_manage_keys.delete.confirmation_watch.button" = "Beobachten stoppen"; // Settings -> About App -"settings.about_app.title" = "About App"; +"settings.about_app.title" = "Über die App"; "settings.about_app.app_name" = "%@ Wallet"; -"settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; -"settings.about_app.whats_new" = "What's New"; +"settings.about_app.description" = "Die Brieftasche %@ wurde für diejenigen gebaut, die Kryptowährungen auf private und unabhängige Weise investieren und speichern möchten.\n\nEs handelt sich um eine nicht-Custodial, Peer-to-Peer-Wallet, bei der nur der Benutzer die Kontrolle über das Guthaben hat. Es sammelt keine Daten und hält den Benutzer unabhängig, indem er das Guthaben des Benutzers nicht an eine bestimmte Wallet-App sperrt.\n\nDie %@ Brieftasche ist vollständig Open-Source und jeder kann bestätigen, dass die App genau so funktioniert, wie sie es vorgibt."; +"settings.about_app.whats_new" = "Das ist neu"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Contact Us"; -"settings.about_app.rate_us" = "Rate Us"; -"settings.about_app.tell_friends" = "Tell Friends"; // Settings -> About App -> Contact -"settings.contact.title" = "Contact Us"; -"settings.contact.via_email" = "via E-mail"; -"settings.contact.via_telegram" = "via Telegram"; +"settings.contact.title" = "Kontaktiere uns"; +"settings.contact.via_email" = "per e-Mail"; +"settings.contact.via_telegram" = "per Telegram"; // Settings -> Privacy -"settings.privacy" = "Privacy"; -"settings.privacy.description" = "%@ doesn't collect any data or use analytics tools that may expose any data about its users. The wallet is designed to ensure a high level of privacy for its users."; -"settings.privacy.statement.user_data_storage" = "User data always remains on the user's device."; -"settings.privacy.statement.data_usage" = "The wallet doesn't collect any data about users."; -"settings.privacy.statement.data_privacy" = "The wallet doesn't share any data about users."; -"settings.privacy.statement.user_account" = "There are no user accounts or databases keeping user data elsewhere."; +"settings.privacy" = "Privatsphäre"; +"settings.privacy.description" = "%@ sammelt keine Daten oder nutzt Analysewerkzeuge, die möglicherweise Daten über ihre Benutzer enthüllen. Die Brieftasche ist so konzipiert, dass sie den Benutzern ein hohes Maß an Privatsphäre garantiert."; +"settings.privacy.statement.user_data_storage" = "Benutzerdaten bleiben immer auf dem Gerät des Benutzers."; +"settings.privacy.statement.data_usage" = "Das Wallet sammelt keine Daten über Benutzer."; +"settings.privacy.statement.data_privacy" = "Das Wallet teilt keine Daten über Benutzer."; +"settings.privacy.statement.user_account" = "Es gibt keine Benutzerkonten oder Datenbanken, die Benutzerdaten woanders speichern."; // Settings -> Appearance -"appearance.title" = "Appearance"; +"appearance.title" = "Darstellung"; -"appearance.theme" = "Theme"; +"appearance.theme" = "Design"; "appearance.theme.system" = "System"; -"appearance.theme.dark" = "Dark"; -"appearance.theme.light" = "Light"; +"appearance.theme.dark" = "Dunkel"; +"appearance.theme.light" = "Hell"; -"appearance.tab_settings" = "Tab Settings"; -"appearance.markets_tab" = "Markets Tab"; -"appearance.launch_screen" = "Launch Screen"; +"appearance.tab_settings" = "Tab-Einstellungen"; +"appearance.markets_tab" = "Markt-Tab"; +"appearance.launch_screen" = "Startbildschirm"; "appearance.launch_screen.auto" = "Auto"; -"appearance.launch_screen.balance" = "Balance"; -"appearance.launch_screen.market_overview" = "Market Overview"; -"appearance.launch_screen.watchlist" = "Watchlist"; +"appearance.launch_screen.balance" = "Guthaben"; +"appearance.launch_screen.market_overview" = "Marktübersicht"; +"appearance.launch_screen.watchlist" = "Merkliste"; -"appearance.app_icon" = "App Icon"; +"appearance.app_icon" = "App-Symbol"; -"appearance.balance_conversion" = "Balance Conversion"; +"appearance.balance_conversion" = "Saldokonvertierung"; -"appearance.balance_value" = "Balance Value"; +"appearance.balance_value" = "Saldo Wert"; "appearance.balance_value.coin_value" = "Coin Value"; "appearance.balance_value.fiat_value" = "Fiat Value"; -"appearance.balance_auto_hide" = "Balance Auto Hide"; - // Settings -> Contacts -"contacts.title" = "Contacts"; -"contacts.list.search_placeholder" = "Search by name"; -"contacts.list.not_found" = "You do not have an added contact"; -"contacts.list.not_found_search" = "No results found"; -"contacts.add_new_contact" = "Add New Contact"; -"contacts.update_contact.already_has_address" = "Selected contact already has an address on %@. This action will replace the address %@ with %@."; -"contacts.update_contact.replace" = "Replace"; -"contacts.list.addresses_count" = "Addresses: %d"; -"contacts.contact.new.title" = "New Contact"; +"contacts.title" = "Kontakte"; +"contacts.list.search_placeholder" = "Nach Namen suchen"; +"contacts.list.not_found" = "Sie haben keinen neuen Kontakt"; +"contacts.list.not_found_search" = "Keine Ergebnisse gefunden"; +"contacts.add_new_contact" = "Neuer Kontakt"; +"contacts.update_contact.already_has_address" = "Der ausgewählte Kontakt hat bereits eine Adresse auf %@. Diese Aktion wird die Adresse %@ durch %@ ersetzen."; +"contacts.update_contact.replace" = "Ersetzen"; +"contacts.list.addresses_count" = "Adresse: %d"; +"contacts.contact.new.title" = "Neuer Kontakt"; "contacts.contact.name.placeholder" = "Name"; -"contacts.contact.update.error.name_already_exist" = "Name Already Exist"; -"contacts.contact.add_address" = "Add Address"; -"contacts.contact.delete" = "Delete Contact"; +"contacts.contact.update.error.name_already_exist" = "Name existiert bereits"; +"contacts.contact.add_address" = "Adresse hinzufügen"; +"contacts.contact.delete" = "Kontakt löschen"; "contacts.contact.address.blockchains" = "Blockchains"; "contacts.contact.address.blockchain" = "Blockchain"; -"contacts.contact.address.delete_address" = "Delete Address"; +"contacts.contact.address.delete_address" = "Adresse löschen"; -"contacts.restore.restored" = "Restored"; -"contacts.restore.parsing_error" = "File has wrong data!"; -"contacts.restore.restore_error" = "Failed to restore contacts"; -"contacts.restore.overwrite_alert.description" = "This action will overwrite your local payment contacts as well as its iCloud copy (if there is one)."; -"contacts.restore.overwrite_alert.replace" = "Replace"; +"contacts.restore.restored" = "Wiederhergestellt"; +"contacts.restore.parsing_error" = "Datei hat falsche Daten!"; +"contacts.restore.restore_error" = "Wiederherstellung der Kontakte fehlgeschlagen"; +"contacts.restore.overwrite_alert.description" = "Diese Aktion wird Ihre lokalen Zahlungskontakte sowie die iCloud-Kopie (falls vorhanden) überschreiben."; +"contacts.restore.overwrite_alert.replace" = "Ersetzen"; -"contacts.add_address.title" = "Add Address"; -"contacts.add_address.create_new" = "Create New Contact"; -"contacts.add_address.add_to_contact" = "Add to Existing Contact"; -"contacts.add_address.exist_address" = "This address is already used for %@"; +"contacts.add_address.title" = "Adresse hinzufügen"; +"contacts.add_address.create_new" = "Neuen Kontakt erstellen"; +"contacts.add_address.add_to_contact" = "Zu vorhandenem Kontakt hinzufügen"; +"contacts.add_address.exist_address" = "Diese Adresse wird bereits für %@ verwendet"; -"contacts.contact.delete_alert.title" = "Delete Contact"; -"contacts.contact.delete_alert.description" = "Are you sure you want to delete this contact?"; -"contacts.contact.delete_alert.delete" = "Delete"; +"contacts.contact.delete_alert.title" = "Kontakt löschen"; +"contacts.contact.delete_alert.description" = "Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?"; +"contacts.contact.delete_alert.delete" = "Löschen"; -"contacts.contact.dismiss_changes.description" = "Are you sure you want to discard these new changes?"; -"contacts.contact.dismiss_changes.discard_changes" = "Discard Changes"; -"contacts.contact.dismiss_changes.keep_editing" = "Keep Editing"; +"contacts.contact.dismiss_changes.description" = "Sind Sie sicher, dass Sie diese neuen Änderungen verwerfen möchten?"; +"contacts.contact.dismiss_changes.discard_changes" = "Änderungen verwerfen"; +"contacts.contact.dismiss_changes.keep_editing" = "Weiter bearbeiten"; -"contacts.add_address.delete_alert.title" = "Delete Address"; -"contacts.add_address.delete_alert.description" = "Are you sure you want to delete this address?"; -"contacts.add_address.delete_alert.delete" = "Delete"; +"contacts.add_address.delete_alert.title" = "Adresse löschen"; +"contacts.add_address.delete_alert.description" = "Sind Sie sicher, dass Sie diese Adresse löschen wollen?"; +"contacts.add_address.delete_alert.delete" = "Löschen"; // Contacts -> Settings -"contacts.settings.title" = "Settings"; +"contacts.settings.title" = "Einstellungen"; -"contacts.settings.restore_contacts" = "Restore Contacts"; -"contacts.settings.backup_contacts" = "Backup Contacts"; +"contacts.settings.restore_contacts" = "Kontakte wiederherstellen"; +"contacts.settings.backup_contacts" = "Kontakte speichern"; "contacts.settings.icloud_sync" = "iCloud Sync"; -"contacts.settings.description" = "Sync payment contacts to iCloud for easy backup and access across multiple devices."; -"contacts.settings.lost_synchronization.description" = "iCloud synchronization is lost. Please check that iCloud Storage is enabled on your device."; -"contacts.settings.merge_disclaimer" = "Your local payment contacts will be merged with ones stored on iCloud."; +"contacts.settings.description" = "Synchronisiere Zahlungskontakte mit iCloud für einfache Sicherung und Zugriff auf mehrere Geräte."; +"contacts.settings.lost_synchronization.description" = "iCloud-Synchronisation ist verloren. Bitte überprüfen Sie, ob iCloud-Speicher auf Ihrem Gerät aktiviert ist."; +"contacts.settings.merge_disclaimer" = "Ihre lokalen Zahlungskontakte werden mit den in iCloud gespeicherten zusammengeführt."; "contacts.settings.alert.title" = "iCloud Sync"; -"contacts.settings.alert.description" = "Please check that iCloud Storage is enabled on your device."; - -"contacts.settings.alert_error.title" = "iCloud Error"; - -// Set PIN - -"set_pin.title" = "Passcode"; -"set_pin.info" = "Your passcode will be used to unlock your wallet"; -"set_pin.wrong_confirmation" = "Passcode did not match. Try again"; - -// Edit PIN - -"edit_pin.title" = "Edit Passcode"; -"edit_pin.unlock_info" = "Current Passcode"; -"edit_pin.new_pin_info" = "New Passcode"; - -// Unlock PIN - -"unlock_pin.info" = "Passcode"; -"unlock_pin.cant_save_pin" = "Ouch! We cannot save your passcode, please contact us asap!"; -"unlock_pin.blocked_until" = "Disabled until: %@"; +"contacts.settings.alert.description" = "Bitte überprüfen Sie, dass der iCloud-Speicher auf Ihrem Gerät aktiviert ist."; +"contacts.settings.alert_error.title" = "iCloud Fehler"; // Key Types -"chart.time_duration.day" = "24H"; -"chart.time_duration.week" = "7D"; +"chart.time_duration.day" = "24Std"; +"chart.time_duration.week" = "7T"; "chart.time_duration.week2" = "2W"; "chart.time_duration.month" = "1M"; "chart.time_duration.month3" = "3M"; "chart.time_duration.halfyear" = "6M"; -"chart.time_duration.year" = "1Y"; -"chart.time_duration.year2" = "2Y"; +"chart.time_duration.year" = "1J"; +"chart.time_duration.year2" = "2J"; "chart.time_duration.all" = "ALL"; "chart.market_cap" = "Market Cap"; -"chart.volume" = "Volume (24h)"; +"chart.volume" = "Lautstärke(24 St.)"; "chart.circulation" = "In Circulation"; "chart.selected.volume" = "Vol."; "chart.market.header" = "Market"; "chart.market.market_cap" = "Market Cap"; -"chart.market.volume" = "Volume (24h)"; +"chart.market.volume" = "Lautstärke(24 St.)"; "chart.market.circulation" = "In Circulation"; "chart.market.total_supply" = "Total Supply"; "chart.performance.week_changes" = "Changes (1W)"; "chart.performance.month_changes" = "Changes (1M)"; -"chart.about.header" = "About"; "chart.about.read_more" = "Read More"; "chart.about.read_less" = "Read Less"; @@ -1277,133 +1390,133 @@ Swag your way to Settings -> %@ and let it shine!"; // Create Wallet -"create_wallet.title" = "New Wallet"; +"create_wallet.title" = "Neues Wallet"; "create_wallet.name" = "Name"; -"create_wallet.advanced_setup" = "Advanced"; -"create_wallet.create" = "Create"; -"create_wallet.advanced" = "Advanced"; -"create_wallet.phrase_count" = "Recovery Phrase"; -"create_wallet.12_words" = "12 words (recommended)"; -"create_wallet.n_words" = "%@ words"; -"create_wallet.word_list" = "Word List"; -"create_wallet.passphrase" = "Passphrase"; -"create_wallet.input.passphrase" = "Passphrase"; -"create_wallet.input.confirm" = "Confirm"; -"create_wallet.passphrase_description" = "Passphrases add an additional security layer for wallets. To restore such a wallet a user required both a recovery phrase as well as a passphrase.\n\nPassphrases also make it easy for users to have many multi-coin wallets using a single mnemonic but different password."; -"create_wallet.error.empty_passphrase" = "Passphrase cannot be empty"; -"create_wallet.error.forbidden_symbols" = "Please use only supported symbols:A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"create_wallet.error.invalid_confirmation" = "Passphrase confirmation does not match"; +"create_wallet.advanced_setup" = "Erweitert"; +"create_wallet.create" = "Erstellen"; +"create_wallet.advanced" = "Erweitert"; +"create_wallet.phrase_count" = "Wiederherstellungsphrase"; +"create_wallet.12_words" = "12 Wörter (empfohlen)"; +"create_wallet.n_words" = "%@ Wörter"; +"create_wallet.word_list" = "Wortliste"; +"create_wallet.passphrase" = "Passwort"; +"create_wallet.input.passphrase" = "Passwort"; +"create_wallet.input.confirm" = "Bestätigen"; +"create_wallet.passphrase_description" = "Passphrases fügen eine zusätzliche Sicherheitsschicht für Brieftaschen hinzu. Um eine solche Brieftasche wiederherzustellen, benötigt ein Benutzer sowohl mnemonische als auch eine Passphrase.\n\nPassphrasen erleichtern es Benutzern auch, mehrere Multicoin-Wallets mit einem einzigen mnemonischen, aber anderen Passwort zu haben."; +"create_wallet.error.empty_passphrase" = "Passphrase darf nicht leer sein"; +"create_wallet.error.forbidden_symbols" = "Bitte verwenden Sie nur unterstützte Symbole: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"create_wallet.error.invalid_confirmation" = "Passphrase-Bestätigung stimmt nicht überein"; // Restore Select -"restore_select.title" = "Choose Blockchains"; +"restore_select.title" = "Blockchains auswählen"; // Lock Info "lock_info.title" = "TimeLock"; -"lock_info.text" = "The sender sent these funds with a spending lock that will expire on the shown date. \n\nNo worries, the received Bitcoins are already yours, but until the lock period expires you cannot spend them on the Bitcoin network."; +"lock_info.text" = "Der Sender hat dieses Guthaben mit einer Ausgabesperre versehen, die zu dem angezeigten Datum abläuft.\n\nKeine Sorge, die erhaltenen Bitcoin sind bereits Ihre. Die Bitcoins sind nur zeitweise gesperrt, so dass Sie sie erst nach Ablauf der Sperrzeit im Bitcoin-Netzwerk ausgeben können."; // Double Spend Info -"double_spend_info.title" = "Double Spend"; -"double_spend_info.header" = "Double Spend Risk! There is another transaction on the blockchain that is trying to spend inputs used in this transaction. Only one transaction will be accepted by the network"; -"double_spend_info.this_hash" = "This Tx"; -"double_spend_info.conflicting_hash" = "Conflicting Tx"; +"double_spend_info.title" = "Double-Spend"; +"double_spend_info.header" = "Double-Spend-Risiko! Es gibt eine weitere Transaktion auf der Blockchain, die versucht, die in dieser Transaktion verwendeten Inputs auszugeben. Nur eine Transaktion wird vom Netzwerk akzeptiert"; +"double_spend_info.this_hash" = "Dieser Tx"; +"double_spend_info.conflicting_hash" = "Gegensätzliche Tx"; // Relative Date -"timestamp.days_ago" = "%lud ago"; -"timestamp.hours_ago" = "%luh ago"; -"timestamp.min_ago" = "%lum ago"; +"timestamp.days_ago" = "vor %luT"; +"timestamp.hours_ago" = "vor %luSt"; +"timestamp.min_ago" = "vor %luMin"; // Intro -"intro.unchain_assets.title" = "Unchain Assets"; -"intro.unchain_assets.description" = "Don't lock yourself in and don't let others do that to you"; -"intro.go_borderless.title" = "Go Borderless"; -"intro.go_borderless.description" = "Bypass conditional barriers and access markets globally"; -"intro.stay_private.title" = "Stay Private"; -"intro.stay_private.description" = "Do not leak your private and financial data to the world"; +"intro.unchain_assets.title" = "Assets lösen"; +"intro.unchain_assets.description" = "Sperren Sie sich nicht ein und lassen Sie nicht zu, dass andere das mit Ihnen tun"; +"intro.go_borderless.title" = "Jetzt loslegen"; +"intro.go_borderless.description" = "Umgehung der bedingten Schranken und globaler Marktzugang"; +"intro.stay_private.title" = "Privat bleiben"; +"intro.stay_private.description" = "Geben Sie Ihre privaten und finanziellen Daten nicht an die Welt weiter"; // Guides -"guides.tab_bar_item" = "Academy"; -"guides.title" = "Academy"; +"guides.tab_bar_item" = "Akademie"; +"guides.title" = "Akademie"; // Add Token -"add_token.title" = "Add Token"; +"add_token.title" = "Token hinzufügen"; "add_token.blockchain" = "Blockchain"; -"add_token.already_added" = "This token is already in the Coin Manager list"; -"add_token.invalid_contract_address" = "Invalid contract address"; +"add_token.already_added" = "Dieser Token befindet sich bereits in der Coin-Verwaltungsliste"; +"add_token.invalid_contract_address" = "Ungültige Vertragsadresse"; "add_token.invalid_bep2_symbol" = "Invalid BEP2 symbol"; -"add_token.contract_address_not_found" = "Contract address not found in %@ blockchain"; -"add_token.bep2_symbol_not_found" = "BEP2 symbol not found"; -"add_token.input_placeholder.contract_address" = "Contract Address"; +"add_token.contract_address_not_found" = "Vertragsadresse in %@ Blockchain nicht gefunden"; +"add_token.bep2_symbol_not_found" = "BEP2-Symbol nicht gefunden"; +"add_token.input_placeholder.contract_address" = "Vertragsadresse"; "add_token.input_placeholder.bep2_symbol" = "BEP2 Symbol"; "add_token.coin_name" = "Coin Name"; "add_token.symbol" = "Symbol"; -"add_token.decimals" = "Decimals"; +"add_token.decimals" = "Dezimalen"; // Wallet Connect "wallet_connect.title" = "WalletConnect"; -"wallet_connect.error.invalid_url" = "Invalid URL Address"; +"wallet_connect.error.invalid_url" = "Ungültige URL-Adresse"; "wallet_connect.url" = "URL"; -"wallet_connect.active_account" = "Active Wallet"; -"wallet_connect.address" = "Address"; -"wallet_connect.network" = "Network"; -"wallet_connect.list.pending_requests" = "Pending Requests"; -"wallet_connect.main.no_any_supported_chains" = "No any supported chains!"; -"wallet_connect.main.unsupported_chains" = "Some chains are unsupported!"; -"wallet_connect.connect_description" = "By clicking approve, you allow this app to view your public address. This is an important security step to protect your data from potential phishing risks."; -"wallet_connect.usage_description" = "You can go to the browser. Do not close this page while interacting in the browser."; -"wallet_connect.no_connection" = "Failed to establish a connection. Try reconnecting again."; -"wallet_connect.button_reconnect" = "Reconnect"; -"wallet_connect.button_disconnect" = "Disconnect"; -"ethereum_transaction.error.title" = "Error"; -"ethereum_transaction.error.insufficient_balance" = "The transaction requires %@ for sending."; -"ethereum_transaction.error.insufficient_balance_with_fee" = "The current %@ balance is below the amount required to process this transaction, including the transaction fee."; -"ethereum_transaction.error.lower_than_base_gas_limit" = "The selected fee value is too low and will be rejected!"; -"ethereum_transaction.error.nonce_already_in_block" = "The transaction is already in block!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Fee not enough to replace the transaction"; -"ethereum_transaction.error.transaction_underpriced" = "Fee not enough to send the transaction"; -"ethereum_transaction.error.tips_higher_than_max_fee" = "Max fee cannot be lower than the tips, because Max fee includes the tips."; -"ethereum_transaction.error.reverted" = "The transaction cannot be executed: %@"; -"wallet_connect.request_title" = "Contract Call"; -"wallet_connect.button.confirm" = "Confirm"; -"wallet_connect.sign.request_title" = "Sign Request"; -"wallet_connect.sign.domain" = "Domain"; +"wallet_connect.active_account" = "Aktive Wallet"; +"wallet_connect.address" = "Adresse"; +"wallet_connect.network" = "Netzwerk"; +"wallet_connect.list.pending_requests" = "Ausstehende Anfragen"; +"wallet_connect.main.no_any_supported_chains" = "Keine unterstützten Ketten!"; +"wallet_connect.main.unsupported_chains" = "Einige Ketten werden nicht unterstützt!"; +"wallet_connect.connect_description" = "Indem Sie auf \"Genehmigen\" klicken, erlauben Sie dieser App, Ihre öffentliche Adresse anzuzeigen. Dies ist ein wichtiger Sicherheitsschritt, um Ihre Daten vor potenziellen Phishing-Risiken zu schützen."; +"wallet_connect.usage_description" = "Sie können den Browser aufrufen. Schließen Sie diese Seite nicht, während Sie im Browser interagieren."; +"wallet_connect.no_connection" = "Verbindung konnte nicht hergestellt werden. Versuchen Sie, die Verbindung erneut herzustellen."; +"wallet_connect.button_reconnect" = "Wiederverbinden"; +"wallet_connect.button_disconnect" = "Verbindung trennen"; +"ethereum_transaction.error.title" = "Fehler"; +"ethereum_transaction.error.insufficient_balance" = "Die Transaktion erfordert %@ zum Senden."; +"ethereum_transaction.error.insufficient_balance_with_fee" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; +"ethereum_transaction.error.lower_than_base_gas_limit" = "Der gewählte Gebührenwert ist zu niedrig und wird abgelehnt!"; +"ethereum_transaction.error.nonce_already_in_block" = "Transaktion bereits im Block!"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Gebühr reicht nicht aus, um die Transaktion zu ersetzen"; +"ethereum_transaction.error.transaction_underpriced" = "Gebühr nicht ausreichend, um die Transaktion zu ersetzen"; +"ethereum_transaction.error.tips_higher_than_max_fee" = "Maximale Gebühr kann nicht niedriger sein als die Tipps, da die Max-Gebühr die Tipps enthält."; +"ethereum_transaction.error.reverted" = "Die Transaktion kann nicht ausgeführt werden: %@"; +"wallet_connect.request_title" = "Vertragsaufruf"; +"wallet_connect.button.confirm" = "Bestätigen"; +"wallet_connect.sign.request_title" = "Signaturanfrage"; +"wallet_connect.sign.domain" = "Domäne"; "wallet_connect.sign.dapp_name" = "dApp"; -"wallet_connect.sign.message" = "Message to sign"; +"wallet_connect.sign.message" = "Zu unterzeichnende Nachricht"; "wallet_connect_list.title" = "WalletConnect"; -"wallet_connect.list.empty_view_text" = "No active sessions"; +"wallet_connect.list.empty_view_text" = "Keine aktiven Sitzungen"; "wallet_connect.list.pairings" = "Pairings"; "wallet_connect.list.version_text" = "Version %@"; -"wallet_connect.list.v1_bottom_text" = "In the first version of WalletConnect, you must go into sessions to see and confirm the request"; -"wallet_connect_list.new_connection" = "New Connection"; -"wallet_connect_list.disconnecting" = "Disconnecting"; +"wallet_connect.list.v1_bottom_text" = "In der ersten Version von WalletConnect müssen Sie die Sitzungen aufrufen, um die Anfrage zu sehen und zu bestätigen"; +"wallet_connect_list.new_connection" = "Neue Verbindung"; +"wallet_connect_list.disconnecting" = "Verbindung wird getrennt"; -"wallet_connect.no_account.description" = "You need to create or import a wallet before you can use WalletConnect."; -"wallet_connect.unbackuped_account.description" = "You need to backup %@ before you can use WalletConnect"; +"wallet_connect.no_account.description" = "Sie müssen eine Wallet erstellen oder importieren bevor Sie WalletConnect verwenden können."; +"wallet_connect.unbackuped_account.description" = "Sie müssen %@ sichern bevor Sie WalletConnect verwenden können"; -"wallet_connect.non_supported_account.description" = "Your current wallet type %@ does not support WalletConnect"; -"wallet_connect.non_supported_account.switch" = "Switch"; +"wallet_connect.non_supported_account.description" = "Ihr aktueller Wallet-Typ %@ unterstützt WalletConnect nicht"; +"wallet_connect.non_supported_account.switch" = "Wechseln"; -"wallet_connect.pending_requests_title" = "Pending Requests"; +"wallet_connect.pending_requests_title" = "Ausstehende Anfragen"; "wallet_connect.paired_dapps.title" = "Paired dApps"; -"wallet_connect.paired_dapps.cant_disconnect" = "Can't Disconnect"; -"wallet_connect.paired_dapps.disconnect_all" = "Delete All"; -"wallet_connect.pending_requests.nonactive_footer" = "To open an request you must activate the desired wallet"; +"wallet_connect.paired_dapps.cant_disconnect" = "Kann nicht trennen"; +"wallet_connect.paired_dapps.disconnect_all" = "Alle löschen"; +"wallet_connect.pending_requests.nonactive_footer" = "Um eine Anfrage zu öffnen, müssen Sie die gewünschte Wallet aktivieren"; // App Status -"app_status.title" = "App Status"; -"app_status.application_status" = "Application status"; -"app_status.linked_wallets" = "Linked Wallets"; -"app_status.version_history" = "Version History"; -"app_status.blockchain_status" = "Blockchain status"; +"app_status.title" = "App-Status"; +"app_status.application_status" = "Antragsstatus"; +"app_status.linked_wallets" = "Verknüpfte Wallets"; +"app_status.version_history" = "Versionsverlauf"; +"app_status.blockchain_status" = "Blockchain-Status"; // FAQ @@ -1412,321 +1525,322 @@ Swag your way to Settings -> %@ and let it shine!"; // Status Info "status_info.title" = "Status"; -"status_info.pending.title" = "Pending"; -"status_info.pending.content" = "The transaction has not been confirmed on the blockchain yet. Transactions sent with a recommended or higher fee setting are generally processed within a few minutes. Transactions sent with a low fee may remain pending for a few hours or even days, and can even be rejected. Note that status of an individual transaction in %@ wallet interface typically updated with a short delay."; -"status_info.processing.title" = "Processing"; -"status_info.processing.content" = "The transaction has been already included in the blockchain but has not reached permanent finality. At this point, it's safe to consider the transaction as completed for smaller payments. For larger payments, it's recommended to wait until the transaction status changes to completed."; -"status_info.completed.title" = "Completed"; -"status_info.confirmed.content" = "Transaction is completed and considered permanent and irreversible."; -"status_info.failed.title" = "Failed"; -"status_info.failed.content" = "The transaction did not get processed and no value transfer took place. Depending on the fail reason, some failed transactions may consume transaction fees. Transactions that were replaced or canceled by another transaction do not consume transaction fees and will also appear as failed. The %@ app is unable to show the reason for failed transactions, but users are able to look it up themselves on a public block explorer i.e. etherscan.io."; +"status_info.pending.title" = "In Bearbeitung"; +"status_info.pending.content" = "Die Transaktion wurde noch nicht in der Blockchain bestätigt. Transaktionen, die mit einer empfohlenen oder höheren Gebühreneinstellung gesendet werden, werden in der Regel innerhalb weniger Minuten abgewickelt. Transaktionen, die mit einer geringen Gebühr gesendet werden, können für einige Stunden oder sogar Tage ausstehend bleiben und sogar abgelehnt werden. Beachten Sie, dass der Status einer individuellen Transaktion in %@ Wallet-Schnittstelle typischerweise mit einer kurzen Verzögerung aktualisiert wird."; +"status_info.processing.title" = "Verarbeite"; +"status_info.processing.content" = "Die Transaktion wurde bereits in die Blockchain aufgenommen, ist aber noch nicht endgültig abgeschlossen. Zu diesem Zeitpunkt kann die Transaktion für kleinere Zahlungen als abgeschlossen betrachtet werden. Bei größeren Zahlungen wird empfohlen, zu warten, bis der Transaktionsstatus auf abgeschlossen wechselt."; +"status_info.completed.title" = "Abgeschlossen"; +"status_info.confirmed.content" = "Die Transaktion ist abgeschlossen und gilt als dauerhaft und unumkehrbar."; +"status_info.failed.title" = "Fehlgeschlagen"; +"status_info.failed.content" = "Die Transaktion wurde nicht verarbeitet und es wurde kein Werttransfer durchgeführt. Abhängig vom Grund des Scheiterns können einige fehlgeschlagene Transaktionen Transaktionsgebühren kosten. Transaktionen, die durch eine andere Transaktion ersetzt oder abgebrochen wurden, verbrauchen keine Transaktionsgebühren und erscheinen auch als fehlgeschlagen. Die %@ App kann den Grund für fehlgeschlagene Transaktionen nicht anzeigen. aber Benutzer können es selbst auf einem öffentlichen Block-Explorer i. e. etherscan.io."; // Onboarding -"onboarding.balance.create" = "New Wallet"; -"onboarding.balance.import" = "Import Wallet"; -"onboarding.balance.watch" = "Watch Wallet"; +"onboarding.balance.create" = "Neues Wallet"; +"onboarding.balance.import" = "Wallet importieren"; +"onboarding.balance.watch" = "Wallet ansehen"; // Manage Accounts -"manage_accounts.n_words" = "%@ words"; -"manage_accounts.n_words_with_passphrase" = "%@ words with passphrase"; +"manage_accounts.n_words" = "%@ Wörter"; +"manage_accounts.n_words_with_passphrase" = "%@ Wörter mit Passphrase"; // Manage Account "manage_account.name" = "Name"; -"manage_account.recovery_phrase" = "Recovery Phrase"; -"manage_account.public_keys" = "Public Keys"; -"manage_account.private_keys" = "Private Keys"; -"manage_account.backup_recovery_phrase" = "Manual Backup"; -"manage_account.cloud_backup_recovery_phrase" = "Backup to iCloud"; -"manage_account.cloud_delete_backup_recovery_phrase" = "Delete Backup from iCloud"; -"manage_account.manual_backup_required" = "Manual Backup Required"; -"manage_account.manual_backup_required.description" = "In order to securely delete your backup on iCloud, you will need to manually backup your recovery phrase first."; -"manage_account.manual_backup_required.button" = "Backup Now"; -"manage_account.unlink" = "Unlink Wallet"; -"manage_account.backup.no_backup_yet_description" = "Complete one of the wallet backup options to start using the wallet."; -"manage_account.backup.has_backup_description" = "It's recommended to have a manual backup for each wallet."; - -"manage_account.cloud_delete_backup_recovery_phrase.description" = "Are you sure you want to delete your wallet backup from iCloud?"; +"manage_account.recovery_phrase" = "Wiederherstellungsphrase"; +"manage_account.public_keys" = "Öffentliche Schlüssel"; +"manage_account.private_keys" = "Private Schlüssel"; +"manage_account.backup_recovery_phrase" = "Manuelle Sicherung"; +"manage_account.cloud_backup_recovery_phrase" = "Sicherung in iCloud"; +"manage_account.cloud_delete_backup_recovery_phrase" = "Sicherung aus iCloud löschen"; +"manage_account.manual_backup_required" = "Manuelle Sicherung erforderlich"; +"manage_account.manual_backup_required.description" = "Um Ihre Sicherheitskopie auf iCloud sicher zu löschen, müssen Sie zuerst Ihre Wiederherstellungsphrase manuell sichern."; +"manage_account.manual_backup_required.button" = "Jetzt Sichern"; +"manage_account.unlink" = "Wallet entfernen"; +"manage_account.backup.no_backup_yet_description" = "Füllen Sie eine der Wallet-Sicherungsoptionen aus, um mit der Wallet zu beginnen."; +"manage_account.backup.has_backup_description" = "Es wird empfohlen, eine manuelle Sicherung für jede Wallet zu haben."; + +"manage_account.cloud_delete_backup_recovery_phrase.description" = "Sind Sie sicher, dass Sie Ihre Wallet-Sicherung aus iCloud löschen möchten?"; // Manage Account -> Public Keys -"public_keys.title" = "Public Keys"; -"public_keys.evm_address" = "EVM Address"; -"public_keys.evm_address.description" = "Allows read-only monitoring of wallets holding assets on Ethereum, Binance Smart Chain and other EVM based blockchains."; +"public_keys.title" = "Öffentliche Schlüssel"; +"public_keys.evm_address" = "EVM-Adresse"; +"public_keys.evm_address.description" = "Ermöglicht die bloße Überwachung von Brieftaschen mit Assets auf Ethereum, Binance Smart Chain und anderen EVM-basierten Blockchains."; "public_keys.account_extended_public_key" = "Account Extended Public Key"; -"public_keys.account_extended_public_key.description" = "Allows read-only monitoring of wallets holding Bitcoin and other UTXO based crypto (i.e. Litecoin, Bitcoin Cash, Dash, etc.)."; +"public_keys.account_extended_public_key.description" = "Ermöglicht die schreibgeschützte Überwachung von Brieftaschen mit Bitcoin und anderen auf UTXO basierenden Verschlüsselungen (z.B. Litecoin, Bitcoin Cash, Dash, etc.)."; // Manage Account -> Private Keys -"private_keys.title" = "Private Keys"; -"private_keys.evm_private_key" = "EVM Private Key"; -"private_keys.evm_private_key.description" = "Grants full control over EVM based crypto i.e. Ethereum, Binance Smart Chain etc within respective wallet."; +"private_keys.title" = "Private Schlüssel"; +"private_keys.evm_private_key" = "EVM privater Schlüssel"; +"private_keys.evm_private_key.description" = "Gewährt volle Kontrolle über EVM-basierte Kryptos, d.h. Ethereum, Binance Smart Chain etc innerhalb der jeweiligen Wallet."; "private_keys.bip32_root_key" = "BIP32 Root Key"; -"private_keys.bip32_root_key.description" = "Grants full control over the assets on the respective wallet."; +"private_keys.bip32_root_key.description" = "Gewährt die volle Kontrolle über die Vermögenswerte auf der jeweiligen Brieftasche."; "private_keys.account_extended_private_key" = "Account Extended Private Key"; -"private_keys.account_extended_private_key.description" = "Grants full control over Bitcoin and other UTXO based crypto i.e. Litecoin, Bitcoin Cash, Dash, etc. within respective wallet."; +"private_keys.account_extended_private_key.description" = "Gewährt volle Kontrolle über Bitcoin und andere auf UTXO basierende Kryptos, z.B. Litecoin, Bitcoin Cash, Dash, etc. innerhalb der jeweiligen Wallet."; // Manage Account -> EVM Address -"evm_address.title" = "EVM Address"; +"evm_address.title" = "EVM-Adresse"; // Birthday Height -"birthday_height.title" = "Birthday Height"; +"birthday_height.title" = "Geburtstagshöhe"; // Birthday Input -"birthday_input.title" = "Birthday Height"; -"birthday_input.description" = "Enter wallet's birthday height for faster synchronization."; -"birthday_input.new_wallet" = "New Wallet"; -"birthday_input.new_wallet.description" = "Doesn't have any transactions"; -"birthday_input.old_wallet" = "Existing Wallet"; -"birthday_input.old_wallet.description" = "Has transactions"; +"birthday_input.title" = "Geburtstagshöhe"; +"birthday_input.description" = "Geben Sie die Geburtstagshöhe der Brieftasche für eine schnellere Synchronisierung ein."; +"birthday_input.new_wallet" = "Neues Wallet"; +"birthday_input.new_wallet.description" = "Keine Transaktionen vorhanden"; +"birthday_input.old_wallet" = "Vorhandene Wallet"; +"birthday_input.old_wallet.description" = "Hat Transaktionen"; "birthday_input.input_placeholder" = "%@ (optional)"; -"restore_setting.birthday_height" = "%@ Birthday Height"; -"restore_setting.download.disclaimer" = "The initial synchronization with the blockchain can consume a lot of internet traffic."; +"restore_setting.birthday_height" = "%@ Geburtstagshöhe"; +"restore_setting.download.disclaimer" = "Die erste Synchronisation mit der Blockchain kann viel Internet-Verkehr verbrauchen."; // EVM Network -"evm_network.rpc_source" = "RPC Source"; -"evm_network.added" = "Added"; -"evm_network.add_new" = "Add New"; +"evm_network.rpc_source" = "RPC-Quelle"; +"evm_network.added" = "Hinzugefügt"; +"evm_network.add_new" = "Neu hinzufügen"; // Add RPC Source -"add_evm_sync_source.title" = "Add RPC Source"; +"add_evm_sync_source.title" = "RPC-Quelle hinzufügen"; "add_evm_sync_source.name" = "Name"; "add_evm_sync_source.rpc_url" = "RPC URL"; -"add_evm_sync_source.basic_auth" = "Basic Auth (optional)"; -"add_evm_sync_source.warning.url_exists" = "RPC Source with this url already exists"; -"add_evm_sync_source.error.invalid_url" = "Entered url is invalid. Valid url must have one of the following schemes: https, wss"; +"add_evm_sync_source.basic_auth" = "Einfacher Auth (optional)"; +"add_evm_sync_source.warning.url_exists" = "RPC-Quelle mit dieser URL existiert bereits"; +"add_evm_sync_source.error.invalid_url" = "Die eingegebene URL ist ungültig. Gültige URL muss eines der folgenden Schemata haben: http, https, ws, wss"; // Send Settings -"evm_send_settings.nonce" = "Transaction Nonce"; -"evm_send_settings.nonce.info" = "The nonce is a unique integer value for a transaction within the user's wallet. It normally increments with each submitted transaction and does not need changing. Advanced users can set it equal to a nonce of a pending transaction in order to cancel and replace that transaction, as long as the new transaction has a sufficiently higher fee to prevent the old one from being confirmed instead (for example, they may want to speed up its confirmation, or to change transaction parameters entirely). When multiple pending transactions have the same nonce, only one gets confirmed, typically the one with the highest fee."; -"evm_send_settings.nonce.errors.already_in_use" = "Used Nonce"; -"evm_send_settings.nonce.errors.already_in_use.info" = "An executed transaction with this nonce already exists."; - -"fee_settings" = "Advanced"; -"fee_settings.fee" = "Fee"; -"fee_settings.fee.info" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network."; -"fee_settings.fee_rate" = "Fee Rate"; -"fee_settings.fee_rate.description" = "Here is the recommended value for hitting the next 2 blocks"; -"fee_settings.inputs_outputs" = "Inputs/Outputs"; -"fee_settings.transaction_settings" = "Transaction Settings"; -"fee_settings.transaction_settings.description" = "Make your Bitcoin transactions harder to trace by changing the way transactions are structured."; -"fee_settings.time_lock" = "Time Lock"; -"fee_settings.time_lock.description" = "TimeLock works only for sending to BIP44 addresses (starting with 1)"; - -"fee_settings.network_fee" = "Network Fee"; -"fee_settings.network_fee.info" = "The estimated cost of sending given transaction on the network."; -"fee_settings.gas_limit" = "Gas Limit"; -"fee_settings.gas_limit.info" = "Transaction complexity is measured in units called \"gas\". It varies depending on the smart contract being executed. The Gas Limit is the estimated maximum gas needed to execute it. The actual gas used will normally be lower."; +"evm_send_settings.nonce" = "Transaktion Nonce"; +"evm_send_settings.nonce.info" = "Das nonce ist ein eindeutiger Integer-Wert für eine Transaktion innerhalb der Wallet des Benutzers. Sie erhöht sich normalerweise mit jeder abgeschlossenen Transaktion und muss sich nicht ändern. Fortgeschrittene Benutzer können es einem Nonce einer ausstehenden Transaktion gleichen, um diese Transaktion zu stornieren und zu ersetzen solange die neue Transaktion eine ausreichend höhere Gebühr hat, um zu verhindern, dass die alte statt dessen bestätigt wird (zum Beispiel Sie möchten möglicherweise die Bestätigung beschleunigen oder die Transaktionsparameter vollständig ändern). Wenn mehrere ausstehende Transaktionen die gleiche Nonce haben, wird nur eine bestätigt, typischerweise die mit der höchsten Gebühr."; +"evm_send_settings.nonce.errors.already_in_use" = "Nonce verwendet"; +"evm_send_settings.nonce.errors.already_in_use.info" = "Eine ausgeführte Transaktion mit diesem nonce existiert bereits."; + +"fee_settings" = "Erweitert"; +"fee_settings.fee" = "Gebühr"; +"fee_settings.fee.info" = "Blockchains verlangen, dass Nutzer Netzwerkgebühren beim Versenden von Transaktionen bezahlen. Die Gebühren sind höher, wenn viele Transaktionen im Netzwerk stattfinden."; +"fee_settings.fee_rate" = "Gebührenrate"; +"fee_settings.fee_rate.description" = "Hier ist der empfohlene Wert, um die nächsten 2 Blöcke zu treffen"; +"fee_settings.inputs_outputs" = "Ein-/Ausgänge"; +"fee_settings.transaction_settings" = "Transaktionseinstellungen"; +"fee_settings.transaction_settings.description" = "Machen Sie Ihre Bitcoin-Transaktionen schwieriger nachvollziehbar, indem Sie die Transaktionsstruktur verändern."; +"fee_settings.time_lock" = "TimeLock"; +"fee_settings.time_lock.description" = "TimeLock funktioniert nur beim Senden an BIP44 Adressen (ab 1)"; + +"fee_settings.network_fee" = "Netzwerkgebühr"; +"fee_settings.network_fee.info" = "Die geschätzten Kosten für den Versand der Transaktion im Netzwerk."; +"fee_settings.gas_limit" = "Gaslimit"; +"fee_settings.gas_limit.info" = "Die Transaktionskomplexität wird in Einheiten mit dem Namen \"Gas\" gemessen, die je nach ausgeführten intelligenten Verträgen variieren. Das Gaslimit ist das geschätzte Höchstgas, das benötigt wird, um es auszuführen. Das tatsächliche Gas wird normalerweise niedriger sein."; "fee_settings.gas_price" = "Gas Price"; -"fee_settings.gas_price.info" = "The fee for transacting on the network is measured in gas units. Gas Price is the amount a user is willing to spend per unit of gas. When the network is busy, gas prices are high, and low when it's idle. An insufficient gas price is often a reason for a transaction to remain pending for an extended period."; - -"fee_settings.base_fee" = "Base Fee"; -"fee_settings.base_fee.info" = "The network protocol determines the base price per gas for each block, called base fee rate. It varies according to the network utilization level from block to block. It can increase or decrease by no more than 12.5% in the next block, making fees more predictable. The value shown here is the current block's base fee rate."; -"fee_settings.max_fee_rate" = "Max Fee Rate"; -"fee_settings.max_fee_rate.info" = "This is the maximum total price per gas the user is willing to pay. It must cover the network's base fee rate and max priority fee rate. The value shown here is suggested based on an estimate of the next block's base fee rate plus the max priority fee rate chosen by the user. The actual fee rate paid will normally be lower. Setting this lower than the current base fee rate will limit the fee paid, but will result in longer waiting times for the transaction to be confirmed, or even in a stuck transaction."; -"fee_settings.tips" = "Max Priority Fee"; -"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users."; - -"fee_settings.errors.insufficient_balance" = "Insufficient balance"; -"fee_settings.errors.unexpected_error" = "Unexpected Error"; -"fee_settings.errors.insufficient_balance.info" = "The current %@ balance is below the amount required to process this transaction, including the transaction fee."; -"fee_settings.errors.low_max_fee" = "Low Fee"; -"fee_settings.errors.low_max_fee.info" = "The set transaction fee amount is insufficient for processing this transaction now."; -"fee_settings.errors.nonce_already_in_block" = "Can't replace transaction."; -"fee_settings.errors.replacement_transaction_underpriced" = "Low Fee for replacement transaction"; -"fee_settings.errors.transaction_underpriced" = "Low Fee for transaction"; -"fee_settings.errors.tips_higher_than_max_fee" = "Max Fee is too low"; -"fee_settings.errors.zero_amount.info" = "Cannot transfer 0 TRX"; -"fee_settings.warning.risk_of_getting_stuck" = "Risky"; -"fee_settings.warning.risk_of_getting_stuck.info" = "The transaction may remain pending for a while or fail entirely."; -"fee_settings.warning.overpricing" = "Fee Too High"; -"fee_settings.warning.overpricing.info" = "The set transaction fee is higher than necessary for processing this transaction now."; +"fee_settings.gas_price.info" = "Die Gebühr für Transaktionen im Netzwerk wird in Gaseinheiten gemessen. Der Gaspreis ist der Betrag, den ein Nutzer für eine Einheit Gas auszugeben bereit ist. Wenn das Netzwerk ausgelastet ist, sind die Gaspreise hoch, und niedrig, wenn es nicht genutzt wird. Ein unzureichender Gaspreis ist oft ein Grund dafür, dass eine Transaktion über einen längeren Zeitraum aussteht."; + +"fee_settings.base_fee" = "Basisgebühr"; +"fee_settings.base_fee.info" = "Das Netzwerkprotokoll bestimmt den Basispreis pro Gas für jeden Block, sogenannte Basisgebühren. Sie variiert je nach Netzwerkauslastung von Block zu Block. Sie kann im nächsten Block um nicht mehr als 12,5% steigen oder sinken, was die Gebühren berechenbarer macht. Der hier angezeigte Wert ist die Basisgebühr des aktuellen Blocks."; +"fee_settings.max_fee_rate" = "Max Gebührenrate"; +"fee_settings.max_fee_rate.info" = "Dies ist der maximale Gesamtpreis pro Gas, den der Nutzer zu zahlen bereit ist. Es muss den Basistarif des Netzwerks und den maximalen Prioritätstarif abdecken. Der hier angezeigte Wert wird anhand einer Schätzung der Basisgebühr des nächsten Blocks plus der vom Nutzer gewählten Maximalgebührengebühr vorgeschlagen. Der tatsächlich gezahlte Gebührensatz ist normalerweise niedriger. Wenn diese Einstellung niedriger als die aktuelle Basisgebühr ist, wird die gezahlte Gebühr begrenzt aber führt zu längeren Wartezeiten für die Bestätigung der Transaktion oder sogar zu einer festgefahrenen Transaktion."; +"fee_settings.tips" = "Max Prioritätsgebühr"; +"fee_settings.tips.info" = "Benutzer zahlen Prioritätsgebühren, um eine Transaktion schneller zu bestätigen. Sie werden manchmal als Tipps bezeichnet. Die maximale Prioritätsgebühr ist der maximale Zusatzpreis pro Gas, den der Nutzer zusätzlich zur Basisgebühr zahlen möchte. Der hier angezeigte Wert wird basierend auf vorausgesagten Netzwerkbedingungen vorgeschlagen. Die tatsächliche Prioritätsgebühr wird normalerweise niedriger sein. Die Einstellung auf Null kann zu einer langen Wartezeit führen, bis die Transaktion bestätigt wird, , da er am Ende der Warteschlange aller Benutzer platziert ist."; + +"fee_settings.errors.insufficient_balance" = "Unzureichendes Guthaben"; +"fee_settings.errors.unexpected_error" = "Unerwarteter Fehler"; +"fee_settings.errors.insufficient_balance.info" = "Das aktuelle %@-Guthaben liegt unter dem für die Abwicklung dieser Transaktion erforderlichen Betrag einschließlich der Transaktionsgebühr."; +"fee_settings.errors.low_max_fee" = "Niedrige Gebühr"; +"fee_settings.errors.low_max_fee.info" = "Der eingestellte Betrag der Transaktionsgebühr reicht nicht aus, um diese Transaktion jetzt zu bearbeiten."; +"fee_settings.errors.nonce_already_in_block" = "Transaktion kann nicht ersetzt werden."; +"fee_settings.errors.replacement_transaction_underpriced" = "Niedrige Gebühr für Ersatztransaktionen"; +"fee_settings.errors.transaction_underpriced" = "Niedrige Transaktionsgebühr"; +"fee_settings.errors.tips_higher_than_max_fee" = "Max Gebühr ist zu niedrig"; +"fee_settings.errors.zero_amount.info" = "Kann 0 TRX nicht übertragen"; +"fee_settings.warning.risk_of_getting_stuck" = "Riskant"; +"fee_settings.warning.risk_of_getting_stuck.info" = "Die Transaktion kann eine Zeit lang ausstehend bleiben oder ganz fehlschlagen."; +"fee_settings.warning.overpricing" = "Gebühr zu hoch"; +"fee_settings.warning.overpricing.info" = "Die festgelegte Transaktionsgebühr ist höher als für die Bearbeitung dieser Transaktion jetzt erforderlich."; // Watch Address -"watch_address.title" = "Watch Wallet"; -"watch_address.watch" = "Next"; -"watch_address.address" = "Address"; -"watch_address.by" = "By"; -"watch_address.watch_by" = "Watch By"; -"watch_address.evm_address" = "EVM Address"; -"watch_address.tron_address" = "TRON Address"; +"watch_address.title" = "Wallet ansehen"; +"watch_address.watch" = "Weiter"; +"watch_address.address" = "Adresse"; +"watch_address.by" = "Von"; +"watch_address.watch_by" = "Beobachten von"; +"watch_address.evm_address" = "EVM-Adresse"; +"watch_address.tron_address" = "TRON Adresse"; "watch_address.public_key" = "Account xPubKey"; -"watch_address.public_key.placeholder" = "Enter Account Extended Public Key "; -"watch_address.public_key.invalid_key" = "Invalid Key"; -"watch_address.choose_blockchain" = "Choose Blockchain"; -"watch_address.choose_coin" = "Choose Coin"; +"watch_address.public_key.placeholder" = "Account Extended Public Key eingeben"; +"watch_address.public_key.invalid_key" = "Ungültiger Schlüssel"; +"watch_address.choose_blockchain" = "Blockchain auswählen"; +"watch_address.choose_coin" = "Coins auswählen"; // Nft Collections "nft_collections.title" = "NFTs"; -"nft_collections.price_mode" = "Price Mode"; -"nft_collections.last_sale" = "Last Sale"; -"nft_collections.average_7d" = "Average 7D"; -"nft_collections.average_30d" = "Average 30D"; -"nft_collections.on_sale" = "On Sale"; -"nft_collections.empty" = "You don’t have any NFTs in your wallet"; +"nft_collections.price_mode" = "Preismodus"; +"nft_collections.last_sale" = "Letzter Verkauf"; +"nft_collections.average_7d" = "Durchschnitt 7D"; +"nft_collections.average_30d" = "Durchschnitt 30D"; +"nft_collections.on_sale" = "Im Verkauf"; +"nft_collections.empty" = "Sie haben keinen NFT in Ihrer Brieftasche"; "top_nft_collections.title" = "Top NFT Collections"; -"top_nft_collections.description" = "Leading NFT collections by trading volume."; +"top_nft_collections.description" = "Führen von NFT-Sammlungen durch Handelsvolumen."; // Nft Asset "nft_asset.tab.overview" = "Overview"; -"nft_asset.tab.activity" = "Activity"; - -"nft_asset.last_sale" = "Last Sale"; -"nft_asset.average_7d" = "7 Day Average"; -"nft_asset.average_30d" = "30 Day Average"; -"nft_asset.floor_price" = "Floor Price"; -"nft_asset.on_sale" = "On Sale"; -"nft_asset.on_auction" = "On Auction"; -"nft_asset.until_date" = "until %@"; -"nft_asset.buy_now" = "Buy Now"; -"nft_asset.minimum_bid" = "Minimum Bid"; -"nft_asset.best_offer" = "Best Offer"; -"nft_asset.properties" = "Properties"; -"nft_asset.description" = "Description"; -"nft_asset.details" = "Details"; -"nft_asset.details.contract_address" = "Contract Address"; +"nft_asset.tab.activity" = "Aktivität"; + +"nft_asset.last_sale" = "Letzter Verkauf"; +"nft_asset.average_7d" = "7-Tages-Durchschnitt"; +"nft_asset.average_30d" = "30-Tages-Durchschnitt"; +"nft_asset.floor_price" = "Mindestpreis"; +"nft_asset.on_sale" = "Im Verkauf"; +"nft_asset.on_auction" = "Auf Auktion"; +"nft_asset.until_date" = "bis %@"; +"nft_asset.buy_now" = "Jetzt kaufen"; +"nft_asset.minimum_bid" = "Mindestgebot"; +"nft_asset.best_offer" = "Bestes Angebot"; +"nft_asset.properties" = "Eigenschaften"; +"nft_asset.description" = "Beschreibung"; +"nft_asset.details" = "Info"; +"nft_asset.details.contract_address" = "Vertragsadresse"; "nft_asset.details.token_id" = "Token ID"; -"nft_asset.details.token_standard" = "Token Standard"; +"nft_asset.details.token_standard" = "Token-Standard"; "nft_asset.details.blockchain" = "Blockchain"; "nft_asset.links" = "Links"; "nft_asset.links.website" = "Website"; -"nft_asset.options.save_to_photos" = "Save to Photos"; -"nft_asset.options.set_as_watch_face" = "Set as Watch Face"; -"nft_asset.save_to_photos.failed" = "Failed to save NFT image"; +"nft_asset.options.save_to_photos" = "In Fotos speichern"; +"nft_asset.options.set_as_watch_face" = "Als Watch-Face festlegen"; +"nft_asset.save_to_photos.failed" = "NFT-Bild konnte nicht gespeichert werden"; // Nft Collection "nft_collection.tab.overview" = "Overview"; -"nft_collection.tab.assets" = "Items"; -"nft_collection.tab.activity" = "Activity"; +"nft_collection.tab.assets" = "Artikel"; +"nft_collection.tab.activity" = "Aktivität"; -"nft_collection.overview.description" = "Description"; -"nft_collection.overview.contracts" = "Contracts"; +"nft_collection.overview.description" = "Beschreibung"; +"nft_collection.overview.contracts" = "Verträge"; "nft_collection.overview.links" = "Links"; "nft_collection.overview.links.website" = "Website"; -"nft_collection.overview.owners" = "Owners"; -"nft_collection.overview.items" = "Items"; +"nft_collection.overview.owners" = "Besitzer"; +"nft_collection.overview.items" = "Artikel"; "nft_collection.overview.24h_volume" = "24h Volume"; -"nft_collection.overview.today_sellers" = "Today's Sellers"; -"nft_collection.overview.floor_price" = "Floor Price"; -"nft_collection.overview.all_time_average" = "All time average"; +"nft_collection.overview.today_sellers" = "Heutige Verkäufer"; +"nft_collection.overview.floor_price" = "Mindestpreis"; +"nft_collection.overview.all_time_average" = "Ganzzeitiger Durchschnitt"; "nft_collection.overview.royalty" = "Royalty"; "nft_collection.overview.inception_date" = "Inception Date"; -"nft.activity.contracts" = "Contracts"; -"nft.activity.empty_list" = "No item activity yet"; -"nft.activity.event_types" = "Event Types"; -"nft.activity.event_type.all" = "All Events"; +"nft.activity.contracts" = "Verträge"; +"nft.activity.empty_list" = "Noch keine Artikel-Aktivität"; +"nft.activity.event_types" = "Ereignistypen"; +"nft.activity.event_type.all" = "Alle Ereignisse"; "nft.activity.event_type.sale" = "Sale"; "nft.activity.event_type.transfer" = "Transfer"; "nft.activity.event_type.mint" = "Mint"; -"nft.activity.event_type.list" = "List"; -"nft.activity.event_type.listCancel" = "List Cancel"; -"nft.activity.event_type.offer" = "Offer"; -"nft.activity.event_type.offerCancel" = "Offer Cancel"; +"nft.activity.event_type.list" = "Liste"; +"nft.activity.event_type.listCancel" = "Liste abbrechen"; +"nft.activity.event_type.offer" = "Angebot"; +"nft.activity.event_type.offerCancel" = "Angebot abbrechen"; // Subscription Info -"subscription_info.title" = "Premium Features"; +"subscription_info.title" = "Premium-Funktionen"; "subscription_info.info1.title" = "Cryptocurrency Analytics"; "subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.info2.title" = "Chart Indicators"; "subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.info3.title" = "Personal Support"; +"subscription_info.info3.title" = "Persönlicher Support"; "subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.get_premium" = "Get Premium"; -"subscription_info.already_have" = "I already have Premium"; +"subscription_info.get_premium" = "Premium erhalten"; +"subscription_info.already_have" = "Ich habe bereits Premium"; // Activate Subscription -"activate_subscription.title" = "Activate"; +"activate_subscription.title" = "Aktivieren"; "activate_subscription.wallet" = "Wallet"; -"activate_subscription.address" = "Address"; +"activate_subscription.address" = "Adresse"; "activate_subscription.message" = "Message to Sign"; -"activate_subscription.sign" = "Sign"; +"activate_subscription.sign" = "Signieren"; "activate_subscription.activating" = "Activating..."; "activate_subscription.failed_to_activate" = "Failed to activate subscription"; -"activate_subscription.activated" = "Activated"; -"activate_subscription.no_subscriptions" = "Your wallet address does not have a subscription to premium features, you need to purchase it to activate the subscription."; +"activate_subscription.activated" = "Aktiviert"; +"activate_subscription.no_subscriptions" = "Ihre Wallet-Adresse hat kein Abonnement für Premium-Funktionen, Sie müssen sie kaufen, um das Abonnement zu aktivieren."; // Launch -"launch.failed_to_launch" = "Failed to launch application due to internal error. Please try restarting the app or report the error to our support team."; -"launch.failed_to_launch.report" = "Report"; +"launch.failed_to_launch" = "Anwendung konnte aufgrund eines internen Fehlers nicht gestartet werden. Bitte versuchen Sie die App neu zu starten oder melden Sie den Fehler an unser Support-Team."; +"launch.failed_to_launch.report" = "Melden"; + // Tron -"tron.send.activation_fee" = "Activation Fee"; -"tron.send.resources_consumed" = "Resources Consumed"; +"tron.send.activation_fee" = "Aktivierungsgebühr"; +"tron.send.resources_consumed" = "Verbrauchte Ressourcen"; "tron.send.bandwidth" = "Bandwidth"; "tron.send.energy" = "Energy"; -"tron.send.fee.info" = "The estimated cost of sending given transaction on the network. (Without excluding Energy, Bandwidth and Activating Fee)"; -"tron.send.resources_consumed.info" = "Bandwidth is the unit that measures the size of the transaction bytes stored in the blockchain database. The larger the transaction, the more bandwidth resources will be consumed.\n\nEnergy is the unit that measures the amount of computation required by the TRON virtual machine to perform specific operations on the TRON network.\n\nSince smart contract transactions require computing resources to execute, each smart contract transaction requires to pay for the energy fee."; -"tron.send.activation_fee.info" = "Transferring TRX or TRC-10 tokens to an inactive account address will activate the account."; -"tron.send.inactive_address" = "This address is not active"; +"tron.send.fee.info" = "Die geschätzten Kosten für den Versand einer Transaktion im Netzwerk (ohne Energie-, Bandbreiten- und Aktivierungsgebühren)"; +"tron.send.resources_consumed.info" = "Bandwidth ist die Einheit, die die Größe der in der Blockchain-Datenbank gespeicherten Transaktionsbytes misst. Je größer die Transaktion, desto mehr Ressourcen werden verbraucht.\n\nEnergie ist die Einheit, die die Anzahl der Berechnungen misst, die von der virtuellen TRON-Maschine benötigt werden, um bestimmte Operationen im TRON-Netzwerk durchzuführen.\n\nDa intelligente Vertragsabschlüsse die Verwendung von Rechnerressourcen erfordern, muss für jeden Smart Contract eine Energiegebühr bezahlt werden."; +"tron.send.activation_fee.info" = "Die Übertragung von TRX- oder TRC-10-Token an eine inaktive Konto-Adresse wird das Konto aktivieren."; +"tron.send.inactive_address" = "Diese Adresse ist nicht aktiv"; // Cex Coin Select -"cex_coin_select.title" = "Choose Coin"; -"cex_coin_select.withdraw" = "Withdraw"; -"cex_coin_select.withdraw.empty" = "You have no assets to withdraw."; -"cex_coin_select.search_placeholder" = "Coin Code or Name"; -"cex_coin_select.suspended" = "Suspended"; +"cex_coin_select.title" = "Coins auswählen"; +"cex_coin_select.withdraw" = "Abheben"; +"cex_coin_select.withdraw.empty" = "Sie haben keine Assets zum Ausziehen."; +"cex_coin_select.search_placeholder" = "Münzcode oder Name"; +"cex_coin_select.suspended" = "Gesperrt"; // Cex Deposit Network Select -"cex_deposit_network_select.title" = "Choose Network"; -"cex_deposit_network_select.description" = "Choose a network and get an address to deposit."; +"cex_deposit_network_select.title" = "Netzwerk auswählen"; +"cex_deposit_network_select.description" = "Wählen Sie ein Netzwerk aus und erhalten Sie eine Adresse zum Einzahlen."; // Cex Deposit -"cex_deposit.title" = "Deposit %@"; -"cex_deposit.address" = "Address"; +"cex_deposit.title" = "Auftragen %@"; +"cex_deposit.address" = "Adresse"; -"cex_deposit.network" = "Network"; +"cex_deposit.network" = "Netzwerk"; "cex_deposit.memo" = "Memo (Tag)"; "cex_deposit.min_amount" = "Min. Amount"; -"cex_deposit.warning_memo" = "Memo (tag) is required, or your will lose your coins."; -"cex_deposit.copy_address" = "Copy"; -"cex_deposit.share_address" = "Share"; -"cex_deposit.failed" = "Failed to load deposit address. Please retry."; -"cex_deposit.memo_warning.title" = "Important"; -"cex_deposit.memo_warning.description" = "Both a memo (tag) and the address are needed to ensure that assets are received. Otherwise your funds will be lost."; +"cex_deposit.warning_memo" = "Memo (Tag) ist erforderlich, sonst verlieren Sie Ihre Münzen."; +"cex_deposit.copy_address" = "Kopieren"; +"cex_deposit.share_address" = "Teilen"; +"cex_deposit.failed" = "Fehler beim Laden der Einzahlungsadresse. Bitte erneut versuchen."; +"cex_deposit.memo_warning.title" = "Wichtig"; +"cex_deposit.memo_warning.description" = "Sowohl eine Memo (Tag) als auch die Adresse werden benötigt, um sicherzustellen, dass Vermögenswerte empfangen werden. Andernfalls gehen Ihre Gelder verloren."; // Cex Widthdraw -"cex_withdraw.network" = "Network"; -"cex_withdraw.network_warning" = "Ensure the network matches the withdrawal address and the deposit platform supports it, or assets may be lost."; -"cex_withdraw.fee" = "Fee"; -"cex_withdraw.fee_from_amount" = "Fee from amount"; -"cex_withdraw.error.insufficient_funds" = "Not enough available balance"; -"cex_withdraw.error.max_amount_violated" = "The maximum amount you can withdraw is %@"; -"cex_withdraw.error.min_amount_violated" = "The minimum amount you can withdraw is %@"; -"cex_withdraw.address_required" = "Address is required"; +"cex_withdraw.network" = "Netzwerk"; +"cex_withdraw.network_warning" = "Stellen Sie sicher, dass das Netzwerk mit der Auszahlungsadresse übereinstimmt und die Einzahlungsplattform sie unterstützt, oder es können Vermögenswerte verloren gehen."; +"cex_withdraw.fee" = "Gebühr"; +"cex_withdraw.fee_from_amount" = "Gebühr aus Betrag"; +"cex_withdraw.error.insufficient_funds" = "Nicht genügend verfügbares Guthaben"; +"cex_withdraw.error.max_amount_violated" = "Der maximale Betrag, den du abheben kannst, ist %@"; +"cex_withdraw.error.min_amount_violated" = "Der Mindestbetrag, den du abheben kannst, ist %@"; +"cex_withdraw.address_required" = "Adresse ist erforderlich"; // Cex Withdraw Network Select -"cex_withdraw_network_select.title" = "Network"; -"cex_withdraw_network_select.description" = "Ensure the network matches the withdrawal address and the deposit platform supports it."; +"cex_withdraw_network_select.title" = "Netzwerk"; +"cex_withdraw_network_select.description" = "Stellen Sie sicher, dass das Netzwerk mit der Auszahlungsadresse übereinstimmt und die Einzahlungsplattform unterstützt sie."; // Cex Withdraw Confirm -"cex_withdraw_confirm.you_withdraw" = "You Withdraw"; -"cex_withdraw_confirm.network" = "Network"; -"cex_withdraw_confirm.withdraw" = "Withdraw"; -"cex_withdraw_confirm.withdraw_failed" = "Withdraw failed"; \ No newline at end of file +"cex_withdraw_confirm.you_withdraw" = "Sie zurückziehen"; +"cex_withdraw_confirm.network" = "Netzwerk"; +"cex_withdraw_confirm.withdraw" = "Abheben"; +"cex_withdraw_confirm.withdraw_failed" = "Fehler bei Auszahlung"; diff --git a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings index 83c7875b6c..1f42381c44 100644 --- a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Pegar"; "button.resend" = "Reenviar"; "button.backup" = "Copia de seguridad"; +"button.restore" = "Restaurar"; "button.copy" = "Copiar"; "button.retry" = "Reintentar"; "button.report" = "Reportar"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "Cantidad Incorrecta"; "alert.no_fee" = "Tarifa Incorrecta"; "alert.warning" = "Aviso"; +"alert.notice" = "Aviso"; "alert.error" = "Erreur"; "alert.unknown_error" = "Error Desconocido"; "alert.success_action" = "Listo"; +"alert.restored" = "Restaurado"; "alert.success" = "Completado"; "alert.added_to_watchlist" = "Añadido a tu Lista de Seguimiento"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "Eliminado del monedero"; "alert.already_added_to_wallet" = "Ya se ha añadido a Wallet"; "alert.not_supported_yet" = "Aún no compatible"; -"alert.copied" = "Copiado"; "alert.created" = "Creado"; "alert.imported" = "Restaurado "; "alert.wallet_added" = "Cartera añadida"; @@ -95,6 +97,16 @@ "selector.any" = "Cualquier"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Inmediato"; +"auto_lock.minute1" = "1 minuto"; +"auto_lock.minute5" = "5 minutos"; +"auto_lock.minute15" = "15 minutos"; +"auto_lock.minute30" = "30 minutos"; +"auto_lock.hour1" = "1 hora"; + // Access Camera "access_camera.message" = "%@ necesita acceso a tu cámara para escanear el código QR. @@ -126,21 +138,24 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; // Restore Type "restore_type.title" = "Importar monedero"; - "restore_type.recovery.title" = "de Frase de Recuperación"; "restore_type.cloud.title" = "desde iCloud"; +"restore_type.file.title" = "del archivo"; "restore_type.cex.title" = "de Exchange Wallet"; "restore_type.recovery.description" = "Importe utilizando la frase de recuperación o la clave privada."; "restore_type.cloud.description" = "Importar desde un archivo de copia de seguridad en su llavero."; +"restore_type.file.description" = "Importar un archivo de copia de seguridad de su carpeta local."; "restore_type.cex.description" = "Conectar a una cartera en un intercambio centralizado."; // Restore Cloud "restore.cloud.title" = "Seleccionar copia de seguridad"; -"restore.cloud.description" = "Seleccione la copia de seguridad de la billetera que desea restaurar."; +"restore.cloud.description" = "Seleccione el archivo de copia de seguridad que desea restaurar."; "restore.cloud.empty" = "No se encontraron copias de seguridad."; +"restore.cloud.wallets" = "Copia de seguridad de la cartera"; "restore.cloud.imported" = "Carteras importadas"; +"restore.cloud.app_backups" = "Copias de seguridad de aplicaciones"; "restore.cloud.password.title" = "Introduzca contraseña"; "restore.cloud.password.placeholder" = "Copiar contraseña"; @@ -226,13 +241,10 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "backup_verify_passphrase.description" = "Introduzca su frase de contraseña"; "backup_verify_passphrase.incorrect_passphrase" = "Contraseña incorrecta"; -// Backup Required - -"backup_required.title" = "Se requiere una copia de seg"; - // Backup Prompt -"backup_prompt.title" = "Copia de seguridad manual"; +"backup_prompt.backup_recovery_phrase" = "Backup del Monedero"; +"backup_prompt.backup_required" = "Se requiere una copia de seg"; "backup_prompt.warning" = "Crear una copia de seguridad de la frase de recuperación y la contraseña asociada que le permitirá recuperar su cartera si su teléfono está perdido, robado, roto, etc."; "backup_prompt.backup" = "Copia de seguridad"; "backup_prompt.backup_manual" = "Copia de seguridad manual"; @@ -243,7 +255,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "backup.cloud.title" = "Copia de seguridad en iCloud"; "backup.cloud.description" = "El almacenamiento en iCloud es un servicio de almacenamiento en la nube de terceros proporcionado por Apple. Es importante tener en cuenta que sus datos se almacenarán en los servidores de Apple, no en sus dispositivos personales. Por lo tanto, está confiando sus datos y entregando la seguridad de su información a un servicio de terceros."; - "backup.cloud.terms.item.1" = "Entiendo que perder el acceso a mi iCloud, resultará en perder el acceso a la copia de seguridad de una cartera respectiva."; "backup.cloud.name.title" = "Nombre de copia de seguridad"; @@ -270,10 +281,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "backup.cloud.cant_create_file" = "No se puede guardar el archivo en iCloud"; "backup.cloud.cant_delete_file" = "No se puede eliminar de iCloud"; "backup.cloud.no_access.title" = "Acceder a iCloud"; -"backup.cloud.no_access.title" = "Acceder a iCloud"; "backup.cloud.no_access.description" = "Para crear una copia de seguridad, necesita proporcionar acceso al almacenamiento de iCloud."; - // Errors "error.send.self_transfer" = "Enviarte a vos mismo no está soportado"; @@ -294,10 +303,12 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "balance.rate_per_coin" = "%@ por %@"; "balance.syncing" = "Sincronizando..."; "balance.searching" = "Buscando transacciones…"; +"balance.stopped" = "Detenido"; "balance.downloading_sapling" = "Descargando Sapling... %d%%"; "balance.downloading_blocks" = "Descargando bloques"; "balance.scanning_blocks" = "Bloques de escaneo"; "balance.enhancing_transactions" = "Mejorar las transacciones"; +"wait_for_synchronization" = "Esperando sincronización"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Sincronizando... %@"; @@ -322,6 +333,9 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "balance.token.locked" = "Bloqueado"; "balance.token.locked.info.title" = "Timelock"; "balance.token.locked.info.description" = "El remitente envió estos fondos con un bloqueo del gasto que expirará en la fecha mostrada.\n\nNo te preocupes, los Bitcoins recibidos ya son tuyos, pero hasta que el período de bloqueo expire, no puedes gastarlos en la red Bitcoin."; +"balance.token.processing" = "Procesando"; +"balance.token.processing.info.title" = "Detenido"; +"balance.token.processing.info.description" = "Transacciones con esta cantidad todavía sincronizadas. Y cuando se confirmen, estos tokens estarán disponibles para gastos"; "balance.token.staked" = "Llevado"; "balance.token.staked.info.title" = "Título tomado"; "balance.token.staked.info.description" = "Texto de descripción tomada"; @@ -402,13 +416,13 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "send.hodler_locktime_off" = "Apagado"; "send.hodler_error.unsupported_address" = "El bloqueo del tiempo solo funciona con direcciones P2PKH (a partir de 1)"; "send.fee_info.title" = "Tasa de comisión"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.fee_info.description" = "Las blockchains requieren que los usuarios paguen tarifas de red al enviar transacciones. Estas tarifas son más altas cuando hay muchas transacciones ocurriendo en la red.\n\nLa billetera %@ estima la tarifa basada en la actividad actual de la blockchain y recomienda el valor óptimo para que la transacción sea procesada en un tiempo razonable.\n\nLa tarifa recomendada se muestra en la cantidad de satoshis que el usuario debe pagar por cada byte de la transacción. Por lo tanto, la tarifa total depende del tamaño total de la transacción, que se mide en bytes.\n\nLos usuarios pueden utilizar los controles proporcionados para aumentar o disminuir el valor de la tarifa. El cambio en la tarifa afecta el monto total de la tarifa que el usuario pagará por la transacción.\n\nEstablecer una tarifa por debajo del valor recomendado puede resultar en que la transacción quede pendiente durante horas o sea rechazada. Cuanto menor sea el valor, más tiempo llevará confirmar la transacción. Para transacciones donde la prioridad es importante, recomendamos establecer una tarifa más alta."; "send.transaction_inputs_outputs_info.title" = "Entradas / salidas de transacciones"; -"send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; +"send.transaction_inputs_outputs_info.description" = "La mayoría de las transacciones de Bitcoin, así como las transacciones en criptomonedas similares como Bitcoin Cash, Dash y Litecoin, generan dos salidas. Una salida es la cantidad que va al receptor y la otra es la salida de cambio que se devuelve al remitente. La forma en que la mayoría de las billeteras construyen las transacciones facilita que un tercero comprenda cuál de las salidas fue para el destinatario y cuál fue la cantidad de cambio devuelta al remitente. Dado que la salida devuelta al remitente se utiliza posteriormente en transacciones futuras, se hace evidente una conexión entre estas dos transacciones.\n\nLa billetera %@ implementa medidas para dificultar que alguien descubra cuál es la salida correspondiente a cada parte.\n\nExisten dos opciones disponibles para los usuarios de %@:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "The order of transaction outputs is randomized on every transaction. Sometimes change can be the first output, sometimes it can be the second. If a user trusts the developer of the app, then consider this a recommended option."; +"send.transaction_inputs_outputs_info.shuffle.description" = "El orden de salida de las transacciones se aleatoriza en cada transacción. A veces el cambio puede ser la primera salida, a veces puede ser la segunda. Si un usuario confía en el desarrollador de la aplicación, considere esta una opción recomendada."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; -"send.transaction_inputs_outputs_info.deterministic.description" = "There is a commonly agreed standard for ordering transaction outputs (known as BIP69). In open-source wallets, that standard ensures wallet users do not need to trust how developers of the app implement the ordering of the outputs. As this standard is new, not many wallets have implemented it yet. As a result, it's somewhat possible to see on the blockchain whether a transaction was sent from a wallet that uses that standard or not."; +"send.transaction_inputs_outputs_info.deterministic.description" = "Hay un estándar comúnmente acordado para ordenar salidas de transacciones (conocido como BIP69). En las carteras de código abierto, ese estándar asegura que los usuarios de cartera no necesitan confiar en cómo los desarrolladores de la aplicación implementan el orden de las salidas. Como este estándar es nuevo, no muchas billeteras lo han implementado todavía. Como resultado, es algo posible ver en el blockchain si una transacción fue enviada desde una cartera que utiliza ese estándar o no."; "send.confirmation.you_send" = "Usted envía"; "send.confirmation.to" = "Para"; @@ -542,7 +556,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "swap.confirmation.maximum_sent" = "Gasto máximo"; "swap.dex_info.description" = "Este servicio de intercambio está impulsado por %@, un intercambio de token descentralizado construido en %@ blockchain.\n\n%@ está completamente automatizado y administrado por contratos inteligentes que facilitan los intercambios de token de una manera conflaible y sin ninguna intención para hacer estafas."; - "swap.dex_info.header_dex_related" = "%@ Relacionado"; "swap.dex_info.header_allowance" = "Permiso de acceso"; "swap.dex_info.content_allowance" = "La cantidad que un intercambio puede gastar en nombre del usuario al ejecutar swaps de token. Se requiere un permiso suficiente para establecer una transacción anterior antes de que pueda tener lugar una transacción de intercambio real."; @@ -726,8 +739,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_overview.roi.day200" = "6 Months"; "coin_overview.roi.year1" = "1 Año"; -"coin_overview.category" = "Categoría"; - +"coin_overview.overview" = "Información General"; +"coin_overview.description_warning" = "Esta es una descripción generada por IA basada en el material de referencia proporcionado para la criptomoneda dada. Puede contener errores."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Tipo de moneda"; @@ -800,7 +813,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.active_addresses_rank.description" = "Tokens clasificados por el número de direcciones únicas que transaccionan con el token."; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; -"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; +"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over a 30-day period."; "coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; "coin_analytics.active_addresses.info5" = "List of all tokens ranked based on the number of daily active addresses transacting with the token over 24h / 7D / 1M intervals."; @@ -1001,6 +1014,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "settings.tab_bar_item" = "Ajustes"; "settings.manage_accounts" = "Gestionar monederos"; "settings.blockchain_settings" = "Configuración de Blockchain"; +"settings.backup_manager" = "Gestor de copias de seguridad"; "settings.security" = "Seguridad"; "settings.experimental_features" = "Experimental"; "settings.personal_support" = "Soporte personal"; @@ -1011,6 +1025,9 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "settings.info_subtitle" = "app descentralizada"; "settings.donate.description" = "¡Junto con tu apoyo, podemos hacer esta aplicación aún mejor!"; "settings.donate.title" = "Donar"; +"settings.rate_us" = "Califícanos"; +"settings.tell_friends" = "Recomendar a un amigo"; +"settings.contact_us" = "Contáctenos"; // Settings -> Base Currency @@ -1073,14 +1090,126 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "blockchain_settings.title" = "Configuración de Blockchain"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Gestor de copias de seguridad"; +"backup_app.backup_manager.restore" = "Restaurar copia de seguridad"; +"backup_app.backup_manager.create" = "Crear nueva copia de seguridad"; + +"backup_app.backup_type.title" = "Guardar copia de seguridad"; +"backup_app.backup_type.cloud" = "A iCloud"; +"backup_app.backup_type.cloud.description" = "Guardando un archivo de copia de seguridad en el llavero."; +"backup_app.backup_type.file" = "a archivos"; +"backup_app.backup_type.file.description" = "Guardando un archivo de copia de seguridad en su carpeta local."; + +"backup_app.backup_list.title" = "Copia de seguridad"; +"backup_app.backup_list.description.restore" = "Lista de contenidos en el archivo de copia de seguridad."; +"backup_app.backup_list.header.wallets" = "Monederos"; +"backup_app.backup_list.header.other" = "Otro"; +"backup_app.backup_list.other.watch_account.title" = "Ver Monedero"; +"backup_app.backup_list.other.watchlist.title" = "Lista de Seguimiento"; +"backup_app.backup_list.other.contacts.title" = "Contactos"; +"backup_app.backup_list.other.blockchain_settings.title" = "RPC personalizado"; +"backup_app.backup_list.other.app_settings.title" = "Ajustes de app"; +"backup_app.backup_list.other.app_settings.description" = "Idioma, moneda, Apariencia ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Copia de seguridad en iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud es un servicio de almacenamiento en la nube proporcionado por Apple. Es importante saber que los datos de la copia de seguridad se almacenarán en los servidores de Apple."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "Entiendo que perder el acceso a mi iCloud, resultará en perder el acceso a la copia de seguridad de una cartera respectiva."; +"backup_app.backup.disclaimer.file.title" = "Copia de seguridad en archivo"; +"backup_app.backup.disclaimer.file.description" = "Los dispositivos de almacenamiento, como los discos duros, los discos USB y el almacenamiento en smartphones, son todos vulnerables a pérdidas debido a daños físicos, robos u otras circunstancias imprevisibles."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Entiendo que el robo o daño de un dispositivo de copia de seguridad resultará en la pérdida de una copia de seguridad a una cartera respectiva."; + +"backup.disclaimer.cloud.title" = "Copia de seguridad en iCloud"; +"backup.disclaimer.cloud.description" = "iCloud es un servicio de almacenamiento en la nube proporcionado por Apple. Es importante saber que los datos de la copia de seguridad se almacenarán en los servidores de Apple."; +"backup.disclaimer.cloud.checkbox_label" = "Entiendo que perder el acceso a mi iCloud, resultará en perder el acceso a la copia de seguridad de una cartera respectiva."; +"backup.disclaimer.file.title" = "Copia de seguridad en archivo"; +"backup.disclaimer.file.description" = "Los dispositivos de almacenamiento, como los discos duros, los discos USB y el almacenamiento en smartphones, son todos vulnerables a pérdidas debido a daños físicos, robos u otras circunstancias imprevisibles."; +"backup.disclaimer.file.checkbox_label" = "Entiendo que el robo o daño de un dispositivo de copia de seguridad resultará en la pérdida de una copia de seguridad a una cartera respectiva."; +"backup_app.backup.name.title" = "Nombre de copia de seguridad"; +"backup_app.backup.name.description" = "Introduzca el nombre del archivo de copia de seguridad."; + +"backup_app.backup.password.title" = "Copiar contraseña"; +"backup_app.backup.password.description" = "Establezca la contraseña de desbloqueo para su copia de seguridad. La contraseña debe contener al menos 8 caracteres, incluyendo una letra minúscula, una letra mayúscula, un número y un carácter especial."; +"backup_app.backup.password.highlighted_description" = "Esta contraseña se utiliza para cifrar el archivo de copia de seguridad de su cartera. No se puede recuperar o reiniciar si se pierde u olvida."; + +"backup_app.restore_type.title" = "Restaurar"; + +"backup_app.restore.notice.description" = "Esta acción sobrescribirá sus contactos de pago locales, así como su copia de iCloud (si hay una)"; +"backup_app.restore.notice.merge" = "Reemplazar"; + +"backup.password.title" = "Copiar contraseña"; +"backup.password.description" = "Establezca la contraseña de desbloqueo para su copia de seguridad. La contraseña debe contener al menos 8 caracteres, incluyendo una letra minúscula, una letra mayúscula, un número y un carácter especial."; +"backup.password.highlighted_description" = "Esta contraseña se utiliza para cifrar el archivo de copia de seguridad de su cartera. No se puede recuperar o reiniciar si se pierde u olvida."; + // Settings -> Security "settings_security.title" = "Seguridad"; -"settings_security.passcode" = "Código de acceso"; -"settings_security.change_pin" = "Editar el código "; -"settings_security.touch_id" = "Identificación táctil"; -"settings_security.face_id" = "Identificación de caras"; -"settings_security.blockchain_settings" = "Configuración de Blockchain"; +"settings_security.enable_passcode" = "Activar contraseña"; +"settings_security.edit_passcode" = "Editar el código"; +"settings_security.disable_passcode" = "Desactivar bloqueo con código"; +"settings_security.auto_lock" = "Bloqueo automático"; +"settings_security.balance_auto_hide" = "Saldo Auto Ocultar"; +"settings_security.balance_auto_hide.description" = "Oculta automáticamente el balance cada vez que se abre la aplicación, independientemente de las preferencias anteriores."; +"settings_security.enable_duress_mode" = "Establecer modo de Duración"; +"settings_security.edit_duress_passcode" = "Editar contraseña de Duración"; +"settings_security.disable_duress_mode" = "Desactivar modo Duración"; +"settings_security.duress_mode.description" = "Un modo especializado diseñado para mantener las billeteras seleccionadas a salvo bajo coacción."; + +// Create Passcode + +"create_passcode.title" = "Crear contraseña"; +"create_passcode.description" = "Su código servirá para abrir su monedero y enviar dinero"; +"create_passcode.description.biometry" = "Establecer un código de acceso para habilitar %@"; +"create_passcode.description.duress_mode" = "Establecer un código de acceso para activar el modo Duración"; +"create_passcode.confirm_passcode" = "Confirmar"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Modo Duración"; +"enable_duress_mode.intro.description" = "Este modo permite a los usuarios configurar múltiples códigos de acceso de la aplicación de desbloqueo donde un código de acceso deseado sólo muestra las carteras especificadas. Diseñado para mantener seguros las carteras seleccionadas bajo coerción o amenazas."; +"enable_duress_mode.intro.notes" = "Notas"; +"enable_duress_mode.intro.biometrics.description" = "La función %@ funcionará para desbloquear el Modo Duración. Puede desactivar %@ para mayor comodidad."; +"enable_duress_mode.intro.passcode_disabling" = "Desactivando contraseña"; +"enable_duress_mode.intro.passcode_disabling.description" = "Deshabilitar el código de acceso en el modo principal restablecerá automáticamente el modo Duración."; +"enable_duress_mode.intro.passcode_change" = "Cambiar contraseña"; +"enable_duress_mode.intro.passcode_change.description" = "Cambiar el código de acceso en el modo Duress también cambiará el código de acceso actual para ese modo."; + +"enable_duress_mode.select.title" = "Seleccionar carteras"; +"enable_duress_mode.select.description" = "Seleccione las carteras que se mostrarán en Modo Duración."; +"enable_duress_mode.select.wallets" = "Monederos"; +"enable_duress_mode.select.watch_wallets" = "Ver Monedero"; + +"enable_duress_mode.passcode.title" = "Durar contraseña"; +"enable_duress_mode.passcode.description" = "Establecer un código de acceso para el modo Duress"; +"enable_duress_mode.passcode.confirm" = "Confirmar"; + +// Edit Passcode + +"edit_passcode.title" = "Editar el código"; +"edit_passcode.enter_new_passcode" = "Introducir nueva contraseña"; +"edit_passcode.confirm_new_passcode" = "Confirmar"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Editar contraseña de Duración"; +"edit_duress_passcode.enter_new_passcode" = "Introduzca una nueva contraseña para el modo Duress"; +"edit_duress_passcode.confirm_new_passcode" = "Confirmar"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Confirmación no válida"; +"set_passcode.already_used" = "Este código de acceso ya está siendo usado"; + +// Unlock + +"unlock.title" = "Desbloquear"; +"unlock.passcode" = "Introducir código"; +"unlock.biometry_reason" = "Desbloquear cartera"; +"unlock.attempts_left" = "Intentos restantes: %@"; +"unlock.disabled_until" = "Inhabilitado hasta: %@"; +"unlock.random" = "Aleatorio"; + "security_settings.delete_alert_button" = "Eliminar del teléfono"; "btc_blockchain_settings.restore_source" = "Restaurar Fuente"; @@ -1124,9 +1253,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "settings.about_app.description" = "La billetera %@ está diseñada para aquellos que buscan invertir y almacenar criptomonedas de manera privada e independiente.\n\nEs una billetera no custodial y peer-to-peer donde solo el usuario tiene control sobre los fondos. No recopila datos y mantiene al usuario independiente al no bloquear los fondos del usuario en una aplicación de billetera específica.\n\nLa billetera %@ es completamente de código abierto y cualquier persona puede confirmar que la aplicación funciona exactamente como dice."; "settings.about_app.whats_new" = "Qué novedades hay"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Contáctenos"; -"settings.about_app.rate_us" = "Califícanos"; -"settings.about_app.tell_friends" = "Recomendar a un amigo"; // Settings -> About App -> Contact @@ -1168,8 +1294,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "appearance.balance_value.coin_value" = "Valor de la moneda"; "appearance.balance_value.fiat_value" = "Valor Fiat"; -"appearance.balance_auto_hide" = "Saldo Auto Ocultar"; - // Settings -> Contacts "contacts.title" = "Contactos"; @@ -1229,25 +1353,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "contacts.settings.alert_error.title" = "Error de iCloud"; -// Set PIN - -"set_pin.title" = "Código de acceso"; -"set_pin.info" = "Su código servirá para abrir su monedero y enviar dinero"; -"set_pin.wrong_confirmation" = "El código de acceso no coincide. Inténtelo de nuevo"; - -// Edit PIN - -"edit_pin.title" = "Editar el código "; -"edit_pin.unlock_info" = "Código actual"; -"edit_pin.new_pin_info" = "Nuevo código de acceso"; - -// Unlock PIN - -"unlock_pin.info" = "Código de acceso"; -"unlock_pin.cant_save_pin" = "¡Ups! ¡No podemos guardar su código, póngase en contacto con nosotros lo antes posible!"; -"unlock_pin.blocked_until" = "Inhabilitado hasta: %@"; - - // Key Types "chart.time_duration.day" = "24H"; @@ -1274,7 +1379,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "chart.performance.week_changes" = "Cambios (1 Semana)"; "chart.performance.month_changes" = "Cambios (1 Mes)"; -"chart.about.header" = "Acerca de"; "chart.about.read_more" = "Leer más"; "chart.about.read_less" = "Leer menos"; @@ -1357,8 +1461,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "wallet_connect.active_account" = "Active su cartera"; "wallet_connect.address" = "Dirección"; "wallet_connect.network" = "La red"; -"wallet_connect.address" = "Dirección"; -"wallet_connect.network" = "La red"; "wallet_connect.list.pending_requests" = "Solicitudes Pendientes"; "wallet_connect.main.no_any_supported_chains" = "¡No hay cadenas compatibles!"; "wallet_connect.main.unsupported_chains" = "¡Algunas cadenas no son soportadas!"; diff --git a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings index 1220bec656..fb82851113 100644 --- a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Coller"; "button.resend" = "Réenvoyer"; "button.backup" = "Sauvegarde"; +"button.restore" = "Restaurer"; "button.copy" = "Copier"; "button.retry" = "Réessayer"; "button.report" = "Signaler"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "Montant incorrect"; "alert.no_fee" = "Commission incorrecte"; "alert.warning" = "Attention"; +"alert.notice" = "Remarque"; "alert.error" = "Erreur"; "alert.unknown_error" = "Erreur inconnue"; "alert.success_action" = "Fait"; +"alert.restored" = "Restauré"; "alert.success" = "Effectué"; "alert.added_to_watchlist" = "Ajouter à la liste de suivi"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "Supprimé du portefeuille"; "alert.already_added_to_wallet" = "Déjà ajouté au portefeuille"; "alert.not_supported_yet" = "Pas pris en charge pour le moment"; -"alert.copied" = "Copié"; "alert.created" = "Établi"; "alert.imported" = "Importé"; "alert.wallet_added" = "Portefeuille ajouté"; @@ -95,6 +97,16 @@ "selector.any" = "N'importe lequel"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Immédiat"; +"auto_lock.minute1" = "1 minute"; +"auto_lock.minute5" = "5 minutes"; +"auto_lock.minute15" = "15 minutes"; +"auto_lock.minute30" = "30 minutes"; +"auto_lock.hour1" = "1 heure"; + // Access Camera "access_camera.message" = "%@ a besoin d'accéder à votre caméra pour scanner le code QR. @@ -126,21 +138,24 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; // Restore Type "restore_type.title" = "Importer le porte-monnaie"; - "restore_type.recovery.title" = "Voir la phase de récupération"; "restore_type.cloud.title" = "depuis iCloud"; +"restore_type.file.title" = "à partir des fichiers"; "restore_type.cex.title" = "depuis Exchange Wallet"; "restore_type.recovery.description" = "Importer à l'aide de la phrase de récupération ou de la clé privée."; "restore_type.cloud.description" = "Importer un fichier de sauvegarde dans votre trousseau de clés."; +"restore_type.file.description" = "Importez un fichier de sauvegarde depuis votre dossier local."; "restore_type.cex.description" = "Connectez-vous à un portefeuille en échange centralisé."; // Restore Cloud "restore.cloud.title" = "Choisir la sauvegarde"; -"restore.cloud.description" = "Sélectionnez la copie de sauvegarde du portefeuille que vous souhaitez restaurer."; +"restore.cloud.description" = "Sélectionnez le fichier de sauvegarde que vous souhaitez restaurer."; "restore.cloud.empty" = "Aucune sauvegarde trouvée."; +"restore.cloud.wallets" = "Sauvegardes du portefeuille"; "restore.cloud.imported" = "Importer le portefeuille"; +"restore.cloud.app_backups" = "Sauvegardes de l'application"; "restore.cloud.password.title" = "Entrez le mot de passe"; "restore.cloud.password.placeholder" = "Mot de passe de la sauvegarde"; @@ -154,7 +169,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; // Restore Binance "restore.binance.description" = "Veuillez fournir les clés API et API Secret pour lier votre échange."; -"restore.binance.api_key" = "API Key"; +"restore.binance.api_key" = "Clé API"; "restore.binance.secret_key" = "Clé secrète"; "restore.binance.connect" = "Connecter"; "restore.binance.connecting" = "Connexion en cours..."; @@ -226,13 +241,10 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "backup_verify_passphrase.description" = "Entrez le mot de passe"; "backup_verify_passphrase.incorrect_passphrase" = "Phrase de passe incorrecte"; -// Backup Required - -"backup_required.title" = "Sauvegarde requise"; - // Backup Prompt -"backup_prompt.title" = "Sauvegarde manuelle"; +"backup_prompt.backup_recovery_phrase" = "Sauvegarder le portefeuille"; +"backup_prompt.backup_required" = "Sauvegarde requise"; "backup_prompt.warning" = "Créez une copie de sauvegarde de la phrase de récupération et du mot de passe qui vous permettra de récupérer votre portefeuille si votre téléphone est perdu, volé, cassé, etc."; "backup_prompt.backup" = "Sauvegarde"; "backup_prompt.backup_manual" = "Sauvegarde manuelle"; @@ -243,7 +255,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "backup.cloud.title" = "Sauvegarder sur iCloud"; "backup.cloud.description" = "Le stockage iCloud est un service tiers de stockage cloud fourni par Apple. Il est important de savoir que vos données seront stockées sur les serveurs d'Apple, pas sur vos appareils personnels. Cela signifie que vous confiez vos données et que vous transmettez la sécurité de vos informations à un service tiers."; - "backup.cloud.terms.item.1" = "Je comprends que la perte d'accès à mon iCloud entraînera la perte d'accès à la sauvegarde du portefeuille correspondant."; "backup.cloud.name.title" = "Nom de la sauvegarde"; @@ -270,10 +281,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "backup.cloud.cant_create_file" = "Impossible de sauvegarder le fichier sur iCloud"; "backup.cloud.cant_delete_file" = "Impossible de supprimer de iCloud"; "backup.cloud.no_access.title" = "Accéder à iCloud"; -"backup.cloud.no_access.title" = "Accéder à iCloud"; "backup.cloud.no_access.description" = "Pour créer une sauvegarde, vous devez fournir un accès au stockage iCloud."; - // Errors "error.send.self_transfer" = "L'envoi à soi-même n'est pas pris en charge"; @@ -294,10 +303,12 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "balance.rate_per_coin" = "%@ par %@"; "balance.syncing" = "Synchronisation..."; "balance.searching" = "Recherche des transactions…..."; +"balance.stopped" = "Arrêté"; "balance.downloading_sapling" = "Téléchargement de la pousse... %d%%"; "balance.downloading_blocks" = "Téléchargement des blocs"; "balance.scanning_blocks" = "Blocs de numérisation"; "balance.enhancing_transactions" = "Amélioration des transactions"; +"wait_for_synchronization" = "Attendez la synchronisation"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Synchronisation... %@"; @@ -322,6 +333,9 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "balance.token.locked" = "Verrouillé"; "balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "L'expéditeur a envoyé ces fonds avec un délai prédéfini qui se terminera à la date indiquée.\n\nPas de soucis, les Bitcoins reçues sont déjà à vous mais vous devez attendre l'expiration de la période de verrouillage pour les dépenser sur le réseau Bitcoin."; +"balance.token.processing" = "Traitement en cours"; +"balance.token.processing.info.title" = "Montant en cours de traitement"; +"balance.token.processing.info.description" = "Les transactions avec ce montant sont toujours en cours de synchronisation. Une fois qu'elles seront confirmées, ces jetons seront disponibles pour être dépensés"; "balance.token.staked" = "Stakée"; "balance.token.staked.info.title" = "Titre de la consommation"; "balance.token.staked.info.description" = "Texte de description de prise"; @@ -402,13 +416,13 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "send.hodler_locktime_off" = "Désactivé"; "send.hodler_error.unsupported_address" = "Time lock ne fonctionne qu'avec les adresses P2PKH (commençant par 1)"; "send.fee_info.title" = "Taux de frais"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.fee_info.description" = "Les blockchains exigent que les utilisateurs paient des frais de réseau lorsqu'ils envoient des transactions. Les frais sont plus élevés lorsque de nombreuses transactions ont lieu sur le réseau.\n\nLe portefeuille %@ estime les frais en fonction de l'activité actuelle de la blockchain et recommande la valeur optimale pour que la transaction soit traitée dans un délai raisonnable.\n\nLe taux de frais recommandé est affiché en tant que montant de satoshis que l'utilisateur doit payer pour un octet unique de la transaction. Ainsi, les frais totaux dépendent de la taille totale de la transaction, mesurée en octets.\n\nLes utilisateurs peuvent utiliser les commandes fournies pour augmenter ou diminuer la valeur du taux de frais. Le changement du taux de frais modifie les frais totaux que l'utilisateur paiera pour la transaction.\n\nLe fait de définir un taux de frais inférieur à la valeur recommandée peut entraîner le blocage d'une transaction pendant des heures ou son rejet. Plus la valeur est basse, plus la confirmation de la transaction prendra de temps. Pour les transactions où la priorité est importante, nous recommandons de définir un taux de frais plus élevé."; "send.transaction_inputs_outputs_info.title" = "Entrées / Sorties de transaction"; -"send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; +"send.transaction_inputs_outputs_info.description" = "La plupart des transactions Bitcoin, ainsi que des transactions dans des cryptomonnaies similaires telles que Bitcoin Cash, Dash et Litecoin, génèrent deux sorties. Une sortie est le montant qui va au destinataire et l'autre est la sortie de change qui est renvoyée à l'expéditeur. La manière dont la plupart des portefeuilles construisent les transactions facilite la compréhension par un tiers de la sortie allant au destinataire et de celle qui est renvoyée à l'expéditeur. Comme la sortie renvoyée à l'expéditeur est utilisée ultérieurement dans des transactions futures, une connexion entre ces deux transactions devient apparente.\n\nLe portefeuille %@ met en œuvre des mesures pour rendre plus difficile à quelqu'un de déterminer quelle sortie va où.\n\nIl existe deux options disponibles pour les utilisateurs de %@ :"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; -"send.transaction_inputs_outputs_info.shuffle.description" = "The order of transaction outputs is randomized on every transaction. Sometimes change can be the first output, sometimes it can be the second. If a user trusts the developer of the app, then consider this a recommended option."; +"send.transaction_inputs_outputs_info.shuffle.description" = "L'ordre des sorties de transaction est aléatoire à chaque transaction. Parfois, le changement peut être la première sortie, parfois la deuxième. Si un utilisateur fait confiance au développeur de l'application, alors considérez cela comme une option recommandée."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Déterministe"; -"send.transaction_inputs_outputs_info.deterministic.description" = "There is a commonly agreed standard for ordering transaction outputs (known as BIP69). In open-source wallets, that standard ensures wallet users do not need to trust how developers of the app implement the ordering of the outputs. As this standard is new, not many wallets have implemented it yet. As a result, it's somewhat possible to see on the blockchain whether a transaction was sent from a wallet that uses that standard or not."; +"send.transaction_inputs_outputs_info.deterministic.description" = "Il existe une norme communément acceptée pour l'ordonnancement des sorties de transaction, connue sous le nom de BIP69 (Bitcoin Improvement Proposal 69). Dans les portefeuilles open source, cette norme garantit que les utilisateurs de portefeuille n'ont pas besoin de faire confiance à la manière dont les développeurs de l'application implémentent l'ordonnancement des sorties. Comme cette norme est relativement récente, peu de portefeuilles l'ont encore mise en œuvre. Par conséquent, il est quelque peu possible de déterminer sur la blockchain si une transaction a été envoyée depuis un portefeuille qui utilise cette norme ou non."; "send.confirmation.you_send" = "Vous envoyez"; "send.confirmation.to" = "À"; @@ -542,7 +556,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "swap.confirmation.maximum_sent" = "Dépense maximale"; "swap.dex_info.description" = "Ce service d'échange est assuré par %@, un protocole d'échange de tokens décentralisé construit sur la blockchain %@.\n\n%@ est entièrement automatisé et géré par des contrats intelligents qui facilitent les échanges de tokens de manière fiable sans aucune possibilité de tricher."; - "swap.dex_info.header_dex_related" = "Relatif à %@"; "swap.dex_info.header_allowance" = "Allocation"; "swap.dex_info.content_allowance" = "Le montant qu\'un échange peut dépenser pour le compte de l\'utilisateur lors de l\'exécution de swaps de jetons. Une provision suffisante est requise avant de pouvoir effectuer une opération d\'échange réelle."; @@ -726,8 +739,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_overview.roi.day200" = "6 Mois"; "coin_overview.roi.year1" = "1 An"; -"coin_overview.category" = "Catégorie"; - +"coin_overview.overview" = "Synthèse"; +"coin_overview.description_warning" = "Ceci est une description générée par intelligence artificielle basée sur le matériel de référence fourni pour la cryptomonnaie donnée. Elle peut contenir des erreurs."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Types de pièces"; @@ -768,29 +781,29 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.cex_volume" = "Volume CEX"; "coin_analytics.cex_volume_rank" = "Rang de volume CEX"; "coin_analytics.cex_volume_rank.description" = "Les tokens classés en fonction du volume de trading du token sur les exchanges centralisés."; -"coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; -"coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; +"coin_analytics.cex_volume.info1" = "Volume total des échanges pour le jeton sur les principales plateformes d'échange centralisées au cours d'une période de 30 jours."; +"coin_analytics.cex_volume.info2" = "Graphique montrant la variation du volume quotidien de trading du jeton sur les principales plateformes d'échange centralisées sur une période d'un an."; "coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; -"coin_analytics.cex_volume.info4" = "List of all tokens ranked based on trading volume on centralized exchanges over 24H / 7D / 1M intervals."; +"coin_analytics.cex_volume.info4" = "Liste de tous les jetons classés en fonction du volume de négociation sur les bourses centralisées au cours des intervalles de 24H, 7D et 1M"; "coin_analytics.dex_volume" = "Volume DEX"; "coin_analytics.dex_volume_rank" = "Rang de volume DEX"; "coin_analytics.dex_volume_rank.description" = "Les tokens classés en fonction du volume de trading du token sur les exchanges décentralisés."; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; -"coin_analytics.dex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading decentralized exchanges over 1 year period."; -"coin_analytics.dex_volume.info3" = "Token's rank based on trading volume on leading decentralized exchanges over 30-day period."; -"coin_analytics.dex_volume.info4" = "List of all tokens ranked based on trading volume on decentralized exchanges over 24H / 7D / 1M intervals."; -"coin_analytics.dex_volume.tracked_dexes" = "DEXes that are being tracked:"; +"coin_analytics.dex_volume.info2" = "Votre demande a été traduite en français comme demandé. Si vous avez besoin d'autres traductions ou d'informations supplémentaires, n'hésitez pas à demander."; +"coin_analytics.dex_volume.info3" = "Classement du jeton en fonction du volume de négociation sur les principales bourses décentralisées au cours d'une période de 30 jours."; +"coin_analytics.dex_volume.info4" = "Liste de tous les jetons classés en fonction du volume de négociation sur les bourses décentralisées sur des intervalles de 24H / 7D / 1M."; +"coin_analytics.dex_volume.tracked_dexes" = "DEXes qui sont suivis :"; "coin_analytics.dex_volume.tracked_dexes.info1" = "Binance-Smart-Chain : PancakeSwap"; "coin_analytics.dex_volume.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap"; "coin_analytics.dex_liquidity" = "Liquidité DEX"; "coin_analytics.dex_liquidity_rank" = "Classement de liquidité DEX"; "coin_analytics.dex_liquidity_rank.description" = "Les tokens classés par liquidité disponible sur les exchanges décentralisés."; -"coin_analytics.dex_liquidity.info1" = "Total currently available liquidity for the token on leading decentralized exchanges."; -"coin_analytics.dex_liquidity.info2" = "Chart showing variation in available liquidity for the token on leading decentralized exchanges over 1 year period."; -"coin_analytics.dex_liquidity.info3" = "List of all tokens ranked based on available liquidity for the token on leading decentralized exchanges."; -"coin_analytics.dex_liquidity.tracked_dexes" = "DEXes that are being tracked:"; +"coin_analytics.dex_liquidity.info1" = "Total de liquidités actuellement disponibles pour le jeton sur les principales bourses décentralisées."; +"coin_analytics.dex_liquidity.info2" = "Graphique montrant une variation de la liquidité disponible pour le jeton sur les principales bourses décentralisées sur une période de 1 an."; +"coin_analytics.dex_liquidity.info3" = "Liste de tous les jetons classés en fonction de la liquidité disponible pour le jeton sur les principaux échanges décentralisés."; +"coin_analytics.dex_liquidity.tracked_dexes" = "DEXes qui sont suivis :"; "coin_analytics.dex_liquidity.tracked_dexes.info1" = "Ethereum : Uniswap V2/3, Balancer V1/2, Bancor V2, Curve, Sushiswap"; "coin_analytics.dex_liquidity.tracked_dexes.info2" = "Binance-Smart-Chain : PancakeSwap, DODO V1/2"; @@ -800,24 +813,24 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.active_addresses_rank.description" = "Tokens classés par nombre d'adresses uniques effectuant des transactions avec le token."; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Total number of unique daily active addresses over 24-hour period."; -"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; -"coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; -"coin_analytics.active_addresses.info5" = "List of all tokens ranked based on the number of daily active addresses transacting with the token over 24h / 7D / 1M intervals."; +"coin_analytics.active_addresses.info3" = "Nombre total d'adresses blockchain uniques transitant avec un jeton sur une période de 30 jours."; +"coin_analytics.active_addresses.info4" = "Le rang du jeton est basé sur le nombre de portefeuilles actifs opérant avec le jeton de période de 30 jours."; +"coin_analytics.active_addresses.info5" = "Liste de tous les jetons classés en fonction du nombre d'adresses actives quotidiennes transitant avec le jeton sur les intervalles 24h / 7D / 1M."; "coin_analytics.transaction_count" = "Nombre de transactions"; "coin_analytics.transaction_count_rank" = "Classement par nombre de tr."; "coin_analytics.transaction_count_rank.description" = "Tokens classés en fonction du nombre de transactions sur une blockchain."; -"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; -"coin_analytics.transaction_count.info2" = "Chart showing variation in transaction count over 1 year period."; -"coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; -"coin_analytics.transaction_count.info4" = "List of all tokens ranked based on the number of transactions with the token over 24h / 7D / 1M intervals."; -"coin_analytics.transaction_count.info5" = "The total number of tokens transferred over the blockchain over the 30 day period."; +"coin_analytics.transaction_count.info1" = "Nombre total de transactions blockchain uniques sur une période de 30 jours."; +"coin_analytics.transaction_count.info2" = "Graphique montrant la variation du nombre de transactions sur une période de 1 an."; +"coin_analytics.transaction_count.info3" = "Rang du jeton basé sur le nombre de transactions avec le jeton de période de 30 jours."; +"coin_analytics.transaction_count.info4" = "Liste de tous les jetons classés en fonction du nombre de transactions avec le jeton sur les intervalles 24h / 7D / 1M."; +"coin_analytics.transaction_count.info5" = "Le nombre total de jetons transférés sur la blockchain sur la période de 30 jours."; "coin_analytics.holders" = "Porteurs"; "coin_analytics.holders_rank" = "Classement des porteurs"; "coin_analytics.holders_rank.description" = "Classement des jetons selon les adresses uniques qui les détiennent sur plusieurs chaînes de blocs."; -"coin_analytics.holders.info1" = "Total number of unique addresses holding the token on various blockchains."; -"coin_analytics.holders.info2" = "Top 10 wallets holding the token on each blockchain."; +"coin_analytics.holders.info1" = "Nombre total d'adresses uniques détenant le jeton sur différentes blockchains."; +"coin_analytics.holders.info2" = "Top 10 des portefeuilles détenant le jeton sur chaque blockchain."; "coin_analytics.holders.tracked_blockchains" = "Tracked blockchains: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; "coin_analytics.holders.in_top_10_addresses" = "dans les 10 premiers détenteurs"; "coin_analytics.holders.count" = "Nombre total de détenteurs : %@"; @@ -826,11 +839,11 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.project_tvl" = "Project TVL"; "coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; "coin_analytics.project_tvl.info_title" = "Project TVL (Total Value Locked)"; -"coin_analytics.project_tvl.info1" = "Total-Value-Locked (or Assets Under Management) in the project's smart contracts."; -"coin_analytics.project_tvl.info2" = "Chart showing variation Total-Value-Locked in project's smart contracts over 1 year period."; -"coin_analytics.project_tvl.info3" = "Token's rank based on current Total-Value-Locked."; -"coin_analytics.project_tvl.info4" = "List of all tokens ranked based on current Total-Value-Locked."; -"coin_analytics.project_tvl.info5" = "Market Cap / TVL ratio for the project."; +"coin_analytics.project_tvl.info1" = "Total-Value-Locked (ou Acsets Under Management) dans les contrats intelligents du projet."; +"coin_analytics.project_tvl.info2" = "Graphique montrant la variation valeur totale verrouillée dans les contrats intelligents du projet sur une période de 1 an."; +"coin_analytics.project_tvl.info3" = "Rang du token en fonction de la valeur totale verrouillée actuelle."; +"coin_analytics.project_tvl.info4" = "Liste de tous les tokens classés en fonction de la valeur totale verrouillée."; +"coin_analytics.project_tvl.info5" = "Rapport entre la capitalisation du marché et la TVL du projet."; "coin_analytics.project_fee" = "Frais de projet"; "coin_analytics.project_fee_rank" = "Rang des frais du projet"; @@ -838,7 +851,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.project_revenue" = "Revenus du projet"; "coin_analytics.project_revenue_rank" = "Classement en fonction des revenus du projet"; -"coin_analytics.project_revenue_rank.description" = "Tokens ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.description" = "Tokens classés en fonction des revenus générés pour les détenteurs via des mécanismes i.e. staking ou token burns."; "coin_analytics.other_data" = "Autres données"; @@ -1001,6 +1014,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "settings.tab_bar_item" = "Paramètres"; "settings.manage_accounts" = "Gérer les portefeuilles"; "settings.blockchain_settings" = "Paramètres de la Blockchain"; +"settings.backup_manager" = "Gestionnaire de sauvegarde"; "settings.security" = "Centre de Sécurité"; "settings.experimental_features" = "Expérimental"; "settings.personal_support" = "Assistance Personnelle"; @@ -1011,6 +1025,9 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "settings.info_subtitle" = "application décentralisée"; "settings.donate.description" = "Ensemble, avec votre soutien, nous pouvons encore améliorer cette application!"; "settings.donate.title" = "Faire un don"; +"settings.rate_us" = "Evaluez-nous"; +"settings.tell_friends" = "Partager avec ami"; +"settings.contact_us" = "Contactez-nous"; // Settings -> Base Currency @@ -1073,14 +1090,126 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "blockchain_settings.title" = "Paramètres de la Blockchain"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Gestionnaire de sauvegarde"; +"backup_app.backup_manager.restore" = "Restaurer la sauvegarde"; +"backup_app.backup_manager.create" = "Créer une nouvelle sauvegarde"; + +"backup_app.backup_type.title" = "Enregistrer la sauvegarde"; +"backup_app.backup_type.cloud" = "vers iCloud"; +"backup_app.backup_type.cloud.description" = "Sauvegarde d'un fichier de sauvegarde dans votre trousse."; +"backup_app.backup_type.file" = "Vers Fichier"; +"backup_app.backup_type.file.description" = "Sauvegarde d'un fichier de copie de sauvegarde dans votre dossier local."; + +"backup_app.backup_list.title" = "Fichier de sauvegarde"; +"backup_app.backup_list.description.restore" = "Liste des contenus dans le fichier de sauvegarde."; +"backup_app.backup_list.header.wallets" = "Portefeuilles"; +"backup_app.backup_list.header.other" = "Autre"; +"backup_app.backup_list.other.watch_account.title" = "Regarder les portefeuilles"; +"backup_app.backup_list.other.watchlist.title" = "Liste de suivi"; +"backup_app.backup_list.other.contacts.title" = "Contacts"; +"backup_app.backup_list.other.blockchain_settings.title" = "RPC personnalisé"; +"backup_app.backup_list.other.app_settings.title" = "Paramètres de l'application"; +"backup_app.backup_list.other.app_settings.description" = "Langue, Devise, Apparence ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Sauvegarder sur iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud est un service de stockage cloud fourni par Apple. Il est important de savoir que vos données de sauvegarde seront stockées sur les serveurs d'Apple."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "Je comprends que la perte d'accès à mon iCloud entraînera la perte d'accès à la sauvegarde du portefeuille correspondant."; +"backup_app.backup.disclaimer.file.title" = "Sauvegarder dans un fichier"; +"backup_app.backup.disclaimer.file.description" = "Les dispositifs de stockage tels que les disques durs, les disques USB et le stockage sur smartphone sont tous vulnérables aux pertes dues à des dommages physiques, des vols ou d'autres circonstances imprévues."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Je comprends que la perte ou l'endommagement d'un périphérique de sauvegarde entraînera la perte des données de sauvegarde correspondantes."; + +"backup.disclaimer.cloud.title" = "Sauvegarder sur iCloud"; +"backup.disclaimer.cloud.description" = "iCloud est un service de stockage cloud fourni par Apple. Il est important de savoir que vos données de sauvegarde seront stockées sur les serveurs d'Apple."; +"backup.disclaimer.cloud.checkbox_label" = "Je comprends que la perte d'accès à mon iCloud entraînera la perte d'accès à la sauvegarde du portefeuille correspondant."; +"backup.disclaimer.file.title" = "Sauvegarder dans un fichier"; +"backup.disclaimer.file.description" = "Les dispositifs de stockage tels que les disques durs, les disques USB et le stockage sur smartphone sont tous vulnérables aux pertes dues à des dommages physiques, des vols ou d'autres circonstances imprévues."; +"backup.disclaimer.file.checkbox_label" = "Je comprends que la perte ou l'endommagement d'un périphérique de sauvegarde entraînera la perte des données de sauvegarde correspondantes."; +"backup_app.backup.name.title" = "Nom de la sauvegarde"; +"backup_app.backup.name.description" = "Entrez le nom du fichier de sauvegarde."; + +"backup_app.backup.password.title" = "Mot de passe de la sauvegarde"; +"backup_app.backup.password.description" = "Définissez le mot de passe de déverrouillage de votre sauvegarde. Il doit contenir au moins 8 symboles, y compris une lettre minuscule, une lettre majuscule, un chiffre et un caractère spécial."; +"backup_app.backup.password.highlighted_description" = "Ce mot de passe est utilisé pour chiffrer le fichier de sauvegarde de votre portefeuille. Si vous le perdez ou l'oubliez, il ne pourra pas être récupéré ou réinitialisé."; + +"backup_app.restore_type.title" = "Restaurer"; + +"backup_app.restore.notice.description" = "Cette action écrasera vos contacts de paiement locaux ainsi que la copie iCloud (s'il y en a une)"; +"backup_app.restore.notice.merge" = "Remplacer"; + +"backup.password.title" = "Mot de passe de la sauvegarde"; +"backup.password.description" = "Définissez le mot de passe de déverrouillage de votre sauvegarde. Il doit contenir au moins 8 symboles, y compris une lettre minuscule, une lettre majuscule, un chiffre et un caractère spécial."; +"backup.password.highlighted_description" = "Ce mot de passe est utilisé pour chiffrer le fichier de sauvegarde de votre portefeuille. Si vous le perdez ou l'oubliez, il ne pourra pas être récupéré ou réinitialisé."; + // Settings -> Security "settings_security.title" = "Centre de Sécurité"; -"settings_security.passcode" = "Code"; -"settings_security.change_pin" = "Modifier le code "; -"settings_security.touch_id" = "Touch ID"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Paramètres de la Blockchain"; +"settings_security.enable_passcode" = "Activer le code d'accès"; +"settings_security.edit_passcode" = "Modifier le code"; +"settings_security.disable_passcode" = "Désactiver le code d'accès"; +"settings_security.auto_lock" = "Verrouillage automatique"; +"settings_security.balance_auto_hide" = "Masquage automatique du solde"; +"settings_security.balance_auto_hide.description" = "Masque automatiquement le solde à chaque ouverture de l'application, indépendamment des préférences précédentes."; +"settings_security.enable_duress_mode" = "Activer le Mode Duress"; +"settings_security.edit_duress_passcode" = "Modifier le code de détresse"; +"settings_security.disable_duress_mode" = "Désactiver le mode Duress"; +"settings_security.duress_mode.description" = "Un mode spécialisé conçu pour protéger les portefeuilles sélectionnés en cas de contrainte."; + +// Create Passcode + +"create_passcode.title" = "Créer un code d'accès"; +"create_passcode.description" = "Votre code secret sera utilisé pour déverrouiller votre porte-monnaie et envoyer de l’argent"; +"create_passcode.description.biometry" = "Définir un code d'accès pour activer %@"; +"create_passcode.description.duress_mode" = "Définir un mot de passe pour activer le mode Duress"; +"create_passcode.confirm_passcode" = "Confirmer"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Mode de Duress"; +"enable_duress_mode.intro.description" = "Ce mode permet à l'utilisateur de configurer plusieurs codes d'accès d'application de déverrouillage où un code d'accès désiré n'affiche que les portefeuilles spécifiés. Conçu pour garder les portefeuilles sélectionnés en sécurité sous la contrainte ou les menaces."; +"enable_duress_mode.intro.notes" = "Notes"; +"enable_duress_mode.intro.biometrics.description" = "La fonction %@ fonctionnera pour déverrouiller le mode Duresse. Vous pouvez désactiver %@ pour plus de commodité."; +"enable_duress_mode.intro.passcode_disabling" = "Désactivation du code d'accès"; +"enable_duress_mode.intro.passcode_disabling.description" = "La désactivation du mot de passe en mode principal réinitialisera automatiquement le Mode Duresse."; +"enable_duress_mode.intro.passcode_change" = "Changement de mot de passe"; +"enable_duress_mode.intro.passcode_change.description" = "La modification du code d'accès en mode Duress modifiera également le code d'accès actuel pour ce mode."; + +"enable_duress_mode.select.title" = "Sélectionnez les portefeuilles"; +"enable_duress_mode.select.description" = "Sélectionnez les portefeuilles qui seront affichés en mode Duresse."; +"enable_duress_mode.select.wallets" = "Portefeuilles"; +"enable_duress_mode.select.watch_wallets" = "Regarder les portefeuilles"; + +"enable_duress_mode.passcode.title" = "Code Duress"; +"enable_duress_mode.passcode.description" = "Définir un code d'accès pour le mode Duress"; +"enable_duress_mode.passcode.confirm" = "Confirmer"; + +// Edit Passcode + +"edit_passcode.title" = "Modifier le code"; +"edit_passcode.enter_new_passcode" = "Entrez le nouveau code d'accès"; +"edit_passcode.confirm_new_passcode" = "Confirmer"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Modifier le code de détresse"; +"edit_duress_passcode.enter_new_passcode" = "Entrez le nouveau mot de passe pour le mode Duress"; +"edit_duress_passcode.confirm_new_passcode" = "Confirmer"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Confirmation invalide"; +"set_passcode.already_used" = "Ce mot de passe est déjà utilisé"; + +// Unlock + +"unlock.title" = "Déverrouiller"; +"unlock.passcode" = "Entrez le code d'accès"; +"unlock.biometry_reason" = "Déverrouiller le portefeuille"; +"unlock.attempts_left" = "Tentatives restantes : %@"; +"unlock.disabled_until" = "Désactivé jusqu'à: %@"; +"unlock.random" = "Aléatoire"; + "security_settings.delete_alert_button" = "Supprimer de l'appareil"; "btc_blockchain_settings.restore_source" = "Restorer la source"; @@ -1096,9 +1225,9 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "btc_transaction_sort_mode.bip69.description" = "Indexation lexicographique"; "blockchain_settings.info.restore_source" = "Restorer la source"; -"blockchain_settings.info.restore_source.content" = "This setting is only relevant when restoring an existing wallet. It is a process of getting transaction history for a given cryptocurrency so the wallet app is able to display past transactions and calculate the user's balance. This needs to happen only once when the user restores previously created wallets.\n\nAt this point, there are two potential ways for a mobile wallet like %@ to do this:\n\n1. from the API Server: There is a third-party predefined server that hosts the entire blockchain and has all the data processed and optimized to provide that data in a fast manner. This method is fast but potentially (not necessarily) less private. It's also a centralized method to restore a wallet as it depends on the availability of a 3rd party server. This option is recommended due to its speed of getting data (5-10 minutes).\n\n2. from Blockchain: The app tries to restore directly from a network of blockchain nodes. This is a decentralized way to restore wallet balance and past transactions. The app pings many of the network nodes and requests data from them without addressing some nodes specifically. This option is slow and can easily take 2-3 hours, the app needs to be open while restoring is happening. This restore method doesn't depend on any entity and should work in all conditions."; +"blockchain_settings.info.restore_source.content" = "Ce paramètre n'est pertinent que lors de la restauration d'un portefeuille existant. Il s'agit d'un processus visant à obtenir l'historique des transactions pour une cryptomonnaie donnée, afin que l'application de portefeuille puisse afficher les transactions passées et calculer le solde de l'utilisateur. Cela doit se produire une seule fois lorsque l'utilisateur restaure des portefeuilles précédemment créés.\n\nÀ ce stade, il existe deux façons potentielles pour une application de portefeuille mobile comme %@ de le faire :\n\nDepuis le serveur API : Il existe un serveur tiers prédéfini qui héberge l'ensemble de la blockchain et dispose de toutes les données traitées et optimisées pour fournir ces données rapidement. Cette méthode est rapide mais potentiellement (pas nécessairement) moins privée. C'est également une méthode centralisée pour restaurer un portefeuille car elle dépend de la disponibilité d'un serveur tiers. Cette option est recommandée en raison de sa rapidité à obtenir des données (5 à 10 minutes).\n\nDepuis la blockchain : L'application tente de restaurer directement à partir d'un réseau de nœuds de blockchain. Il s'agit d'une manière décentralisée de restaurer le solde du portefeuille et les transactions passées. L'application envoie des requêtes à de nombreux nœuds du réseau sans spécifiquement adresser certaines nœuds. Cette option est lente et peut facilement prendre 2 à 3 heures, l'application doit rester ouverte pendant la restauration. Cette méthode de restauration ne dépend d'aucune entité et devrait fonctionner dans toutes les conditions."; "blockchain_settings.info.rpc_source" = "Source RPC"; -"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer. %@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; +"blockchain_settings.info.rpc_source.content" = "Ce paramètre contrôle la manière dont cette application interagit avec les blockchains lors de l'envoi ou de la réception de transactions.\n\nDans le cas de Bitcoin, Bitcoin Cash, Litecoin et Dash, la communication avec les nœuds du réseau blockchain est entièrement pair à pair. %@ envoie des requêtes à de nombreux nœuds et communique avec l'un d'entre eux. À chaque connexion, l'application se connecte à un nœud différent.\n\nDans le cas d'Ethereum, de Binance Smart Chain et d'autres blockchains EVM, il n'existe aucune autre alternative pour les portefeuilles mobiles que d'interagir avec les blockchains respectifs que via des fournisseurs de services RPC tiers (comme Infura.io) ou des nœuds personnels. Cela signifie essentiellement que votre communication avec cette blockchain n'est pas décentralisée. Cela n'affecte en rien vos fonds, seulement la capacité à se connecter au réseau blockchain.\n\nSoyez assuré que nous avons cela à l'œil et essaierons bientôt de fournir une manière décentralisée de synchroniser. Patience."; // Manage Accounts @@ -1124,9 +1253,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "settings.about_app.description" = "Le portefeuille %@ est conçu pour ceux qui cherchent à investir et à stocker des cryptomonnaies de manière privée et indépendante.\n\nC'est un portefeuille non-dépositaire, peer-to-peer où seul l'utilisateur a le contrôle sur les fonds. Il ne collecte aucune donnée et garde l'utilisateur indépendant en ne verrouillant pas les fonds de l'utilisateur sur une application spécifique de portefeuille.\n\nLe portefeuille %@ est entièrement open-source et tout le monde peut confirmer que l'application fonctionne exactement comme il le prétend."; "settings.about_app.whats_new" = "Nouveautés"; "settings.about_app.website" = "Site Web"; -"settings.about_app.contact" = "Contactez-nous"; -"settings.about_app.rate_us" = "Evaluez-nous"; -"settings.about_app.tell_friends" = "Partager avec ami"; // Settings -> About App -> Contact @@ -1168,8 +1294,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "appearance.balance_value.coin_value" = "Valeur de la pièce"; "appearance.balance_value.fiat_value" = "Valeur Fiat"; -"appearance.balance_auto_hide" = "Masquer le Solde Automatiquement"; - // Settings -> Contacts "contacts.title" = "Contacts"; @@ -1229,25 +1353,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "contacts.settings.alert_error.title" = "Erreur iCloud"; -// Set PIN - -"set_pin.title" = "Code"; -"set_pin.info" = "Votre code sera utilisé pour déverrouiller votre porte-monnaie et envoyer de l’argent"; -"set_pin.wrong_confirmation" = "Le Code ne correspond pas. Réessayez"; - -// Edit PIN - -"edit_pin.title" = "Modifier le code "; -"edit_pin.unlock_info" = "Le code actuel"; -"edit_pin.new_pin_info" = "Le code nouveu"; - -// Unlock PIN - -"unlock_pin.info" = "Code"; -"unlock_pin.cant_save_pin" = "Oops! Nous ne pouvons pas enregistrer votre code, veuillez nous contacter au plus vite!"; -"unlock_pin.blocked_until" = "Désactivé jusqu'à: %@"; - - // Key Types "chart.time_duration.day" = "24 h"; @@ -1274,7 +1379,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "chart.performance.week_changes" = "Changes (1S)"; "chart.performance.month_changes" = "Changements (1 m)"; -"chart.about.header" = "À propos"; "chart.about.read_more" = "Plus d'infos"; "chart.about.read_less" = "Moins d’infos"; @@ -1357,8 +1461,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "wallet_connect.active_account" = "Activer le portefeuille"; "wallet_connect.address" = "Adresses"; "wallet_connect.network" = "Réseau"; -"wallet_connect.address" = "Adresses"; -"wallet_connect.network" = "Réseau"; "wallet_connect.list.pending_requests" = "Demandes en attente"; "wallet_connect.main.no_any_supported_chains" = "Aucune chaîne prise en charge !"; "wallet_connect.main.unsupported_chains" = "Certaines chaînes ne sont pas prises en charge !"; @@ -1537,12 +1639,11 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "fee_settings.gas_price.info" = "Les frais pour effectuer des transactions sur le réseau sont mesurés en unités de gas. Le prix du gas est le montant qu'un utilisateur est prêt à dépenser par unité de gas. Le prix du gas est élevé lorsque le réseau est encombré, et faible lorsqu'il est inactif. Souvent, une transaction reste en suspens pendant une période prolongée si le montant du gas est insuffisant."; "fee_settings.base_fee" = "Frais de base"; -"fee_settings.base_fee.info" = "The network protocol determines the base price per gas for each block, called base fee rate. It varies according to the network utilization level from block to block. It can increase or decrease by no more than 12.5% in the next block, making fees more predictable. The value shown here is the current block's base fee rate."; +"fee_settings.base_fee.info" = "Le protocole réseau détermine le prix de base par unité de gaz pour chaque bloc, appelé taux de frais de base. Il varie en fonction du niveau d'utilisation du réseau de bloc en bloc. Il peut augmenter ou diminuer de pas plus de 12,5 % dans le bloc suivant, rendant les frais plus prévisibles. La valeur affichée ici est le taux de frais de base du bloc actuel."; "fee_settings.max_fee_rate" = "Taux de frais max"; -"fee_settings.max_fee_rate.info" = "This is the maximum total price per gas the user is willing to pay. It must cover the network's base fee rate and max priority fee rate. The value shown here is suggested based on an estimate of the next block's base fee rate plus the max priority fee rate chosen by the user. The actual fee rate paid will normally be lower. Setting this lower than the current base fee rate will limit the fee paid, but will result in longer waiting times for the transaction to be confirmed, or even in a stuck transaction."; +"fee_settings.max_fee_rate.info" = "Ceci est le prix total maximal par unité de gaz que l'utilisateur est prêt à payer. Il doit couvrir le taux de frais de base du réseau et le taux de frais prioritaire maximal. La valeur affichée ici est suggérée en fonction d'une estimation du taux de frais de base du bloc suivant plus le taux de frais prioritaire maximal choisi par l'utilisateur. Le taux de frais réellement payé sera généralement inférieur. Définir une valeur inférieure au taux de frais de base actuel limitera les frais payés, mais entraînera des délais d'attente plus longs pour la confirmation de la transaction, voire une transaction bloquée."; "fee_settings.tips" = "Frais de priorité max"; -"fee_settings.tips.info" = "Users pay priority fees to incentivize a transaction to be confirmed more quickly. They are sometimes called tips. The max priority fee rate is the maximum additional price per gas the user is willing to pay on top of the base fee rate. The value shown here is suggested based on predicted network conditions. The actual priority fee will normally be lower. Setting this to zero may result in a long waiting time for transaction to be confirmed, as it is placed at the end of the pending transactions queue from all users. -"; +"fee_settings.tips.info" = "Les utilisateurs paient des frais de priorité pour encourager une transaction à être confirmée plus rapidement. Ils sont parfois appelés conseils. Le taux de frais de priorité max est le prix supplémentaire maximum par gaz que l'utilisateur est prêt à payer en plus du taux de frais de base. La valeur montrée ici est suggérée en fonction des conditions de réseau prévues. Les frais de priorité réels seront normalement inférieurs. Le fait de le définir à zéro peut entraîner un long délai d'attente pour la confirmation de la transaction, car il est placé à la fin de la file d'attente des transactions en attente de tous les utilisateurs."; "fee_settings.errors.insufficient_balance" = "Solde insuffisant"; "fee_settings.errors.unexpected_error" = "Erreur inattendue"; diff --git a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings index 5155fa3f06..85bec3c76f 100644 --- a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "불여넣기"; "button.resend" = "재전송"; "button.backup" = "백업"; +"button.restore" = "복구"; "button.copy" = "복사"; "button.retry" = "재시도"; "button.report" = "보고"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "잘못된 금액"; "alert.no_fee" = "잘못된 요금"; "alert.warning" = "경고"; +"alert.notice" = "공지사항"; "alert.error" = "오류"; "alert.unknown_error" = "알수없는 오류"; "alert.success_action" = "완료"; +"alert.restored" = "복원됨"; "alert.success" = "성공"; "alert.added_to_watchlist" = "관심목록에 추가되었습니다"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "Removed from Wallet"; "alert.already_added_to_wallet" = "이미 지갑에 추가됨"; "alert.not_supported_yet" = "아직 지원하지 않음"; -"alert.copied" = "복사됨"; "alert.created" = "생성됨"; "alert.imported" = "복구됨"; "alert.wallet_added" = "지갑이 추가됨"; @@ -95,6 +97,16 @@ "selector.any" = "어떤"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "즉시"; +"auto_lock.minute1" = "1분"; +"auto_lock.minute5" = "5분"; +"auto_lock.minute15" = "15분"; +"auto_lock.minute30" = "30분"; +"auto_lock.hour1" = "1시간"; + // Access Camera "access_camera.message" = "%@이 QR코드를 스캔하기 위해선 카메라에 대한 접근이 필요합니다. \n\n설정 -> %@으로 가서 카메라 접근을 허용해 주세요."; @@ -124,21 +136,24 @@ // Restore Type "restore_type.title" = "다음에서 복원하기"; - "restore_type.recovery.title" = "복구 문구에서"; "restore_type.cloud.title" = "아이클라우드에서"; +"restore_type.file.title" = "파일에서"; "restore_type.cex.title" = "거래소 지갑에서"; "restore_type.recovery.description" = "이 문구는 복구 문구 또는 개인 키를 사용하여 가져올 수 있습니다."; "restore_type.cloud.description" = "키체인의 백업 파일에서 가져옵니다."; +"restore_type.file.description" = "로컬 폴더에서 백업 파일을 가져오십시오."; "restore_type.cex.description" = "중앙집중형 거래소의 지갑에 연결하세요."; // Restore Cloud "restore.cloud.title" = "백업 선택"; -"restore.cloud.description" = "지갑을 복원하려면 백업 사본을 선택하세요."; +"restore.cloud.description" = "복원하려는 백업 파일을 선택하세요."; "restore.cloud.empty" = "백업이 없습니다."; +"restore.cloud.wallets" = "지갑 백업"; "restore.cloud.imported" = "수입 지갑"; +"restore.cloud.app_backups" = "앱 백업"; "restore.cloud.password.title" = "암호를 입력하세요"; "restore.cloud.password.placeholder" = "백업 비밀번호"; @@ -224,13 +239,10 @@ "backup_verify_passphrase.description" = "암호 문구 입력"; "backup_verify_passphrase.incorrect_passphrase" = "잘못된 암호"; -// Backup Required - -"backup_required.title" = "백업이 필요합니다"; - // Backup Prompt -"backup_prompt.title" = "수동으로 백업합니다"; +"backup_prompt.backup_recovery_phrase" = "백업용 지갑"; +"backup_prompt.backup_required" = "백업이 필요합니다"; "backup_prompt.warning" = "휴대전화 분실, 도난, 파손 등의 경우 지갑을 복구할 수 있도록 복구 문구 및 관련 비밀번호의 백업 사본을 만드십시오."; "backup_prompt.backup" = "백업"; "backup_prompt.backup_manual" = "수동으로 백업합니다"; @@ -241,7 +253,6 @@ "backup.cloud.title" = "iCloud에 백업"; "backup.cloud.description" = "iCloud 저장소는 Apple에서 제공하는 타사 클라우드 저장소 서비스입니다. 데이터가 개인 기기가 아닌 Apple 서버에 저장된다는 점을 아는 것이 중요합니다. 이것은 귀하가 귀하의 데이터를 위탁하고 귀하의 정보 보안을 제3자 서비스에 넘기고 있음을 의미합니다."; - "backup.cloud.terms.item.1" = "내 iCloud에 대한 액세스 권한을 상실하면 해당 지갑의 백업에 대한 액세스 권한도 잃게 된다는 점을 이해합니다."; "backup.cloud.name.title" = "백업 이름"; @@ -268,10 +279,8 @@ "backup.cloud.cant_create_file" = "파일을 iCloud에 저장할 수 없음"; "backup.cloud.cant_delete_file" = "iCloud에서 삭제할 수 없습니다"; "backup.cloud.no_access.title" = "아이클라우드 접근하기"; -"backup.cloud.no_access.title" = "아이클라우드 접근하기"; "backup.cloud.no_access.description" = "iCloud 스토리지에 백업을 만들려면 액세스 권한을 제공해야 합니다."; - // Errors "error.send.self_transfer" = "자체로 전송이 지원되지 않음"; @@ -292,10 +301,12 @@ "balance.rate_per_coin" = "%@ 당 %@"; "balance.syncing" = "동기화 중입니다..."; "balance.searching" = "거래 검색 중..."; +"balance.stopped" = "은행 정지"; "balance.downloading_sapling" = "묘목 다운로드 중... %d%%"; "balance.downloading_blocks" = "블록 다운로드"; "balance.scanning_blocks" = "스캐닝 블록"; "balance.enhancing_transactions" = "거래 향상"; +"wait_for_synchronization" = "동기화 대기 중"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "동기화 중... %@"; @@ -320,6 +331,9 @@ "balance.token.locked" = "잠김"; "balance.token.locked.info.title" = "시간 자물쇠"; "balance.token.locked.info.description" = "발송자는 이러한 자금들을 표시된 날짜에 만료가 되는 잠금 지출과 함께 보냈습니다.\n\n걱정 하지마십시오, 수신된 비트코인은 이미 귀하의 것입니다 하지만 잠금 기간이 만료될때 까지 비트코인 네트워크에서 사용하실 수 없습니다."; +"balance.token.processing" = "처리 중"; +"balance.token.processing.info.title" = "처리 중인 금액"; +"balance.token.processing.info.description" = "Transactions with this amount still syncing. And when they are confirmed, these tokens will be available for spending"; "balance.token.staked" = "판돈"; "balance.token.staked.info.title" = "스테이크 타이틀"; "balance.token.staked.info.description" = "측설 설명 텍스트"; @@ -400,7 +414,7 @@ "send.hodler_locktime_off" = "끄기"; "send.hodler_error.unsupported_address" = "P2PKH (1으로 시작하는) 주소만 시간 잠금이 가능합니다."; "send.fee_info.title" = "수수료율"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within a reasonable amount of time.\n\nThe recommended fee rate is shown as the amount of satoshi the user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting a higher fee rate."; "send.transaction_inputs_outputs_info.title" = "트랜잭션 입력/출력"; "send.transaction_inputs_outputs_info.description" = "Most Bitcoin transactions, as well as transactions in alike cryptocurrencies including Bitcoin Cash, Dash, and Litecoin, generate two outputs. One output is the amount that goes to the receiver and the other is the change output that is returned to the sender. The way most wallets construct transactions makes it easy for a third party to understand which of the outputs went to the receiving party and which one was the change amount returned to the sender. As the output returned to the sender is later used in future transactions, a connection between these two transactions becomes apparent.\n\nThe %@ wallet implements measures to make it harder for someone to figure out which output goes where.\n\nThere are two options available to %@ users:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; @@ -540,7 +554,6 @@ "swap.confirmation.maximum_sent" = "최대 속도"; "swap.dex_info.description" = "이 거래소 서비스는 %@ 블록체인에 구축된 분산형 토큰 교환 프로토콜인 %@에 의해 구동됩니다.\n\n%@는 스마트 계약에 의해 완전 자동화되고 관리되므로 부정 행위 없이 토큰 교환을 안정적으로 수행할 수 있습니다."; - "swap.dex_info.header_dex_related" = "%@ 관련됨"; "swap.dex_info.header_allowance" = "허용량"; "swap.dex_info.content_allowance" = "토큰 스왑을 실행할 때 거래소가 사용자를 대신하여 지출할 수 있는 금액. 실제 스왑 거래가 발생하기 전에 선행 거래 설정 충분한 여유가 필요합니다."; @@ -724,8 +737,8 @@ "coin_overview.roi.day200" = "6 Month"; "coin_overview.roi.year1" = "1 Year"; -"coin_overview.category" = "Category"; - +"coin_overview.overview" = "Overview"; +"coin_overview.description_warning" = "이것은 주어진 암호화폐에 대한 참고 자료를 기반으로 한 AI 생성된 설명입니다. 오류가 포함될 수 있습니다."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Coin Types"; @@ -804,10 +817,10 @@ "coin_analytics.transaction_count" = "트랜잭션 수"; "coin_analytics.transaction_count_rank" = "Tx Count Rank"; -"coin_analytics.transaction_count_rank.description" = "Tokens ranked by number of transactions on a blockchain."; +"coin_analytics.transaction_count_rank.description" = "Tokens are ranked by a number of transactions on a blockchain."; "coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; "coin_analytics.transaction_count.info2" = "Chart showing variation in transaction count over 1 year period."; -"coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; +"coin_analytics.transaction_count.info3" = "Token's rank is based on the number of transactions within the token 30-day period."; "coin_analytics.transaction_count.info4" = "List of all tokens ranked based on the number of transactions with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count.info5" = "The total number of tokens transferred over the blockchain over the 30-day period."; @@ -999,9 +1012,10 @@ "settings.tab_bar_item" = "설정"; "settings.manage_accounts" = "지갑 관리하기"; "settings.blockchain_settings" = "블록 체인 설정"; +"settings.backup_manager" = "백업 관리자"; "settings.security" = "보안 센터"; "settings.experimental_features" = "실험"; -"settings.personal_support" = "Personal Support"; +"settings.personal_support" = "개인 지원"; "settings.base_currency" = "기초 통화"; "settings.language" = "언어"; "settings.faq" = "FAQ"; @@ -1009,6 +1023,9 @@ "settings.info_subtitle" = "분산형 앱"; "settings.donate.description" = "함께, 여러분의 지원으로 우리는 이 앱을 더 좋게 만들 수 있습니다!"; "settings.donate.title" = "후원하기"; +"settings.rate_us" = "앱 평가"; +"settings.tell_friends" = "친구에게 추천하기"; +"settings.contact_us" = "문의하기"; // Settings -> Base Currency @@ -1071,14 +1088,126 @@ "blockchain_settings.title" = "블록 체인 설정"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "백업 관리자"; +"backup_app.backup_manager.restore" = "백업 복원"; +"backup_app.backup_manager.create" = "새로운 백업 생성"; + +"backup_app.backup_type.title" = "백업 저장"; +"backup_app.backup_type.cloud" = "iCloud로"; +"backup_app.backup_type.cloud.description" = "백업 복사 파일을 키체인에 저장합니다."; +"backup_app.backup_type.file" = "파일로 저장"; +"backup_app.backup_type.file.description" = "백업 복사 파일을 로컬 폴더에 저장합니다."; + +"backup_app.backup_list.title" = "백업 파일"; +"backup_app.backup_list.description.restore" = "백업 파일의 내용 목록"; +"backup_app.backup_list.header.wallets" = "지갑"; +"backup_app.backup_list.header.other" = "기타"; +"backup_app.backup_list.other.watch_account.title" = "관찰 주소"; +"backup_app.backup_list.other.watchlist.title" = "Watchlist"; +"backup_app.backup_list.other.contacts.title" = "연락처"; +"backup_app.backup_list.other.blockchain_settings.title" = "사용자 지정 RPC"; +"backup_app.backup_list.other.app_settings.title" = "앱 설정"; +"backup_app.backup_list.other.app_settings.description" = "언어, 통화, 외관..."; + +"backup_app.backup.disclaimer.cloud.title" = "iCloud에 백업"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud는 Apple에서 제공하는 클라우드 저장소 서비스입니다. 귀하의 백"; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "내 iCloud에 대한 액세스 권한을 상실하면 해당 지갑의 백업에 대한 액세스 권한도 잃게 된다는 점을 이해합니다."; +"backup_app.backup.disclaimer.file.title" = "파일로 백업"; +"backup_app.backup.disclaimer.file.description" = "하드 드라이브, USB 드라이브, 스마트폰 저장소 등의 저장 장치는 물리적인 손상, 도난 또는 예기치 않은 사건으로 인해 모두 손실의 위험이 있습니다."; +"backup_app.backup.disclaimer.file.checkbox_label" = "백업 장치의 도난 또는 손상은 해당 지갑의 백업을 손실할 수 있음을 이해합니다."; + +"backup.disclaimer.cloud.title" = "iCloud에 백업"; +"backup.disclaimer.cloud.description" = "iCloud는 Apple에서 제공하는 클라우드 저장소 서비스입니다. 귀하의 백"; +"backup.disclaimer.cloud.checkbox_label" = "내 iCloud에 대한 액세스 권한을 상실하면 해당 지갑의 백업에 대한 액세스 권한도 잃게 된다는 점을 이해합니다."; +"backup.disclaimer.file.title" = "파일로 백업"; +"backup.disclaimer.file.description" = "하드 드라이브, USB 드라이브, 스마트폰 저장소 등의 저장 장치는 물리적인 손상, 도난 또는 예기치 않은 사건으로 인해 모두 손실의 위험이 있습니다."; +"backup.disclaimer.file.checkbox_label" = "백업 장치의 도난 또는 손상은 해당 지갑의 백업을 손실할 수 있음을 이해합니다."; +"backup_app.backup.name.title" = "백업 이름"; +"backup_app.backup.name.description" = "백업 파일의 이름을 입력하세요."; + +"backup_app.backup.password.title" = "백업 비밀번호"; +"backup_app.backup.password.description" = "백업에 대한 잠금 해제 암호를 설정하십시오. 최소 8개의 기호로 구성되어야 하며 소문자, 대문자, 숫자 및 특수 문자가 하나 이상 포함되어야 합니다."; +"backup_app.backup.password.highlighted_description" = "이 암호는 지갑의 백업 파일을 암호화하는 데 사용됩니다. 암호를 분실하거나 잊어버리면 복구하거나 재설정할 수 없습니다."; + +"backup_app.restore_type.title" = "복구"; + +"backup_app.restore.notice.description" = "이 작업은 로컬 결제 연락처와 iCloud 복사본(있는 경우)을 덮어씁니다."; +"backup_app.restore.notice.merge" = "위젯 변경"; + +"backup.password.title" = "백업 비밀번호"; +"backup.password.description" = "백업에 대한 잠금 해제 암호를 설정하십시오. 최소 8개의 기호로 구성되어야 하며 소문자, 대문자, 숫자 및 특수 문자가 하나 이상 포함되어야 합니다."; +"backup.password.highlighted_description" = "이 암호는 지갑의 백업 파일을 암호화하는 데 사용됩니다. 암호를 분실하거나 잊어버리면 복구하거나 재설정할 수 없습니다."; + // Settings -> Security "settings_security.title" = "보안 센터"; -"settings_security.passcode" = "비밀번호"; -"settings_security.change_pin" = "비밀번호 변경"; -"settings_security.touch_id" = "터치 아이디"; -"settings_security.face_id" = "얼굴인식 아이디"; -"settings_security.blockchain_settings" = "블록 체인 설정"; +"settings_security.enable_passcode" = "비밀번호 활성화"; +"settings_security.edit_passcode" = "비밀번호 변경"; +"settings_security.disable_passcode" = "비밀번호 비활성화"; +"settings_security.auto_lock" = "자동 잠금"; +"settings_security.balance_auto_hide" = "밸런스 자동 숨기기"; +"settings_security.balance_auto_hide.description" = "앱이 열릴 때마다 잔고를 자동으로 숨깁니다. 이전 설정에 관계없이 적용됩니다."; +"settings_security.enable_duress_mode" = "긴급 상황 모드 설정"; +"settings_security.edit_duress_passcode" = "긴급 상황 비밀번호 편집"; +"settings_security.disable_duress_mode" = "긴급 상황 모드 비활성화"; +"settings_security.duress_mode.description" = "강요 상황에서 선택한 지갑을 안전하게 보관하기 위해 디자인된 특별한 모드입니다."; + +// Create Passcode + +"create_passcode.title" = "비밀번호 만들기"; +"create_passcode.description" = "너의 비밀번호는 너의 지갑을 열어주고 돈을 보내는데 사용될 것입니다."; +"create_passcode.description.biometry" = "%@을(를) 활성화하려면 비밀번호를 설정하세요."; +"create_passcode.description.duress_mode" = "긴급 상황 모드를 활성화하려면 비밀번호를 설정하세요."; +"create_passcode.confirm_passcode" = "확인"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "긴급 상황 모드"; +"enable_duress_mode.intro.description" = "이 모드는 사용자가 여러 언락 앱 비밀번호를 설정하도록 허용하며, 원하는 비밀번호는 지정된 지갑만 표시합니다. 강요나 위협에도 선택한 지갑을 안전하게 보관하기 위해 디자인되었습니다."; +"enable_duress_mode.intro.notes" = "메모"; +"enable_duress_mode.intro.biometrics.description" = "%@ 기능은 긴급 상황 모드를 해제하는 데 사용됩니다. 편의를 위해 %@을(를) 비활성화할 수 있습니다."; +"enable_duress_mode.intro.passcode_disabling" = "비밀번호 비활성화"; +"enable_duress_mode.intro.passcode_disabling.description" = "기본 모드에서 비밀번호를 비활성화하면 긴급 상황 모드가 자동으로 재설정됩니다."; +"enable_duress_mode.intro.passcode_change" = "비밀번호 변경"; +"enable_duress_mode.intro.passcode_change.description" = "긴급 상황 모드에서 비밀번호를 변경하면 해당 모드의 현재 비밀번호도 변경됩니다."; + +"enable_duress_mode.select.title" = "지갑 선택"; +"enable_duress_mode.select.description" = "긴급 상황 모드에서 표시할 지갑을 선택하세요."; +"enable_duress_mode.select.wallets" = "지갑"; +"enable_duress_mode.select.watch_wallets" = "관찰 주소"; + +"enable_duress_mode.passcode.title" = "긴급 상황 비밀번호"; +"enable_duress_mode.passcode.description" = "긴급 상황 모드용 비밀번호를 설정하세요"; +"enable_duress_mode.passcode.confirm" = "확인"; + +// Edit Passcode + +"edit_passcode.title" = "비밀번호 변경"; +"edit_passcode.enter_new_passcode" = "새 비밀번호를 입력하세요"; +"edit_passcode.confirm_new_passcode" = "확인"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "긴급 상황 비밀번호 편집"; +"edit_duress_passcode.enter_new_passcode" = "긴급 상황 모드용 새 비밀번호를 입력하세요"; +"edit_duress_passcode.confirm_new_passcode" = "확인"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "유효하지 않은 확인입니다"; +"set_passcode.already_used" = "이 비밀번호는 이미 사용 중입니다."; + +// Unlock + +"unlock.title" = "잠금 해제"; +"unlock.passcode" = "비밀번호 입력"; +"unlock.biometry_reason" = "지갑 잠금 해제"; +"unlock.attempts_left" = "남은 시도 횟수: %@"; +"unlock.disabled_until" = "비활성화될 때까지: %@"; +"unlock.random" = "랜덤"; + "security_settings.delete_alert_button" = "휴대전화에서 삭제"; "btc_blockchain_settings.restore_source" = "매개 변수 복원"; @@ -1095,7 +1224,7 @@ "blockchain_settings.info.restore_source" = "매개 변수 복원"; "blockchain_settings.info.restore_source.content" = "This setting is only relevant when restoring an existing wallet. It is a process of getting transaction history for a given cryptocurrency so the wallet app is able to display past transactions and calculate the user's balance. This needs to happen only once when the user restores previously created wallets.\n\nAt this point, there are two potential ways for a mobile wallet like %@ to do this:\n\n1. from the API Server: There is a third-party predefined server that hosts the entire blockchain and has all the data processed and optimized to provide that data in a fast manner. This method is fast but potentially (not necessarily) less private. It's also a centralized method to restore a wallet as it depends on the availability of a 3rd party server. This option is recommended due to its speed of getting data (5-10 minutes).\n\n2. from Blockchain: The app tries to restore directly from a network of blockchain nodes. This is a decentralized way to restore wallet balance and past transactions. The app pings many of the network nodes and requests data from them without addressing some nodes specifically. This option is slow and can easily take 2-3 hours, the app needs to be open while restoring is happening. This restore method doesn't depend on any entity and should work in all conditions."; -"blockchain_settings.info.rpc_source" = "RPC Source"; +"blockchain_settings.info.rpc_source" = "RPC 소스"; "blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer. %@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; // Manage Accounts @@ -1122,9 +1251,6 @@ "settings.about_app.description" = "%@ 지갑은 암호화폐를 개인적이고 독립적인 방식으로 투자하고 보관하려는 사람들을 위해 만들어졌습니다. \n\n이 지갑은 사용자만이 자금에 대한 통제를 갖는 비보관형 P2P 지갑입니다. 데이터를 수집하지 않으며 사용자의 자금을 특정 지갑 앱에 잠그지 않아 독립성을 유지합니다. \n\n%@ 지갑은 완전한 오픈소스이며 누구든지 앱이 주장하는 대로 정확하게 작동하는지 확인할 수 있습니다."; "settings.about_app.whats_new" = "새로운 기능"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "문의하기"; -"settings.about_app.rate_us" = "앱 평가"; -"settings.about_app.tell_friends" = "친구에게 추천하기"; // Settings -> About App -> Contact @@ -1166,8 +1292,6 @@ "appearance.balance_value.coin_value" = "코인 가치"; "appearance.balance_value.fiat_value" = "법정 가치"; -"appearance.balance_auto_hide" = "밸런스 자동 숨기기"; - // Settings -> Contacts "contacts.title" = "연락처"; @@ -1227,25 +1351,6 @@ "contacts.settings.alert_error.title" = "iCloud 오류"; -// Set PIN - -"set_pin.title" = "비밀번호"; -"set_pin.info" = "너의 비밀번호는 너의 지갑을 열어주고 돈을 보내는데 사용될 것입니다."; -"set_pin.wrong_confirmation" = "비밀번호가 일치하지 않는다. 다시 시도하세요."; - -// Edit PIN - -"edit_pin.title" = "비밀번호 변경"; -"edit_pin.unlock_info" = "현재 비밀번호"; -"edit_pin.new_pin_info" = "새로운 비밀번호"; - -// Unlock PIN - -"unlock_pin.info" = "비밀번호"; -"unlock_pin.cant_save_pin" = "아야! 저희는 귀하의 비밀번호를 저장할 수 없습니다, 가능한 빨리 연락해 주시기 바랍니다!"; -"unlock_pin.blocked_until" = "~때까지 사용 안 함: %@"; - - // Key Types "chart.time_duration.day" = "24시"; @@ -1272,7 +1377,6 @@ "chart.performance.week_changes" = "Changes (1W)"; "chart.performance.month_changes" = "Changes (1M)"; -"chart.about.header" = "에 대해"; "chart.about.read_more" = "Read More"; "chart.about.read_less" = "Read Less"; @@ -1355,8 +1459,6 @@ "wallet_connect.active_account" = "지갑 활성화"; "wallet_connect.address" = "주소"; "wallet_connect.network" = "회로망"; -"wallet_connect.address" = "주소"; -"wallet_connect.network" = "회로망"; "wallet_connect.list.pending_requests" = "보류 중인 요청"; "wallet_connect.main.no_any_supported_chains" = "지원되는 체인이 없습니다!"; "wallet_connect.main.unsupported_chains" = "일부 체인은 지원되지 않습니다!"; @@ -1495,7 +1597,7 @@ // EVM Network -"evm_network.rpc_source" = "RPC Source"; +"evm_network.rpc_source" = "RPC 소스"; "evm_network.added" = "추가됨"; "evm_network.add_new" = "신규로 추가"; @@ -1652,7 +1754,7 @@ "subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.info2.title" = "Chart Indicators"; "subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.info3.title" = "Personal Support"; +"subscription_info.info3.title" = "개인 지원"; "subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.get_premium" = "Get Premium"; "subscription_info.already_have" = "I already have Premium"; diff --git a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings index e287e3e05c..3aeef9f339 100644 --- a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Colar"; "button.resend" = "Enviar novamente"; "button.backup" = "Backup"; +"button.restore" = "Restaurar"; "button.copy" = "Copiar"; "button.retry" = "Tentar de Novo"; "button.report" = "Reportar"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "Quantia Errada"; "alert.no_fee" = "Taxa Errada"; "alert.warning" = "Aviso"; +"alert.notice" = "Aviso"; "alert.error" = "Erro"; "alert.unknown_error" = "Erro Desconhecido"; "alert.success_action" = "Concluído"; +"alert.restored" = "Restaurado"; "alert.success" = "Sucesso or Completado"; "alert.added_to_watchlist" = "Adicionado à Lista"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "Removido da Carteira"; "alert.already_added_to_wallet" = "Já adicionado à carteira"; "alert.not_supported_yet" = "Não suportado ainda"; -"alert.copied" = "Copiado"; "alert.created" = "Criado"; "alert.imported" = "Importado"; "alert.wallet_added" = "Carteira Adicionada"; @@ -95,6 +97,16 @@ "selector.any" = "Qualquer"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Imediato"; +"auto_lock.minute1" = "1 minuto"; +"auto_lock.minute5" = "5 Minutos"; +"auto_lock.minute15" = "15 Minutos"; +"auto_lock.minute30" = "30 Minutos"; +"auto_lock.hour1" = "1 hora"; + // Access Camera "access_camera.message" = "%@ absolutely needs access to your tremendous camera for scanning that fantastic QR code. Believe me!. @@ -126,21 +138,24 @@ Go to Settings - > %@ and allow access to the camera."; // Restore Type "restore_type.title" = "Importar carteira"; - "restore_type.recovery.title" = "da Frase de Recuperação"; "restore_type.cloud.title" = "do iCloud"; +"restore_type.file.title" = "de Arquivos"; "restore_type.cex.title" = "da Exchange Wallet"; "restore_type.recovery.description" = "Import like a pro! Recovery phrase or private key, you choose. It's your call, folks!"; "restore_type.cloud.description" = "Import straight from your keychain, folks. It's like having the keys to the kingdom right at your fingertips!"; -"restore_type.cex.description" = "Let's make a connection, folks! Connect to that wallet on the centralized exchange and get ready to win big!"; +"restore_type.file.description" = "Importar um arquivo de backup da sua pasta local."; +"restore_type.cex.description" = "Conecte a uma carteira em uma troca centralizada."; // Restore Cloud "restore.cloud.title" = "Selecionar Backup"; -"restore.cloud.description" = "Time to choose, folks! Pick the backup wallet you want to bring back. It's all about making the right choice!"; +"restore.cloud.description" = "Selecione o arquivo de backup que você deseja restaurar."; "restore.cloud.empty" = "Can you believe it, folks? No backups found. It's a clean slate, a fresh start!"; +"restore.cloud.wallets" = "Backups da Carteira"; "restore.cloud.imported" = "Carteiras importadas"; +"restore.cloud.app_backups" = "Backups do Aplicativo"; "restore.cloud.password.title" = "Digite a senha"; "restore.cloud.password.placeholder" = "Fazer Backup da Senha"; @@ -160,7 +175,7 @@ Go to Settings - > %@ and allow access to the camera."; "restore.binance.connecting" = "Conectando..."; "restore.binance.get_api_keys" = "Obter chaves API"; "restore.binance.failed_to_connect" = "Falha ao conectar sua chave de API"; -"restore.binance.invalid_qr_code" = "Invalid QR Code"; +"restore.binance.invalid_qr_code" = "QR Code Inválido"; // Coin Settings @@ -226,13 +241,10 @@ Go to Settings - > %@ and allow access to the camera."; "backup_verify_passphrase.description" = "Insira a senha"; "backup_verify_passphrase.incorrect_passphrase" = "Senha incorreta"; -// Backup Required - -"backup_required.title" = "Backup necessário"; - // Backup Prompt -"backup_prompt.title" = "Backup Manual"; +"backup_prompt.backup_recovery_phrase" = "Carteira de Backup"; +"backup_prompt.backup_required" = "Backup necessário"; "backup_prompt.warning" = "Listen up, folks. You've got to create a backup of that recovery phrase and the password that goes with it. This is your lifeline, in case your phone decides to go rogue – lost, stolen, broken, you name it. Protect your assets, it's the smart play!"; "backup_prompt.backup" = "Backup"; "backup_prompt.backup_manual" = "Backup Manual"; @@ -243,18 +255,17 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.title" = "Backup para o iCloud"; "backup.cloud.description" = "Let's get this straight, folks. iCloud storage, it's from Apple, okay? But here's the deal: when you use it, your data, it's not hanging out on your own gadgets. Nope, it's on Apple's servers. So, you're putting your trust, your data's security, in the hands of a third-party service. That's the real story here."; - "backup.cloud.terms.item.1" = "Compreendo que perder acesso ao meu iCloud resultará em perder acesso ao backup de uma respectiva carteira."; "backup.cloud.name.title" = "Nome do Backup"; "backup.cloud.name.description" = "Give it a name, folks! This backup file needs an identity. Make it memorable!"; "backup.cloud.name.empty" = "Whoops, folks! You can't leave that backup name hanging empty. Fill it up with something good!"; "backup.cloud.name.error.empty" = "Remember, folks, that backup name, it can't be an empty slate. Give it some character!"; -"backup.cloud.name.error.already_exist" = "Well, folks, looks like you're not the only one with that backup name. It's already in the club!"; +"backup.cloud.name.error.already_exist" = "O nome do backup já existe!"; "backup.cloud.name.placeholder" = "Nome"; "backup.cloud.password.title" = "Definir Senha"; -"backup.cloud.password.description" = "Here's the deal, folks: You've got to set an unlock password for your backup. It's gotta be strong, okay? We're talking at least 8 symbols, including one lowercase letter, one uppercase letter, a number, and a special character. It's your vault's first line of defense!"; +"backup.cloud.password.description" = "Defina a senha de desbloqueio para o seu backup. Deve consistir de pelo menos 8 símbolos e incluir pelo menos uma letra minúscula, uma letra maiúscula, um número e um caractere especial."; "backup.cloud.password.highlighted_description" = "Não esqueça esta senha! Ela é separada da sua senha do iCloud da Apple e não pode ser recuperada ou redefinida."; "backup.cloud.password.placeholder" = "Senha"; "backup.cloud.password.confirm.placeholder" = "Confirmar"; @@ -270,10 +281,8 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.cant_create_file" = "Não é possível salvar o arquivo no iCloud"; "backup.cloud.cant_delete_file" = "Não é possível excluir do iCloud"; "backup.cloud.no_access.title" = "Acessar o iCloud"; -"backup.cloud.no_access.title" = "Acessar o iCloud"; "backup.cloud.no_access.description" = "Para criar um backup, você precisa fornecer acesso ao armazenamento iCloud."; - // Errors "error.send.self_transfer" = "Não é possível enviar para você mesmo"; @@ -294,10 +303,12 @@ Go to Settings - > %@ and allow access to the camera."; "balance.rate_per_coin" = "%@ por %@"; "balance.syncing" = "Sincronizando..."; "balance.searching" = "Procurando transações..."; +"balance.stopped" = "Parado"; "balance.downloading_sapling" = "Baixando prévias.. %d%%"; "balance.downloading_blocks" = "Baixando Blocos"; "balance.scanning_blocks" = "Verificando Blocos"; "balance.enhancing_transactions" = "Aprimorando Transações"; +"wait_for_synchronization" = "Aguarde a sincronização"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Sincronizando... %@"; @@ -322,6 +333,9 @@ Go to Settings - > %@ and allow access to the camera."; "balance.token.locked" = "Bloqueado"; "balance.token.locked.info.title" = "TravaTempo"; "balance.token.locked.info.description" = "O remetente enviou esses fundos com um fechamento de gastos que expirará na data mostrada. \n\nNão se preocupe, o Bitcoin recebido já é seu, mas até que o período de bloqueio expire, você não pode gastá-los na rede Bitcoin."; +"balance.token.processing" = "Processando"; +"balance.token.processing.info.title" = "Processando quantia"; +"balance.token.processing.info.description" = "As transações com esse valor ainda estão sendo sincronizadas. E quando forem confirmadas, esses tokens estarão disponíveis para gastar"; "balance.token.staked" = "Investido"; "balance.token.staked.info.title" = "Título Implantado"; "balance.token.staked.info.description" = "Texto com Descrição Implantada"; @@ -402,7 +416,7 @@ Go to Settings - > %@ and allow access to the camera."; "send.hodler_locktime_off" = "Desligado"; "send.hodler_error.unsupported_address" = "O bloqueio de tempo só funciona com endereços P2PKH (iniciando com 1)"; "send.fee_info.title" = "Taxa de tarifa"; -"send.fee_info.description" = "Blockchains require users to pay network fees when sending transactions. The fees are higher when a lot of transactions are taking place on the network.\n\nThe %@ wallet estimates fee based on the current blockchain activity and recommends the optimal value in order for the transaction to be processed within reasonable amount of time.\n\nThe recommended fee rate shown as the amount of satoshi user needs to pay for a single byte of the transaction. Thus, the total fee depends on the total size of the transaction which is measured in bytes.\n\nUsers may use provided controls to increase or decrease the fee rate value. The change in fee rate changes the total fee for the transaction the user will pay.\n\nSetting fee rate below recommended value may result in a transaction being held as pending for hours, or being rejected. The lower the value, the longer it will take for the transaction to confirm. For transactions where priority is important, we recommend setting higher fee rate."; +"send.fee_info.description" = "Blockchains exigem que os usuários paguem taxas de rede ao enviar transações. As taxas são mais altas quando muitas transações ocorrem na rede.\n\nA carteira %@ estima a taxa com base na atividade atual do blockchain e recomenda o valor ideal para que a transação seja processada dentro de um período de tempo razoável. \n\nA taxa recomendada mostrada como a quantidade de satoshi que o usuário precisa pagar por um único byte da transação. Assim, a taxa total depende do tamanho total da transação que é medido em bytes.\n\nOs usuários podem usar os controles fornecidos para aumentar ou diminuir o valor da taxa. A alteração na taxa altera a taxa total da transação que o usuário pagará.\n\nDefinir uma taxa abaixo do valor recomendado pode fazer com que uma transação seja mantida como pendente por horas ou rejeitada. Quanto menor o valor, mais tempo levará para a transação ser confirmada. Para transações onde a prioridade é importante, recomendamos definir uma taxa de comissão mais elevada."; "send.transaction_inputs_outputs_info.title" = "Entradas / Saídas de Transação"; "send.transaction_inputs_outputs_info.description" = "A maioria das transações em Bitcoin, bem como as transações em criptomoedas, incluindo Bitcoin Cash, Dash, e Litecoin, geram duas saídas. Uma saída é a quantidade que vai para o destinatário e a outra é a mudança de saída que é retornada ao remetente. A maneira como a maioria das carteiras constrói as transações torna mais fácil para um terceiro entender qual das saídas foi para a parte receptora e qual foi a quantia alterada retornada ao remetente. Como a saída retornada ao remetente será usada mais tarde em transações futuras, uma conexão entre essas duas transações se torna evidente.\n\nA carteira %@ implementa medidas para tornar mais difícil para alguém descobrir qual saída vai para onde.\n\nExistem duas opções disponíveis para usuários %@:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Embaralhar"; @@ -542,7 +556,6 @@ Go to Settings - > %@ and allow access to the camera."; "swap.confirmation.maximum_sent" = "Gastos máximos"; "swap.dex_info.description" = "Este serviço de troca é fornecido por %@, um protocolo descentralizado de troca de tokens construído na blockchain %@. \n\n%@ É totalmente automatizado e gerenciado por contratos inteligentes que facilitam a troca de tokens de uma forma confiável sem qualquer meio de trapaça."; - "swap.dex_info.header_dex_related" = "%@ Relacionado"; "swap.dex_info.header_allowance" = "Preço"; "swap.dex_info.content_allowance" = "O montante que uma exchange pode gastar em nome do usuário na execução de trocas simbólicas. É necessária uma margem suficiente para que uma transação de troca efetiva possa ocorrer."; @@ -726,8 +739,8 @@ Go to Settings - > %@ and allow access to the camera."; "coin_overview.roi.day200" = "6 Meses"; "coin_overview.roi.year1" = "1 Ano"; -"coin_overview.category" = "Categoria"; - +"coin_overview.overview" = "Visão geral"; +"coin_overview.description_warning" = "Esta é uma descrição gerada por IA com base no material de referência fornecido para a criptomoeda em questão. Pode conter erros."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Tipos de moeda"; @@ -826,7 +839,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.project_tvl" = "Projeto TVL"; "coin_analytics.tvl_ratio" = "Proporção de Capital de M. / TVB"; "coin_analytics.project_tvl.info_title" = "Projeto TVB (Total de Valor Bloqueado)"; -"coin_analytics.project_tvl.info1" = "Valor-Total-Bloqueado (ou Ativos Sob Gerenciamento) nos contratos inteligentes do projeto."; +"coin_analytics.project_tvl.info1" = "Valor-Total-Bloqueado (ou ativos sob gestão) nos contratos inteligentes do projeto."; "coin_analytics.project_tvl.info2" = "Gráfico mostrando a variação Valor-Total-Bloqueado nos contratos inteligentes do projeto durante o período de 1 ano."; "coin_analytics.project_tvl.info3" = "Classificação do token com base no Valor-Total-Bloqueado atual."; "coin_analytics.project_tvl.info4" = "Lista de todos os tokens classificados com base no Valor-Total-Bloqueado atual."; @@ -1001,6 +1014,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings.tab_bar_item" = "Configurações"; "settings.manage_accounts" = "Gerenciar carteiras"; "settings.blockchain_settings" = "Configurações da Blockchain"; +"settings.backup_manager" = "Gerenciador de Backup"; "settings.security" = "Segurança"; "settings.experimental_features" = "Experimental"; "settings.personal_support" = "Suporte Pessoal"; @@ -1011,6 +1025,9 @@ Go to Settings - > %@ and allow access to the camera."; "settings.info_subtitle" = "aplicativo descentralizado"; "settings.donate.description" = "Juntos, com o seu apoio, podemos fazer este aplicativo ainda melhor!"; "settings.donate.title" = "Doar"; +"settings.rate_us" = "Avalie-Nos"; +"settings.tell_friends" = "Indique para amigos"; +"settings.contact_us" = "Fale Conosco"; // Settings -> Base Currency @@ -1032,7 +1049,7 @@ Go to Settings - > %@ and allow access to the camera."; // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Conta"; -"settings.personal_support.telegram_username.placeholder" = "@username"; +"settings.personal_support.telegram_username.placeholder" = "@nomedeusuario"; "settings.personal_support.description" = "Digite seu nome de conta do Telegram para abrir um chat de suporte pessoal e enviaremos uma mensagem para você."; "settings.personal_support.request" = "Solicitar"; "settings.personal_support.requested" = "Solicitado"; @@ -1073,14 +1090,126 @@ Go to Settings - > %@ and allow access to the camera."; "blockchain_settings.title" = "Configurações da Blockchain"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Gerenciador de Backup"; +"backup_app.backup_manager.restore" = "Restaurar Backup"; +"backup_app.backup_manager.create" = "Criar Novo Backup"; + +"backup_app.backup_type.title" = "Salvar Backup"; +"backup_app.backup_type.cloud" = "para o iCloud"; +"backup_app.backup_type.cloud.description" = "Salvar um arquivo de cópia de segurança no chaveiro."; +"backup_app.backup_type.file" = "para Arquivos"; +"backup_app.backup_type.file.description" = "Salvando um arquivo de cópia de segurança em sua pasta local."; + +"backup_app.backup_list.title" = "Arquivo de backup"; +"backup_app.backup_list.description.restore" = "Lista de conteúdo do arquivo de backup."; +"backup_app.backup_list.header.wallets" = "Carteiras"; +"backup_app.backup_list.header.other" = "Outros"; +"backup_app.backup_list.other.watch_account.title" = "Ver Endereço"; +"backup_app.backup_list.other.watchlist.title" = "Lista de observação"; +"backup_app.backup_list.other.contacts.title" = "Contatos"; +"backup_app.backup_list.other.blockchain_settings.title" = "RPC Personalizado"; +"backup_app.backup_list.other.app_settings.title" = "Configurações do Aplicativo"; +"backup_app.backup_list.other.app_settings.description" = "Linguagem, Moeda, Aparência ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Backup para o iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud é um serviço de armazenamento na nuvem fornecido pela Apple. É importante saber que seus dados de backup serão armazenados nos servidores da Apple."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "Compreendo que perder acesso ao meu iCloud resultará em perder acesso ao backup de uma respectiva carteira."; +"backup_app.backup.disclaimer.file.title" = "Backup em arquivo"; +"backup_app.backup.disclaimer.file.description" = "Dispositivos de armazenamento, ou seja, discos rígidos, discos USB, armazenamento em smartphone, etc. são todos vulneráveis a perdas devido a danos físicos, roubo ou outras circunstâncias imprevistas."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Eu entendo que o roubo ou dano de um dispositivo de backup resultará na perda de um backup de uma respectiva carteira."; + +"backup.disclaimer.cloud.title" = "Backup para o iCloud"; +"backup.disclaimer.cloud.description" = "iCloud é um serviço de armazenamento na nuvem fornecido pela Apple. É importante saber que seus dados de backup serão armazenados nos servidores da Apple."; +"backup.disclaimer.cloud.checkbox_label" = "Compreendo que perder acesso ao meu iCloud resultará em perder acesso ao backup de uma respectiva carteira."; +"backup.disclaimer.file.title" = "Backup em arquivo"; +"backup.disclaimer.file.description" = "Dispositivos de armazenamento, ou seja, discos rígidos, discos USB, armazenamento em smartphone, etc. são todos vulneráveis a perdas devido a danos físicos, roubo ou outras circunstâncias imprevistas."; +"backup.disclaimer.file.checkbox_label" = "Eu entendo que o roubo ou dano de um dispositivo de backup resultará na perda de um backup de uma respectiva carteira."; +"backup_app.backup.name.title" = "Nome do Backup"; +"backup_app.backup.name.description" = "Give it a name, folks! This backup file needs an identity. Make it memorable!"; + +"backup_app.backup.password.title" = "Fazer Backup da Senha"; +"backup_app.backup.password.description" = "Defina a senha de desbloqueio para o seu backup. Deve consistir de pelo menos 8 símbolos e incluir pelo menos uma letra minúscula, uma letra maiúscula, um número e um caractere especial."; +"backup_app.backup.password.highlighted_description" = "Esta senha é usada para criptografar o arquivo de backup da sua carteira. Ela não pode ser recuperada ou redefinida se for perdida ou esquecida."; + +"backup_app.restore_type.title" = "Restaurar"; + +"backup_app.restore.notice.description" = "Esta ação substituirá seus contatos de pagamento locais, bem como sua cópia do iCloud (se houver uma)"; +"backup_app.restore.notice.merge" = "Substituir"; + +"backup.password.title" = "Fazer Backup da Senha"; +"backup.password.description" = "Defina a senha de desbloqueio para o seu backup. Deve consistir de pelo menos 8 símbolos e incluir pelo menos uma letra minúscula, uma letra maiúscula, um número e um caractere especial."; +"backup.password.highlighted_description" = "Esta senha é usada para criptografar o arquivo de backup da sua carteira. Ela não pode ser recuperada ou redefinida se for perdida ou esquecida."; + // Settings -> Security "settings_security.title" = "Segurança"; -"settings_security.passcode" = "Senha"; -"settings_security.change_pin" = "Editar senha"; -"settings_security.touch_id" = "Touch ID"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Configurações da Blockchain"; +"settings_security.enable_passcode" = "Ativar Senha"; +"settings_security.edit_passcode" = "Editar Senha"; +"settings_security.disable_passcode" = "Desativar Senha"; +"settings_security.auto_lock" = "Auto-Bloqueio"; +"settings_security.balance_auto_hide" = "Ocultar Saldo Automaticamente"; +"settings_security.balance_auto_hide.description" = "Oculta automaticamente o saldo cada vez que o aplicativo é aberto, independentemente das preferências anteriores."; +"settings_security.enable_duress_mode" = "Definir modo de dureza"; +"settings_security.edit_duress_passcode" = "Editar Senha do Modo Coação"; +"settings_security.disable_duress_mode" = "Desativar Modo de Coação"; +"settings_security.duress_mode.description" = "Um modo especializado projetado para manter as carteiras selecionadas seguras sob coação."; + +// Create Passcode + +"create_passcode.title" = "Criar senha"; +"create_passcode.description" = "Sua senha será usada para desbloquear sua carteira"; +"create_passcode.description.biometry" = "Defina uma senha para ativar %@"; +"create_passcode.description.duress_mode" = "Defina uma senha para ativar o modo de Coação"; +"create_passcode.confirm_passcode" = "Confirmar"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Modo de dureza"; +"enable_duress_mode.intro.description" = "Esta modalidade permite ao usuário configurar várias senhas de desbloqueio de aplicativos, onde uma senha desejada mostra apenas carteiras especificadas. Projetado para manter carteiras selecionadas seguras sob coação ou ameaças."; +"enable_duress_mode.intro.notes" = "Anotações"; +"enable_duress_mode.intro.biometrics.description" = "O recurso %@ funcionará para desbloquear o Modo de Coação. Você pode desativar %@ por conveniência."; +"enable_duress_mode.intro.passcode_disabling" = "Desabilitação da Senha"; +"enable_duress_mode.intro.passcode_disabling.description" = "Desativar a senha no modo principal redefinirá automaticamente o Modo de Coação."; +"enable_duress_mode.intro.passcode_change" = "Alteração de Senha"; +"enable_duress_mode.intro.passcode_change.description" = "Alterar a senha no Modo de Coação também irá alterar a senha atual para esse modo."; + +"enable_duress_mode.select.title" = "Escolher Carteiras"; +"enable_duress_mode.select.description" = "Selecione as carteiras que serão exibidas no Modo de Coação."; +"enable_duress_mode.select.wallets" = "Carteiras"; +"enable_duress_mode.select.watch_wallets" = "Ver Endereço"; + +"enable_duress_mode.passcode.title" = "Senha do Modo Coação"; +"enable_duress_mode.passcode.description" = "Defina uma senha para o modo de Coação"; +"enable_duress_mode.passcode.confirm" = "Confirmar"; + +// Edit Passcode + +"edit_passcode.title" = "Editar Senha"; +"edit_passcode.enter_new_passcode" = "Digite a nova senha"; +"edit_passcode.confirm_new_passcode" = "Confirmar"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Editar Senha do Modo Coação"; +"edit_duress_passcode.enter_new_passcode" = "Defina uma senha para o modo de Coação"; +"edit_duress_passcode.confirm_new_passcode" = "Confirmar"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Confirmação inválida"; +"set_passcode.already_used" = "Esta senha já está sendo usada"; + +// Unlock + +"unlock.title" = "Desbloquear"; +"unlock.passcode" = "Insira a senha"; +"unlock.biometry_reason" = "Desbloquear carteira"; +"unlock.attempts_left" = "Tentativas restantes: %@"; +"unlock.disabled_until" = "Desativar até: %@"; +"unlock.random" = "Aleatório"; + "security_settings.delete_alert_button" = "Excluir do telefone"; "btc_blockchain_settings.restore_source" = "Restaurar Fonte"; @@ -1096,9 +1225,9 @@ Go to Settings - > %@ and allow access to the camera."; "btc_transaction_sort_mode.bip69.description" = "Indexação Lexicográfica"; "blockchain_settings.info.restore_source" = "Restaurar Fonte"; -"blockchain_settings.info.restore_source.content" = "This setting is only relevant when restoring an existing wallet. It is a process of getting transaction history for a given cryptocurrency so the wallet app is able to display past transactions and calculate the user's balance. This needs to happen only once when the user restores previously created wallets.\n\nAt this point, there are two potential ways for a mobile wallet like %@ to do this:\n\n1. from the API Server: There is a third-party predefined server that hosts the entire blockchain and has all the data processed and optimized to provide that data in a fast manner. This method is fast but potentially (not necessarily) less private. It's also a centralized method to restore a wallet as it depends on the availability of a 3rd party server. This option is recommended due to its speed of getting data (5-10 minutes).\n\n2. from Blockchain: The app tries to restore directly from a network of blockchain nodes. This is a decentralized way to restore wallet balance and past transactions. The app pings many of the network nodes and requests data from them without addressing some nodes specifically. This option is slow and can easily take 2-3 hours, the app needs to be open while restoring is happening. This restore method doesn't depend on any entity and should work in all conditions."; +"blockchain_settings.info.restore_source.content" = "Esta configuração só é relevante ao restaurar uma carteira existente. É um processo de obtenção do histórico de transações de uma determinada criptomoeda para que o aplicativo de carteira seja capaz de exibir transações anteriores e calcular o saldo do usuário. Isso precisa acontecer apenas uma vez quando o usuário restaura carteiras criadas anteriormente.\n\nNeste ponto, existem duas maneiras possíveis para uma carteira móvel como %@ fazer isso:\n\n1. do Servidor API: Existe um servidor predefinido de terceiros que hospeda todo o blockchain e tem todos os dados processados e otimizados para fornecer esses dados de forma rápida. Este método é rápido, mas potencialmente (não necessariamente) menos privado. É também um método centralizado para restaurar uma carteira, pois depende da disponibilidade de um servidor de terceiros. Esta opção é recomendada devido à velocidade de obtenção de dados (5 a 10 minutos).\n\n2. da Blockchain: o aplicativo tenta restaurar diretamente de uma rede de nós da blockchain. Esta é uma forma descentralizada de restaurar o saldo da carteira e transações anteriores. O aplicativo executa ping em muitos nós da rede e solicita dados deles sem abordar alguns nós especificamente. Esta opção é lenta e pode levar facilmente de 2 a 3 horas; o aplicativo precisa estar aberto durante a restauração. Este método de restauração não depende de nenhuma entidade e deve funcionar em todas as condições."; "blockchain_settings.info.rpc_source" = "Fonte RPC"; -"blockchain_settings.info.rpc_source.content" = "This setting controls how this app interacts with blockchains when sending or receiving transactions.\n\nIn the case of Bitcoin, Bitcoin Cash, Litecoin, and Dash, the communication with blockchain network nodes is fully peer-to-peer. %@ pings many nodes and communicates with one of them. Each time the app connects to a different node.\n\nIn the case of Ethereum, Binance Smart Chain, and other EVM blockchains, there are no alternatives for mobile wallets to interact with respective blockchains other than via third-party RPC service providers (i.e. Infura.io) or personal nodes. That essentially means your communication with that blockchain is not decentralized. This doesn't impact your funds in any way, only the ability to connect to the blockchain network.\n\nRest assured, we are keeping this on the radar and will soon try to provide a decentralized way to sync. Patience."; +"blockchain_settings.info.rpc_source.content" = "Esta configuração controla como este aplicativo interage com blockchains ao enviar ou receber transações.\n\nNo caso de Bitcoin, Bitcoin Cash, Litecoin e Dash, a comunicação com os nós da rede blockchain é totalmente peer-to-peer. %@ executa ping em vários nós e se comunica com um deles. Cada vez que o aplicativo se conecta a um nó diferente.\n\nNo caso de Ethereum, Binance Smart Chain e outros blockchains EVM, não há alternativas para carteiras móveis interagirem com os respectivos blockchains, a não ser por meio de provedores de serviços RPC terceirizados ( ou seja, Infura.io) ou nós pessoais. Isso significa essencialmente que sua comunicação com esse blockchain não é descentralizada. Isso não afeta seus fundos de forma alguma, apenas a capacidade de se conectar à rede blockchain.\n\nFique tranquilo, estamos mantendo isso no radar e em breve tentaremos fornecer uma forma descentralizada de sincronização. Paciência."; // Manage Accounts @@ -1124,9 +1253,6 @@ Go to Settings - > %@ and allow access to the camera."; "settings.about_app.description" = "A carteira %@ foi construída para aqueles que buscam investir e armazenar criptomoedas de forma privada e independente.\n\nÉ uma carteira não-custódia, peer-to-peer onde apenas o usuário tem controle sobre os fundos. Não coleta nenhum dado e mantém o usuário independente ao não bloquear os fundos do usuário para um aplicativo específico de carteira.\n\nA carteira %@ é totalmente de código aberto e qualquer um pode confirmar que o aplicativo funciona exatamente como afirma."; "settings.about_app.whats_new" = "O que há de novo"; "settings.about_app.website" = "Site"; -"settings.about_app.contact" = "Fale Conosco"; -"settings.about_app.rate_us" = "Avalie-Nos"; -"settings.about_app.tell_friends" = "Indique para amigos"; // Settings -> About App -> Contact @@ -1168,8 +1294,6 @@ Go to Settings - > %@ and allow access to the camera."; "appearance.balance_value.coin_value" = "Valor da Moeda"; "appearance.balance_value.fiat_value" = "Valor Fiat"; -"appearance.balance_auto_hide" = "Ocultar Saldo Automaticamente"; - // Settings -> Contacts "contacts.title" = "Contatos"; @@ -1219,35 +1343,16 @@ Go to Settings - > %@ and allow access to the camera."; "contacts.settings.restore_contacts" = "Restaurar contatos"; "contacts.settings.backup_contacts" = "Backup de contatos"; -"contacts.settings.icloud_sync" = "iCloud Sync"; +"contacts.settings.icloud_sync" = "Sincronização com iCloud"; "contacts.settings.description" = "Sincronize os contatos de pagamento no iCloud para facilitar o backup e o acesso entre vários dispositivos."; "contacts.settings.lost_synchronization.description" = "A sincronização do iCloud foi perdida. Por favor, verifique se o armazenamento do iCloud está ativado no seu dispositivo."; "contacts.settings.merge_disclaimer" = "Seus contatos de pagamento locais serão mesclados com os armazenados no iCloud."; -"contacts.settings.alert.title" = "iCloud Sync"; +"contacts.settings.alert.title" = "Sincronização com iCloud"; "contacts.settings.alert.description" = "Por favor, verifique se o armazenamento do iCloud está ativado no seu dispositivo."; "contacts.settings.alert_error.title" = "erro do iCloud"; -// Set PIN - -"set_pin.title" = "Senha"; -"set_pin.info" = "Sua senha será usada para desbloquear sua carteira"; -"set_pin.wrong_confirmation" = "Senhas não conferem. Tente novamente"; - -// Edit PIN - -"edit_pin.title" = "Editar senha"; -"edit_pin.unlock_info" = "Senha Atual"; -"edit_pin.new_pin_info" = "Nova Senha"; - -// Unlock PIN - -"unlock_pin.info" = "Senha"; -"unlock_pin.cant_save_pin" = "Opa! Não podemos salvar a sua senha, por favor, entre em contato conosco assim que possível!"; -"unlock_pin.blocked_until" = "Desativar até: %@"; - - // Key Types "chart.time_duration.day" = "24H"; @@ -1274,7 +1379,6 @@ Go to Settings - > %@ and allow access to the camera."; "chart.performance.week_changes" = "Alterações (1S)"; "chart.performance.month_changes" = "Alterações (1M)"; -"chart.about.header" = "Sobre"; "chart.about.read_more" = "Ler Mais"; "chart.about.read_less" = "Ler Menos"; @@ -1357,8 +1461,6 @@ Go to Settings - > %@ and allow access to the camera."; "wallet_connect.active_account" = "Carteira ativa"; "wallet_connect.address" = "Endereço"; "wallet_connect.network" = "Rede"; -"wallet_connect.address" = "Endereço"; -"wallet_connect.network" = "Rede"; "wallet_connect.list.pending_requests" = "Solicitações Pendentes"; "wallet_connect.main.no_any_supported_chains" = "Nenhuma cadeia suportada!"; "wallet_connect.main.unsupported_chains" = "Algumas cadeias não são suportadas!"; @@ -1541,7 +1643,7 @@ Go to Settings - > %@ and allow access to the camera."; "fee_settings.max_fee_rate" = "Taxa Máxima"; "fee_settings.max_fee_rate.info" = "Este é o preço total máximo por gas que o usuário está disposto a pagar. Ela deve cobrir a taxa base da rede e a taxa máxima de prioridade. O valor mostrado aqui é sugerido com base em uma estimativa da taxa base do próximo bloco mais a taxa máxima de prioridade escolhida pelo usuário. A taxa real paga normalmente será menor. Definir isso mais baixo que a taxa base atual irá limitar a taxa paga, mas resultará em períodos de espera mais longos para que a transação seja confirmada, ou até mesmo em uma transação travada."; "fee_settings.tips" = "Taxa de prioridade máxima"; -"fee_settings.tips.info" = "Os usuários pagam taxas de prioridade para incentivar uma transação a ser confirmada mais rapidamente. Às vezes são chamados de tips. A taxa máxima de prioridade é o preço adicional máximo por gas que o usuário está disposto a pagar acima da taxa base. O valor mostrado aqui é sugerido com base nas condições previstas de rede. A taxa de prioridade real normalmente será menor. Definir como zero pode resultar em um longo tempo de espera para que a transação seja confirmada, como é colocado no final da fila de transações pendentes de todos os usuários."; +"fee_settings.tips.info" = "Os usuários pagam taxas de prioridade para incentivar uma transação a ser confirmada mais rapidamente. Às vezes são chamados de dicas. A taxa máxima de prioridade é o preço adicional máximo por gás que o usuário está disposto a pagar acima da taxa base. O valor mostrado aqui é sugerido com base nas condições previstas de rede. A taxa de prioridade real normalmente será menor. Definir como zero pode resultar em um longo tempo de espera para que a transação seja confirmada, como é colocado no final da fila de transações pendentes de todos os usuários."; "fee_settings.errors.insufficient_balance" = "Saldo insuficiente"; "fee_settings.errors.unexpected_error" = "Erro inesperado"; @@ -1567,7 +1669,7 @@ Go to Settings - > %@ and allow access to the camera."; "watch_address.watch_by" = "Assistir Por"; "watch_address.evm_address" = "Endereço EVM"; "watch_address.tron_address" = "Endereço TRON"; -"watch_address.public_key" = "Account xPubKey"; +"watch_address.public_key" = "xPubKey da Conta"; "watch_address.public_key.placeholder" = "Insira a chave pública estendida da conta "; "watch_address.public_key.invalid_key" = "Chave Inválida"; "watch_address.choose_blockchain" = "Escolha Blockchain"; @@ -1680,7 +1782,7 @@ Go to Settings - > %@ and allow access to the camera."; "tron.send.activation_fee" = "Taxa de ativação"; "tron.send.resources_consumed" = "Recursos consumidos"; -"tron.send.bandwidth" = "Bandwidth"; +"tron.send.bandwidth" = "Largura de Banda"; "tron.send.energy" = "Energy"; "tron.send.fee.info" = "O custo estimado de envio de determinada transação na rede. (Sem excluir Energy, Bandwidth e Taxa de Ativação)"; "tron.send.resources_consumed.info" = "A bandwidth é a unidade que mede o tamanho dos bytes de transação armazenados no banco de dados blockchain. Quanto maior a transação, mais recursos de largura de banda serão consumidos.\n\nEnergy é a unidade que mede a quantidade de computação necessária pela máquina virtual TRON para realizar operações específicas na rede TRON.\n\nComo as transações de contrato inteligente exigem recursos de computação para executar, cada transação de contrato inteligente requer o pagamento da taxa de energy."; diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index d5ece57cf3..83a3f2cf49 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Вставить"; "button.resend" = "Отправить повторно"; "button.backup" = "Резервная копия"; +"button.restore" = "Восстановить"; "button.copy" = "Копировать"; "button.retry" = "Повторить"; "button.report" = "Пожаловаться"; @@ -33,11 +34,14 @@ "alert.saved_to_icloud" = "Сохранено в iCloud"; "alert.no_internet" = "Нет интернета"; "alert.wrong_amount" = "Неправильная сумма"; -"alert.no_fee" = "Неправильная комиссия"; +"alert.no_fee" = "Неверная комиссия"; "alert.warning" = "Внимание"; +"alert.notice" = "Уведомление"; "alert.error" = "Ошибка"; "alert.unknown_error" = "Неизвестная ошибка"; "alert.success_action" = "Готово"; +"alert.restored" = "Восстановлено"; + "alert.success" = "Успешно"; "alert.added_to_watchlist" = "Добавлено в избранное"; @@ -46,6 +50,7 @@ "alert.removed_from_wallet" = "Удалено из кошелька"; "alert.already_added_to_wallet" = "Уже добавлено в кошелек"; "alert.not_supported_yet" = "Ещё не поддерживается"; + "alert.copied" = "Скопировано"; "alert.created" = "Создан"; "alert.imported" = "Импортировано"; @@ -61,7 +66,7 @@ "alert.sent" = "Отправлено"; "alert.swapping" = "Обмен"; "alert.swapped" = "Обмен выполнен"; -"alert.approving" = "Подтверждение"; +"alert.approving" = "Разрешение"; "alert.approved" = "Разрешено"; "alert.revoking" = "Отмена"; "alert.revoked" = "Отменен"; @@ -95,6 +100,16 @@ "selector.any" = "Любое"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Немедленно"; +"auto_lock.minute1" = "1 минута"; +"auto_lock.minute5" = "5 минут"; +"auto_lock.minute15" = "15 минут"; +"auto_lock.minute30" = "30 минут"; +"auto_lock.hour1" = "1 час"; + // Access Camera "access_camera.message" = "%@ требуется доступ к вашей камере для сканирования QR-кода. @@ -106,13 +121,15 @@ "restore.title" = "Импорт кошелька"; "restore.advanced" = "Доп. настройки"; -"restore.import_by" = "Через"; +"restore.import_by" = "Импорт по"; + "restore.restore_type.mnemonic" = "Фраза восстановления"; "restore.restore_type.private_key" = "Приватный ключ"; "restore.mnemonic.placeholder" = "Введите фразу восстановления"; "restore.private_key.placeholder" = "Введите приватный ключ EVM, BIP32 Root Key или Account Extended Private Key"; "restore.private_key.invalid_key" = "Неверный ключ"; -"restore_error.mnemonic_word_count" = "Неправильное количество слов. Должно быть 12-24 слова. Вы ввели: %@"; +"restore_error.mnemonic_word_count" = "Неверное количество слов. Должно быть от 12 до 24 слов. Вы ввели: %@"; + "restore.checksum_error" = "Неверная контрольная сумма"; "restore.passphrase" = "Кодовая фраза"; "restore.input.passphrase" = "Кодовая фраза"; @@ -122,27 +139,32 @@ "restore.non_standard_import.description" = "На этой странице представлен специальный механизм восстановления кошелька для пользователей %@ с нестандартным кошельком. Как правило, такие кошельки могли быть созданы в версиях %@ 0.27–0.28 с использованием неанглоязычного списка мнемонических слов и/или специального символа в парольной фразе кошелька (например, диакритического знака). \n\nЕсли вы являетесь затронутым пользователем, то баланс вашего кошелька будет отображаться как 0 после восстановления такого кошелька в версии 0.29 или выше.Эта страница позволит вам восстановить доступ к вашему нестандартному кошельку. После восстановления рекомендуется создать новый кошелек (который будет соответствовать стандарту) и перевести туда средства."; "restore.warning.non_recommended.description" = "Похоже, этот кошелек использует нестандартный символ в списке мнемонических слов и/или парольной фразе. Если вы не видите баланс или транзакции, пожалуйста, прочтите подробности ниже. -\n\nПОЖАЛУЙСТА НАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ."; +\n\nПОЖАЛУЙСТА НАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; + "restore.error.non_standard.description" = "Это нестандартный кошелек.\n\nНАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; // Restore Type "restore_type.title" = "Импорт кошелька"; - "restore_type.recovery.title" = "Фраза восстановления"; "restore_type.cloud.title" = "iCloud"; +"restore_type.file.title" = "из файлов"; "restore_type.cex.title" = "с кошелька биржи"; "restore_type.recovery.description" = "Импорт с помощью фразы восстановления или приватного ключа."; "restore_type.cloud.description" = "Импорт из файла резервной копии в вашем keychain."; +"restore_type.file.description" = "Импортируйте файл резервной копии из локальной папки."; "restore_type.cex.description" = "Подключиться к кошельку на централизованной бирже."; // Restore Cloud -"restore.cloud.title" = "Резерв. копия"; -"restore.cloud.description" = "Выберите резервную копию кошелька, который вы хотите восстановить."; +"restore.cloud.title" = "Выберите резервную копию"; +"restore.cloud.description" = "Выберите файл резервной копии, который вы хотите восстановить."; "restore.cloud.empty" = "Резервные копии не найдены."; +"restore.cloud.wallets" = "Резервные копии кошелька"; "restore.cloud.imported" = "Импортированные кошельки"; +"restore.cloud.app_backups" = "Резервные копии приложений"; + "restore.cloud.password.title" = "Введите пароль"; "restore.cloud.password.placeholder" = "Пароль резервной копии"; @@ -151,11 +173,12 @@ // Restore Cex "restore.cex.title" = "Выберите CEX"; -"restore.cex.description" = "Выберите централизованный обмен, к которому вы хотите подключиться."; +"restore.cex.description" = "Выберите централизованную биржу, к которой вы хотите подключиться."; // Restore Binance -"restore.binance.description" = "Пожалуйста, предоставьте ключ API и секрет API, чтобы связать вашу биржу."; +"restore.binance.description" = "Пожалуйста, предоставьте API ключ и секрет API, чтобы связать вашу биржу."; + "restore.binance.api_key" = "API ключ"; "restore.binance.secret_key" = "Секретный ключ"; "restore.binance.connect" = "Подключиться"; @@ -168,9 +191,9 @@ "coin_settings.title" = "Настройки блокчейна"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (рекомендованный)"; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; "sync_mode.from_blockchain" = "Из блокчейна"; -"blockchain_settings.description" = "Выберите формат адреса для получения средств. При восстановлении существующего кошелька должен быть выбран правильный формат."; +"blockchain_settings.description" = "Выберите формат адреса для получения средств. Правильный формат должен быть выбран при восстановлении существующего кошелька."; // Coin Platforms @@ -186,7 +209,7 @@ // Recovery Phrase "recovery_phrase.title" = "Фраза восстановления"; -"recovery_phrase.warning" = "Никогда ни с кем не делитесь вашей фразой восстановления. Команда %@ никогда не запросит вашу фразу восстановления."; +"recovery_phrase.warning" = "Никогда не делитесь этим ключом с кем-либо. Команда поддержки кошелька %@ никогда не будет запрашивать его."; "recovery_phrase.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; "recovery_phrase.passphrase" = "Кодовая фраза"; "recovery_phrase.copy_warning.title" = "Риск копирования фразы восстановления"; @@ -194,7 +217,8 @@ // EVM Private Key -"evm_private_key.title" = "Приватный Ключ EVM"; +"evm_private_key.title" = "Приватный ключ EVM"; + "evm_private_key.tap_to_show" = "Нажмите, чтобы показать приватный ключ"; // Extended Key @@ -210,7 +234,8 @@ // Backup "backup.title" = "Фраза восстановления"; -"backup.description" = "Напишите эти слова в правильном порядке и храните их в безопасном месте"; +"backup.description" = "Запишите эти слова в правильном порядке и храните их в безопасном месте"; + "backup.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; "backup.passphrase" = "Кодовая фраза"; "backup.verify" = "Подтвердить"; @@ -219,7 +244,8 @@ // Backup Verify Words "backup_verify_words.title" = "Подтвердить"; -"backup_verify_words.description" = "Выберите два запрошенные слова из вашей фразы восстановления"; +"backup_verify_words.description" = "Выберите два запрошенных слова из вашей фразы восстановления кошелька"; + "backup_verify_words.incorrect_word" = "Неверное слово"; // Backup Verify Passphrase @@ -228,13 +254,11 @@ "backup_verify_passphrase.description" = "Введите кодовую фразу"; "backup_verify_passphrase.incorrect_passphrase" = "Неверная кодовая фраза"; -// Backup Required - -"backup_required.title" = "Требуется резервная копия"; - // Backup Prompt -"backup_prompt.title" = "Ручное резервное копирование"; +"backup_prompt.backup_recovery_phrase" = "Резервная копия"; +"backup_prompt.backup_required" = "Необходима резервная копия"; + "backup_prompt.warning" = "Создайте резервную копию вашей фразы восстановления и пароля, которые позволят вам восстановить ваш кошелек, если телефон потерян, украден, сломал и т.д."; "backup_prompt.backup" = "Резервная копия"; "backup_prompt.backup_manual" = "Ручное резервное копирование"; @@ -244,7 +268,7 @@ // Backup to iCloud "backup.cloud.title" = "Резерв. копирование в iCloud"; -"backup.cloud.description" = "Хранилище iCloud является облачным хранилищем, предоставляемым сторонней компанией и доступным через Apple. Важно знать, что ваши данные будут храниться на серверах Apple, а не на ваших личных устройствах. Это означает, что вы доверяете свои данные и передаете безопасность своей информации стороннему сервису."; +"backup.cloud.description" = "iCloud - это облачное хранилище, предоставляемое Apple. Важно знать, что ваши данные будут храниться на серверах Apple, а не на ваших личных устройствах. Это означает, что вы доверяете свои данные и передаете безопасность вашей информации стороннему сервису."; "backup.cloud.terms.item.1" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; @@ -256,7 +280,8 @@ "backup.cloud.name.placeholder" = "Название"; "backup.cloud.password.title" = "Установить пароль"; -"backup.cloud.password.description" = "Установите пароль разблокировки для своей резервной копии. Пароль должен содержать не менее 8 символов и включать как минимум одну строчную букву, заглавную букву, цифру и специальный символ."; +"backup.cloud.password.description" = "Установите пароль разблокировки для вашей резервной копии. Пароль должен содержать как минимум 8 символов и включать хотя бы одну строчную букву, заглавную букву, цифру и специальный символ."; + "backup.cloud.password.highlighted_description" = "Не забудьте этот пароль! Он отличается от вашего пароля для Apple iCloud и не может быть восстановлен или сброшен."; "backup.cloud.password.placeholder" = "Пароль"; "backup.cloud.password.confirm.placeholder" = "Подтвердить"; @@ -272,7 +297,6 @@ "backup.cloud.cant_create_file" = "Не удается сохранить файл в iCloud"; "backup.cloud.cant_delete_file" = "Не удается удалить из iCloud"; "backup.cloud.no_access.title" = "Доступ к iCloud"; -"backup.cloud.no_access.title" = "Доступ к iCloud"; "backup.cloud.no_access.description" = "Для создания резервной копии необходимо предоставить доступ к iCloud памяти."; @@ -289,20 +313,23 @@ "balance.tab_bar_item" = "Баланс"; "balance.send" = "Отправить"; "balance.withdraw" = "Вывод средств"; -"balance.swap" = "Обменять"; +"balance.swap" = "Обмен"; "balance.receive" = "Получить"; -"balance.deposit" = "Получить"; +"balance.deposit" = "Пополнение"; "balance.address" = "Адрес"; "balance.rate_per_coin" = "%@ за %@"; -"balance.syncing" = "Синхронизация..."; +"balance.syncing" = "Идет синхронизация..."; "balance.searching" = "Поиск транзакций..."; +"balance.stopped" = "Остановлено"; "balance.downloading_sapling" = "Загрузка sapling... %d%%"; "balance.downloading_blocks" = "Загрузка блоков"; "balance.scanning_blocks" = "Сканирование блоков"; "balance.enhancing_transactions" = "Улучшение транзакций"; +"wait_for_synchronization" = "Дождитесь синхронизации"; "balance.searching.count" = "%@ tx"; -"balance.syncing_percent" = "Синхронизация... %@"; +"balance.syncing_percent" = "Идет синхронизация... %@"; + "balance.synced_through" = "до %@"; "balance.add_coin" = "Добавить токен"; "balance.invalid_api_key" = "Недействительный ключ API"; @@ -323,7 +350,11 @@ // Token Balance Page "balance.token.locked" = "Заблокировано"; "balance.token.locked.info.title" = "TimeLock"; -"balance.token.locked.info.description" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; +"balance.token.locked.info.description" = "Отправитель отправил эти средства с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткоины уже принадлежат вам, но до истечения срока блокировки вы не сможете потратить их в сети Bitcoin."; +"balance.token.processing" = "В процессе"; +"balance.token.processing.info.title" = "Обрабатываемая сумма"; +"balance.token.processing.info.description" = "Транзакции с этой суммой все еще синхронизируются. Когда они будут подтверждены, эти токены будут доступны для траты"; + "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; @@ -357,7 +388,8 @@ "deposit.qr_code_description.watch" = "Отслеживаемый адрес %@"; "deposit.account" = "Account"; -"deposit.not_active" = "не активен"; +"deposit.not_active" = "Не активен"; + "deposit.not_active.title" = "Неактивный адрес"; "deposit.not_active.tron_description" = "Недавно созданные учетные записи в блокчейне TRON неактивны и не могут быть запрошены или изучены. Они должны быть активированы.\n\nАктивация новой учетной записи в цепочке Tron требует комиссию в размере 1 TRX. Для активации достаточно просто перевести токены TRX или TRC-10 на неактивный адрес аккаунта"; @@ -404,13 +436,14 @@ "send.hodler_locktime_off" = "Выкл."; "send.hodler_error.unsupported_address" = "TimeLock работает только при отправке на платёжные адреса, начинающиеся с 1... (также известных как BIP44 адреса)"; "send.fee_info.title" = "Комиссия"; -"send.fee_info.description" = "Блокчейн требует от пользователей оплаты сетевых сборов при отправке транзакций. Комиссия выше, когда в сети проходит много транзакций.\n\nКошелек %@ оценивает комиссию на основе текущей активности блокчейна и рекомендует оптимальное значение для того, чтобы транзакция была обработана в разумные сроки.\n\nРекомендованная ставка комиссии показана как количество сатоши, которое пользователь должен заплатить за один байт транзакции. Таким образом, общая сумма комиссии зависит от общего размера транзакции, который измеряется в байтах.\n\n\nПользователи могут использовать предусмотренные элементы управления, чтобы увеличить или уменьшить значение ставки комиссии. Изменение ставки комиссии изменяет общую плату за транзакцию, которую заплатит пользователь.\n\n\nУстановка комиссии ниже рекомендуемого значения может привести к тому, что транзакция будет находиться в ожидании в течение нескольких часов или будет отклонена. Чем ниже значение, тем больше времени потребуется для подтверждения транзакции. Для транзакций, где важен приоритет, мы рекомендуем установить более высокую ставку комиссии."; +"send.fee_info.description" = "Блокчейны требуют от пользователей уплачивать сетевые комиссии при отправке транзакций. Комиссии выше, когда в сети происходит больше транзакций.\n\nКошелек %@ оценивает комиссию на основе текущей активности блокчейна и рекомендует оптимальное значение, чтобы транзакция была обработана в разумное время.\n\nРекомендуемая ставка комиссии показана как количество сатоши, которое пользователь должен заплатить за один байт транзакции. Таким образом, общая комиссия зависит от общего размера транзакции, который измеряется в байтах.\n\nПользователи могут использовать предоставленные элементы управления для увеличения или уменьшения значения ставки комиссии. Изменение ставки комиссии влияет на общую комиссию за транзакцию, которую пользователь заплатит.\n\nУстановка ставки комиссии ниже рекомендуемого значения может привести к тому, что транзакция будет ожидать обработки в течение часов или будет отклонена. Чем ниже значение, тем дольше будет проходить подтверждение транзакции. Для транзакций, где важен приоритет, рекомендуется установить более высокую ставку комиссии."; "send.transaction_inputs_outputs_info.title" = "Вводы / выводы транзакций"; -"send.transaction_inputs_outputs_info.description" = "Большинство транзакций с биткоином, а также транзакций с подобными криптовалютами, включая Bitcoin Cash, Dash и Litecoin, генерируют два выхода. Один выход - это сумма, которая поступает получателю, а другой - это изменение, которое возвращается отправителю. То, как большинство кошельков строят транзакции, позволяет третьей стороне легко понять, какой из выходов достался получающей стороне, а какой - сумма сдачи, возвращенная отправителю. Поскольку сумма, возвращенная отправителю, впоследствии используется в будущих транзакциях, связь между этими двумя транзакциями становится очевидной.\n\nВ кошельке %@ реализованы меры, чтобы затруднить кому-либо выяснение того, какой вывод куда идет.\n\nДля пользователей %@ доступны два варианта:"; +"send.transaction_inputs_outputs_info.description" = "Большинство транзакций Bitcoin, а также транзакций в подобных криптовалютах, включая Bitcoin Cash, Dash и Litecoin, создают два выхода. Один выход - это сумма, которая идет получателю, а другой - это выход сдачи, который возвращается отправителю. Способ, которым большинство кошельков строят транзакции, делает легким понимание третьей стороной, какой из выходов отправляется получателю, а какой - сдача, возвращаемая отправителю. Поскольку выход, возвращаемый отправителю, затем используется в будущих транзакциях, становится явным соединение между этими двумя транзакциями.\n\nКошелек %@ реализует меры для усиления защиты от попыток установить, какой выход идет куда.\n\nДля пользователей %@ доступны два варианта:"; "send.transaction_inputs_outputs_info.shuffle.title" = "1. Shuffle"; "send.transaction_inputs_outputs_info.shuffle.description" = "Порядок выхода транзакций меняется случайным образом в каждой транзакции. Иногда изменение может быть первым выводом, иногда - вторым. Если пользователь доверяет разработчику приложения, то рекомендуем использовать этот вариант."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; -"send.transaction_inputs_outputs_info.deterministic.description" = "Существует общепринятый стандарт упорядочивания выходов транзакций (известный как BIP69). В кошельках с открытым исходным кодом этот стандарт гарантирует, что пользователям кошелька не нужно доверять тому, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт является новым, не многие кошельки его еще внедрили. В результате на блокчейне можно увидеть, была ли транзакция отправлена из кошелька, использующего этот стандарт, или нет."; +"send.transaction_inputs_outputs_info.deterministic.description" = "Существует широко признанный стандарт для упорядочивания выходов транзакции, известный как BIP69. В открытых кошельках этот стандарт обеспечивает, чтобы пользователи кошелька не должны были полагаться на то, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт относительно новый, не многие кошельки его уже реализовали. В результате, на блокчейне в некоторых случаях можно узнать, отправлена ли транзакция из кошелька, который использует этот стандарт или нет."; + "send.confirmation.you_send" = "Вы отправляете"; "send.confirmation.to" = "Кому"; @@ -420,23 +453,26 @@ "send.confirmation.account" = "Account"; "send.confirmation.memo" = "Memo"; "send.confirmation.memo_placeholder" = "Memo"; -"send.confirmation.total" = "Всего"; +"send.confirmation.total" = "Итого"; + "send.confirmation.fee" = "Комиссия"; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.slide_to_send" = "Проведите для отправки"; "send.confirmation.sending" = "Отправка"; -"send.confirmation.resend_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее с более высокой комиссией. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; +"send.confirmation.resend_description" = "Это действие попытается аннулировать предыдущую транзакцию, отправив ее повторно с более высокой комиссией. Если исходная транзакция остается в ожидании, когда отправляется новая, существует высокая вероятность (но не гарантия), что она будет аннулирована и заменена. Только одна из этих двух транзакций будет включена в блокчейн."; "send.confirmation.resend" = "Отправить повторно"; -"send.confirmation.cancel_description" = "Это действие аннулирует предыдущую транзакцию, переотправив ее себе как транзакцию с нулевой суммой. Если исходная транзакция ожидает обработки во время отправки новой, то вероятность ее аннулирования и замены довольно высока. Только одна из этих двух транзакций будет включена в блокчейн."; +"send.confirmation.cancel_description" = "Это действие попытается аннулировать предыдущую транзакцию, отправив ее заново как новую транзакцию с нулевой суммой к самому себе. Если исходная транзакция остается в ожидании, когда отправляется новая, существует высокая вероятность (но не гарантия), что она будет аннулирована и заменена. Только одна из этих двух транзакций будет включена в блокчейн."; "send.confirmation.cancel" = "Отменить транзакцию"; -"send.confirmation.nonce" = "Memo"; +"send.confirmation.nonce" = "Nonce"; + "send.confirmation.method" = "Метод"; "send.amount_error.balance" = "Недостаточно средств"; "send.address_error.own_address" = "Невозможно отправить TRX самому себе"; "send.amount_error.maximum_amount" = "Макс. сумма %@"; "send.amount_error.minimum_amount" = "Мин. сумма %@"; "send.amount_error.min_required_balance" = "Мин. обязательный остаток %@"; -"send.amount_warning.coin_needed_for_fee" = "Вы можете оставить некоторую сумму в размере %@ на балансе, чтобы оплачивать будущие транзакции."; +"send.amount_warning.coin_needed_for_fee" = "Рассмотрите возможность оставить %@ на балансе, чтобы иметь возможность оплачивать будущие транзакции."; + "send.token.insufficient_fee_alert" = "Комиссии за транзакцию %@ (%@) взимаются в %@. Вам нужно %@."; "send.fee_settings.amount_error.balance.title" = "Недостаточный баланс"; @@ -458,7 +494,7 @@ // Donate -"donate.list.title" = "Поддержи нас"; +"donate.list.title" = "Пожертвовать с помощью"; "donate.list.get_address" = "Получить адрес"; "donate.list.get_address.title" = "Адреса"; "donate.title" = "Пожертвовать %@"; @@ -471,18 +507,20 @@ // Swap -"swap.title" = "Обменять"; +"swap.title" = "Обмен"; "swap.no_assets" = "У вас нет активов для обмена."; -"swap.you_pay" = "Платите"; +"swap.you_pay" = "Вы платите"; "swap.estimated" = "приблизительно"; "swap.balance" = "Баланс"; "swap.allowance" = "Разрешение"; -"swap.you_get" = "Получите"; +"swap.you_get" = "Вы получите"; + "swap.token" = "Выбрать"; "swap.advanced_settings" = "Настройки обмена"; "swap.proceed_button" = "Далее"; "swap.approve.title" = "Разрешение обмена"; -"swap.approve.description" = "Вы должны предоставить разрешение на использование смарт-контракта для замены данного токена от вашего имени. Это разрешение устанавливает сумму, которая может быть использована смарт-контрактом. Это не повлияет на ваш баланс, но требует небольшой комиссии для выполнения утвержденной транзакции. \n\nХотя это может быть сделано по требованию перед каждой сделкой, предварительно одобрить более высокую сумму для будущих сделок."; +"swap.approve.description" = "Вы должны предоставить разрешение смарт-контракту для обмена заданным токеном от вашего имени. Это разрешение устанавливает сумму, которую может использовать смарт-контракт. Это не влияет на ваш баланс, но требует небольшой комиссии для выполнения транзакции на подтверждение.\n\nХотя это можно сделать по мере необходимости перед каждой сделкой, более дешево предварительно одобрить большую сумму для будущих сделок."; + "swap.approve.amount_error.already_approved" = "У вас уже есть разрешение на эту сумму"; "swap.approving_button" = "Разрешение..."; "swap.revoke_warning" = "Вы можете обменять %@, или вы должны отменить и одобрить новую сумму"; @@ -500,6 +538,7 @@ "swap.price_impact" = "Отклонение от рын. цены"; "swap.maximum_paid" = "Макс. сумма"; "swap.minimum_got" = "Гарантированная сумма"; + "swap.estimate_short" = "(прим.)"; "swap.minimum_short" = "(мин)"; "swap.maximum_short" = "(макс)"; @@ -570,6 +609,7 @@ "market.tab_bar_item" = "Рынки"; "market.title" = "Рынки"; "market.category.overview" = "Обзор"; + "market.category.posts" = "Новости"; "market.category.watchlist" = "Избранное"; "market.total_market_cap" = "Общая капитализация"; @@ -684,14 +724,15 @@ "market.advanced_search.week2" = "2 недели"; "market.advanced_search.month" = "1 месяц"; "market.advanced_search.month6" = "6 месяцев"; -"market.advanced_search.year" = "1 Год"; +"market.advanced_search.year" = "1 год"; "market.advanced_search_results.title" = "Результаты"; "market.global.total_market_cap.title" = "Полная рын. кап."; "market.global.total_market_cap.description" = "Общая рыночная стоимость всех криптовалют"; -"market.global.volume_24h.title" = "Объем торгов (24ч)"; +"market.global.volume_24h.title" = "Объем Торгов (24ч)"; + "market.global.volume_24h.description" = "24-часовой объем крипторынка"; "market.global.defi_cap.title" = "Капитализация DeFi"; @@ -717,6 +758,7 @@ "coin_overview.market_cap" = "Рын. капитализация"; "coin_overview.circulating_supply" = "В обороте"; "coin_overview.total_supply" = "Макс.выпуск"; + "coin_overview.diluted_market_cap" = "Разводненная рын.кап."; "coin_overview.genesis_date" = "Дата старта"; "coin_overview.trading_volume" = "Объем торговли"; @@ -726,9 +768,10 @@ "coin_overview.roi.day14" = "2 недели"; "coin_overview.roi.day30" = "1 месяц"; "coin_overview.roi.day200" = "6 месяцев"; -"coin_overview.roi.year1" = "1 Год"; +"coin_overview.roi.year1" = "1 год"; -"coin_overview.category" = "Категория"; +"coin_overview.overview" = "Обзор"; +"coin_overview.description_warning" = "Это описание, сгенерированное искусственным интеллектом на основе предоставленного справочного материала для данной криптовалюты. Оно может содержать ошибки."; "coin_overview.blockchains" = "Блокчейны"; "coin_overview.bips" = "BIPы"; @@ -763,7 +806,8 @@ "coin_analytics.not_available" = "В этом проекте нет аналитических данных"; "coin_analytics.technical_indicators" = "Технические индикаторы"; -"coin_analytics.technical_indicators.info1" = "Общая оценка: Это общий обзор технических средств актива с учетом различных технических показателей и временных рамок. Он обеспечивает консенсусную точку зрения (купить, продать или нейтраль) на основе этих показателей."; +"coin_analytics.technical_indicators.info1" = "Сводка: Это общий обзор технических характеристик актива, учитывающий различные технические индикаторы и временные рамки. Она предоставляет консенсусное мнение (Покупать, Продавать или Нейтрально) на основе этих индикаторов."; + "coin_analytics.technical_indicators.info2" = "Скользящие средние (MA): Это обычно используемые технические индикаторы, позволяющие сгладить данные о ценах для создания индикатора следующего тренда. Они показывают среднюю цену за определенный период времени. Существует несколько типов MAs:\n\nSimple Moving среднее значение (SMA): Это вычисляет среднее значение выбранного диапазона цен, обычно закрывают цены, по количеству периодов в этом диапазоне.\n\nЭкспоненциальное скользящее среднее (EMA): Это даёт больше веса для последних цен, тем самым реагируя быстрее на последние изменения цен."; "coin_analytics.technical_indicators.info3" = "Осцилляторы: Это технические индикаторы, которые колеблются со временем в диапазоне (выше и ниже центральной линии или между заданными уровнями). Они предназначены для идентификации перекупленных и перепродаваемых условий на рынке. Вот несколько распространенных осцилляторов:\n\nИндекс относительной силы (RSI): Это измеряет скорость и изменение движений цен. Обычно он используется для идентификации перекупленных или перепроданных условий.\n\nДвижение среднего сближения (MACD): Используется для выявления потенциальных сигналов на покупку и продажу. Она запускает технические сигналы, когда она пересекает линии сигнала выше (покупать) или ниже (продавать)."; @@ -773,7 +817,8 @@ "coin_analytics.cex_volume.info1" = "Общий объем торгов по токену на ведущих централизованных биржах за 30-дневный период."; "coin_analytics.cex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих централизованных биржах за 1 год."; "coin_analytics.cex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих централизованных биржах за 30-дневный период."; -"coin_analytics.cex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на децентрализованных биржах за 24ч/7дн/1мес."; +"coin_analytics.cex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на централизованных биржах за последние 24ч/7Д / 1М."; + "coin_analytics.dex_volume" = "Объем DEX"; "coin_analytics.dex_volume_rank" = "Рейтинг объема DEX"; @@ -812,7 +857,8 @@ "coin_analytics.transaction_count.info1" = "Общее количество уникальных транзакций блокчейна с токеном более 30 дней."; "coin_analytics.transaction_count.info2" = "График, отражающий колебания количества транзакций за 1 год."; "coin_analytics.transaction_count.info3" = "Рейтинг токена основан на количестве транзакций с токеном за 30-дневный период."; -"coin_analytics.transaction_count.info4" = "Список всех токенов, ранжированных на основе количества транзакций с интервалом 24ч / 7D / 1М."; +"coin_analytics.transaction_count.info4" = "Список всех токенов, ранжированных на основе количества транзакций с интервалом 24ч / 7Д / 1М."; + "coin_analytics.transaction_count.info5" = "Общее количество токенов, отправленных через блокчейн за 30-дневный период."; "coin_analytics.holders" = "Держатели"; @@ -828,7 +874,7 @@ "coin_analytics.project_tvl" = "Проект TVL"; "coin_analytics.tvl_ratio" = "Рын.кап / Соотношение TVL "; "coin_analytics.project_tvl.info_title" = "Проект TVL (совокупная сумма средств заблокирована)"; -"coin_analytics.project_tvl.info1" = "TVL (или Активы под управлением) в проектных смарт-контрактах."; +"coin_analytics.project_tvl.info1" = "TVL (или активы под управлением) в проектных смарт-контрактах."; "coin_analytics.project_tvl.info2" = "График, отражающий колебания TVL в проектных смарт-контрактах за период, превышающий 1 год."; "coin_analytics.project_tvl.info3" = "Рейтинг токена по текущему TVL."; "coin_analytics.project_tvl.info4" = "Список всех токенов, ранжированных по текущему TVL."; @@ -861,8 +907,7 @@ "coin_analytics.audits.no_reports" = "Нет аудиторских отчетов"; "coin_analytics.last_30d" = "Последние 30 дн."; -"coin_analytics.current" = "текущее"; - +"coin_analytics.current" = "текущий"; "coin_analytics.overall_score" = "Общий балл"; "coin_analytics.overall_score.excellent" = "Отлично"; "coin_analytics.overall_score.good" = "Хорошо"; @@ -975,7 +1020,7 @@ "tx_info.to_hash" = "Кому"; "tx_info.spender" = "Покупатель"; "tx_info.contact_name" = "Имя контакта"; -"tx_info.button_explorer" = "Посмотреть на %@"; +"tx_info.button_explorer" = "Просмотреть на %@"; "tx_info.rate" = "Исторический курс"; "tx_info.options.speed_up" = "Ускорить"; "tx_info.options.cancel" = "Отменить транзакцию"; @@ -990,6 +1035,7 @@ "tx_info.raw_transaction" = "Неподтвержденная транзакция"; "tx_info.memo" = "Memo"; "tx_info.service" = "Сервис"; + "tx_info.view_on" = "Посмотреть на %@"; "tx_info.you_pay" = "Платите"; "tx_info.you_get" = "Получите"; @@ -1003,6 +1049,8 @@ "settings.tab_bar_item" = "Настройки"; "settings.manage_accounts" = "Кошельки"; "settings.blockchain_settings" = "Настройки блокчейна"; +"settings.backup_manager" = "Менеджер резерв. копирования"; + "settings.security" = "Безопасность"; "settings.experimental_features" = "Экспериментальные функции"; "settings.personal_support" = "Персональная поддержка"; @@ -1013,10 +1061,14 @@ "settings.info_subtitle" = "децентрализованное приложение"; "settings.donate.description" = "С вашей поддержкой мы вместе сможем сделать это приложение еще лучше!"; "settings.donate.title" = "Поддержи нас"; +"settings.rate_us" = "Оцените нас"; +"settings.tell_friends" = "Расскажите друзьям"; +"settings.contact_us" = "Свяжитесь с нами"; // Settings -> Base Currency "settings.base_currency.title" = "Базовая валюта"; + "settings.base_currency.other" = "Другое"; "settings.base_currency.disclaimer" = "Отказ от ответственности"; "settings.base_currency.disclaimer.description" = "Данные об обменном курсе предоставлены третьим лицом Coingecko.сom\n\n Приложение %@ Wallet не гарантирует, что эти данные всегда верны и соответствуют рыночным. Шанс на несоответствие повышается при выборе базовой валюты, отличающейся от %@."; @@ -1034,6 +1086,7 @@ // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Account"; + "settings.personal_support.telegram_username.placeholder" = "@username"; "settings.personal_support.description" = "Введите имя аккаунта Telegram, чтобы открыть личный чат поддержки, и мы отправим вам сообщение."; "settings.personal_support.request" = "Запрос"; @@ -1075,9 +1128,134 @@ "blockchain_settings.title" = "Настройки блокчейна"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Менеджер резерв. копирования"; +"backup_app.backup_manager.restore" = "Восстановить резервную копию"; +"backup_app.backup_manager.create" = "Создать новую резерв. копию"; + +"backup_app.backup_type.title" = "Сохранить резервную копию"; +"backup_app.backup_type.cloud" = "в iCloud"; +"backup_app.backup_type.cloud.description" = "Сохранение файла резервной копии в вашем keychain."; +"backup_app.backup_type.file" = "в файлы"; +"backup_app.backup_type.file.description" = "Сохранение файла резервной копии в локальную папку."; + +"backup_app.backup_list.title" = "Файл резервной копии"; +"backup_app.backup_list.description.restore" = "Список содержимого в файле резервной копии."; +"backup_app.backup_list.header.wallets" = "Кошельки"; +"backup_app.backup_list.header.other" = "Other stuff"; +"backup_app.backup_list.other.watch_account.title" = "Просмотр кошелька"; +"backup_app.backup_list.other.watchlist.title" = "Избранное"; +"backup_app.backup_list.other.contacts.title" = "Контакты"; +"backup_app.backup_list.other.blockchain_settings.title" = "Пользовательские RPC"; +"backup_app.backup_list.other.app_settings.title" = "Настройки приложения"; +"backup_app.backup_list.other.app_settings.description" = "Язык, валюта, внешний вид ..."; + +"backup_app.backup.disclaimer.cloud.title" = "Резерв. копирование /nв iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud - это облачный сервис для хранения данных, предоставляемый компанией Apple. Важно знать, что ваши данные резервной копии будут сохранены на серверах Apple."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; +"backup_app.backup.disclaimer.file.title" = "Резерв. копирование в файл"; +"backup_app.backup.disclaimer.file.description" = "Устройства для хранения данных, такие как жесткие диски, USB-накопители, хранилище на смартфонах и др., все подвержены потере из-за физического повреждения, кражи или других непредвиденных обстоятельств."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Я понимаю, что кража или повреждение устройства резервного копирования может привести к потере резервной копии для соответствующего кошелька."; + +"backup.disclaimer.cloud.title" = "Резерв. копирование в iCloud"; +"backup.disclaimer.cloud.description" = "iCloud - это облачный сервис для хранения данных, предоставляемый компанией Apple. Важно знать, что ваши данные резервной копии будут сохранены на серверах Apple."; +"backup.disclaimer.cloud.checkbox_label" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; +"backup.disclaimer.file.title" = "Резерв. копирование в файл"; +"backup.disclaimer.file.description" = "Устройства для хранения данных, такие как жесткие диски, USB-накопители, хранилище на смартфонах и др., все подвержены потере из-за физического повреждения, кражи или других непредвиденных обстоятельств."; +"backup.disclaimer.file.checkbox_label" = "Я понимаю, что кража или повреждение устройства резервного копирования может привести к потере резервной копии для соответствующего кошелька."; +"backup_app.backup.name.title" = "Имя резервной копии"; +"backup_app.backup.name.description" = "Введите имя файла резервной копии."; + +"backup_app.backup.password.title" = "Пароль резервной копии"; +"backup_app.backup.password.description" = "Установите пароль разблокировки для вашей резервной копии. Пароль должен содержать как минимум 8 символов и включать хотя бы одну строчную букву, заглавную букву, цифру и специальный символ."; +"backup_app.backup.password.highlighted_description" = "Этот пароль используется для шифрования файла резервной копии вашего кошелька. Его невозможно восстановить или сбросить в случае утери или забвения."; + +"backup_app.restore_type.title" = "Восстановить"; + +"backup_app.restore.notice.description" = "Это действие перезапишет ваши локальные контакты для платежей, а также копию в iCloud (если таковая имеется)"; +"backup_app.restore.notice.merge" = "Заменить"; + +"backup.password.title" = "Пароль резервной копии"; +"backup.password.description" = "Установите пароль разблокировки для вашей резервной копии. Пароль должен содержать как минимум 8 символов и включать хотя бы одну строчную букву, заглавную букву, цифру и специальный символ."; +"backup.password.highlighted_description" = "Этот пароль используется для шифрования файла резервной копии вашего кошелька. Его невозможно восстановить или сбросить в случае утери или забвения."; + // Settings -> Security "settings_security.title" = "Безопасность"; +"settings_security.enable_passcode" = "Включить код доступа"; +"settings_security.edit_passcode" = "Изменить код"; +"settings_security.disable_passcode" = "Отключить код доступа"; +"settings_security.auto_lock" = "Автоблокировка"; +"settings_security.balance_auto_hide" = "Автоскрытие баланса"; +"settings_security.balance_auto_hide.description" = "Автоматически скрывает баланс при открытии приложения, независимо от предыдущих настроек."; +"settings_security.enable_duress_mode" = "Установить режим Duress"; +"settings_security.edit_duress_passcode" = "Изменить Duress код"; +"settings_security.disable_duress_mode" = "Отключить режим Duress"; +"settings_security.duress_mode.description" = "Специализированный режим, разработанный для обеспечения безопасности выбранных кошельков в условиях принуждения."; + +// Create Passcode + +"create_passcode.title" = "Создать код доступа"; +"create_passcode.description" = "Ваш пароль будет использоваться для разблокировки вашего кошелька"; +"create_passcode.description.biometry" = "Установите код доступа для включения %@"; +"create_passcode.description.duress_mode" = "Установите код для включения режима Duress"; +"create_passcode.confirm_passcode" = "Подтвердить"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Режим Duress"; +"enable_duress_mode.intro.description" = "Этот режим позволяет пользователю установить несколько паролей для разблокировки приложения, при этом заданный пароль отображает только определенные кошельки. Создан для обеспечения безопасности выбранных кошельков в случае принуждения или угроз."; +"enable_duress_mode.intro.notes" = "Примечания"; +"enable_duress_mode.intro.biometrics.description" = "Функция %@ будет работать для разблокировки режима Duress. Вы можете отключить %@ для удобства."; +"enable_duress_mode.intro.passcode_disabling" = "Отключение кода доступа"; +"enable_duress_mode.intro.passcode_disabling.description" = "Отключение пароля в основном режиме автоматически сбросит режим Duress."; +"enable_duress_mode.intro.passcode_change" = "Изменить код доступа"; +"enable_duress_mode.intro.passcode_change.description" = "Изменение пароля в режиме Duress также изменит текущий пароль для этого режима."; + +"enable_duress_mode.select.title" = "Выберите кошельки"; +"enable_duress_mode.select.description" = "Выберите кошельки, которые будут отображаться в режиме Duress."; +"enable_duress_mode.select.wallets" = "Кошельки"; +"enable_duress_mode.select.watch_wallets" = "Просмотр кошелька"; + +"enable_duress_mode.passcode.title" = "Duress код"; +"enable_duress_mode.passcode.description" = "Установите код для режима Duress"; +"enable_duress_mode.passcode.confirm" = "Подтвердить"; + +// Edit Passcode + +"edit_passcode.title" = "Изменить код"; +"edit_passcode.enter_new_passcode" = "Введите новый код"; +"edit_passcode.confirm_new_passcode" = "Подтвердить"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Изменить Duress код"; +"edit_duress_passcode.enter_new_passcode" = "Введите новый код для режима Duress"; +"edit_duress_passcode.confirm_new_passcode" = "Подтвердить"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Недействительное подтверждение"; +"set_passcode.already_used" = "Этот пароль уже используется"; + +// Unlock + +"unlock.title" = "Разблокировать"; +"unlock.passcode" = "Введите код доступа"; +"unlock.biometry_reason" = "Разблокировать кошелек"; +"unlock.attempts_left" = "Осталось попыток: %@"; +"unlock.disabled_until" = "Отключено до: %@"; +"unlock.random" = "Случайный"; + +"security_settings.delete_alert_button" = "Удалить с телефона"; + +"btc_blockchain_settings.restore_source" = "Источник восстановления"; +"btc_blockchain_settings.restore_source.description" = "Выберите источник данных для восстановления кошелька с транзакциями."; +"btc_blockchain_settings.restore_source.alert" = "После изменения источника восстановления, кошелек должен будет повторно синхронизироваться с блокчейном %@."; + +"btc_restore_mode.recommended" = "Рекомендовано"; +"btc_restore_mode.more_private" = "Более приватно"; "settings_security.passcode" = "Код доступа"; "settings_security.change_pin" = "Изменить код"; "settings_security.touch_id" = "Touch ID"; @@ -1123,16 +1301,14 @@ "settings.about_app.title" = "О приложении"; "settings.about_app.app_name" = "%@ кошелек"; -"settings.about_app.description" = "Кошелек %@ создан для тех, кто хочет инвестировать и хранить криптовалюты частным и независимым образом.\n\nЭто одноранговый кошелек, где только пользователь имеет контроль над средствами. Он не собирает никаких данных и обеспечивает независимость пользователя, не привязывая средства пользователя к определенному приложению.\n\nКошелек %@ имеет полностью открытый исходный код, и любой может подтвердить, что приложение работает именно так, как заявлено."; +"settings.about_app.description" = "Кошелек %@ создан для тех, кто ищет возможность инвестировать и хранить криптовалюты в частном и независимом режиме.\n\nЭто некастодиальный кошелек с принципом равноправия, в котором только пользователь имеет полный контроль над своими средствами. Он не собирает никаких данных и сохраняет независимость пользователя, не привязывая средства пользователя к конкретному приложению кошелька.\n\nКошелек %@ полностью открытого исходного кода, и любой может подтвердить, что приложение работает именно так, как утверждается."; "settings.about_app.whats_new" = "Что нового"; "settings.about_app.website" = "Веб-сайт"; -"settings.about_app.contact" = "Связаться с нами"; -"settings.about_app.rate_us" = "Оценить нас"; -"settings.about_app.tell_friends" = "Рассказать друзьям"; // Settings -> About App -> Contact -"settings.contact.title" = "Связаться с нами"; +"settings.contact.title" = "Свяжитесь с нами"; + "settings.contact.via_email" = "по электронной почте"; "settings.contact.via_telegram" = "через Telegram"; @@ -1170,8 +1346,6 @@ "appearance.balance_value.coin_value" = "Токен значение"; "appearance.balance_value.fiat_value" = "Фиатное значение"; -"appearance.balance_auto_hide" = "Автоскрытие баланса"; - // Settings -> Contacts "contacts.title" = "Контакты"; @@ -1192,9 +1366,10 @@ "contacts.contact.address.delete_address" = "Удалить адрес"; "contacts.restore.restored" = "Восстановлено"; -"contacts.restore.parsing_error" = "Файл имеет неправильные данные!"; +"contacts.restore.parsing_error" = "Файл содержит неверные данные!"; "contacts.restore.restore_error" = "Не удалось восстановить контакты"; -"contacts.restore.overwrite_alert.description" = "Это действие перезапишет ваши локальные платежные контакты, а также их копии в iCloud (при наличии таковых)."; +"contacts.restore.overwrite_alert.description" = "Это действие перезапишет ваши локальные контакты для платежей, а также копию в iCloud (если таковая имеется)."; + "contacts.restore.overwrite_alert.replace" = "Заменить"; "contacts.add_address.title" = "Добавить адрес"; @@ -1222,37 +1397,19 @@ "contacts.settings.backup_contacts" = "Резерв. копирование контактов"; "contacts.settings.icloud_sync" = "iCloud синхр."; -"contacts.settings.description" = "Синхронизируйте платежные контакты с iCloud для легкого резервного копирования и доступа к ним на нескольких устройствах."; +"contacts.settings.description" = "Синхронизируйте контакты с iCloud для удобного резервного копирования и доступа с разных устройств."; "contacts.settings.lost_synchronization.description" = "Синхронизация iCloud утеряна. Пожалуйста, проверьте, что iCloud Storage включен на вашем устройстве."; "contacts.settings.merge_disclaimer" = "Ваши локальные платежные контакты будут объединены с данными, хранящимися в iCloud."; "contacts.settings.alert.title" = "iCloud синхр."; "contacts.settings.alert.description" = "Пожалуйста, убедитесь, что iCloud хранилище включено на вашем устройстве."; -"contacts.settings.alert_error.title" = "Ошибка iCloud"; - -// Set PIN - -"set_pin.title" = "Код доступа"; -"set_pin.info" = "Код доступа будет использоваться для разблокировки вашего кошелька"; -"set_pin.wrong_confirmation" = "Код доступа не совпадает. Попробуйте ещё раз"; - -// Edit PIN - -"edit_pin.title" = "Изменить код"; -"edit_pin.unlock_info" = "Текущий код"; -"edit_pin.new_pin_info" = "Новый код"; - -// Unlock PIN - -"unlock_pin.info" = "Код доступа"; -"unlock_pin.cant_save_pin" = "Ой! Мы не можем сохранить ваш код доступа. Пожалуйста, свяжитесь с нами как можно скорее!"; -"unlock_pin.blocked_until" = "Отключено до: %@"; +"contacts.settings.alert_error.title" = "Ошибка iCloud"; // Key Types -"chart.time_duration.day" = "24Ч"; +"chart.time_duration.day" = "24ч"; "chart.time_duration.week" = "7Д"; "chart.time_duration.week2" = "2Н"; "chart.time_duration.month" = "1М"; @@ -1276,9 +1433,9 @@ "chart.performance.week_changes" = "Изменения (за 1Н)"; "chart.performance.month_changes" = "Изменения (за 1М)"; -"chart.about.header" = "О Проекте"; -"chart.about.read_more" = "Подробнее"; -"chart.about.read_less" = "Свернуть"; +"chart.about.read_more" = "Читать далее"; +"chart.about.read_less" = "Скрыть"; + "coin_page.return_of_investments" = "ROI"; @@ -1308,7 +1465,8 @@ // Lock Info "lock_info.title" = "TimeLock"; -"lock_info.text" = "Эти средства были отправлены с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткойны уже ваши, но до истечения периода блокировки, вы не сможете их потратить в сети Bitcoin."; +"lock_info.text" = "Отправитель отправил эти средства с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткоины уже принадлежат вам, но до истечения срока блокировки вы не сможете потратить их в сети Bitcoin."; + // Double Spend Info @@ -1327,9 +1485,9 @@ "intro.unchain_assets.title" = "Непривязанные активы"; "intro.unchain_assets.description" = "Распоряжайтесь активами свободно и без ограничений"; "intro.go_borderless.title" = "Избавьтесь от границ"; -"intro.go_borderless.description" = "Преодолейте барьеры и получите доступ к рынкам"; +"intro.go_borderless.description" = "Обходите условные барьеры и получайте доступ к мировым рынкам."; "intro.stay_private.title" = "Сохраняйте конфиденциальность"; -"intro.stay_private.description" = "Не допускайте утечку личных и финансовых данных"; +"intro.stay_private.description" = "Не раскрывайте свои личные и финансовые данные миру"; // Guides @@ -1341,9 +1499,10 @@ "add_token.title" = "Добавить токен"; "add_token.blockchain" = "Blockchain"; "add_token.already_added" = "Этот токен уже есть в списке"; -"add_token.invalid_contract_address" = "Неверный адрес контракта"; +"add_token.invalid_contract_address" = "Недействительный адрес контракта"; "add_token.invalid_bep2_symbol" = "Неверный символ BEP2"; -"add_token.contract_address_not_found" = "Адрес контракта не найден в %@ блокчейне "; +"add_token.contract_address_not_found" = "Адрес контракта не найден в %@ блокчейне"; + "add_token.bep2_symbol_not_found" = "Символ BEP2 не найден"; "add_token.input_placeholder.contract_address" = "Адрес контракта"; "add_token.input_placeholder.bep2_symbol" = "Символ BEP2"; @@ -1356,9 +1515,7 @@ "wallet_connect.title" = "WalletConnect"; "wallet_connect.error.invalid_url" = "Неверный URL-адрес"; "wallet_connect.url" = "URL"; -"wallet_connect.active_account" = "Активный Кошелек"; -"wallet_connect.address" = "Адрес"; -"wallet_connect.network" = "Сеть"; +"wallet_connect.active_account" = "Активный кошелек"; "wallet_connect.address" = "Адрес"; "wallet_connect.network" = "Сеть"; "wallet_connect.list.pending_requests" = "Ожидающие запросы"; @@ -1374,7 +1531,7 @@ "ethereum_transaction.error.insufficient_balance_with_fee" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; "ethereum_transaction.error.lower_than_base_gas_limit" = "Выбранное значение комиссии слишком низкое и будет отклонено!"; "ethereum_transaction.error.nonce_already_in_block" = "Транзакция уже в блоке!"; -"ethereum_transaction.error.replacement_transaction_underpriced" = "Недостаточно комиссии для замены транзакции"; +"ethereum_transaction.error.replacement_transaction_underpriced" = "Комиссия недостаточна для замены транзакции"; "ethereum_transaction.error.transaction_underpriced" = "Комиссия недостаточна для отправки транзакции"; "ethereum_transaction.error.tips_higher_than_max_fee" = "Максимальное вознаграждение не может быть меньше чаевых, так как максимальное вознаграждение включает чаевые."; "ethereum_transaction.error.reverted" = "Транзакция не может быть выполнена: %@"; @@ -1450,9 +1607,9 @@ "manage_account.backup_recovery_phrase" = "Ручное резервное копирование"; "manage_account.cloud_backup_recovery_phrase" = "Резерв. копирование в iCloud"; "manage_account.cloud_delete_backup_recovery_phrase" = "Удалить резерв. копию из iCloud"; -"manage_account.manual_backup_required" = "Требуется резервное копирование вручную"; +"manage_account.manual_backup_required" = "Требуется ручное создание резерв.копии"; "manage_account.manual_backup_required.description" = "Чтобы безопасно удалить резервную копию в iCloud, вам необходимо сначала сделать резервную копию фразы восстановления вручную."; -"manage_account.manual_backup_required.button" = "Сделать резервную копию"; +"manage_account.manual_backup_required.button" = "Сделать резерв. копию"; "manage_account.unlink" = "Отключить кошелек"; "manage_account.backup.no_backup_yet_description" = "Завершите одну из опций резервного копирования кошелька, чтобы начать использовать кошелек."; "manage_account.backup.has_backup_description" = "Рекомендуется создать ручную резервную копию для каждого кошелька."; @@ -1469,7 +1626,7 @@ // Manage Account -> Private Keys "private_keys.title" = "Приватные ключи"; -"private_keys.evm_private_key" = "Приватный Ключ EVM"; +"private_keys.evm_private_key" = "Приватный ключ EVM"; "private_keys.evm_private_key.description" = "Предоставляет полный контроль над криптовалютами на базе EVM, т.е. Ethereum, Binance Smart Chain и т.д. в пределах соответствующего кошелька."; "private_keys.bip32_root_key" = "BIP32 Root Key"; "private_keys.bip32_root_key.description" = "Предоставляет полный контроль над активами на соответствующем кошельке."; @@ -1509,14 +1666,13 @@ "add_evm_sync_source.name" = "Название"; "add_evm_sync_source.rpc_url" = "RPC URL"; "add_evm_sync_source.basic_auth" = "Базовая авторизация (опц.)"; -"add_evm_sync_source.warning.url_exists" = "RPC Источник с таким url уже существует"; -"add_evm_sync_source.error.invalid_url" = "Введенный url неверен. Допустимый url должен иметь одну из следующих схем: http, https, ws, wss"; +"add_evm_sync_source.warning.url_exists" = "RPC Источник с таким URL уже существует"; +"add_evm_sync_source.error.invalid_url" = "Введенный URL-адрес неверен. Допустимый url должен иметь одну из следующих схем: https, wss"; // Send Settings "evm_send_settings.nonce" = "Nonce транзакции"; -"evm_send_settings.nonce.info" = "Nonce - это уникальное целочисленное значение для транзакции в кошельке пользователя. Обычно оно увеличивается с каждой транзакцией и не нуждается в изменении. Продвинутые пользователи могут установить его равным nonce отложенной транзакции, чтобы отменить и заменить эту транзакцию, при условии, что новая транзакция имеет достаточно большую плату, чтобы старая не была подтверждена вместо нее (например, они могут захотеть ускорить ее подтверждение или полностью изменить параметры транзакции). Когда несколько ожидающих транзакций имеют одинаковый nonce, подтверждается только одна, обычно с наибольшей комиссией. -"; +"evm_send_settings.nonce.info" = "Nonce - это уникальное целочисленное значение для транзакции в кошельке пользователя. Обычно оно увеличивается с каждой транзакцией и не нуждается в изменении. Продвинутые пользователи могут установить его равным nonce отложенной транзакции, чтобы отменить и заменить эту транзакцию, при условии, что новая транзакция имеет достаточно большую плату, чтобы старая не была подтверждена вместо нее (например, они могут захотеть ускорить ее подтверждение или полностью изменить параметры транзакции). Когда несколько ожидающих транзакций имеют одинаковый nonce, подтверждается только одна, обычно с наибольшей комиссией."; "evm_send_settings.nonce.errors.already_in_use" = "Использованый Nonce"; "evm_send_settings.nonce.errors.already_in_use.info" = "Выполненная транзакция с таким nonce уже существует."; @@ -1542,6 +1698,7 @@ "fee_settings.base_fee" = "Базовая комиссия"; "fee_settings.base_fee.info" = "Сетевой протокол определяет базовую стоимость газа для каждого блока, которая называется базовая ставка комиссии. Он варьирует в зависимости от уровня загрузки сети от блока к блоку. В следующем блоке он может увеличиться или уменьшиться не более чем на 12.5%, что делает взимаемый тариф более предсказуемым. Здесь отражена базовая ставка комиссии текущего блока."; + "fee_settings.max_fee_rate" = "Макс. ставка комиссии"; "fee_settings.max_fee_rate.info" = "Это максимальная общая цена газа, которую пользователь готов заплатить. Она должна покрывать базовую тарифную ставку сети и максимальный приоритетный тариф. Указанное здесь значение предлагается на основе оценки суммы базовой тарифной ставки следующего блока и максимального приоритетного тарифа, выбранного пользователем. Фактический размер платёжного тарифа обычно меньше. Снижение настройки текущего базового тарифа ограничит оплачиваемую сумму тарифа, но приведет к удлинению периода ожидания подтверждения транзакции или даже к ее подвисанию."; "fee_settings.tips" = "Макс. приоритет. комиссия"; @@ -1552,7 +1709,7 @@ "fee_settings.errors.insufficient_balance.info" = "Текущий баланс %@ ниже суммы, необходимой для обработки этой транзакции, включая комиссию за транзакцию."; "fee_settings.errors.low_max_fee" = "Низкая комиссия"; "fee_settings.errors.low_max_fee.info" = "Установленная сумма комиссии недостаточно для обработки этой транзакции."; -"fee_settings.errors.nonce_already_in_block" = "Не удается заменить транзакцию"; +"fee_settings.errors.nonce_already_in_block" = "Не удается заменить транзакцию."; "fee_settings.errors.replacement_transaction_underpriced" = "Низкая комиссия на замену транзакции"; "fee_settings.errors.transaction_underpriced" = "Низкая комиссия за транзакцию"; "fee_settings.errors.tips_higher_than_max_fee" = "Слишком низкая макс. комиссия"; @@ -1610,7 +1767,7 @@ "nft_asset.details" = "Подробности"; "nft_asset.details.contract_address" = "Адрес контракта"; "nft_asset.details.token_id" = "ID токена"; -"nft_asset.details.token_standard" = "Стандартный токен"; +"nft_asset.details.token_standard" = "Стандарт Токена"; "nft_asset.details.blockchain" = "Blockchain"; "nft_asset.links" = "Ссылки"; "nft_asset.links.website" = "Веб-сайт"; @@ -1655,13 +1812,13 @@ "subscription_info.title" = "Премиум-функции"; "subscription_info.info1.title" = "Анализ криптовалют"; -"subscription_info.info1.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.info1.text" = "Найдите перспективные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; "subscription_info.info2.title" = "Индикаторы"; -"subscription_info.info2.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.info2.text" = "Найдите перспективные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; "subscription_info.info3.title" = "Персональная поддержка"; -"subscription_info.info3.text" = "Найдите выгодные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; -"subscription_info.get_premium" = "Получите Премиум"; -"subscription_info.already_have" = "У меня уже есть Премиум"; +"subscription_info.info3.text" = "Найдите перспективные возможности, избегайте мошенничества и максимизируйте свою прибыль в динамичном мире криптовалют."; +"subscription_info.get_premium" = "Получите премиум"; +"subscription_info.already_have" = "У меня уже есть премиум"; // Activate Subscription @@ -1673,11 +1830,12 @@ "activate_subscription.activating" = "Активация..."; "activate_subscription.failed_to_activate" = "Не удалось активировать подписку"; "activate_subscription.activated" = "Активировано"; -"activate_subscription.no_subscriptions" = "Адрес вашего кошелька не имеет подписки на премиум-функции. Вам нужно его приобрести, чтобы активировать подписку."; +"activate_subscription.no_subscriptions" = "Ваш адрес кошелька не имеет подписки на премиум-функции. Вам необходимо ее приобрести, чтобы активировать подписку."; // Launch -"launch.failed_to_launch" = "Не удалось запустить приложение из-за внутренней ошибки. Пожалуйста, попробуйте перезапустить приложение или сообщите об ошибке нашей службе поддержки."; +"launch.failed_to_launch" = "Не удалось запустить приложение из-за внутренней ошибки. Попробуйте перезапустить приложение или сообщите об ошибке нашей службе поддержки."; + "launch.failed_to_launch.report" = "Пожаловаться"; // Tron diff --git a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings index 1ae5eebcb1..c49160678f 100644 --- a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "Yapıştır"; "button.resend" = "Yeniden Gönder"; "button.backup" = "Yedekle"; +"button.restore" = "Geri Yükle"; "button.copy" = "Kopyala"; "button.retry" = "Yeniden Dene"; "button.report" = "Şikayet Et"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "Yanlış Tutar"; "alert.no_fee" = "Yanlış Masraf"; "alert.warning" = "Uyarı"; +"alert.notice" = "Bildirim"; "alert.error" = "Hata"; "alert.unknown_error" = "Bilinmeyen Hata"; "alert.success_action" = "Bitti"; +"alert.restored" = "Geri yüklendi "; "alert.success" = "Başarılı"; "alert.added_to_watchlist" = "Added To Watchlist"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "Removed from Wallet"; "alert.already_added_to_wallet" = "Zaten cüzdana eklendi"; "alert.not_supported_yet" = "Henüz Desteklenmiyor"; -"alert.copied" = "Kopyalandı"; "alert.created" = "Oluşturuldu"; "alert.imported" = "Geri yüklendi"; "alert.wallet_added" = "Cüzdan Eklendi"; @@ -95,6 +97,16 @@ "selector.any" = "Herhangi"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "Hemen"; +"auto_lock.minute1" = "1 dakika"; +"auto_lock.minute5" = "5 dakika"; +"auto_lock.minute15" = "15 dakika"; +"auto_lock.minute30" = "30 dakika"; +"auto_lock.hour1" = "1 saat"; + // Access Camera "access_camera.message" = "%@, QR kodunu okuyabilmek için kameranıza erişime ihtiyaç duyuyor. @@ -126,21 +138,24 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; // Restore Type "restore_type.title" = "Cuzdan Yükle"; - "restore_type.recovery.title" = "Kurtarma İfadesinden"; "restore_type.cloud.title" = "iCloud'dan"; +"restore_type.file.title" = "Dosyadan"; "restore_type.cex.title" = "Exchange Wallet'tan"; "restore_type.recovery.description" = "Kurtarma ifadesini veya özel anahtarı kullanarak içe aktarın."; "restore_type.cloud.description" = "Anahtar zincirinizdeki bir yedek dosyadan içe aktarın."; +"restore_type.file.description" = "Yerel klasörünüzden bir yedek dosyası içe aktarın."; "restore_type.cex.description" = "Merkezi borsada bir cüzdana bağlanın."; // Restore Cloud "restore.cloud.title" = "Yedekleme Seç"; -"restore.cloud.description" = "Geri yüklemek istediğiniz cüzdanın yedek kopyasını seçin."; +"restore.cloud.description" = "Geri yüklemek istediğiniz yedekleme dosyasını seçin"; "restore.cloud.empty" = "Yedek bulunamadı."; +"restore.cloud.wallets" = "Cüzdan Yedekleri"; "restore.cloud.imported" = "Ithal cüzdanlar"; +"restore.cloud.app_backups" = "Uygulama Yedekleri"; "restore.cloud.password.title" = "Parolanı Gir"; "restore.cloud.password.placeholder" = "Yedek Şifre"; @@ -226,13 +241,10 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "backup_verify_passphrase.description" = "Parolayı girin"; "backup_verify_passphrase.incorrect_passphrase" = "Yanlış parola"; -// Backup Required - -"backup_required.title" = "Yedekleme Gerekli"; - // Backup Prompt -"backup_prompt.title" = "Manuel Yedekleme"; +"backup_prompt.backup_recovery_phrase" = "Cüzdanı yedekle"; +"backup_prompt.backup_required" = "Yedekleme Gerekli"; "backup_prompt.warning" = "Telefonunuzun kaybolması, çalınması, kırılması vb. durumlarda cüzdanınızı kurtarmanıza olanak sağlayacak kurtarma ifadesinin ve ilgili parolanın yedek bir kopyasını oluşturun."; "backup_prompt.backup" = "Yedekle"; "backup_prompt.backup_manual" = "Manuel Yedekleme"; @@ -243,7 +255,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "backup.cloud.title" = "iCloud'a yedekle"; "backup.cloud.description" = "iCloud depolama, Apple tarafından sağlanan bir üçüncü taraf bulut depolama hizmetidir. Verilerinizin kişisel cihazlarınızda değil, Apple'ın sunucularında saklanacağını bilmek önemlidir. Bu, verilerinizi emanet ettiğiniz ve bilgilerinizin güvenliğini üçüncü bir hizmete devrettiğiniz anlamına gelir."; - "backup.cloud.terms.item.1" = "iCloud'uma erişimi kaybetmenin, ilgili cüzdanın yedeğine erişimi kaybetmeyle sonuçlanacağını anlıyorum."; "backup.cloud.name.title" = "Yedek Adı"; @@ -270,10 +281,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "backup.cloud.cant_create_file" = "Dosya iCloud'a kaydedilemiyor"; "backup.cloud.cant_delete_file" = "iCloud'dan silinemiyor"; "backup.cloud.no_access.title" = "iCloud'a erişim"; -"backup.cloud.no_access.title" = "iCloud'a erişim"; "backup.cloud.no_access.description" = "Yedek oluşturmak için, iCloud depolama alanına erişim sağlamanız gerekmektedir."; - // Errors "error.send.self_transfer" = "Kendine gönderme desteklenmiyor"; @@ -294,10 +303,12 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "balance.rate_per_coin" = "%@ = 1 %@"; "balance.syncing" = "Eşitleniyor..."; "balance.searching" = "İşlemler aranıyor ..."; +"balance.stopped" = "Durduruldu"; "balance.downloading_sapling" = "Sapling indiriliyor... %d%%"; "balance.downloading_blocks" = "Blokları İndirme"; "balance.scanning_blocks" = "Tarama Blokları"; "balance.enhancing_transactions" = "İşlemleri Geliştirme"; +"wait_for_synchronization" = "Senkronizasyonu bekleyin"; "balance.searching.count" = "bakiye.araması.miktar"; "balance.syncing_percent" = "Senkronize ediliyor... %@"; @@ -322,6 +333,9 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "balance.token.locked" = "Kilitli"; "balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "Gönderici bu fonları belirtilen tarihte sona erecek şekilde bir harcama kilidiyle gönderdi.\n\nEndişelenmeyin, alınan Bitcoin'ler zaten sizindir ancak kilitleme süresi sona erene kadar bunları Bitcoin ağında harcayamazsınız."; +"balance.token.processing" = "İşleniyor"; +"balance.token.processing.info.title" = "İşlem Sayısı"; +"balance.token.processing.info.description" = "Bu tutardaki işlemler hâlâ senkronize ediliyor. Ve onaylandığında, bu tokenler harcama için kullanılabilir"; "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; @@ -542,7 +556,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "swap.confirmation.maximum_sent" = "Maksimum Harcama"; "swap.dex_info.description" = "Bu takas etme hizmeti, %@ blok zinciri üzerine inşa edilmiş ve merkezi olmayan jeton takas etme protokolü olan %@ tarafından desteklenmektedir.\n\n%@, tamamen otomatiktir ve hile yapmadan jeton takaslarını güvenilir bir şekilde kolaylaştıran akıllı sözleşmelerle yönetilir."; - "swap.dex_info.header_dex_related" = "%@"; "swap.dex_info.header_allowance" = "Ödenek"; "swap.dex_info.content_allowance" = "Bir borsanın, token takaslarını yürütürken kullanıcının adına harcayabileceği ödenek miktarı. Fiili bir takas işleminin gerçekleşmesinde yeterli ödenek gereklidir."; @@ -726,8 +739,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_overview.roi.day200" = "6 Month"; "coin_overview.roi.year1" = "1 Year"; -"coin_overview.category" = "Category"; - +"coin_overview.overview" = "Overview"; +"coin_overview.description_warning" = "Bu, verilen kripto para birimi için sağlanan referans materyale dayanarak yapay zeka tarafından oluşturulmuş bir açıklamadır. Hatalar içerebilir."; "coin_overview.blockchains" = "Blockchains"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "Coin Types"; @@ -868,11 +881,11 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.overall_score.poor" = "Poor"; "coin_analytics.overall_score.cex_volume" = "The overall score is based on the average daily trading volume on centralized exchanges over the last 7 days."; "coin_analytics.overall_score.dex_volume" = "The overall score is based on the average daily trading volume on decentralized exchanges over the last 7 days."; -"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total avilable liquidity on decentralized exchanges."; +"coin_analytics.overall_score.dex_liquidity" = "The overall score is based on the total available liquidity on decentralized exchanges."; "coin_analytics.overall_score.active_addresses" = "The overall score is based on the average daily active addresses over the last 7 days."; "coin_analytics.overall_score.project_tvl" = "The overall score is based on the total value locked (assets under management) on the project represented by the given token."; "coin_analytics.overall_score.transaction_count" = "The overall score is based on the total value locked (assets under management) on the project represented by the given token."; -"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding respective token."; +"coin_analytics.overall_score.holders" = "The overall score is based on the total number of addresses holding the respective token."; "coin_analytics.rank" = "Rank"; "coin_analytics.30_day_rank" = "30-Day Rank"; @@ -1001,9 +1014,14 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings.tab_bar_item" = "Ayarlar"; "settings.manage_accounts" = "Cüzdanları yönet"; "settings.blockchain_settings" = "Blockchain Ayarları"; +"settings.backup_manager" = "Yedekleme yöneticisi"; "settings.security" = "Güvenlik"; "settings.experimental_features" = "Deneysel"; -"settings.personal_support" = "Personal Support"; +"settings.personal_support" = "Kişisel Destek + + + +"; "settings.base_currency" = "Para birimi"; "settings.language" = "Dil"; "settings.faq" = "FAQ"; @@ -1011,6 +1029,9 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings.info_subtitle" = "decentralized uygulama"; "settings.donate.description" = "Desteğinizle birlikte bu uygulamayı daha da iyi hale getirebiliriz!"; "settings.donate.title" = "Bağış yapmak"; +"settings.rate_us" = "Bizi Değerlendirin"; +"settings.tell_friends" = "Arkadaşlarına bahset"; +"settings.contact_us" = "Bize Ulaşın"; // Settings -> Base Currency @@ -1034,9 +1055,9 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings.personal_support.telegram_username.title" = "Account"; "settings.personal_support.telegram_username.placeholder" = "@username"; "settings.personal_support.description" = "Enter your Telegram account name to open a personal support chat and we'll send message to you."; -"settings.personal_support.request" = "Request"; -"settings.personal_support.requested" = "Requested"; -"settings.personal_support.failed" = "Request failed"; +"settings.personal_support.request" = "İstek"; +"settings.personal_support.requested" = "Talep Edildi"; +"settings.personal_support.failed" = "Talep Başarısız oldu"; "settings.personal_support.need_subscription" = "This feature only for %@ Wallet premium users. More info in our official site."; "settings.personal_support.requested.description" = "You've already requested a private chat, find it on Telegram"; "settings.personal_support.requested.open_telegram" = "Abrir Telegram"; @@ -1073,14 +1094,126 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "blockchain_settings.title" = "Blockchain Ayarları"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "Yedekleme yöneticisi"; +"backup_app.backup_manager.restore" = "Yedeği Geri Yükle"; +"backup_app.backup_manager.create" = "Yeni Yedek Oluştur"; + +"backup_app.backup_type.title" = "Yedeği kaydet"; +"backup_app.backup_type.cloud" = "iCloud'a"; +"backup_app.backup_type.cloud.description" = "Anahtar zincirinizde bir yedek kopya dosyasını kaydetme."; +"backup_app.backup_type.file" = "Dosyalara"; +"backup_app.backup_type.file.description" = "Bir yedek kopya dosyasını yerel klasörünüze kaydetme."; + +"backup_app.backup_list.title" = "Yedek Dosyası"; +"backup_app.backup_list.description.restore" = "Yedek dosyasındaki içeriklerin listesi."; +"backup_app.backup_list.header.wallets" = "Cüzdanlar"; +"backup_app.backup_list.header.other" = "Diğer"; +"backup_app.backup_list.other.watch_account.title" = "Cüzdan İzle"; +"backup_app.backup_list.other.watchlist.title" = "İzleme Listesi"; +"backup_app.backup_list.other.contacts.title" = "Kişiler"; +"backup_app.backup_list.other.blockchain_settings.title" = "Özel RPC"; +"backup_app.backup_list.other.app_settings.title" = "Uygulama Ayarları"; +"backup_app.backup_list.other.app_settings.description" = "Dil, Para Birimi, Görünüm ..."; + +"backup_app.backup.disclaimer.cloud.title" = "iCloud'a yedekle"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud, Apple tarafından sağlanan bir bulut depolama hizmetidir. Yedek verilerinizin Apple'ın sunucularında saklanacağını bilmek önemlidir."; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "iCloud'uma erişimi kaybetmenin, ilgili cüzdanın yedeğine erişimi kaybetmeyle sonuçlanacağını anlıyorum."; +"backup_app.backup.disclaimer.file.title" = "Dosyaya yedekle"; +"backup_app.backup.disclaimer.file.description" = "Depolama cihazları, yani sabit sürücüler, USB sürücüler , akıllı telefondaki depolama vb. fiziksel hasar, hırsızlık veya diğer öngörülemeyen durumlar nedeniyle kayba karşı savunmasızdır."; +"backup_app.backup.disclaimer.file.checkbox_label" = "Bir yedekleme cihazının çalınmasının veya hasar görmesinin, ilgili cüzdanın yedeğinin kaybolmasına neden olacağını anlıyorum."; + +"backup.disclaimer.cloud.title" = "iCloud'a yedekle"; +"backup.disclaimer.cloud.description" = "iCloud, Apple tarafından sağlanan bir bulut depolama hizmetidir. Yedek verilerinizin Apple'ın sunucularında saklanacağını bilmek önemlidir."; +"backup.disclaimer.cloud.checkbox_label" = "iCloud'uma erişimi kaybetmenin, ilgili cüzdanın yedeğine erişimi kaybetmeyle sonuçlanacağını anlıyorum."; +"backup.disclaimer.file.title" = "Dosyaya yedekle"; +"backup.disclaimer.file.description" = "Depolama cihazları, yani sabit sürücüler, USB sürücüler , akıllı telefondaki depolama vb. fiziksel hasar, hırsızlık veya diğer öngörülemeyen durumlar nedeniyle kayba karşı savunmasızdır."; +"backup.disclaimer.file.checkbox_label" = "Bir yedekleme cihazının çalınmasının veya hasar görmesinin, ilgili cüzdanın yedeğinin kaybolmasına neden olacağını anlıyorum."; +"backup_app.backup.name.title" = "Yedek Adı"; +"backup_app.backup.name.description" = "Yedekleme dosyası için ad girin."; + +"backup_app.backup.password.title" = "Yedek Şifre"; +"backup_app.backup.password.description" = "Yedeklemeniz için kilit açma parolasını ayarlayın. En az 8 sembolden oluşmalı ve en az bir küçük harf, büyük harf, sayı ve özel karakter içermelidir."; +"backup_app.backup.password.highlighted_description" = "Bu şifre, cüzdanınızın yedek dosyasını şifrelemek için kullanılır. Kaybolur veya unutulursa kurtarılamaz veya sıfırlanamaz."; + +"backup_app.restore_type.title" = "Geri Yükle"; + +"backup_app.restore.notice.description" = "Bu işlem, cihazdaki ödeme kişilerinizin yanı sıra iCloud kopyasının (varsa) üzerine yazacaktır."; +"backup_app.restore.notice.merge" = "Değiştir"; + +"backup.password.title" = "Yedek Şifre"; +"backup.password.description" = "Yedeklemeniz için kilit açma parolasını ayarlayın. En az 8 sembolden oluşmalı ve en az bir küçük harf, büyük harf, sayı ve özel karakter içermelidir."; +"backup.password.highlighted_description" = "Bu şifre, cüzdanınızın yedek dosyasını şifrelemek için kullanılır. Kaybolur veya unutulursa kurtarılamaz veya sıfırlanamaz."; + // Settings -> Security "settings_security.title" = "Güvenlik"; -"settings_security.passcode" = "Kod"; -"settings_security.change_pin" = "Kod Değiştir"; -"settings_security.touch_id" = "Dokunma Kimliği"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Blockchain Ayarları"; +"settings_security.enable_passcode" = "Giriş kodunu etkinleştir"; +"settings_security.edit_passcode" = "Kod Değiştir"; +"settings_security.disable_passcode" = "Şifre Kodunu Devre Dışı Bırak"; +"settings_security.auto_lock" = "Otomatik kilit"; +"settings_security.balance_auto_hide" = "Denge Otomatik Gizleme"; +"settings_security.balance_auto_hide.description" = "Uygulama her açıldığında, önceki tercihlere bakılmaksızın bakiyeyi otomatik olarak gizler."; +"settings_security.enable_duress_mode" = "Baskı Modunu Ayarla"; +"settings_security.edit_duress_passcode" = "Duress Şifresini Düzenle"; +"settings_security.disable_duress_mode" = "Baskı Modunu Devre Dışı Bırak"; +"settings_security.duress_mode.description" = "Seçilen cüzdanları zorlama altında güvende tutmak için tasarlanmış özel bir mod."; + +// Create Passcode + +"create_passcode.title" = "Şifre Kodu Oluştur"; +"create_passcode.description" = "Şifre, programı açmak ve para göndermek için kullanılacak"; +"create_passcode.description.biometry" = "%@ etkinleştirmek için bir şifre kodu ayarlayın"; +"create_passcode.description.duress_mode" = "Duress Modunu etkinleştirmek için bir şifre kodu ayarlayın"; +"create_passcode.confirm_passcode" = "Onayla"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "Duress Modu"; +"enable_duress_mode.intro.description" = "Bu mod, kullanıcıların sadece belirtilen cüzdanları gösteren istenen şifre kodlarıyla birden fazla uygulama kilidini ayarlamalarına olanak tanır. Seçilen cüzdanları zorlama veya tehditler altında güvende tutmak için tasarlanmıştır."; +"enable_duress_mode.intro.notes" = "Notlar"; +"enable_duress_mode.intro.biometrics.description" = "%@ özelliği Duress Modunu kilidini açmak için çalışacaktır. Kolaylık için %@ özelliğini devre dışı bırakabilirsiniz."; +"enable_duress_mode.intro.passcode_disabling" = "Şifre Kodu Devre Dışı Bırakma"; +"enable_duress_mode.intro.passcode_disabling.description" = "Ana modda şifre kodunu devre dışı bırakmak, Duress Modunu otomatik olarak sıfırlar."; +"enable_duress_mode.intro.passcode_change" = "Şifre Kodu Değiştirme"; +"enable_duress_mode.intro.passcode_change.description" = "Duress Modunda şifre kodunu değiştirmek, o mod için mevcut şifre kodunu da değiştirir."; + +"enable_duress_mode.select.title" = "Cüzdanları Seçin"; +"enable_duress_mode.select.description" = "Duress Modunda gösterilecek cüzdanları seçin."; +"enable_duress_mode.select.wallets" = "Cüzdanlar"; +"enable_duress_mode.select.watch_wallets" = "Cüzdan İzle"; + +"enable_duress_mode.passcode.title" = "Duress Şifre Kodu"; +"enable_duress_mode.passcode.description" = "Duress Modu için bir şifre kodu ayarlayın"; +"enable_duress_mode.passcode.confirm" = "Onayla"; + +// Edit Passcode + +"edit_passcode.title" = "Kod Değiştir"; +"edit_passcode.enter_new_passcode" = "Yeni Şifreyi Girin"; +"edit_passcode.confirm_new_passcode" = "Onayla"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "Duress Şifresini Düzenle"; +"edit_duress_passcode.enter_new_passcode" = "Duress Modu için yeni şifreyi girin"; +"edit_duress_passcode.confirm_new_passcode" = "Onayla"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "Geçersiz onay kodu"; +"set_passcode.already_used" = "Bu şifre zaten kullanılıyor"; + +// Unlock + +"unlock.title" = "Kilidi Aç"; +"unlock.passcode" = "Şifre Girin"; +"unlock.biometry_reason" = "Cüzdanı aç"; +"unlock.attempts_left" = "%@ deneme kaldı"; +"unlock.disabled_until" = "Engellendi: %@"; +"unlock.random" = "Rastgele"; + "security_settings.delete_alert_button" = "Cihazdan sil"; "btc_blockchain_settings.restore_source" = "Geri Yükleme Ayarları"; @@ -1124,9 +1257,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings.about_app.description" = "%@ cüzdan, kripto para birimlerine özel ve bağımsız bir şekilde yatırım yapmak ve depolamak isteyenler için tasarlanmıştır.\n\nBu, fonlar üzerinde yalnızca kullanıcının kontrolüne sahip olduğu, velayet gerektirmeyen, eşler arası bir cüzdandır. Herhangi bir veri toplamaz ve kullanıcının fonlarını belirli bir cüzdan uygulamasına kilitlemeyerek kullanıcıyı bağımsız tutar.\n\n%@ cüzdan tamamen açık kaynaklıdır ve herkes uygulamanın tam olarak iddia ettiği gibi çalıştığını onaylayabilir."; "settings.about_app.whats_new" = "Yenilikler"; "settings.about_app.website" = "Website"; -"settings.about_app.contact" = "Bize Ulaşın"; -"settings.about_app.rate_us" = "Bizi Değerlendirin"; -"settings.about_app.tell_friends" = "Arkadaşlarına bahset"; // Settings -> About App -> Contact @@ -1168,8 +1298,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "appearance.balance_value.coin_value" = "Para değeri"; "appearance.balance_value.fiat_value" = "Fiat Değeri"; -"appearance.balance_auto_hide" = "Denge Otomatik Gizleme"; - // Settings -> Contacts "contacts.title" = "Kişiler"; @@ -1229,25 +1357,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "contacts.settings.alert_error.title" = "iCloud Hatası"; -// Set PIN - -"set_pin.title" = "Kod"; -"set_pin.info" = "Şifre, programı açmak ve para göndermek için kullanılacak"; -"set_pin.wrong_confirmation" = "Şifre uyuşmadı. Tekrar deneyin"; - -// Edit PIN - -"edit_pin.title" = "Kod Değiştir"; -"edit_pin.unlock_info" = "Şimdiki Kod"; -"edit_pin.new_pin_info" = "Yeni Kod"; - -// Unlock PIN - -"unlock_pin.info" = "Kod"; -"unlock_pin.cant_save_pin" = "Eyvah! PİNninizi kaydetemiyoruz, acilen bizimle iletişime geçin!"; -"unlock_pin.blocked_until" = "Engellendi: %@"; - - // Key Types "chart.time_duration.day" = "24S"; @@ -1274,7 +1383,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "chart.performance.week_changes" = "Changes (1W)"; "chart.performance.month_changes" = "Changes (1M)"; -"chart.about.header" = "About"; "chart.about.read_more" = "Read More"; "chart.about.read_less" = "Read Less"; @@ -1357,8 +1465,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "wallet_connect.active_account" = "Aktif Cüzdan"; "wallet_connect.address" = "Adres"; "wallet_connect.network" = "Ağ"; -"wallet_connect.address" = "Adres"; -"wallet_connect.network" = "Ağ"; "wallet_connect.list.pending_requests" = "Bekleyen Talepler"; "wallet_connect.main.no_any_supported_chains" = "Desteklenen zincir yok!"; "wallet_connect.main.unsupported_chains" = "Bazı zincirler desteklenmiyor!"; @@ -1654,7 +1760,11 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "subscription_info.info1.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.info2.title" = "Chart Indicators"; "subscription_info.info2.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; -"subscription_info.info3.title" = "Personal Support"; +"subscription_info.info3.title" = "Kişisel Destek + + + +"; "subscription_info.info3.text" = "Spot Lucrative Opportunities, Avoid Scams, and Maximize Your Profits in the Dynamic World of Cryptocurrency."; "subscription_info.get_premium" = "Get Premium"; "subscription_info.already_have" = "I already have Premium"; diff --git a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings index b5f7d72cbf..db208008cc 100644 --- a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings @@ -12,6 +12,7 @@ "button.paste" = "粘帖"; "button.resend" = "重新发送"; "button.backup" = "备份"; +"button.restore" = "恢复"; "button.copy" = "复制"; "button.retry" = "重试"; "button.report" = "报告"; @@ -35,9 +36,11 @@ "alert.wrong_amount" = "金额有误"; "alert.no_fee" = "错误费用"; "alert.warning" = "警告"; +"alert.notice" = "通知"; "alert.error" = "出错"; "alert.unknown_error" = "未知错误"; "alert.success_action" = "完成"; +"alert.restored" = "恢复"; "alert.success" = "成功"; "alert.added_to_watchlist" = "添加到观察名单"; @@ -46,7 +49,6 @@ "alert.removed_from_wallet" = "已从钱包删除"; "alert.already_added_to_wallet" = "已添加到钱包"; "alert.not_supported_yet" = "目前不支持"; -"alert.copied" = "已复制"; "alert.created" = "已创建"; "alert.imported" = "已恢复"; "alert.wallet_added" = "钱包已添加"; @@ -95,6 +97,16 @@ "selector.any" = "任何"; +"face_id" = "Face ID"; +"touch_id" = "Touch ID"; + +"auto_lock.immediate" = "即时的"; +"auto_lock.minute1" = "1 分钟"; +"auto_lock.minute5" = "5 分钟"; +"auto_lock.minute15" = "15 分钟"; +"auto_lock.minute30" = "30 分钟"; +"auto_lock.hour1" = "1 小时"; + // Access Camera "access_camera.message" = "%@ 需要访问您的相机以扫描二维码。 @@ -126,21 +138,24 @@ // Restore Type "restore_type.title" = "恢复自"; - "restore_type.recovery.title" = "恢复短语"; "restore_type.cloud.title" = "来自 iCloud"; +"restore_type.file.title" = "从文件"; "restore_type.cex.title" = "来自Exchange 钱包"; "restore_type.recovery.description" = "使用恢复短语或私钥导入。"; "restore_type.cloud.description" = "从您的密钥链备份文件中导入。"; +"restore_type.file.description" = "从本地文件夹导入备份文件。"; "restore_type.cex.description" = "在集中交易所上连接到钱包。"; // Restore Cloud "restore.cloud.title" = "选择备份"; -"restore.cloud.description" = "选择想要恢复的钱包备份副本。"; +"restore.cloud.description" = "选择您想要恢复的备份文件。"; "restore.cloud.empty" = "未找到备份。"; +"restore.cloud.wallets" = "钱包备份"; "restore.cloud.imported" = "导入钱包"; +"restore.cloud.app_backups" = "应用备份"; "restore.cloud.password.title" = "请输入密码"; "restore.cloud.password.placeholder" = "备份密码"; @@ -226,13 +241,10 @@ "backup_verify_passphrase.description" = "输入密码"; "backup_verify_passphrase.incorrect_passphrase" = "密码不正确"; -// Backup Required - -"backup_required.title" = "需要备份"; - // Backup Prompt -"backup_prompt.title" = "进行手动备份"; +"backup_prompt.backup_recovery_phrase" = "备份钱包"; +"backup_prompt.backup_required" = "需要备份"; "backup_prompt.warning" = "创建恢复短语和相关密码的备份副本,允许您在手机丢失时恢复钱包, • 盗窃、故障等问题"; "backup_prompt.backup" = "备份"; "backup_prompt.backup_manual" = "进行手动备份"; @@ -243,7 +255,6 @@ "backup.cloud.title" = "备份到iCloud"; "backup.cloud.description" = "iCloud 存储是 Apple 提供的第三方云存储服务。请注意,您的数据将存储在 Apple 的服务器上,而不是您的个人设备上。这意味着您正在委托您的数据并将信息的安全性移交给第三方服务。"; - "backup.cloud.terms.item.1" = "我了解无法访问我的 iCloud 将导致无法访问相应钱包的备份。"; "backup.cloud.name.title" = "备份名称"; @@ -270,10 +281,8 @@ "backup.cloud.cant_create_file" = "无法将文件保存到iCloud。"; "backup.cloud.cant_delete_file" = "无法从 iCloud 删除"; "backup.cloud.no_access.title" = "访问 iCloud"; -"backup.cloud.no_access.title" = "访问 iCloud"; "backup.cloud.no_access.description" = "要创建备份,您需要提供访问 iCloud 存储。"; - // Errors "error.send.self_transfer" = "不支持发送给自己"; @@ -294,10 +303,12 @@ "balance.rate_per_coin" = "%@ / %@"; "balance.syncing" = "正在同步..."; "balance.searching" = "正在搜索交易…..."; +"balance.stopped" = "已停止"; "balance.downloading_sapling" = "正在下载保存... %d%%"; "balance.downloading_blocks" = "正在下载块"; "balance.scanning_blocks" = "扫描块"; "balance.enhancing_transactions" = "加强交易"; +"wait_for_synchronization" = "等待同步"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "正在同步... %@"; @@ -322,6 +333,9 @@ "balance.token.locked" = "锁住"; "balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "汇款人通过将在所显示日期失效的消费锁定汇出这些资金。\n\n无需担心,收到的比特币已经属于您,但在锁定期限结束前,您无法在比特币网络上消费它们。"; +"balance.token.processing" = "正在处理"; +"balance.token.processing.info.title" = "处理金额"; +"balance.token.processing.info.description" = "此金额的交易仍然在同步。当它们被确认时,这些代币将用于支出"; "balance.token.staked" = "已关注"; "balance.token.staked.info.title" = "关注的标题"; "balance.token.staked.info.description" = "相关的描述文本"; @@ -542,7 +556,6 @@ "swap.confirmation.maximum_sent" = "最大支出"; "swap.dex_info.description" = "此兑换服务由基于%@区块链的分散式代币兑换协议%@提供。\n\n%@由不存在任何欺诈手段、以可靠方式推动代币兑换的智能合约来实现完全自动化并管理。"; - "swap.dex_info.header_dex_related" = "%@关联"; "swap.dex_info.header_allowance" = "补贴"; "swap.dex_info.content_allowance" = "当执行令牌交换时,交易所可以代表用户花费的金额。 在进行实际互换交易之前,需要先确定足够的备抵。"; @@ -726,8 +739,8 @@ "coin_overview.roi.day200" = "6月"; "coin_overview.roi.year1" = "1 年"; -"coin_overview.category" = "分类"; - +"coin_overview.overview" = "行情"; +"coin_overview.description_warning" = "这是一个基于给定的加密货币所提供的参考材料生成的 AI 描述。它可能包含错误。"; "coin_overview.blockchains" = "区块链"; "coin_overview.bips" = "BIPs"; "coin_overview.coin_types" = "代币平台选择"; @@ -770,7 +783,7 @@ "coin_analytics.cex_volume_rank.description" = "对中心化交易所的代币按交易量排名的代币。"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; -"coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; +"coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info4" = "List of all tokens ranked based on trading volume on centralized exchanges over 24H / 7D / 1M intervals."; "coin_analytics.dex_volume" = "DEX交易量"; @@ -800,16 +813,16 @@ "coin_analytics.active_addresses_rank.description" = "按与代币交易的唯一地址的数量排名的代币。"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; -"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; -"coin_analytics.active_addresses.info4" = "Token's rank based on the number of active wallets transacting with the token 30-day period."; +"coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over a 30-day period."; +"coin_analytics.active_addresses.info4" = "Token's rank is based on the number of active wallets transacting with the token during a 30-day period."; "coin_analytics.active_addresses.info5" = "List of all tokens ranked based on the number of daily active addresses transacting with the token over 24h / 7D / 1M intervals."; "coin_analytics.transaction_count" = "交易数"; "coin_analytics.transaction_count_rank" = "Tx计数排名"; -"coin_analytics.transaction_count_rank.description" = "按区块链上交易的数量排名的代币。"; -"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; +"coin_analytics.transaction_count_rank.description" = "Tokens are ranked by a number of transactions on a blockchain."; +"coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with tokens over a 30-day period."; "coin_analytics.transaction_count.info2" = "显示1年周期的交易计数变化的图表。"; -"coin_analytics.transaction_count.info3" = "基于30天周期与代币交易的数量的代币排名。"; +"coin_analytics.transaction_count.info3" = "Token's rank is based on the number of transactions within the token 30-day period."; "coin_analytics.transaction_count.info4" = "基于24时 / 7天 / 1月间隔的交易数量排名的所有代币的列表。"; "coin_analytics.transaction_count.info5" = "30天周期内在区块链上传输的代币总数。"; @@ -826,19 +839,19 @@ "coin_analytics.project_tvl" = "项目TVL"; "coin_analytics.tvl_ratio" = "市值 / TVL比率"; "coin_analytics.project_tvl.info_title" = "项目TVL(锁定总价值)"; -"coin_analytics.project_tvl.info1" = "Total-Value-Locked (or Assets Under Management) in the project's smart contracts."; +"coin_analytics.project_tvl.info1" = "Total-value-locked (or Assets Under Management) in the project's smart contracts."; "coin_analytics.project_tvl.info2" = "Chart showing variation Total-Value-Locked in project's smart contracts over 1 year period."; -"coin_analytics.project_tvl.info3" = "基于当前锁定总价值的代币排名。"; +"coin_analytics.project_tvl.info3" = "Token's rank is based on current Total-Value-Locked."; "coin_analytics.project_tvl.info4" = "基于当前锁定总价值排名的所有代币的列表。"; "coin_analytics.project_tvl.info5" = "Market Cap / TVL ratio for the project."; "coin_analytics.project_fee" = "项目费用"; "coin_analytics.project_fee_rank" = "项目收费等级"; -"coin_analytics.project_fee_rank.description" = "按各项目产生的费用排列的代币。收取费用的方式因项目而异。"; +"coin_analytics.project_fee_rank.description" = "Tokens are ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; "coin_analytics.project_revenue" = "项目收入"; "coin_analytics.project_revenue_rank" = "项目收入排名"; -"coin_analytics.project_revenue_rank.description" = "按通过如质押或代币销毁等机制为持有者生成的收入排名的代币。"; +"coin_analytics.project_revenue_rank.description" = "Tokens are ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; "coin_analytics.other_data" = "其它数据"; @@ -868,7 +881,7 @@ "coin_analytics.overall_score.poor" = "差的"; "coin_analytics.overall_score.cex_volume" = "总的分数是根据过去7天中央交易所的平均每日交易量计算的。"; "coin_analytics.overall_score.dex_volume" = "总的分数是根据过去7天分散交易所的平均每日交易量计算的。"; -"coin_analytics.overall_score.dex_liquidity" = "总分数是根据分散交易所的可动用流动资金总额计算的。"; +"coin_analytics.overall_score.dex_liquidity" = "总分数是根据分散交易所的可用流动资金总额计算的。"; "coin_analytics.overall_score.active_addresses" = "总得分是根据过去7天中每日平均活动地址计算的。"; "coin_analytics.overall_score.project_tvl" = "总体分数是根据给定令牌所代表的项目的总值锁定(管理中的资产)计算的。"; "coin_analytics.overall_score.transaction_count" = "总的分数是根据过去7天的平均每日交易数计算的。"; @@ -1001,6 +1014,7 @@ "settings.tab_bar_item" = "设置"; "settings.manage_accounts" = "管理钱包"; "settings.blockchain_settings" = "区块链设置"; +"settings.backup_manager" = "备份管理器"; "settings.security" = "安全中心"; "settings.experimental_features" = "实验性功能"; "settings.personal_support" = "个人支持"; @@ -1011,6 +1025,9 @@ "settings.info_subtitle" = "分布式应用程序"; "settings.donate.description" = "在你的支持下,我们可以使这个应用更好!"; "settings.donate.title" = "捐助"; +"settings.rate_us" = "评价我们"; +"settings.tell_friends" = "分享给好友"; +"settings.contact_us" = "联系我们"; // Settings -> Base Currency @@ -1073,14 +1090,126 @@ "blockchain_settings.title" = "区块链设置"; +// Settings -> Backup Manager + +"backup_app.backup_manager.title" = "备份管理器"; +"backup_app.backup_manager.restore" = "还原备份"; +"backup_app.backup_manager.create" = "创建新备份"; + +"backup_app.backup_type.title" = "保存备份"; +"backup_app.backup_type.cloud" = "到 iCloud"; +"backup_app.backup_type.cloud.description" = "在您的密钥链中保存备份拷贝文件。"; +"backup_app.backup_type.file" = "到文件"; +"backup_app.backup_type.file.description" = "保存备份文件到您的本地文件夹。"; + +"backup_app.backup_list.title" = "备份文件"; +"backup_app.backup_list.description.restore" = "备份文件中的内容列表。"; +"backup_app.backup_list.header.wallets" = "钱包"; +"backup_app.backup_list.header.other" = "其他"; +"backup_app.backup_list.other.watch_account.title" = "观察地址"; +"backup_app.backup_list.other.watchlist.title" = "关注"; +"backup_app.backup_list.other.contacts.title" = "联系人"; +"backup_app.backup_list.other.blockchain_settings.title" = "自定义 RPC"; +"backup_app.backup_list.other.app_settings.title" = "应用设置"; +"backup_app.backup_list.other.app_settings.description" = "语言、 货币、 外观..."; + +"backup_app.backup.disclaimer.cloud.title" = "备份到iCloud"; +"backup_app.backup.disclaimer.cloud.description" = "iCloud 是苹果提供的云存储服务。知道您的备份数据将保存在苹果服务器上是很重要的。"; +"backup_app.backup.disclaimer.cloud.checkbox_label" = "我了解无法访问我的 iCloud 将导致无法访问相应钱包的备份。"; +"backup_app.backup.disclaimer.file.title" = "备份至文件"; +"backup_app.backup.disclaimer.file.description" = "存储设备包括硬盘驱动器、USB驱动器以及智能手机上的存储等。由于有形损害、盗窃或其他无法预见的情况,所有人都容易遭受损失。"; +"backup_app.backup.disclaimer.file.checkbox_label" = "我的理解是,备份设备被盗或损坏将导致备份丢失,进而导致钱包丢失。"; + +"backup.disclaimer.cloud.title" = "备份到iCloud"; +"backup.disclaimer.cloud.description" = "iCloud 是苹果提供的云存储服务。知道您的备份数据将保存在苹果服务器上是很重要的。"; +"backup.disclaimer.cloud.checkbox_label" = "我了解无法访问我的 iCloud 将导致无法访问相应钱包的备份。"; +"backup.disclaimer.file.title" = "备份至文件"; +"backup.disclaimer.file.description" = "存储设备包括硬盘驱动器、USB驱动器以及智能手机上的存储等。由于有形损害、盗窃或其他无法预见的情况,所有人都容易遭受损失。"; +"backup.disclaimer.file.checkbox_label" = "我的理解是,备份设备被盗或损坏将导致备份丢失,进而导致钱包丢失。"; +"backup_app.backup.name.title" = "备份名称"; +"backup_app.backup.name.description" = "请输入备份文件的名称。"; + +"backup_app.backup.password.title" = "备份密码"; +"backup_app.backup.password.description" = "为您的备份设置解锁密码。密码长度必须至少为8个字符,并且包含至少一个小写字母、大写字母、数字和特殊字符。"; +"backup_app.backup.password.highlighted_description" = "此密码用于加密您的钱包备份文件。一旦丢失或遗忘,将无法恢复或重置。"; + +"backup_app.restore_type.title" = "恢复自"; + +"backup_app.restore.notice.description" = "此操作将覆盖您的本地付款联系人以及它的 iCloud 副本(如有)。"; +"backup_app.restore.notice.merge" = "替换"; + +"backup.password.title" = "备份密码"; +"backup.password.description" = "为您的备份设置解锁密码。密码长度必须至少为8个字符,并且包含至少一个小写字母、大写字母、数字和特殊字符。"; +"backup.password.highlighted_description" = "此密码用于加密您的钱包备份文件。一旦丢失或遗忘,将无法恢复或重置。"; + // Settings -> Security "settings_security.title" = "安全中心"; -"settings_security.passcode" = "密码"; -"settings_security.change_pin" = "编辑密码"; -"settings_security.touch_id" = "指纹识别"; -"settings_security.face_id" = "面部识别"; -"settings_security.blockchain_settings" = "区块链设置"; +"settings_security.enable_passcode" = "启用密码"; +"settings_security.edit_passcode" = "编辑密码"; +"settings_security.disable_passcode" = "禁用密码"; +"settings_security.auto_lock" = "自动锁定"; +"settings_security.balance_auto_hide" = "自动隐藏余额"; +"settings_security.balance_auto_hide.description" = "每次打开应用程序时自动隐藏平衡,不管先前的偏好设置。"; +"settings_security.enable_duress_mode" = "设置压力模式"; +"settings_security.edit_duress_passcode" = "编辑货币密码"; +"settings_security.disable_duress_mode" = "禁用压力模式"; +"settings_security.duress_mode.description" = "专用模式用于在胁迫下使选定的钱包安全。"; + +// Create Passcode + +"create_passcode.title" = "创建密码"; +"create_passcode.description" = "您的密码将用于解锁钱包和发送资金"; +"create_passcode.description.biometry" = "设置密码以启用 %@"; +"create_passcode.description.duress_mode" = "设置密码以启用压力模式"; +"create_passcode.confirm_passcode" = "确认"; + +// Enable Duress Mode + +"enable_duress_mode.intro.title" = "服用模式"; +"enable_duress_mode.intro.description" = "此模式允许用户设置多个解锁应用密码,当需要的密码只显示指定的钱包。 设计用来使选定的钱包在胁迫或威胁下安全。"; +"enable_duress_mode.intro.notes" = "注"; +"enable_duress_mode.intro.biometrics.description" = "%@ 功能可以解锁时尚模式。您可以为方便而禁用 %@。"; +"enable_duress_mode.intro.passcode_disabling" = "密码已禁用"; +"enable_duress_mode.intro.passcode_disabling.description" = "在主模式下禁用密码将自动重置压力模式。"; +"enable_duress_mode.intro.passcode_change" = "密码更改"; +"enable_duress_mode.intro.passcode_change.description" = "更改时尚模式中的密码也会更改当前模式下的密码。"; + +"enable_duress_mode.select.title" = "选择钱包"; +"enable_duress_mode.select.description" = "选择将显示在时尚模式中的钱包。"; +"enable_duress_mode.select.wallets" = "钱包"; +"enable_duress_mode.select.watch_wallets" = "观察地址"; + +"enable_duress_mode.passcode.title" = "密码错误"; +"enable_duress_mode.passcode.description" = "设置口令模式密码"; +"enable_duress_mode.passcode.confirm" = "确认"; + +// Edit Passcode + +"edit_passcode.title" = "编辑密码"; +"edit_passcode.enter_new_passcode" = "输入新密码"; +"edit_passcode.confirm_new_passcode" = "确认"; + +// Edit Duress Passcode + +"edit_duress_passcode.title" = "编辑货币密码"; +"edit_duress_passcode.enter_new_passcode" = "输入新的密码来复位模式"; +"edit_duress_passcode.confirm_new_passcode" = "确认"; + +// Set Passcode + +"set_passcode.invalid_confirmation" = "无效确认"; +"set_passcode.already_used" = "此密码已被使用"; + +// Unlock + +"unlock.title" = "解锁​​​​"; +"unlock.passcode" = "输入密码"; +"unlock.biometry_reason" = "解锁钱包"; +"unlock.attempts_left" = "剩余尝试次数: %@"; +"unlock.disabled_until" = "禁用至: %@"; +"unlock.random" = "随机"; + "security_settings.delete_alert_button" = "从手机中删除"; "btc_blockchain_settings.restore_source" = "恢复参数"; @@ -1124,9 +1253,6 @@ "settings.about_app.description" = "%@ 钱包是为那些寻求投资和以私人和独立方式存储加密货币的人建造的。\n\n它是一个非保管的、对等的钱包,在那里只有用户能够控制资金。 它不收集任何数据,并且通过不将用户的资金锁定到特定的钱包应用程序来保持用户独立。\n\n %@ 钱包完全开源,任何人都可以确认应用程序的工作完全如其所声称的那样正常。"; "settings.about_app.whats_new" = "新增内容"; "settings.about_app.website" = "网站"; -"settings.about_app.contact" = "联系我们"; -"settings.about_app.rate_us" = "评价我们"; -"settings.about_app.tell_friends" = "分享给好友"; // Settings -> About App -> Contact @@ -1168,8 +1294,6 @@ "appearance.balance_value.coin_value" = "币值"; "appearance.balance_value.fiat_value" = "纤维值"; -"appearance.balance_auto_hide" = "自动隐藏余额"; - // Settings -> Contacts "contacts.title" = "联系人"; @@ -1229,25 +1353,6 @@ "contacts.settings.alert_error.title" = "iCloud错误"; -// Set PIN - -"set_pin.title" = "密码"; -"set_pin.info" = "您的密码将用于解锁钱包和发送资金"; -"set_pin.wrong_confirmation" = "密码不匹配。请重试。"; - -// Edit PIN - -"edit_pin.title" = "编辑密码"; -"edit_pin.unlock_info" = "当前密码"; -"edit_pin.new_pin_info" = "新密码"; - -// Unlock PIN - -"unlock_pin.info" = "密码"; -"unlock_pin.cant_save_pin" = "哎哟!我们无法保存您的密码,请尽快与我们联系,谢谢。"; -"unlock_pin.blocked_until" = "禁用至: %@"; - - // Key Types "chart.time_duration.day" = "24小时"; @@ -1274,7 +1379,6 @@ "chart.performance.week_changes" = "变更(1周)"; "chart.performance.month_changes" = "变更(1月)"; -"chart.about.header" = "关于"; "chart.about.read_more" = "查看更多"; "chart.about.read_less" = "关闭"; @@ -1357,8 +1461,6 @@ "wallet_connect.active_account" = "激活钱包"; "wallet_connect.address" = "地址"; "wallet_connect.network" = "網絡"; -"wallet_connect.address" = "地址"; -"wallet_connect.network" = "網絡"; "wallet_connect.list.pending_requests" = "待处理请求"; "wallet_connect.main.no_any_supported_chains" = "没有任何支持的链!"; "wallet_connect.main.unsupported_chains" = "一些链是不支持的!"; From c81093b129fa282185f2ed27a55418aef6cfd845 Mon Sep 17 00:00:00 2001 From: ant013 Date: Fri, 13 Oct 2023 14:33:38 +0600 Subject: [PATCH 58/63] Make ui-fixes for app backup feature --- .../RestoreCloudViewController.swift | 7 ++-- .../RestoreFileConfigurationModule.swift | 1 + ...storeFileConfigurationViewController.swift | 13 ++++++- .../RestoreFileConfigurationViewModel.swift | 17 ++++++++- .../BackupDisclaimerView.swift | 2 +- .../Backup/BackupList/BackupListView.swift | 38 ++++++++++--------- .../en.lproj/Localizable.strings | 2 +- 7 files changed, 54 insertions(+), 26 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift index e119ec7eb6..2d2fbf8531 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCloud/RestoreCloudViewController.swift @@ -174,13 +174,12 @@ class RestoreCloudViewController: ThemeViewController { extension RestoreCloudViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { - guard !walletViewItem.isEmpty else { + guard !walletViewItem.isEmpty || !fullBackupViewItem.isEmpty else { return [] } - var sections = [ - descriptionSection, - ] + var sections = [ descriptionSection ] + if !walletViewItem.notImported.isEmpty { sections.append( section(id: "not_imported", headerTitle: "restore.cloud.wallets".localized, viewItems: viewModel.walletViewItem.notImported) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift index 2f0430fd91..0c74369fb6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationModule.swift @@ -5,6 +5,7 @@ class RestoreFileConfigurationModule { let viewModel = RestoreFileConfigurationViewModel( cloudBackupManager: App.shared.cloudBackupManager, appBackupProvider: App.shared.appBackupProvider, + contactBookManager: App.shared.contactManager, rawBackup: rawBackup ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift index c99e24a21e..1143d7943c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewController.swift @@ -52,6 +52,13 @@ class RestoreFileConfigurationViewController: KeyboardAwareViewController { restoreButton.addTarget(self, action: #selector(onTapRestore), for: .touchUpInside) restoreButton.set(style: .yellow) + viewModel.showMergeAlertPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.showMergeAlert() + } + .store(in: &cancellables) + viewModel.finishedPublisher .receive(on: DispatchQueue.main) .sink { [weak self] success in @@ -75,6 +82,10 @@ class RestoreFileConfigurationViewController: KeyboardAwareViewController { } @objc private func onTapRestore() { + viewModel.onTapRestore() + } + + private func showMergeAlert() { let viewController = BottomSheetModule.viewController( image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeJacob)), title: "alert.notice".localized, @@ -83,7 +94,7 @@ class RestoreFileConfigurationViewController: KeyboardAwareViewController { ], buttons: [ .init(style: .red, title: "backup_app.restore.notice.merge".localized, actionType: .afterClose) { [weak self] in - self?.viewModel.onTapRestore() + self?.viewModel.restore() }, .init(style: .transparent, title: "button.cancel".localized, actionType: .afterClose), ] diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift index 10a56c7e9a..e7e96509a5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestoreFileConfiguration/RestoreFileConfigurationViewModel.swift @@ -4,13 +4,16 @@ import Combine class RestoreFileConfigurationViewModel { private let cloudBackupManager: CloudBackupManager private let appBackupProvider: AppBackupProvider + private let contactBookManager: ContactBookManager private let rawBackup: RawFullBackup + private let showMergeAlertSubject = PassthroughSubject() private let finishedSubject = PassthroughSubject() - init(cloudBackupManager: CloudBackupManager, appBackupProvider: AppBackupProvider, rawBackup: RawFullBackup) { + init(cloudBackupManager: CloudBackupManager, appBackupProvider: AppBackupProvider, contactBookManager: ContactBookManager, rawBackup: RawFullBackup) { self.cloudBackupManager = cloudBackupManager self.appBackupProvider = appBackupProvider + self.contactBookManager = contactBookManager self.rawBackup = rawBackup } @@ -61,10 +64,22 @@ extension RestoreFileConfigurationViewModel { } func onTapRestore() { + if contactBookManager.state.data?.contacts.isEmpty ?? true { + restore() + } else { + showMergeAlertSubject.send() + } + } + + func restore() { appBackupProvider.restore(raw: rawBackup) finishedSubject.send(true) } + var showMergeAlertPublisher: AnyPublisher { + showMergeAlertSubject.eraseToAnyPublisher() + } + var finishedPublisher: AnyPublisher { finishedSubject.eraseToAnyPublisher() } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift index 2193670173..840c33584a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift @@ -6,7 +6,7 @@ struct BackupDisclaimerView: View { @ObservedObject var viewModel: BackupAppViewModel var onDismiss: (() -> Void)? - @State var isOn: Bool = true + @State var isOn: Bool = false var body: some View { let backupDisclaimer = (viewModel.destination ?? .local).backupDisclaimer diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift index f4e194cbb7..5bd429518a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift @@ -10,28 +10,30 @@ struct BackupListView: View { ThemeView { BottomGradientWrapper { VStack(spacing: .margin24) { - VStack(spacing: 0) { - ListSectionHeader(text: "backup_app.backup_list.header.wallets".localized) + if !viewModel.accountItems.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "backup_app.backup_list.header.wallets".localized) - ListSection { - ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppModule.AccountItem) in - if viewModel.selected[item.id] != nil { - let selected = binding(for: item.accountId) + ListSection { + ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppModule.AccountItem) in + if viewModel.selected[item.id] != nil { + let selected = binding(for: item.accountId) - ClickableRow(action: { - viewModel.toggle(item: item) - }) { - HStack { - AccountView(item: item) + ClickableRow(action: { + viewModel.toggle(item: item) + }) { + HStack { + AccountView(item: item) - Toggle(isOn: selected) {} - .labelsHidden() - .toggleStyle(CheckboxStyle()) + Toggle(isOn: selected) {} + .labelsHidden() + .toggleStyle(CheckboxStyle()) + } + } + } else { + ListRow { + AccountView(item: item) } - } - } else { - ListRow { - AccountView(item: item) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index b9242c198c..9188baae28 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -271,7 +271,7 @@ Go to Settings - > %@ and allow access to the camera."; "backup.cloud.password.confirm.placeholder" = "Confirm"; "backup.cloud.password.save" = "Save and Backup"; -"backup.cloud.password.error.empty_passphrase" = "Passphrase cannot be empty"; +"backup.cloud.password.error.empty_passphrase" = "Password cannot be empty"; "backup.cloud.password.error.forbidden_symbols" = "Please use only supported symbols: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "At least 8 characters, including one uppercase letter, one lowercase letter, one number, and one symbol"; "backup.cloud.password.error.invalid_password" = "Incorrect Password"; From 2e5240d513b325a6373cd5e9efd25eecf5100c9c Mon Sep 17 00:00:00 2001 From: ant013 Date: Wed, 11 Oct 2023 14:25:49 +0600 Subject: [PATCH 59/63] Update zcash to 2.0.1, fix initMode --- .../project.pbxproj | 2 +- .../Core/Adapters/ZcashAdapter.swift | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index b162652128..2c54e14b5b 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -11360,7 +11360,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; requirement = { kind = exactVersion; - version = 2.0.0; + version = 2.0.1; }; }; D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index 8c6f4f27d3..d4ec54d612 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -65,15 +65,6 @@ class ZcashAdapter { private(set) var syncing: Bool = true - private func defaultFee(network: ZcashNetwork, height: Int? = nil) -> Zatoshi { - network.constants.defaultFee(for: height ?? BlockHeight.max) - } - - private func defaultFeeDecimal(network: ZcashNetwork, height: Int? = nil) -> Decimal { - // todo update fee settings - fee// defaultFee(network: network, height: height).decimalValue.decimalValue - } - init(wallet: Wallet, restoreSettings: RestoreSettings) throws { logger = App.shared.logger.scoped(with: "ZCashKit") // HsToolKit.Logger(minLogLevel: .debug) // @@ -84,23 +75,28 @@ class ZcashAdapter { network = ZcashNetworkBuilder.network(for: .mainnet) // todo: update fee settings - fee = Zatoshi(10_000).decimalValue.decimalValue//network.constants.defaultFee().decimalValue.decimalValue + fee = network.constants.defaultFee().decimalValue.decimalValue token = wallet.token transactionSource = wallet.transactionSource uniqueId = wallet.account.id + var existingMode: WalletInitMode? + if let dbUrl = try? Self.spendParamsURL(uniqueId: uniqueId), + Self.exist(url: dbUrl) { + existingMode = .existingWallet + } switch wallet.account.origin { case .created: birthday = Self.newBirthdayHeight(network: network) - initMode = .newWallet + initMode = existingMode ?? .newWallet case .restored: if let height = restoreSettings.birthdayHeight { birthday = max(height, network.constants.saplingActivationHeight) } else { birthday = network.constants.saplingActivationHeight } - initMode = .restoreWallet + initMode = existingMode ?? .restoreWallet } let seedData = [UInt8](seed) @@ -360,7 +356,7 @@ class ZcashAdapter { blockHeight: transaction.minedHeight, confirmationsThreshold: ZcashSDK.defaultRewindDistance, date: Date(timeIntervalSince1970: Double(transaction.timestamp)), - fee: defaultFeeDecimal(network: network, height: transaction.minedHeight), + fee: transaction.fee?.decimalValue.decimalValue, failed: transaction.failed, lockInfo: nil, conflictingHash: nil, @@ -379,7 +375,7 @@ class ZcashAdapter { blockHeight: transaction.minedHeight, confirmationsThreshold: ZcashSDK.defaultRewindDistance, date: Date(timeIntervalSince1970: Double(transaction.timestamp)), - fee: defaultFeeDecimal(network: network, height: transaction.minedHeight), + fee: transaction.fee?.decimalValue.decimalValue, failed: transaction.failed, lockInfo: nil, conflictingHash: nil, @@ -531,6 +527,16 @@ extension ZcashAdapter { return url } + private static func exist(url: URL) -> Bool { + let fileManager = FileManager.default + + do { + return try fileManager.fileExists(coordinatingAccessAt: url).exists + } catch { + return false + } + } + private static func fsBlockDbRootURL(uniqueId: String, network: ZcashNetwork) throws -> URL { try dataDirectoryUrl().appendingPathComponent(network.networkType.chainName + uniqueId + ZcashSDK.defaultFsCacheName, isDirectory: true) } From b117625228b303cdce2f877acb0fbaa4e9f6dde4 Mon Sep 17 00:00:00 2001 From: ant013 Date: Mon, 16 Oct 2023 15:01:12 +0600 Subject: [PATCH 60/63] Update unstoppable domain library. Add key for api - fix ui colors. - Fix bug with double restoring backup wallet files. --- .github/workflows/deploy_appstore.yml | 1 + .github/workflows/deploy_dev.yml | 1 + UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj | 2 +- .../Configuration/Development.template.xcconfig | 1 + .../Configuration/Production.template.xcconfig | 1 + .../UnstoppableWallet/Core/Providers/AppConfig.swift | 4 ++++ UnstoppableWallet/UnstoppableWallet/Info.plist | 2 ++ .../RestorePassphrase/RestorePassphraseService.swift | 4 +++- .../Modules/Settings/Appearance/AppearanceView.swift | 1 + .../Modules/Settings/Security/SecuritySettingsView.swift | 2 ++ .../Modules/Settings/SimpleActivate/SimpleActivateView.swift | 1 + .../SwapSettings/ViewModels/AddressResolutionProvider.swift | 2 +- .../UserInterface/SwiftUI/InputTextView.swift | 2 ++ fastlane/Fastfile | 4 ++++ 14 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_appstore.yml b/.github/workflows/deploy_appstore.yml index c4d55d882e..b5062e0172 100644 --- a/.github/workflows/deploy_appstore.yml +++ b/.github/workflows/deploy_appstore.yml @@ -72,3 +72,4 @@ jobs: XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY: ${{ secrets.XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY }} XCCONFIG_PROD_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_PROD_OPEN_SEA_API_KEY }} XCCONFIG_PROD_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_PROD_TRONGRID_API_KEY }} + XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY }} diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml index e23c0debf4..6bb55e450d 100644 --- a/.github/workflows/deploy_dev.yml +++ b/.github/workflows/deploy_dev.yml @@ -73,3 +73,4 @@ jobs: XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY: ${{ secrets.XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY }} XCCONFIG_DEV_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_DEV_OPEN_SEA_API_KEY }} XCCONFIG_DEV_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_DEV_TRONGRID_API_KEY }} + XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY }} diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 2c54e14b5b..face58f0d5 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -11384,7 +11384,7 @@ repositoryURL = "https://github.com/unstoppabledomains/resolution-swift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.2.1; + minimumVersion = 6.1.0; }; }; D3AF5A8729FFD85800C1399E /* XCRemoteSwiftPackageReference "RxSwift" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig b/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig index aff8f26efb..47efb59f52 100644 --- a/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig +++ b/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig @@ -17,5 +17,6 @@ wallet_connect_v2_project_key = shared_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.shared.dev private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.dev open_sea_api_key = +unstoppable_domains_api_key = default_words = diff --git a/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig b/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig index baea417de9..155be341f9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig +++ b/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig @@ -17,3 +17,4 @@ wallet_connect_v2_project_key = shared_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.shared private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet open_sea_api_key = +unstoppable_domains_api_key = \ No newline at end of file diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift index 37f71ced2b..84adc69ca9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift @@ -125,6 +125,10 @@ struct AppConfig { (Bundle.main.object(forInfoDictionaryKey: "WallectConnectV2ProjectKey") as? String).flatMap { $0.isEmpty ? nil : $0 } } + static var unstoppableDomainsApiKey: String? { + (Bundle.main.object(forInfoDictionaryKey: "UnstoppableDomainsApiKey") as? String).flatMap { $0.isEmpty ? nil : $0 } + } + static var defaultWords: String { Bundle.main.object(forInfoDictionaryKey: "DefaultWords") as? String ?? "" } diff --git a/UnstoppableWallet/UnstoppableWallet/Info.plist b/UnstoppableWallet/UnstoppableWallet/Info.plist index 74dc5a88e1..2a2711f369 100644 --- a/UnstoppableWallet/UnstoppableWallet/Info.plist +++ b/UnstoppableWallet/UnstoppableWallet/Info.plist @@ -141,5 +141,7 @@ ${wallet_connect_v2_project_key} OpenSeaApiKey ${open_sea_api_key} + UnstoppableDomainsApiKey + ${unstoppable_domains_api_key} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift index fb4bb50045..cc1801a8d7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreFile/RestorePassphrase/RestorePassphraseService.swift @@ -32,7 +32,9 @@ extension RestorePassphraseService { switch restoredBackup.source { case let .wallet(walletBackup): let rawBackup = try appBackupProvider.decrypt(walletBackup: walletBackup, name: restoredBackup.name, passphrase: passphrase) - appBackupProvider.restore(raws: [rawBackup]) + if walletBackup.version == 2 { // in 2th version we use enabled_wallets and just restore wallet. + appBackupProvider.restore(raws: [rawBackup]) + } switch rawBackup.account.type { case .cex: return .success diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift index 99ed6a32b8..7293296368 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift @@ -34,6 +34,7 @@ struct AppearanceView: View { Toggle(isOn: $viewModel.showMarketTab.animation()) { Text("appearance.markets_tab".localized).themeBody() } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift index ca2853cb07..ee7712b48a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Security/SecuritySettingsView.swift @@ -59,6 +59,7 @@ struct SecuritySettingsView: View { Toggle(isOn: $viewModel.isBiometryToggleOn) { Text(biometryType.title).themeBody() } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) .onChange(of: viewModel.isBiometryToggleOn) { isOn in if !viewModel.isPasscodeSet, isOn { createPasscodeReason = .biometry(type: biometryType) @@ -75,6 +76,7 @@ struct SecuritySettingsView: View { Toggle(isOn: $viewModel.balanceAutoHide) { Text("settings_security.balance_auto_hide".localized).themeBody() } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift index 7c76736706..021eb449c3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/SimpleActivate/SimpleActivateView.swift @@ -16,6 +16,7 @@ struct SimpleActivateView: View { Toggle(isOn: $viewModel.activated) { Text(toggleText).themeBody() } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) } } ListSectionFooter(text: description) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/AddressResolutionProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/AddressResolutionProvider.swift index 9ac61f5365..ebc72f8dea 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/AddressResolutionProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SwapSettings/ViewModels/AddressResolutionProvider.swift @@ -5,7 +5,7 @@ class AddressResolutionProvider { private let resolution: Resolution? init() { - resolution = try? Resolution() + resolution = try? Resolution(apiKey: AppConfig.unstoppableDomainsApiKey ?? "") } func isValid(domain: String) -> Single { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift index 2a7cdfeea7..584eeee5e4 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift @@ -55,6 +55,7 @@ struct InputTextView: View { text: text, onCommit: { commit() } ) + .accentColor(.themeYellow) } else { TextField( placeholder, @@ -62,6 +63,7 @@ struct InputTextView: View { onEditingChanged: { editingChanged($0) }, onCommit: { commit() } ) + .accentColor(.themeYellow) } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3a6be9b4b3..b4af76914c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -29,6 +29,7 @@ XCCONFIG_DEV_HS_PROVIDER_API_KEY = ENV["XCCONFIG_DEV_HS_PROVIDER_API_KEY"] XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY = ENV["XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY"] XCCONFIG_DEV_OPEN_SEA_API_KEY = ENV["XCCONFIG_DEV_OPEN_SEA_API_KEY"] XCCONFIG_DEV_TRONGRID_API_KEY = ENV["XCCONFIG_DEV_TRONGRID_API_KEY"] +XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY"] XCCONFIG_PROD_INFURA_PROJECT_ID = ENV["XCCONFIG_PROD_INFURA_PROJECT_ID"] XCCONFIG_PROD_INFURA_PROJECT_SECRET = ENV["XCCONFIG_PROD_INFURA_PROJECT_SECRET"] @@ -46,6 +47,7 @@ XCCONFIG_PROD_HS_PROVIDER_API_KEY = ENV["XCCONFIG_PROD_HS_PROVIDER_API_KEY"] XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY = ENV["XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY"] XCCONFIG_PROD_OPEN_SEA_API_KEY = ENV["XCCONFIG_PROD_OPEN_SEA_API_KEY"] XCCONFIG_PROD_TRONGRID_API_KEY = ENV["XCCONFIG_PROD_TRONGRID_API_KEY"] +XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY"] def delete_temp_keychain(name) delete_keychain( @@ -120,6 +122,7 @@ def apply_dev_xcconfig update_dev_xcconfig('wallet_connect_v2_project_key', XCCONFIG_DEV_WALLET_CONNECT_V2_PROJECT_KEY) update_dev_xcconfig('open_sea_api_key', XCCONFIG_DEV_OPEN_SEA_API_KEY) update_dev_xcconfig('trongrid_api_key', XCCONFIG_DEV_TRONGRID_API_KEY) + update_dev_xcconfig('unstoppable_domains_api_key', XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY) end def apply_prod_xcconfig @@ -139,6 +142,7 @@ def apply_prod_xcconfig update_prod_xcconfig('wallet_connect_v2_project_key', XCCONFIG_PROD_WALLET_CONNECT_V2_PROJECT_KEY) update_prod_xcconfig('open_sea_api_key', XCCONFIG_PROD_OPEN_SEA_API_KEY) update_prod_xcconfig('trongrid_api_key', XCCONFIG_PROD_TRONGRID_API_KEY) + update_prod_xcconfig('unstoppable_domains_api_key', XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY) end def force_update_devices(type, username) From 364dc846f1b102cf6214f0e9aee84d79056501a4 Mon Sep 17 00:00:00 2001 From: _imadia Date: Mon, 16 Oct 2023 17:22:29 +0600 Subject: [PATCH 61/63] Apply text changes --- .../UnstoppableWallet/en.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 9188baae28..321cf6ae23 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1153,7 +1153,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.balance_auto_hide.description" = "Automatically hides balance each time the app is opened, regardless of previous preferences."; "settings_security.enable_duress_mode" = "Set Duress Mode"; "settings_security.edit_duress_passcode" = "Edit Duress Passcode"; -"settings_security.disable_duress_mode" = "Disable Duress Mode"; +"settings_security.disable_duress_mode" = "Disable Duress Passcode"; "settings_security.duress_mode.description" = "A specialized mode designed to keep selected wallets safe under coercion."; // Create Passcode From cd8b8de10c9730709d56e734bdef39c46ea15641 Mon Sep 17 00:00:00 2001 From: imadia Date: Mon, 16 Oct 2023 18:13:57 +0600 Subject: [PATCH 62/63] New Crowdin updates --- .../de.lproj/Localizable.strings | 9 +- .../es.lproj/Localizable.strings | 4 +- .../fr.lproj/Localizable.strings | 4 +- .../ko.lproj/Localizable.strings | 4 +- .../pt-BR.lproj/Localizable.strings | 2 +- .../ru.lproj/Localizable.strings | 261 +++++++----------- .../tr.lproj/Localizable.strings | 4 +- .../zh.lproj/Localizable.strings | 2 +- 8 files changed, 114 insertions(+), 176 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index c29930b622..af95c2b15c 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -185,7 +185,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "sync_mode.from_blockchain" = "Von Blockchain"; "blockchain_settings.description" = "Wählen Sie das Adressformat für den Empfang von Zahlungen. Ein korrektes Format sollte bei der Wiederherstellung einer bestehenden Brieftasche gewählt werden."; - // Coin Platforms "coin_platforms.native" = "Nativ"; @@ -206,7 +205,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "recovery_phrase.copy_warning.title" = "Risiko der Wiederherstellungs-Phrase-Kopie"; "recovery_phrase.copy_warning.description" = "Als Sicherheitsmaßnahme empfehlen wir, Kopiermaßnahmen nicht auf Wiederherstellungs-Phrase zu verwenden."; - // EVM Private Key "evm_private_key.title" = "EVM privater Schlüssel"; @@ -273,7 +271,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "backup.cloud.password.confirm.placeholder" = "Bestätigen"; "backup.cloud.password.save" = "Speichern und sichern"; -"backup.cloud.password.error.empty_passphrase" = "Passphrase darf nicht leer sein"; +"backup.cloud.password.error.empty_passphrase" = "Passwort darf nicht leer sein"; "backup.cloud.password.error.forbidden_symbols" = "Bitte verwende nur unterstützte Symbole: \\ _ # @ | % ]]>"; "backup.cloud.password.error.minimum_requirement" = "Das Passwort muss mindestens 8 Zeichen enthalten, darunter ein Großbuchstabe, ein Kleinbuchstabe, eine Zahl und ein Symbol."; "backup.cloud.password.error.invalid_password" = "Falsches Passwort"; @@ -323,7 +321,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "balance.sort_by" = "Sortieren nach"; "balance.sort.header" = "Sortieren nach"; "balance.sort.valueHighToLow" = "Guthaben"; - "balance.sort.az" = "Name"; "balance.sort.price_change" = "Preisänderung"; @@ -342,7 +339,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "balance.token.staked" = "Absteckung"; "balance.token.staked.info.title" = "Absteckter Titel"; "balance.token.staked.info.description" = "Absteckungstext"; - "balance.token.frozen" = "Frozen"; "balance.token.frozen.info.title" = "gefrorener Titel"; "balance.token.frozen.info.description" = "gefrorener Beschreibungstext"; @@ -1157,7 +1153,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "settings_security.balance_auto_hide.description" = "Bei jedem Öffnen der App wird der Saldo automatisch ausgeblendet, unabhängig von den vorherigen Einstellungen."; "settings_security.enable_duress_mode" = "Duress Modus setzen"; "settings_security.edit_duress_passcode" = "Duress Code bearbeiten"; -"settings_security.disable_duress_mode" = "Duress Modus deaktivieren"; +"settings_security.disable_duress_mode" = "Duress Code deaktivieren"; "settings_security.duress_mode.description" = "Ein spezialisierter Modus, der entworfen wurde, um ausgewählte Brieftaschen unter Zwang zu schützen."; // Create Passcode @@ -1782,7 +1778,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "launch.failed_to_launch" = "Anwendung konnte aufgrund eines internen Fehlers nicht gestartet werden. Bitte versuchen Sie die App neu zu starten oder melden Sie den Fehler an unser Support-Team."; "launch.failed_to_launch.report" = "Melden"; - // Tron "tron.send.activation_fee" = "Aktivierungsgebühr"; diff --git a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings index 1f42381c44..26061133e9 100644 --- a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings @@ -271,7 +271,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "backup.cloud.password.confirm.placeholder" = "Confirmar"; "backup.cloud.password.save" = "Guardar y hacer una copia de seguridad"; -"backup.cloud.password.error.empty_passphrase" = "La frase de contraseña no puede estar vacía"; +"backup.cloud.password.error.empty_passphrase" = "La contraseña no puede estar vacía"; "backup.cloud.password.error.forbidden_symbols" = "Utilice solo símbolos admitidos: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "Al menos 8 caracteres, incluyendo una letra mayúscula, una letra minúscula, un número y un símbolo"; "backup.cloud.password.error.invalid_password" = "Contraseña incorrecta"; @@ -1153,7 +1153,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "settings_security.balance_auto_hide.description" = "Oculta automáticamente el balance cada vez que se abre la aplicación, independientemente de las preferencias anteriores."; "settings_security.enable_duress_mode" = "Establecer modo de Duración"; "settings_security.edit_duress_passcode" = "Editar contraseña de Duración"; -"settings_security.disable_duress_mode" = "Desactivar modo Duración"; +"settings_security.disable_duress_mode" = "Desactivar Duress Passcode"; "settings_security.duress_mode.description" = "Un modo especializado diseñado para mantener las billeteras seleccionadas a salvo bajo coacción."; // Create Passcode diff --git a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings index fb82851113..5c2652a0cc 100644 --- a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings @@ -271,7 +271,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "backup.cloud.password.confirm.placeholder" = "Confirmer"; "backup.cloud.password.save" = "Enregistrez une copie de sauvegarde"; -"backup.cloud.password.error.empty_passphrase" = "La phrase d'authentification ne peut pas être laissée en blanc"; +"backup.cloud.password.error.empty_passphrase" = "Le mot de passe ne peut pas être vide"; "backup.cloud.password.error.forbidden_symbols" = "Veuillez utiliser uniquement les symboles pris en charge : A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "Votre mot de passe doit contenir au moins 8 caractères, y compris une lettre majuscule, une lettre minuscule, un chiffre et un symbole"; "backup.cloud.password.error.invalid_password" = "Mot de passe incorrect"; @@ -1153,7 +1153,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "settings_security.balance_auto_hide.description" = "Masque automatiquement le solde à chaque ouverture de l'application, indépendamment des préférences précédentes."; "settings_security.enable_duress_mode" = "Activer le Mode Duress"; "settings_security.edit_duress_passcode" = "Modifier le code de détresse"; -"settings_security.disable_duress_mode" = "Désactiver le mode Duress"; +"settings_security.disable_duress_mode" = "Désactiver le mot de passe de détresse"; "settings_security.duress_mode.description" = "Un mode spécialisé conçu pour protéger les portefeuilles sélectionnés en cas de contrainte."; // Create Passcode diff --git a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings index 85bec3c76f..5f6144c48f 100644 --- a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings @@ -269,7 +269,7 @@ "backup.cloud.password.confirm.placeholder" = "확인"; "backup.cloud.password.save" = "저장 및 백업"; -"backup.cloud.password.error.empty_passphrase" = "암호 문구는 필수 입력 사항입니다"; +"backup.cloud.password.error.empty_passphrase" = "비밀번호는 필수 정보입니다."; "backup.cloud.password.error.forbidden_symbols" = "지원되는 기호만 사용하세요: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "대문자 1개, 소문자 1개, 숫자 1개, 기호 1개를 포함하여 8자 이상"; "backup.cloud.password.error.invalid_password" = "잘못된 비밀번호"; @@ -1151,7 +1151,7 @@ "settings_security.balance_auto_hide.description" = "앱이 열릴 때마다 잔고를 자동으로 숨깁니다. 이전 설정에 관계없이 적용됩니다."; "settings_security.enable_duress_mode" = "긴급 상황 모드 설정"; "settings_security.edit_duress_passcode" = "긴급 상황 비밀번호 편집"; -"settings_security.disable_duress_mode" = "긴급 상황 모드 비활성화"; +"settings_security.disable_duress_mode" = "협박 비밀번호를 비활성화합니다."; "settings_security.duress_mode.description" = "강요 상황에서 선택한 지갑을 안전하게 보관하기 위해 디자인된 특별한 모드입니다."; // Create Passcode diff --git a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings index 3aeef9f339..d05302f46a 100644 --- a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings @@ -1153,7 +1153,7 @@ Go to Settings - > %@ and allow access to the camera."; "settings_security.balance_auto_hide.description" = "Oculta automaticamente o saldo cada vez que o aplicativo é aberto, independentemente das preferências anteriores."; "settings_security.enable_duress_mode" = "Definir modo de dureza"; "settings_security.edit_duress_passcode" = "Editar Senha do Modo Coação"; -"settings_security.disable_duress_mode" = "Desativar Modo de Coação"; +"settings_security.disable_duress_mode" = "Desabilitar senha de cobrança"; "settings_security.duress_mode.description" = "Um modo especializado projetado para manter as carteiras selecionadas seguras sob coação."; // Create Passcode diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index 83a3f2cf49..b3f70670ad 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -17,7 +17,7 @@ "button.retry" = "Повторить"; "button.report" = "Пожаловаться"; "button.add" = "Добавить"; -"button.approve" = "Разрешить"; +"button.approve" = "Одобрить"; "button.revoke" = "Отменить"; "button.reject" = "Отклонить"; "button.connect" = "Подключиться"; @@ -41,7 +41,6 @@ "alert.unknown_error" = "Неизвестная ошибка"; "alert.success_action" = "Готово"; "alert.restored" = "Восстановлено"; - "alert.success" = "Успешно"; "alert.added_to_watchlist" = "Добавлено в избранное"; @@ -50,8 +49,6 @@ "alert.removed_from_wallet" = "Удалено из кошелька"; "alert.already_added_to_wallet" = "Уже добавлено в кошелек"; "alert.not_supported_yet" = "Ещё не поддерживается"; - -"alert.copied" = "Скопировано"; "alert.created" = "Создан"; "alert.imported" = "Импортировано"; "alert.wallet_added" = "Кошелек добавлен"; @@ -122,14 +119,12 @@ "restore.title" = "Импорт кошелька"; "restore.advanced" = "Доп. настройки"; "restore.import_by" = "Импорт по"; - "restore.restore_type.mnemonic" = "Фраза восстановления"; "restore.restore_type.private_key" = "Приватный ключ"; "restore.mnemonic.placeholder" = "Введите фразу восстановления"; "restore.private_key.placeholder" = "Введите приватный ключ EVM, BIP32 Root Key или Account Extended Private Key"; "restore.private_key.invalid_key" = "Неверный ключ"; "restore_error.mnemonic_word_count" = "Неверное количество слов. Должно быть от 12 до 24 слов. Вы ввели: %@"; - "restore.checksum_error" = "Неверная контрольная сумма"; "restore.passphrase" = "Кодовая фраза"; "restore.input.passphrase" = "Кодовая фраза"; @@ -140,7 +135,6 @@ \n\nЕсли вы являетесь затронутым пользователем, то баланс вашего кошелька будет отображаться как 0 после восстановления такого кошелька в версии 0.29 или выше.Эта страница позволит вам восстановить доступ к вашему нестандартному кошельку. После восстановления рекомендуется создать новый кошелек (который будет соответствовать стандарту) и перевести туда средства."; "restore.warning.non_recommended.description" = "Похоже, этот кошелек использует нестандартный символ в списке мнемонических слов и/или парольной фразе. Если вы не видите баланс или транзакции, пожалуйста, прочтите подробности ниже. \n\nПОЖАЛУЙСТА НАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; - "restore.error.non_standard.description" = "Это нестандартный кошелек.\n\nНАЖМИТЕ, ЧТОБЫ ПОЛУЧИТЬ БОЛЬШЕ ИНФОРМАЦИИ"; // Restore Type @@ -165,7 +159,6 @@ "restore.cloud.imported" = "Импортированные кошельки"; "restore.cloud.app_backups" = "Резервные копии приложений"; - "restore.cloud.password.title" = "Введите пароль"; "restore.cloud.password.placeholder" = "Пароль резервной копии"; "restore.cloud.password.description" = "Введите пароль резервной копии для импорта вашего кошелька из iCloud."; @@ -178,7 +171,6 @@ // Restore Binance "restore.binance.description" = "Пожалуйста, предоставьте API ключ и секрет API, чтобы связать вашу биржу."; - "restore.binance.api_key" = "API ключ"; "restore.binance.secret_key" = "Секретный ключ"; "restore.binance.connect" = "Подключиться"; @@ -218,7 +210,6 @@ // EVM Private Key "evm_private_key.title" = "Приватный ключ EVM"; - "evm_private_key.tap_to_show" = "Нажмите, чтобы показать приватный ключ"; // Extended Key @@ -235,7 +226,6 @@ "backup.title" = "Фраза восстановления"; "backup.description" = "Запишите эти слова в правильном порядке и храните их в безопасном месте"; - "backup.tap_to_show" = "Нажмите, чтобы показать фразу восстановления"; "backup.passphrase" = "Кодовая фраза"; "backup.verify" = "Подтвердить"; @@ -245,7 +235,6 @@ "backup_verify_words.title" = "Подтвердить"; "backup_verify_words.description" = "Выберите два запрошенных слова из вашей фразы восстановления кошелька"; - "backup_verify_words.incorrect_word" = "Неверное слово"; // Backup Verify Passphrase @@ -258,7 +247,6 @@ "backup_prompt.backup_recovery_phrase" = "Резервная копия"; "backup_prompt.backup_required" = "Необходима резервная копия"; - "backup_prompt.warning" = "Создайте резервную копию вашей фразы восстановления и пароля, которые позволят вам восстановить ваш кошелек, если телефон потерян, украден, сломал и т.д."; "backup_prompt.backup" = "Резервная копия"; "backup_prompt.backup_manual" = "Ручное резервное копирование"; @@ -269,7 +257,6 @@ "backup.cloud.title" = "Резерв. копирование в iCloud"; "backup.cloud.description" = "iCloud - это облачное хранилище, предоставляемое Apple. Важно знать, что ваши данные будут храниться на серверах Apple, а не на ваших личных устройствах. Это означает, что вы доверяете свои данные и передаете безопасность вашей информации стороннему сервису."; - "backup.cloud.terms.item.1" = "Я понимаю, что закрытие доступа к моему iCloud, приведет к потере доступа к резервной копии соответствующего кошелька."; "backup.cloud.name.title" = "Имя резервной копии"; @@ -281,13 +268,12 @@ "backup.cloud.password.title" = "Установить пароль"; "backup.cloud.password.description" = "Установите пароль разблокировки для вашей резервной копии. Пароль должен содержать как минимум 8 символов и включать хотя бы одну строчную букву, заглавную букву, цифру и специальный символ."; - "backup.cloud.password.highlighted_description" = "Не забудьте этот пароль! Он отличается от вашего пароля для Apple iCloud и не может быть восстановлен или сброшен."; "backup.cloud.password.placeholder" = "Пароль"; "backup.cloud.password.confirm.placeholder" = "Подтвердить"; "backup.cloud.password.save" = "Сохранить и создать резервную копию"; -"backup.cloud.password.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; +"backup.cloud.password.error.empty_passphrase" = "Пароль не может быть пустым"; "backup.cloud.password.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "Не менее 8 символов, включая одну заглавную букву, одну строчную букву, одну цифру и один символ"; "backup.cloud.password.error.invalid_password" = "Неверный пароль"; @@ -299,7 +285,6 @@ "backup.cloud.no_access.title" = "Доступ к iCloud"; "backup.cloud.no_access.description" = "Для создания резервной копии необходимо предоставить доступ к iCloud памяти."; - // Errors "error.send.self_transfer" = "Отправка самому себе невозможна"; @@ -329,7 +314,6 @@ "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Идет синхронизация... %@"; - "balance.synced_through" = "до %@"; "balance.add_coin" = "Добавить токен"; "balance.invalid_api_key" = "Недействительный ключ API"; @@ -337,7 +321,7 @@ "balance.empty.description" = "Вы еще не добавили токены в этот кошелек."; "balance.watch_empty.description" = "У кошелька с этим адресом нет баланса"; "balance.sort_by" = "Сортировать"; -"balance.sort.header" = "Сортировать"; +"balance.sort.header" = "Rank them like"; "balance.sort.valueHighToLow" = "Баланс"; "balance.sort.az" = "Название"; "balance.sort.price_change" = "По изменению цены (%)"; @@ -354,7 +338,6 @@ "balance.token.processing" = "В процессе"; "balance.token.processing.info.title" = "Обрабатываемая сумма"; "balance.token.processing.info.description" = "Транзакции с этой суммой все еще синхронизируются. Когда они будут подтверждены, эти токены будут доступны для траты"; - "balance.token.staked" = "Staked"; "balance.token.staked.info.title" = "Staked title"; "balance.token.staked.info.description" = "Staked Description Text"; @@ -389,7 +372,6 @@ "deposit.account" = "Account"; "deposit.not_active" = "Не активен"; - "deposit.not_active.title" = "Неактивный адрес"; "deposit.not_active.tron_description" = "Недавно созданные учетные записи в блокчейне TRON неактивны и не могут быть запрошены или изучены. Они должны быть активированы.\n\nАктивация новой учетной записи в цепочке Tron требует комиссию в размере 1 TRX. Для активации достаточно просто перевести токены TRX или TRC-10 на неактивный адрес аккаунта"; @@ -444,7 +426,6 @@ "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "Существует широко признанный стандарт для упорядочивания выходов транзакции, известный как BIP69. В открытых кошельках этот стандарт обеспечивает, чтобы пользователи кошелька не должны были полагаться на то, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт относительно новый, не многие кошельки его уже реализовали. В результате, на блокчейне в некоторых случаях можно узнать, отправлена ли транзакция из кошелька, который использует этот стандарт или нет."; - "send.confirmation.you_send" = "Вы отправляете"; "send.confirmation.to" = "Кому"; "send.confirmation.contact_name" = "Имя контакта"; @@ -454,7 +435,6 @@ "send.confirmation.memo" = "Memo"; "send.confirmation.memo_placeholder" = "Memo"; "send.confirmation.total" = "Итого"; - "send.confirmation.fee" = "Комиссия"; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.slide_to_send" = "Проведите для отправки"; @@ -464,7 +444,6 @@ "send.confirmation.cancel_description" = "Это действие попытается аннулировать предыдущую транзакцию, отправив ее заново как новую транзакцию с нулевой суммой к самому себе. Если исходная транзакция остается в ожидании, когда отправляется новая, существует высокая вероятность (но не гарантия), что она будет аннулирована и заменена. Только одна из этих двух транзакций будет включена в блокчейн."; "send.confirmation.cancel" = "Отменить транзакцию"; "send.confirmation.nonce" = "Nonce"; - "send.confirmation.method" = "Метод"; "send.amount_error.balance" = "Недостаточно средств"; "send.address_error.own_address" = "Невозможно отправить TRX самому себе"; @@ -472,7 +451,6 @@ "send.amount_error.minimum_amount" = "Мин. сумма %@"; "send.amount_error.min_required_balance" = "Мин. обязательный остаток %@"; "send.amount_warning.coin_needed_for_fee" = "Рассмотрите возможность оставить %@ на балансе, чтобы иметь возможность оплачивать будущие транзакции."; - "send.token.insufficient_fee_alert" = "Комиссии за транзакцию %@ (%@) взимаются в %@. Вам нужно %@."; "send.fee_settings.amount_error.balance.title" = "Недостаточный баланс"; @@ -514,13 +492,11 @@ "swap.balance" = "Баланс"; "swap.allowance" = "Разрешение"; "swap.you_get" = "Вы получите"; - "swap.token" = "Выбрать"; "swap.advanced_settings" = "Настройки обмена"; "swap.proceed_button" = "Далее"; "swap.approve.title" = "Разрешение обмена"; "swap.approve.description" = "Вы должны предоставить разрешение смарт-контракту для обмена заданным токеном от вашего имени. Это разрешение устанавливает сумму, которую может использовать смарт-контракт. Это не влияет на ваш баланс, но требует небольшой комиссии для выполнения транзакции на подтверждение.\n\nХотя это можно сделать по мере необходимости перед каждой сделкой, более дешево предварительно одобрить большую сумму для будущих сделок."; - "swap.approve.amount_error.already_approved" = "У вас уже есть разрешение на эту сумму"; "swap.approving_button" = "Разрешение..."; "swap.revoke_warning" = "Вы можете обменять %@, или вы должны отменить и одобрить новую сумму"; @@ -538,125 +514,122 @@ "swap.price_impact" = "Отклонение от рын. цены"; "swap.maximum_paid" = "Макс. сумма"; "swap.minimum_got" = "Гарантированная сумма"; - -"swap.estimate_short" = "(прим.)"; -"swap.minimum_short" = "(мин)"; -"swap.maximum_short" = "(макс)"; +"swap.estimate_short" = "(ballpark)"; +"swap.minimum_short" = "(low end)"; +"swap.maximum_short" = "(high end)"; // Swap Advanced Settings -"swap.advanced_settings.slippage" = "Допустимость отклонений"; -"swap.advanced_settings.slippage.footer" = "Ваша транзакция будет отменена, если цена изменится в неблагоприятную сторону более чем на этот процент"; -"swap.advanced_settings.deadline" = "Срок транзакции"; -"swap.advanced_settings.deadline.footer" = "Ваша транзакция будет отменена, если перевод займет больше указанного срока."; -"swap.advanced_settings.recipient.footer" = "После операции обмена сумма будет переведена на указанный адрес"; -"swap.advanced_settings.deadline_minute" = "%@ мин"; +"swap.advanced_settings.slippage" = "Допустимое отклонение"; +"swap.advanced_settings.slippage.footer" = "If prices dip more than this, no deal!"; +"swap.advanced_settings.deadline" = "Срок выполнения транзакции"; +"swap.advanced_settings.deadline.footer" = "No dilly-dallying! Time's ticking."; +"swap.advanced_settings.recipient.footer" = "Where's the money going after the swap?"; +"swap.advanced_settings.deadline_minute" = "Hurry up, %@ min!"; "swap.advanced_settings.recipient_address" = "Адрес получателя"; -"swap.advanced_settings.warning.unusual_slippage" = "Ваша транзакция может быть подвержена фронтраннингу."; -"swap.advanced_settings.service_fee_description" = "Комиссия за услугу обмена на платформе обычно 0.3% или 0.6%"; -"swap.advanced_settings.error.lower_slippage" = "Возможно, ваша транзакция не удалась."; -"swap.advanced_settings.error.higher_slippage" = "Сопротивление скольжению не может превышать %@%%"; -"swap.advanced_settings.error.invalid_address" = "Неверный адрес"; -"swap.advanced_settings.error.invalid_slippage" = "Неверное отклонение"; -"swap.advanced_settings.error.invalid_deadline" = "Недопустимый срок"; - -"swap.one_inch.error.cannot_estimate" = "Ошибка оценки"; -"swap.one_inch.error.cannot_estimate.info" = "Проверьте баланс и убедитесь, что на нем достаточно %@ для покрытия комиссии. Или попробуйте увеличить предел скольжения цены и повторите попытку снова. Следующая попытка через 3 секунды..."; -"swap.one_inch.error.insufficient_liquidity" = "Недостаточно ликвидности"; -"swap.one_inch.error.insufficient_liquidity.info" = "Кажется, для этой сделки не хватает ликвидности. Попробуйте уменьшить сумму сделки."; +"swap.advanced_settings.warning.unusual_slippage" = "Watch out! Others might jump the line!"; +"swap.advanced_settings.service_fee_description" = "A small thank you fee for our service. Usually just a tiny 0.3% or 0.6%!"; +"swap.advanced_settings.error.lower_slippage" = "This might flop."; +"swap.advanced_settings.error.higher_slippage" = "Can't go over %@%%, buddy!"; +"swap.advanced_settings.error.invalid_address" = "Wonky address alert!"; +"swap.advanced_settings.error.invalid_slippage" = "Недействительное отклонение"; +"swap.advanced_settings.error.invalid_deadline" = "Недействительный срок выполнения"; + +"swap.one_inch.error.cannot_estimate" = "Math's a bit tricky."; +"swap.one_inch.error.cannot_estimate.info" = "Check your stash. Maybe up the wiggle room and try again. Give it another go in 3..."; +"swap.one_inch.error.insufficient_liquidity" = "Pool's a bit shallow!"; +"swap.one_inch.error.insufficient_liquidity.info" = "Not enough in the pot. Lower the ante."; "swap.service" = "Сервис"; "swap.service.title" = "Сервис"; // Swap Approving -"swap.approve.subtitle" = "Обменять"; +"swap.approve.subtitle" = "Обмен"; // Swap Confirmation -"swap.confirmation.slide_to_swap" = "Проведите для обмена"; +"swap.confirmation.slide_to_swap" = "Slide to the deal!"; "swap.confirmation.swapping" = "Обмен"; -"swap.confirmation.impact_too_high" = "%@ отключил действие \"swap\" для этой сделки, так как вы получаете чрезвычайно невыгодную цену. Это связано с крайне низкой ликвидностью. \nЕсли вы все еще хотите поменять валюту, используйте веб-сайт %@ вместо этого."; -"swap.confirmation.impact_warning" = "Важно! Вы получаете чрезвычайно неблагоприятную цену. Это связано с крайне низкой ликвидностью."; - -"swap.confirmation.minimum_received" = "Получено минимум"; -"swap.confirmation.maximum_sent" = "Максимальная трата"; +"swap.confirmation.impact_too_high" = "Hold up! %@ says this deal's a dud. Check out %@ instead!"; +"swap.confirmation.impact_warning" = "Watch out! It's a wild ride!"; -"swap.dex_info.description" = "Этот обменный сервис работает при поддержке %@ - децентрализованного протокола обмена токенов, созданного в блокчейне %@.\n\n%@ полностью автоматизирован и управляется смарт-контрактами, которые надежным способом упрощают обмен токенами без осуществления каких-либо махинаций."; +"swap.confirmation.minimum_received" = "You'll at least get"; +"swap.confirmation.maximum_sent" = "At most you'll send"; -"swap.dex_info.header_dex_related" = "%@"; +"swap.dex_info.description" = "This swanky service is by %@, the big shots in decentralized trading on the %@ chain. 100% automated, 100% reliable!"; +"swap.dex_info.header_dex_related" = "%@Related"; "swap.dex_info.header_allowance" = "Разрешение"; -"swap.dex_info.content_allowance" = "Сумма, которую exchange может потратить от имени пользователя при выполнении обмена токенов. Действующая транзакция, устанавливающая достаточный уровень допустимости, необходима для того, чтобы транзакция обмена была осуществлена."; +"swap.dex_info.content_allowance" = "How much the exchange can use on your behalf. Gotta get this before the main event."; "swap.dex_info.header_price_impact" = "Отклонение от рын. цены"; -"swap.dex_info.content_price_impact" = "Ожидаемое отклонение цены от указанной цены, обычно увеличивается при увеличении суммы обмена."; -"swap.dex_info.header_swap_fee" = "Комиссия за обмен"; -"swap.dex_info.content_swap_fee" = "Плата за услугу обмена на платформе, показана в валюте, в которой продает пользователь. Для большинства заказов составляет или 0,3% или 0,6%."; +"swap.dex_info.content_price_impact" = "How wild the price might get. Bigger swaps, bigger waves."; +"swap.dex_info.header_swap_fee" = "Thank you fee"; +"swap.dex_info.content_swap_fee" = "Our little thank you note. Usually 0.3% or 0.6%."; "swap.dex_info.header_guaranteed_amount" = "Гарантированная сумма"; -"swap.dex_info.content_guaranteed_amount" = "Минимальная сумма, которую получит пользователь в результате обмена."; -"swap.dex_info.header_maximum_spend" = "Максимальная трата"; -"swap.dex_info.content_maximum_spend" = "Максимальная сумма, которую получит пользователь в результате обмена."; +"swap.dex_info.content_guaranteed_amount" = "The least you're gonna get after swapping."; +"swap.dex_info.header_maximum_spend" = "Max Spend"; +"swap.dex_info.content_maximum_spend" = "The most you're gonna use in the swap."; -"swap.dex_info.header_other" = "Другое"; -"swap.dex_info.header_transaction_fee" = "Комиссия за транзакцию"; -"swap.dex_info.content_transaction_fee" = "Примерная стоимость услуги по обработке данной транзакции на блокчейне %@. Стоимость транзакций, связанных с %@, обычно выше, чем стоимость транзакций по передаче обычных токенов."; -"swap.dex_info.header_transaction_speed" = "Скорость транзакции"; -"swap.dex_info.content_transaction_speed" = "Обработка транзакций с более высокой комиссией будет ускорена. Также вено и обратное."; +"swap.dex_info.header_other" = "Other stuff"; +"swap.dex_info.header_transaction_fee" = "The cost of doing business"; +"swap.dex_info.content_transaction_fee" = "What you're paying to use the %@ chain. %@ stuff might be pricier."; +"swap.dex_info.header_transaction_speed" = "How fast it goes"; +"swap.dex_info.content_transaction_speed" = "Pay more, go faster. It's that simple."; -"swap.dex_info.link_button" = "%@ сайт"; +"swap.dex_info.link_button" = "Check out %@"; // Market "market.tab_bar_item" = "Рынки"; "market.title" = "Рынки"; "market.category.overview" = "Обзор"; - -"market.category.posts" = "Новости"; +"market.category.posts" = "Real News, Not Fake!"; "market.category.watchlist" = "Избранное"; -"market.total_market_cap" = "Общая капитализация"; +"market.total_market_cap" = "The Big Money Cap"; "market.24h_volume" = "Объем торгов (24ч)"; "market.defi_cap" = "Капитализация DeFi"; "market.defi_tvl" = "TVL в DeFi"; -"market.project_has_no_coin" = "У этого проекта нет токена"; +"market.project_has_no_coin" = "No coin? Sad!"; -"market.top.section.header.see_all" = "Посмотреть всё"; -"market.top.section.header.top_gainers" = "Взлеты"; -"market.top.section.header.top_losers" = "Падения"; -"market.top.section.header.top_sectors" = "Топ секторы"; -"market.top.section.header.news" = "Новости"; -"market.top.volume.title" = "Объём"; -"market.top.market_cap.title" = "Рын. кап."; -"market.top.diluted_market_cap.title" = "Разводненная рын.кап."; +"market.top.section.header.see_all" = "See Everything!"; +"market.top.section.header.top_gainers" = "Total Winners"; +"market.top.section.header.top_losers" = "Not Winning… Yet!"; +"market.top.section.header.top_sectors" = "Top of the Tops"; +"market.top.section.header.news" = "Real News, Not Fake!"; +"market.top.volume.title" = "Volume? Huge!"; +"market.top.market_cap.title" = "Massive MCap"; +"market.top.diluted_market_cap.title" = "Dilluted MCap"; -"market.market_field.mcap" = "Рын. кап."; -"market.market_field.vol" = "Объём"; +"market.market_field.mcap" = "Massive MCap"; +"market.market_field.vol" = "Volume? Huge!"; -"market.tvl.market_field.value" = "USD"; -"market.tvl.market_field.diff" = "Процент"; +"market.tvl.market_field.value" = "Big Bucks"; +"market.tvl.market_field.diff" = "Up or Down?"; -"market.tvl.platform_field.all" = "Все"; +"market.tvl.platform_field.all" = "All of 'em"; "market.sort_by" = "Сортировать"; -"market.top.title" = "Лучшие токены"; -"market.top.description" = "Топ токенов по рыночной капитализации"; +"market.top.title" = "Best Coins Ever"; +"market.top.description" = "Top Coins, because we only deal with the best!"; -"market.top.highest_cap" = "Наивысшая кап."; -"market.top.lowest_cap" = "Наименьшая кап."; -"market.top.highest_volume" = "Наивысший объем"; -"market.top.lowest_volume" = "Наименьший объем"; -"market.top.top_gainers" = "Взлеты"; -"market.top.top_losers" = "Падения"; -"market.top.top_collections" = "Топ NFT коллекции"; -"market.top.floor_price" = "Минимальная цена:"; -"market.top.top_platforms" = "Топ платформы"; -"market.top.protocols" = "Протоколы"; +"market.top.highest_cap" = "The Richest Cap"; +"market.top.lowest_cap" = "Room for Growth Cap"; +"market.top.highest_volume" = "Lots of Noise"; +"market.top.lowest_volume" = "Quiet Winners"; +"market.top.top_gainers" = "Total Winners"; +"market.top.top_losers" = "Not Winning… Yet!"; +"market.top.top_collections" = "Top Art Stash"; +"market.top.floor_price" = "Floor"; +"market.top.top_platforms" = "Best Stages"; +"market.top.protocols" = "The Rules"; -"top_platforms.title" = "Рейтинг платформ"; -"top_platforms.description" = "Лучшие ведущие блокчейн-платформы кумулятивного рынка проектов."; +"top_platforms.title" = "Platform Ranks"; +"top_platforms.description" = "The best places to build greatness."; "top_platform.title" = "%@ Экосистема"; -"top_platform.description" = "Капитализация рынка всех протоколов на блокчейне %@"; +"top_platform.description" = "Where the money's at on the %@ stage"; "market_discovery.title" = "Токены"; "market_discovery.filters" = "Фильтры"; @@ -664,7 +637,7 @@ "market_discovery.top_coins" = "Топ токены"; "market_discovery.not_found" = "Ничего не найдено"; -"market_watchlist.empty.caption" = "У вас нет токенов в избранном."; +"market_watchlist.empty.caption" = "It's lonely here."; "market.search.title" = "Поиск"; "market.search.empty_text" = "Ничего не найдено"; @@ -684,7 +657,7 @@ "market.advanced_search.liquidity" = "Ликвидность DEX"; "market.advanced_search.blockchains" = "Блокчейны"; "market.advanced_search.price_period" = "Ценовой период"; -"market.advanced_search.price_change" = "По изменению цены (%)"; +"market.advanced_search.price_change" = "Изменение цены"; "market.advanced_search.outperformed_btc" = "Обошел BTC"; "market.advanced_search.outperformed_eth" = "Обошел ETH"; @@ -732,7 +705,6 @@ "market.global.total_market_cap.description" = "Общая рыночная стоимость всех криптовалют"; "market.global.volume_24h.title" = "Объем Торгов (24ч)"; - "market.global.volume_24h.description" = "24-часовой объем крипторынка"; "market.global.defi_cap.title" = "Капитализация DeFi"; @@ -758,8 +730,7 @@ "coin_overview.market_cap" = "Рын. капитализация"; "coin_overview.circulating_supply" = "В обороте"; "coin_overview.total_supply" = "Макс.выпуск"; - -"coin_overview.diluted_market_cap" = "Разводненная рын.кап."; +"coin_overview.diluted_market_cap" = "Dilluted MCap"; "coin_overview.genesis_date" = "Дата старта"; "coin_overview.trading_volume" = "Объем торговли"; @@ -772,7 +743,6 @@ "coin_overview.overview" = "Обзор"; "coin_overview.description_warning" = "Это описание, сгенерированное искусственным интеллектом на основе предоставленного справочного материала для данной криптовалюты. Оно может содержать ошибки."; - "coin_overview.blockchains" = "Блокчейны"; "coin_overview.bips" = "BIPы"; "coin_overview.coin_types" = "Типы токенов"; @@ -807,7 +777,6 @@ "coin_analytics.technical_indicators" = "Технические индикаторы"; "coin_analytics.technical_indicators.info1" = "Сводка: Это общий обзор технических характеристик актива, учитывающий различные технические индикаторы и временные рамки. Она предоставляет консенсусное мнение (Покупать, Продавать или Нейтрально) на основе этих индикаторов."; - "coin_analytics.technical_indicators.info2" = "Скользящие средние (MA): Это обычно используемые технические индикаторы, позволяющие сгладить данные о ценах для создания индикатора следующего тренда. Они показывают среднюю цену за определенный период времени. Существует несколько типов MAs:\n\nSimple Moving среднее значение (SMA): Это вычисляет среднее значение выбранного диапазона цен, обычно закрывают цены, по количеству периодов в этом диапазоне.\n\nЭкспоненциальное скользящее среднее (EMA): Это даёт больше веса для последних цен, тем самым реагируя быстрее на последние изменения цен."; "coin_analytics.technical_indicators.info3" = "Осцилляторы: Это технические индикаторы, которые колеблются со временем в диапазоне (выше и ниже центральной линии или между заданными уровнями). Они предназначены для идентификации перекупленных и перепродаваемых условий на рынке. Вот несколько распространенных осцилляторов:\n\nИндекс относительной силы (RSI): Это измеряет скорость и изменение движений цен. Обычно он используется для идентификации перекупленных или перепроданных условий.\n\nДвижение среднего сближения (MACD): Используется для выявления потенциальных сигналов на покупку и продажу. Она запускает технические сигналы, когда она пересекает линии сигнала выше (покупать) или ниже (продавать)."; @@ -819,7 +788,6 @@ "coin_analytics.cex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих централизованных биржах за 30-дневный период."; "coin_analytics.cex_volume.info4" = "Список всех токенов, ранжированных по объему торговли на централизованных биржах за последние 24ч/7Д / 1М."; - "coin_analytics.dex_volume" = "Объем DEX"; "coin_analytics.dex_volume_rank" = "Рейтинг объема DEX"; "coin_analytics.dex_volume_rank.description" = "Торговый объем токена на децентрализованных биржах."; @@ -858,7 +826,6 @@ "coin_analytics.transaction_count.info2" = "График, отражающий колебания количества транзакций за 1 год."; "coin_analytics.transaction_count.info3" = "Рейтинг токена основан на количестве транзакций с токеном за 30-дневный период."; "coin_analytics.transaction_count.info4" = "Список всех токенов, ранжированных на основе количества транзакций с интервалом 24ч / 7Д / 1М."; - "coin_analytics.transaction_count.info5" = "Общее количество токенов, отправленных через блокчейн за 30-дневный период."; "coin_analytics.holders" = "Держатели"; @@ -869,7 +836,7 @@ "coin_analytics.holders.tracked_blockchains" = "Отслеживаемые блокчейны: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; "coin_analytics.holders.in_top_10_addresses" = "в топ-10 держателей"; "coin_analytics.holders.count" = "Всего держателей: %@"; -"coin_analytics.holders.see_all" = "Посмотреть всё"; +"coin_analytics.holders.see_all" = "See Everything!"; "coin_analytics.project_tvl" = "Проект TVL"; "coin_analytics.tvl_ratio" = "Рын.кап / Соотношение TVL "; @@ -897,7 +864,7 @@ "coin_analytics.treasuries" = "Treasuries"; "coin_analytics.treasuries.filters" = "Фильтры"; -"coin_analytics.treasuries.filter.all" = "Все"; +"coin_analytics.treasuries.filter.all" = "All of 'em"; "coin_analytics.treasuries.filter.public" = "Публичный"; "coin_analytics.treasuries.filter.private" = "Приватный"; "coin_analytics.treasuries.filter.etf" = "ETF"; @@ -908,6 +875,7 @@ "coin_analytics.last_30d" = "Последние 30 дн."; "coin_analytics.current" = "текущий"; + "coin_analytics.overall_score" = "Общий балл"; "coin_analytics.overall_score.excellent" = "Отлично"; "coin_analytics.overall_score.good" = "Хорошо"; @@ -971,7 +939,7 @@ "transactions.all_blockchains" = "Все блокчейны"; "transactions.all_coins" = "Все токены"; "transactions.choose_coin" = "Выберите токен"; -"transactions.filter_all" = "Все"; +"transactions.filter_all" = "All of 'em"; "transactions.empty_text" = "У вас ещё нет незавершенных или прошлых транзакций"; "transactions.pending" = "В обработке"; "transactions.processing" = "В процессе"; @@ -982,8 +950,8 @@ "transactions.send" = "Отправить"; "transactions.burn" = "Сжечь"; "transactions.mint" = "Минт"; -"transactions.approve" = "Разрешить"; -"transactions.swap" = "Обменять"; +"transactions.approve" = "Одобрить"; +"transactions.swap" = "Обмен"; "transactions.contract_call" = "Вызов контракта"; "transactions.contract_creation" = "Создание контракта"; "transactions.external_call" = "Внешний вызов"; @@ -998,7 +966,7 @@ "transactions.today" = "Сегодня"; "transactions.yesterday" = "Вчера"; -"transactions.types.all" = "Все"; +"transactions.types.all" = "All of 'em"; "transactions.types.incoming" = "Получено"; "transactions.types.outgoing" = "Отправлено"; "transactions.types.swap" = "Обмены"; @@ -1035,10 +1003,9 @@ "tx_info.raw_transaction" = "Неподтвержденная транзакция"; "tx_info.memo" = "Memo"; "tx_info.service" = "Сервис"; - -"tx_info.view_on" = "Посмотреть на %@"; -"tx_info.you_pay" = "Платите"; -"tx_info.you_get" = "Получите"; +"tx_info.view_on" = "Просмотреть на %@"; +"tx_info.you_pay" = "Вы платите"; +"tx_info.you_get" = "Вы получите"; "tx_info.you_paid" = "Вы заплатили"; "tx_info.you_got" = "Вы получили"; "tx_info.price" = "Цена"; @@ -1049,8 +1016,7 @@ "settings.tab_bar_item" = "Настройки"; "settings.manage_accounts" = "Кошельки"; "settings.blockchain_settings" = "Настройки блокчейна"; -"settings.backup_manager" = "Менеджер резерв. копирования"; - +"settings.backup_manager" = "Резерв. копирования"; "settings.security" = "Безопасность"; "settings.experimental_features" = "Экспериментальные функции"; "settings.personal_support" = "Персональная поддержка"; @@ -1068,10 +1034,9 @@ // Settings -> Base Currency "settings.base_currency.title" = "Базовая валюта"; - -"settings.base_currency.other" = "Другое"; +"settings.base_currency.other" = "Other stuff"; "settings.base_currency.disclaimer" = "Отказ от ответственности"; -"settings.base_currency.disclaimer.description" = "Данные об обменном курсе предоставлены третьим лицом Coingecko.сom\n\n Приложение %@ Wallet не гарантирует, что эти данные всегда верны и соответствуют рыночным. Шанс на несоответствие повышается при выборе базовой валюты, отличающейся от %@."; +"settings.base_currency.disclaimer.description" = "Данные об обменных курсах предоставляются сторонним сервисом - Coingecko.com.\n\nПриложение кошелька %@ не гарантирует, что эти значения всегда верны и соответствуют рыночным данным. Вероятность несоответствия выше, если вы выбираете базовую валюту отличную от %@."; "settings.base_currency.disclaimer.set" = "Установить"; // Settings -> Manage Wallet @@ -1086,8 +1051,7 @@ // Settings -> Personal Support "settings.personal_support.telegram_username.title" = "Account"; - -"settings.personal_support.telegram_username.placeholder" = "@username"; +"settings.personal_support.telegram_username.placeholder" = "@username!"; "settings.personal_support.description" = "Введите имя аккаунта Telegram, чтобы открыть личный чат поддержки, и мы отправим вам сообщение."; "settings.personal_support.request" = "Запрос"; "settings.personal_support.requested" = "Запрошено"; @@ -1130,7 +1094,7 @@ // Settings -> Backup Manager -"backup_app.backup_manager.title" = "Менеджер резерв. копирования"; +"backup_app.backup_manager.title" = "Резерв. копирования"; "backup_app.backup_manager.restore" = "Восстановить резервную копию"; "backup_app.backup_manager.create" = "Создать новую резерв. копию"; @@ -1186,12 +1150,12 @@ "settings_security.enable_passcode" = "Включить код доступа"; "settings_security.edit_passcode" = "Изменить код"; "settings_security.disable_passcode" = "Отключить код доступа"; -"settings_security.auto_lock" = "Автоблокировка"; +"settings_security.auto_lock" = "Блокировка"; "settings_security.balance_auto_hide" = "Автоскрытие баланса"; "settings_security.balance_auto_hide.description" = "Автоматически скрывает баланс при открытии приложения, независимо от предыдущих настроек."; "settings_security.enable_duress_mode" = "Установить режим Duress"; "settings_security.edit_duress_passcode" = "Изменить Duress код"; -"settings_security.disable_duress_mode" = "Отключить режим Duress"; +"settings_security.disable_duress_mode" = "Отключить Duress код"; "settings_security.duress_mode.description" = "Специализированный режим, разработанный для обеспечения безопасности выбранных кошельков в условиях принуждения."; // Create Passcode @@ -1256,19 +1220,6 @@ "btc_restore_mode.recommended" = "Рекомендовано"; "btc_restore_mode.more_private" = "Более приватно"; -"settings_security.passcode" = "Код доступа"; -"settings_security.change_pin" = "Изменить код"; -"settings_security.touch_id" = "Touch ID"; -"settings_security.face_id" = "Face ID"; -"settings_security.blockchain_settings" = "Настройки блокчейна"; -"security_settings.delete_alert_button" = "Удалить с телефона"; - -"btc_blockchain_settings.restore_source" = "Источник восстановления"; -"btc_blockchain_settings.restore_source.description" = "Выберите источник данных для восстановления кошелька с транзакциями."; -"btc_blockchain_settings.restore_source.alert" = "После изменения источника восстановления, кошелек должен будет повторно синхронизироваться с блокчейном %@."; - -"btc_restore_mode.recommended" = "Рекомендовано"; -"btc_restore_mode.more_private" = "Более Приватно"; "btc_transaction_sort_mode.shuffle" = "Shuffle"; "btc_transaction_sort_mode.shuffle.description" = "Случайное индексирование"; @@ -1308,7 +1259,6 @@ // Settings -> About App -> Contact "settings.contact.title" = "Свяжитесь с нами"; - "settings.contact.via_email" = "по электронной почте"; "settings.contact.via_telegram" = "через Telegram"; @@ -1369,7 +1319,6 @@ "contacts.restore.parsing_error" = "Файл содержит неверные данные!"; "contacts.restore.restore_error" = "Не удалось восстановить контакты"; "contacts.restore.overwrite_alert.description" = "Это действие перезапишет ваши локальные контакты для платежей, а также копию в iCloud (если таковая имеется)."; - "contacts.restore.overwrite_alert.replace" = "Заменить"; "contacts.add_address.title" = "Добавить адрес"; @@ -1404,7 +1353,6 @@ "contacts.settings.alert.title" = "iCloud синхр."; "contacts.settings.alert.description" = "Пожалуйста, убедитесь, что iCloud хранилище включено на вашем устройстве."; - "contacts.settings.alert_error.title" = "Ошибка iCloud"; // Key Types @@ -1436,7 +1384,6 @@ "chart.about.read_more" = "Читать далее"; "chart.about.read_less" = "Скрыть"; - "coin_page.return_of_investments" = "ROI"; // Create Wallet @@ -1453,10 +1400,10 @@ "create_wallet.passphrase" = "Кодовая фраза"; "create_wallet.input.passphrase" = "Кодовая фраза"; "create_wallet.input.confirm" = "Подтвердить"; -"create_wallet.passphrase_description" = "Кодовая фраза добавляет дополнительный слой безопасности для кошельков. Чтобы восстановить такой кошелек требуется как мнемоник, так и кодовая фраза.\n\\Кодовая фраза также облегчает пользователям возможность иметь множество мульти-монетных кошельков, используя одну фразу восстановления, но другой пароль."; +"create_wallet.passphrase_description" = "Think of passphrases as your wallet's secret handshake. If you ever need to bring back a lost wallet, you'll need the recovery phrase and this passphrase. Plus, with just one mnemonic, you can unlock a bunch of multi-coin wallets with different passphrases. Neat, right?"; "create_wallet.error.empty_passphrase" = "Поле для кодовой фразы не может быть пустым"; -"create_wallet.error.forbidden_symbols" = "Пожалуйста, используйте только поддерживаемые символы: A-Z a-z 0-9 ' \" ` & / ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; -"create_wallet.error.invalid_confirmation" = "Подтвержденная кодовая фраза не совпадает"; +"create_wallet.error.forbidden_symbols" = "Hold on there, cowboy! Let's stick to the symbols we know and love::A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; +"create_wallet.error.invalid_confirmation" = "Hmm, your passphrase confirmations aren't matching. Let's try that again."; // Restore Select @@ -1467,7 +1414,6 @@ "lock_info.title" = "TimeLock"; "lock_info.text" = "Отправитель отправил эти средства с временной блокировкой, которая истекает в указанную дату.\n\nНе волнуйтесь, полученные биткоины уже принадлежат вам, но до истечения срока блокировки вы не сможете потратить их в сети Bitcoin."; - // Double Spend Info "double_spend_info.title" = "Двойная трата"; @@ -1502,7 +1448,6 @@ "add_token.invalid_contract_address" = "Недействительный адрес контракта"; "add_token.invalid_bep2_symbol" = "Неверный символ BEP2"; "add_token.contract_address_not_found" = "Адрес контракта не найден в %@ блокчейне"; - "add_token.bep2_symbol_not_found" = "Символ BEP2 не найден"; "add_token.input_placeholder.contract_address" = "Адрес контракта"; "add_token.input_placeholder.bep2_symbol" = "Символ BEP2"; @@ -1698,8 +1643,7 @@ "fee_settings.base_fee" = "Базовая комиссия"; "fee_settings.base_fee.info" = "Сетевой протокол определяет базовую стоимость газа для каждого блока, которая называется базовая ставка комиссии. Он варьирует в зависимости от уровня загрузки сети от блока к блоку. В следующем блоке он может увеличиться или уменьшиться не более чем на 12.5%, что делает взимаемый тариф более предсказуемым. Здесь отражена базовая ставка комиссии текущего блока."; - -"fee_settings.max_fee_rate" = "Макс. ставка комиссии"; +"fee_settings.max_fee_rate" = "Top Dollar Rate"; "fee_settings.max_fee_rate.info" = "Это максимальная общая цена газа, которую пользователь готов заплатить. Она должна покрывать базовую тарифную ставку сети и максимальный приоритетный тариф. Указанное здесь значение предлагается на основе оценки суммы базовой тарифной ставки следующего блока и максимального приоритетного тарифа, выбранного пользователем. Фактический размер платёжного тарифа обычно меньше. Снижение настройки текущего базового тарифа ограничит оплачиваемую сумму тарифа, но приведет к удлинению периода ожидания подтверждения транзакции или даже к ее подвисанию."; "fee_settings.tips" = "Макс. приоритет. комиссия"; "fee_settings.tips.info" = "Пользователи платят приоритетную комиссию, в целях ускорения процесса подтверждения транзакции. Иногда эти тарифы называются чаевыми. Максимальный размер приоритетного сбора - это максимальная дополнительная цена за газ, которую пользователь готов оплатить поверх базовой ставки комиссии. Приведенная здесь стоимость определяется исходя из прогнозируемых сетевых условий. Фактический размер приоритетного тарифа будет меньше. Обнуление этого тарифа может привести к удлинению периода ожидания подтверждения транзакции ввиду её размещения в конце очереди незавершенных транзакций ото всех пользователей."; @@ -1744,8 +1688,8 @@ "nft_collections.on_sale" = "В продаже"; "nft_collections.empty" = "В вашем кошельке нет NFT"; -"top_nft_collections.title" = "Топ NFT коллекции"; -"top_nft_collections.description" = "Ведущие коллекции NFT по объему торгов."; +"top_nft_collections.title" = "Top Art Stash"; +"top_nft_collections.description" = "Популярные коллекции NFT по объему торгов."; // Nft Asset @@ -1835,7 +1779,6 @@ // Launch "launch.failed_to_launch" = "Не удалось запустить приложение из-за внутренней ошибки. Попробуйте перезапустить приложение или сообщите об ошибке нашей службе поддержки."; - "launch.failed_to_launch.report" = "Пожаловаться"; // Tron diff --git a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings index c49160678f..e71ffddc43 100644 --- a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings @@ -271,7 +271,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "backup.cloud.password.confirm.placeholder" = "Onayla"; "backup.cloud.password.save" = "Kaydet ve Yedekle"; -"backup.cloud.password.error.empty_passphrase" = "Parola boş olamaz"; +"backup.cloud.password.error.empty_passphrase" = "Parola boş bırakılamaz"; "backup.cloud.password.error.forbidden_symbols" = "Lütfen yalnızca desteklenen sembolleri kullanın: A-Z a-z 0-9 ' \" ` & / ? ! : ; . , ~ * $ = + - [ ] ( ) { } < > \\ _ # @ | %"; "backup.cloud.password.error.minimum_requirement" = "Bir büyük harf, bir küçük harf, bir rakam ve bir sembol dahil olmak üzere en az 8 karakter"; "backup.cloud.password.error.invalid_password" = "Yanlış parola"; @@ -1157,7 +1157,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings_security.balance_auto_hide.description" = "Uygulama her açıldığında, önceki tercihlere bakılmaksızın bakiyeyi otomatik olarak gizler."; "settings_security.enable_duress_mode" = "Baskı Modunu Ayarla"; "settings_security.edit_duress_passcode" = "Duress Şifresini Düzenle"; -"settings_security.disable_duress_mode" = "Baskı Modunu Devre Dışı Bırak"; +"settings_security.disable_duress_mode" = "Zorlama Şifresini Devre Dışı Bırak"; "settings_security.duress_mode.description" = "Seçilen cüzdanları zorlama altında güvende tutmak için tasarlanmış özel bir mod."; // Create Passcode diff --git a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings index db208008cc..d47a040e84 100644 --- a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings @@ -1153,7 +1153,7 @@ "settings_security.balance_auto_hide.description" = "每次打开应用程序时自动隐藏平衡,不管先前的偏好设置。"; "settings_security.enable_duress_mode" = "设置压力模式"; "settings_security.edit_duress_passcode" = "编辑货币密码"; -"settings_security.disable_duress_mode" = "禁用压力模式"; +"settings_security.disable_duress_mode" = "禁用冒险密码"; "settings_security.duress_mode.description" = "专用模式用于在胁迫下使选定的钱包安全。"; // Create Passcode From 464f68e7c37233b5397cf8f2e1f2b0f490320217 Mon Sep 17 00:00:00 2001 From: Esenbek Date: Tue, 17 Oct 2023 15:11:55 +0600 Subject: [PATCH 63/63] Update checkpoints --- UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index face58f0d5..aa247239a7 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -11424,7 +11424,7 @@ repositoryURL = "https://github.com/horizontalsystems/Checkpoints"; requirement = { kind = exactVersion; - version = 1.0.13; + version = 1.0.14; }; }; D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit.Swift" */ = {