From 83dbe85a8547ef56ba6efbbd013043a46109bcba Mon Sep 17 00:00:00 2001 From: Vladyslav shepitko Date: Fri, 11 Jun 2021 17:46:55 +0300 Subject: [PATCH] Support UI for user to add custom RPC server --- AlphaWallet.xcodeproj/project.pbxproj | 28 +- .../add_hide_tokens.imageset/Contents.json | 3 + .../Coordinators/DappBrowserCoordinator.swift | 20 +- ...RequestSwitchCustomChainCoordinator.swift} | 37 ++- AlphaWallet/Browser/Types/DappCommand.swift | 1 - AlphaWallet/Extensions/Error.swift | 40 ++- AlphaWallet/InCoordinator.swift | 34 +- .../Localization/en.lproj/Localizable.strings | 19 ++ .../Localization/es.lproj/Localizable.strings | 19 ++ .../Localization/ja.lproj/Localizable.strings | 19 ++ .../Localization/ko.lproj/Localizable.strings | 19 ++ .../zh-Hans.lproj/Localizable.strings | 19 ++ AlphaWallet/Models/RestartTaskQueue.swift | 2 + .../AddRPCServerCoordinator.swift | 77 +++++ .../EnabledServersCoordinator.swift | 32 +- .../Coordinators/SettingsCoordinator.swift | 17 +- .../Settings/Models/AddCustomChain.swift | 43 +-- AlphaWallet/Settings/Types/RPCServers.swift | 13 + .../AddRPCServerViewController.swift | 306 ++++++++++++++++++ .../EnabledServersViewController.swift | 49 ++- .../ViewModels/AddRPCServerViewModel.swift | 41 +++ AlphaWallet/Settings/Views/SwitchView.swift | 75 +++++ .../TokensViewController.swift | 4 + AlphaWallet/Tokens/Views/TextField.swift | 9 + .../Extensions/IntExtensionsTests.swift | 13 + .../SettingsCoordinatorTests.swift | 3 + 26 files changed, 870 insertions(+), 72 deletions(-) rename AlphaWallet/Browser/Coordinators/{SwitchCustomChainCoordinator.swift => DappRequestSwitchCustomChainCoordinator.swift} (85%) create mode 100644 AlphaWallet/Settings/Coordinators/AddRPCServerCoordinator.swift create mode 100644 AlphaWallet/Settings/ViewControllers/AddRPCServerViewController.swift create mode 100644 AlphaWallet/Settings/ViewModels/AddRPCServerViewModel.swift create mode 100644 AlphaWallet/Settings/Views/SwitchView.swift create mode 100644 AlphaWalletTests/Extensions/IntExtensionsTests.swift diff --git a/AlphaWallet.xcodeproj/project.pbxproj b/AlphaWallet.xcodeproj/project.pbxproj index aa182a09f..ff3c9c180 100644 --- a/AlphaWallet.xcodeproj/project.pbxproj +++ b/AlphaWallet.xcodeproj/project.pbxproj @@ -295,7 +295,7 @@ 5E7C72B0A10A92E591696E48 /* ContactUsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AE6FAE0DF969B4F52E9 /* ContactUsBannerView.swift */; }; 5E7C72B359B220D0CBA7DCF1 /* DappsAutoCompletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76EE22984D66A3C18E70 /* DappsAutoCompletionViewController.swift */; }; 5E7C72B3D5F75999937FCFA1 /* TokenListFormatRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74A1A13A1A6CB9E61BAC /* TokenListFormatRowViewModel.swift */; }; - 5E7C72B4302A10E137EEF94A /* SwitchCustomChainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C11AF59E7CA26B3A2BB /* SwitchCustomChainCoordinator.swift */; }; + 5E7C72B4302A10E137EEF94A /* DappRequestSwitchCustomChainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C11AF59E7CA26B3A2BB /* DappRequestSwitchCustomChainCoordinator.swift */; }; 5E7C72C1DBECCAFA0151A498 /* AssetDefinitionInMemoryBackingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C785114A3266813AC92A6 /* AssetDefinitionInMemoryBackingStore.swift */; }; 5E7C72C8A15397C5A40BFE76 /* WhatIsEthereumInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C774BCA281E4B077DBBFA /* WhatIsEthereumInfoViewController.swift */; }; 5E7C72CEFE98436DB8EC0E05 /* BrowserHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73D55C366BCC53208686 /* BrowserHistoryCell.swift */; }; @@ -502,6 +502,7 @@ 5E7C7AD1BA92A8FFF930F8DC /* BrowserURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C8CA3706DC14167786C /* BrowserURLParserTests.swift */; }; 5E7C7AD59EA28935E32B3E91 /* KeystoreBackupIntroductionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F829626688EA50E1B68 /* KeystoreBackupIntroductionViewController.swift */; }; 5E7C7AD6B20E857DAF560E4E /* AssetAttributeValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E05C83CCB7E910ADC43 /* AssetAttributeValues.swift */; }; + 5E7C7ADB4D80243679028A6D /* IntExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F4F209C6EE3828E18EC /* IntExtensionsTests.swift */; }; 5E7C7AE2EF04A23EC7C5ADFD /* ImportMagicTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7535095323B035CA47C0 /* ImportMagicTokenViewController.swift */; }; 5E7C7AFF5433B2B8B3415C55 /* DefaultActivityCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B6849D43348C5109712 /* DefaultActivityCellViewModel.swift */; }; 5E7C7B0367CFB413C6885474 /* GenerateSellMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7624D6F7EA55F6F167B3 /* GenerateSellMagicLinkViewControllerViewModel.swift */; }; @@ -757,6 +758,10 @@ 8769888D24C6ED04002BF62B /* TransactionInProgressCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */; }; 8769BCA8256D15BF0095EA5B /* BlockieImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */; }; 876C80C92673349300B16595 /* CoinTickersFetcherCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80C82673349300B16595 /* CoinTickersFetcherCache.swift */; }; + 876C80CB267386E300B16595 /* AddRPCServerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80CA267386E300B16595 /* AddRPCServerCoordinator.swift */; }; + 876C80CD2673940B00B16595 /* SwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80CC2673940B00B16595 /* SwitchView.swift */; }; + 876C80CF267398F900B16595 /* AddRPCServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80CE267398F900B16595 /* AddRPCServerViewController.swift */; }; + 876C80D12673992300B16595 /* AddRPCServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80D02673992300B16595 /* AddRPCServerViewModel.swift */; }; 87713EAE264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */; }; 877D00AF25ADF60A008E22CC /* TransactionConfiguratorTransactionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877D00AE25ADF60A008E22CC /* TransactionConfiguratorTransactionsTests.swift */; }; 8782035D2431E66600792F12 /* FilterTokensCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782035C2431E66600792F12 /* FilterTokensCoordinator.swift */; }; @@ -1439,7 +1444,7 @@ 5E7C7C097681EC7E022B6C44 /* ENSReverseLookupEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ENSReverseLookupEncode.swift; sourceTree = ""; }; 5E7C7C0AC267283F3F2A6E37 /* EmptyDapps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDapps.swift; sourceTree = ""; }; 5E7C7C0CFD047ED7C488FB45 /* OpenSea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSea.swift; sourceTree = ""; }; - 5E7C7C11AF59E7CA26B3A2BB /* SwitchCustomChainCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCustomChainCoordinator.swift; sourceTree = ""; }; + 5E7C7C11AF59E7CA26B3A2BB /* DappRequestSwitchCustomChainCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappRequestSwitchCustomChainCoordinator.swift; sourceTree = ""; }; 5E7C7C12E88EB0B73AA1E562 /* TokenCardRowViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCardRowViewModelProtocol.swift; sourceTree = ""; }; 5E7C7C1720AD49046D2B4023 /* TransactionDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewModel.swift; sourceTree = ""; }; 5E7C7C34A7BDCFE17CEF8F79 /* OpenSeaNonFungibleTokenAttributeCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenAttributeCellViewModel.swift; sourceTree = ""; }; @@ -1520,6 +1525,7 @@ 5E7C7F1F965C69B80A234F1F /* EditedTransactionConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditedTransactionConfiguration.swift; sourceTree = ""; }; 5E7C7F21FA7A02F6341FB58D /* AssetDefinitionsOverridesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionsOverridesViewController.swift; sourceTree = ""; }; 5E7C7F3DD81D44A996789FC4 /* UniversalLinkInPasteboardCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalLinkInPasteboardCoordinator.swift; sourceTree = ""; }; + 5E7C7F4F209C6EE3828E18EC /* IntExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntExtensionsTests.swift; sourceTree = ""; }; 5E7C7F54FD247553A7427881 /* TokensViewControllerCollectiblesCollectionViewHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewControllerCollectiblesCollectionViewHeader.swift; sourceTree = ""; }; 5E7C7F55495A6095B3E86248 /* EditMyDappViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMyDappViewController.swift; sourceTree = ""; }; 5E7C7F5A42365FDA22AEE6F7 /* ActivityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityViewModel.swift; sourceTree = ""; }; @@ -1672,6 +1678,10 @@ 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressCoordinator.swift; sourceTree = ""; }; 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = ""; }; 876C80C82673349300B16595 /* CoinTickersFetcherCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinTickersFetcherCache.swift; sourceTree = ""; }; + 876C80CA267386E300B16595 /* AddRPCServerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerCoordinator.swift; sourceTree = ""; }; + 876C80CC2673940B00B16595 /* SwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchView.swift; sourceTree = ""; }; + 876C80CE267398F900B16595 /* AddRPCServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerViewController.swift; sourceTree = ""; }; + 876C80D02673992300B16595 /* AddRPCServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerViewModel.swift; sourceTree = ""; }; 87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodedFunctionCall+Decode.swift"; sourceTree = ""; }; 877D00AE25ADF60A008E22CC /* TransactionConfiguratorTransactionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConfiguratorTransactionsTests.swift; sourceTree = ""; }; 8782035C2431E66600792F12 /* FilterTokensCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterTokensCoordinator.swift; sourceTree = ""; }; @@ -2254,6 +2264,7 @@ 5E7C7C7CB95B7EE4B2547585 /* EnabledServersCoordinator.swift */, 5E7C7C51CEC4AAFDFBD75482 /* ConsoleCoordinator.swift */, 5E7C72571AB0FECB26FEB1B1 /* ClearDappBrowserCacheCoordinator.swift */, + 876C80CA267386E300B16595 /* AddRPCServerCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -2512,6 +2523,7 @@ 87ED8F98248541380005C69B /* AdvancedSettingsViewModel.swift */, 87ED8F9C248647C80005C69B /* SettingViewHeaderViewModel.swift */, 87ED8FAE2488EA9C0005C69B /* SupportViewModel.swift */, + 876C80D02673992300B16595 /* AddRPCServerViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -2838,7 +2850,7 @@ 5E7C7721E0E4D4EFDD35E196 /* ScanQRCodeCoordinator.swift */, 76F1D4E689C3ECFD38CBBC47 /* DappBrowserCoordinator.swift */, 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */, - 5E7C7C11AF59E7CA26B3A2BB /* SwitchCustomChainCoordinator.swift */, + 5E7C7C11AF59E7CA26B3A2BB /* DappRequestSwitchCustomChainCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -2972,6 +2984,7 @@ 5E7C7C468EC03C073E0EEA03 /* SettingsViewController.swift */, 87ED8F96248540F90005C69B /* AdvancedSettingsViewController.swift */, 87ED8FAC2488EA610005C69B /* SupportViewController.swift */, + 876C80CE267398F900B16595 /* AddRPCServerViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3583,6 +3596,7 @@ 5E7C7553BB089397B7E74BE0 /* WalletSecurityLevelIndicator.swift */, 87ED8F92248534E30005C69B /* SwitchTableViewCell.swift */, 5E7C7DC3AEBE4049927B7625 /* EnableServersHeaderView.swift */, + 876C80CC2673940B00B16595 /* SwitchView.swift */, ); path = Views; sourceTree = ""; @@ -3671,6 +3685,7 @@ isa = PBXGroup; children = ( 5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */, + 5E7C7F4F209C6EE3828E18EC /* IntExtensionsTests.swift */, ); path = Extensions; sourceTree = ""; @@ -4954,6 +4969,7 @@ 29A0E1851F706B8C00BAAAED /* String.swift in Sources */, 296AF9A71F736EC70058AF78 /* RPCServers.swift in Sources */, 296AF9A91F737F6F0058AF78 /* SendRawTransactionRequest.swift in Sources */, + 876C80D12673992300B16595 /* AddRPCServerViewModel.swift in Sources */, 293112121FC4F48400966EEA /* ServiceProvider.swift in Sources */, 2912CD2F1F6A83A100C6CBE3 /* ImportWalletViewController.swift in Sources */, 874DED1724C1BB0E006C8FCE /* SelectAssetViewController.swift in Sources */, @@ -5011,6 +5027,7 @@ 2996F1431F6C96FF005C33AE /* ImportWalletViewModel.swift in Sources */, 291ED08D1F6F5F0A00E7E93A /* KeyStoreError.swift in Sources */, 296AF9A31F733AB30058AF78 /* WalletCoordinator.swift in Sources */, + 876C80CB267386E300B16595 /* AddRPCServerCoordinator.swift in Sources */, 442FCBBFCC5926B4D416E6D3 /* GetNameCoordinator.swift in Sources */, 442FC5F70AF003F331F7C841 /* GetSymbolCoordinator.swift in Sources */, 87ED8F9D248647C80005C69B /* SettingViewHeaderViewModel.swift in Sources */, @@ -5317,6 +5334,7 @@ 5E7C7ACB2F44B820940EACEB /* TokenInstanceActionViewController.swift in Sources */, 87B651F7256D4BFE000EF927 /* ClaimPaidOrderCoordinator.swift in Sources */, 874D099425EE339700A58EF2 /* TypedDataView.swift in Sources */, + 876C80CF267398F900B16595 /* AddRPCServerViewController.swift in Sources */, 5E7C76AE768F82036ED2B2D8 /* TokenInstanceAction.swift in Sources */, 5E7C77E30FA9E84DA58C937E /* Optional.swift in Sources */, 5E7C784B592A446BE35D3DE9 /* AlphaWalletAddress.swift in Sources */, @@ -5484,6 +5502,7 @@ 5E7C76DE2F995FE2ABEE74C0 /* ActivitiesViewController.swift in Sources */, 5E7C7AFF5433B2B8B3415C55 /* DefaultActivityCellViewModel.swift in Sources */, 5E7C7041A8A78E0EF84A94A2 /* ActivityViewCell.swift in Sources */, + 876C80CD2673940B00B16595 /* SwitchView.swift in Sources */, 5E7C7E2109BCEB05899A4F9A /* DefaultActivityItemViewCell.swift in Sources */, 5E7C78F7C4848D484AD4183A /* DefaultActivityView.swift in Sources */, 5E7C710F30F8EAC43A706500 /* DefaultActivityViewModel.swift in Sources */, @@ -5529,7 +5548,7 @@ 5E7C711131E8E0B8AD983B70 /* EthChainIdRequest.swift in Sources */, 5E7C7711465975C681496C9C /* EnableChain.swift in Sources */, 5E7C77AC072B44B7E60030B5 /* RestartTaskQueue.swift in Sources */, - 5E7C72B4302A10E137EEF94A /* SwitchCustomChainCoordinator.swift in Sources */, + 5E7C72B4302A10E137EEF94A /* DappRequestSwitchCustomChainCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5619,6 +5638,7 @@ 8499D13528C1FABDC27EA7B1 /* SmartContractHelperTests.swift in Sources */, 5E7C7B89694C62A14CBE8105 /* FakeEventsDataStore.swift in Sources */, 5E7C7E5C76318C0E802F377F /* TokenScriptFilterParserTests.swift in Sources */, + 5E7C7ADB4D80243679028A6D /* IntExtensionsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AlphaWallet/Assets.xcassets/add_hide_tokens.imageset/Contents.json b/AlphaWallet/Assets.xcassets/add_hide_tokens.imageset/Contents.json index 9a151c148..75f809ec8 100644 --- a/AlphaWallet/Assets.xcassets/add_hide_tokens.imageset/Contents.json +++ b/AlphaWallet/Assets.xcassets/add_hide_tokens.imageset/Contents.json @@ -17,5 +17,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } } diff --git a/AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift b/AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift index 78b3ba31d..252b9ae2b 100644 --- a/AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift +++ b/AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift @@ -363,7 +363,7 @@ final class DappBrowserCoordinator: NSObject, Coordinator { } private func addCustomWallet(callbackID: Int, customChain: WalletAddEthereumChainObject, inViewController viewController: UIViewController) { - let coordinator = SwitchCustomChainCoordinator(config: config, server: server, callbackId: callbackID, customChain: customChain, restartQueue: restartQueue, currentUrl: currentUrl, inViewController: viewController) + let coordinator = DappRequestSwitchCustomChainCoordinator(config: config, server: server, callbackId: callbackID, customChain: customChain, restartQueue: restartQueue, currentUrl: currentUrl, inViewController: viewController) coordinator.delegate = self addCoordinator(coordinator) coordinator.start() @@ -817,45 +817,45 @@ extension DappBrowserCoordinator { } } -extension DappBrowserCoordinator: SwitchCustomChainCoordinatorDelegate { - func notifySuccessful(withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) { +extension DappBrowserCoordinator: DappRequestSwitchCustomChainCoordinatorDelegate { + func notifySuccessful(withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { let callback = DappCallback(id: callbackId, value: .walletAddEthereumChain) browserViewController.notifyFinish(callbackID: callbackId, value: .success(callback)) removeCoordinator(coordinator) } - func switchBrowserToExistingServer(_ server: RPCServer, url: URL?, inCoordinator coordinator: SwitchCustomChainCoordinator) { + func switchBrowserToExistingServer(_ server: RPCServer, url: URL?, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { `switch`(toServer: server, url: url) removeCoordinator(coordinator) } - func restartToEnableAndSwitchBrowserToServer(inCoordinator coordinator: SwitchCustomChainCoordinator) { + func restartToEnableAndSwitchBrowserToServer(inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { delegate?.restartToEnableAndSwitchBrowserToServer(inCoordinator: self) removeCoordinator(coordinator) } - func restartToAddEnableAAndSwitchBrowserToServer(inCoordinator coordinator: SwitchCustomChainCoordinator) { + func restartToAddEnableAAndSwitchBrowserToServer(inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { delegate?.restartToAddEnableAAndSwitchBrowserToServer(inCoordinator: self) removeCoordinator(coordinator) } - func userCancelled(withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) { + func userCancelled(withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { browserViewController.notifyFinish(callbackID: callbackId, value: .failure(DAppError.cancelled)) removeCoordinator(coordinator) } - func failed(withErrorMessage errorMessage: String, withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) { + func failed(withErrorMessage errorMessage: String, withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { let error = DAppError.nodeError(errorMessage) browserViewController.notifyFinish(callbackID: callbackId, value: .failure(error)) removeCoordinator(coordinator) } - func failed(withError error: DAppError, withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) { + func failed(withError error: DAppError, withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) { browserViewController.notifyFinish(callbackID: callbackId, value: .failure(error)) removeCoordinator(coordinator) } - func cleanup(coordinator: SwitchCustomChainCoordinator) { + func cleanup(coordinator: DappRequestSwitchCustomChainCoordinator) { removeCoordinator(coordinator) } } \ No newline at end of file diff --git a/AlphaWallet/Browser/Coordinators/SwitchCustomChainCoordinator.swift b/AlphaWallet/Browser/Coordinators/DappRequestSwitchCustomChainCoordinator.swift similarity index 85% rename from AlphaWallet/Browser/Coordinators/SwitchCustomChainCoordinator.swift rename to AlphaWallet/Browser/Coordinators/DappRequestSwitchCustomChainCoordinator.swift index 8997d8806..04837b0f6 100644 --- a/AlphaWallet/Browser/Coordinators/SwitchCustomChainCoordinator.swift +++ b/AlphaWallet/Browser/Coordinators/DappRequestSwitchCustomChainCoordinator.swift @@ -2,19 +2,19 @@ import UIKit -protocol SwitchCustomChainCoordinatorDelegate: class { - func notifySuccessful(withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) - func restartToEnableAndSwitchBrowserToServer(inCoordinator coordinator: SwitchCustomChainCoordinator) - func restartToAddEnableAAndSwitchBrowserToServer(inCoordinator coordinator: SwitchCustomChainCoordinator) - func switchBrowserToExistingServer(_ server: RPCServer, url: URL?, inCoordinator coordinator: SwitchCustomChainCoordinator) - func userCancelled(withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) - func failed(withErrorMessage errorMessage: String, withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) - func failed(withError error: DAppError, withCallbackId callbackId: Int, inCoordinator coordinator: SwitchCustomChainCoordinator) +protocol DappRequestSwitchCustomChainCoordinatorDelegate: class { + func notifySuccessful(withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func restartToEnableAndSwitchBrowserToServer(inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func restartToAddEnableAAndSwitchBrowserToServer(inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func switchBrowserToExistingServer(_ server: RPCServer, url: URL?, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func userCancelled(withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func failed(withErrorMessage errorMessage: String, withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) + func failed(withError error: DAppError, withCallbackId callbackId: Int, inCoordinator coordinator: DappRequestSwitchCustomChainCoordinator) //This might not always been called. We call it when there's no other delegate function to call to inform the delegate to remove this coordinator - func cleanup(coordinator: SwitchCustomChainCoordinator) + func cleanup(coordinator: DappRequestSwitchCustomChainCoordinator) } -class SwitchCustomChainCoordinator: NSObject, Coordinator { +class DappRequestSwitchCustomChainCoordinator: NSObject, Coordinator { private var addCustomChain: (chain: AddCustomChain, callbackId: Int)? private let config: Config private let server: RPCServer @@ -25,7 +25,7 @@ class SwitchCustomChainCoordinator: NSObject, Coordinator { private let viewController: UIViewController var coordinators: [Coordinator] = [] - weak var delegate: SwitchCustomChainCoordinatorDelegate? + weak var delegate: DappRequestSwitchCustomChainCoordinatorDelegate? init(config: Config, server: RPCServer, callbackId: Int, customChain: WalletAddEthereumChainObject, restartQueue: RestartTaskQueue, currentUrl: URL?, inViewController viewController: UIViewController) { self.config = config @@ -127,14 +127,14 @@ class SwitchCustomChainCoordinator: NSObject, Coordinator { } } -extension SwitchCustomChainCoordinator: EnableChainDelegate { +extension DappRequestSwitchCustomChainCoordinator: EnableChainDelegate { //Don't need to notify browser/dapp since we are restarting UI func notifyEnableChainQueuedSuccessfully(in enableChain: EnableChain) { delegate?.restartToEnableAndSwitchBrowserToServer(inCoordinator: self) } } -extension SwitchCustomChainCoordinator: AddCustomChainDelegate { +extension DappRequestSwitchCustomChainCoordinator: AddCustomChainDelegate { //Don't need to notify browser/dapp since we are restarting UI func notifyAddCustomChainQueuedSuccessfully(in addCustomChain: AddCustomChain) { guard self.addCustomChain != nil else { @@ -144,11 +144,18 @@ extension SwitchCustomChainCoordinator: AddCustomChainDelegate { delegate?.restartToAddEnableAAndSwitchBrowserToServer(inCoordinator: self) } - func notifyAddCustomChainFailed(error: DAppError, in addCustomChain: AddCustomChain) { + func notifyAddCustomChainFailed(error: AddCustomChainError, in addCustomChain: AddCustomChain) { guard self.addCustomChain != nil else { delegate?.cleanup(coordinator: self) return } - delegate?.failed(withError: error, withCallbackId: callbackId, inCoordinator: self) + let dAppError: DAppError + switch error { + case .cancelled: + dAppError = .cancelled + case .others(let message): + dAppError = .nodeError(message) + } + delegate?.failed(withError: dAppError, withCallbackId: callbackId, inCoordinator: self) } } \ No newline at end of file diff --git a/AlphaWallet/Browser/Types/DappCommand.swift b/AlphaWallet/Browser/Types/DappCommand.swift index da95f1dbd..c98b06b0b 100644 --- a/AlphaWallet/Browser/Types/DappCommand.swift +++ b/AlphaWallet/Browser/Types/DappCommand.swift @@ -121,7 +121,6 @@ struct WalletAddEthereumChainObject: Decodable { let nativeCurrency: NativeCurrency? var blockExplorerUrls: [String]? let chainName: String? - let chainType: String? let chainId: String let rpcUrls: [String]? } diff --git a/AlphaWallet/Extensions/Error.swift b/AlphaWallet/Extensions/Error.swift index f1aabad96..d9f999f55 100644 --- a/AlphaWallet/Extensions/Error.swift +++ b/AlphaWallet/Extensions/Error.swift @@ -11,22 +11,7 @@ extension Error { case let error as AnyError: switch error.error { case let error as APIKit.SessionTaskError: - switch error { - case .connectionError(let error): - return error.localizedDescription - case .requestError(let error): - return error.localizedDescription - case .responseError(let error): - guard let JSONError = error as? JSONRPCError else { - return error.localizedDescription - } - switch JSONError { - case .responseError(_, let message, _): - return message - case .responseNotFound, .resultObjectParseError, .errorObjectParseError, .unsupportedVersion, .unexpectedTypeObject, .missingBothResultAndError, .nonArrayResponse: - return R.string.localizable.undefinedError() - } - } + return generatePrettyError(forSessionTaskError: error) default: return error.errorDescription ?? error.description } @@ -34,6 +19,8 @@ extension Error { return error.errorDescription ?? R.string.localizable.unknownError() case let error as NSError: return error.localizedDescription + case let error as APIKit.SessionTaskError: + return generatePrettyError(forSessionTaskError: error) default: return R.string.localizable.undefinedError() } @@ -41,4 +28,23 @@ extension Error { var code: Int { return (self as NSError).code } var domain: String { return (self as NSError).domain } -} + + private func generatePrettyError(forSessionTaskError error: APIKit.SessionTaskError) -> String { + switch error { + case .connectionError(let error): + return error.localizedDescription + case .requestError(let error): + return error.localizedDescription + case .responseError(let error): + guard let JSONError = error as? JSONRPCError else { + return error.localizedDescription + } + switch JSONError { + case .responseError(_, let message, _): + return message + case .responseNotFound, .resultObjectParseError, .errorObjectParseError, .unsupportedVersion, .unexpectedTypeObject, .missingBothResultAndError, .nonArrayResponse: + return R.string.localizable.undefinedError() + } + } + } +} \ No newline at end of file diff --git a/AlphaWallet/InCoordinator.swift b/AlphaWallet/InCoordinator.swift index c8287042a..9bec44ad6 100644 --- a/AlphaWallet/InCoordinator.swift +++ b/AlphaWallet/InCoordinator.swift @@ -520,6 +520,7 @@ class InCoordinator: NSObject, Coordinator { keystore: keystore, config: config, sessions: walletSessions, + restartQueue: restartQueue, promptBackupCoordinator: promptBackupCoordinator, analyticsCoordinator: analyticsCoordinator, walletConnectCoordinator: walletConnectCoordinator @@ -769,6 +770,9 @@ class InCoordinator: NSObject, Coordinator { case .addServer(let server): restartQueue.remove(each) RPCServer.customRpcs.append(server) + case .removeServer(let server): + restartQueue.remove(each) + removeServer(server) case .enableServer(let server): restartQueue.remove(each) var c = config @@ -782,10 +786,30 @@ class InCoordinator: NSObject, Coordinator { } } + private func removeServer(_ server: CustomRPC) { + //Must disable server first because we (might) not have done that if the user had disabled and then remove the server in the UI at the same time. And if we fallback to mainnet when an enabled server's chain ID is not found, this can lead to mainnet appearing twice in the Wallet tab + let servers = config.enabledServers.filter { $0.chainID != server.chainID } + var config = config + config.enabledServers = servers + guard let i = RPCServer.customRpcs.firstIndex(of: server) else { return } + RPCServer.customRpcs.remove(at: i) + switchBrowserServer(awayFrom: server, config: config) + } + + private func switchBrowserServer(awayFrom server: CustomRPC, config: Config) { + if Config.getChainId() == server.chainID { + //To be safe, we find a network that is either mainnet/testnet depending on the chain that was removed + let isTestnet = server.isTestnet + if let targetServer = config.enabledServers.first(where: { $0.isTestnet == isTestnet }) { + Config.setChainId(targetServer.chainID) + } + } + } + private func processRestartQueueAfterRestart(config: Config, coordinator: InCoordinator, restartQueue: RestartTaskQueue) { for each in restartQueue.queue { switch each { - case .addServer, .enableServer, .switchDappServer: + case .addServer, .removeServer, .enableServer, .switchDappServer: break case .loadUrlInDappBrowser(let url): restartQueue.remove(each) @@ -892,6 +916,14 @@ extension InCoordinator: SettingsCoordinatorDelegate { transactionsStorage.deleteAll() } } + + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: SettingsCoordinator) { + processRestartQueueAndRestartUI() + } + + func restartToRemoveServer(in coordinator: SettingsCoordinator) { + processRestartQueueAndRestartUI() + } } extension InCoordinator: UrlSchemeResolver { diff --git a/AlphaWallet/Localization/en.lproj/Localizable.strings b/AlphaWallet/Localization/en.lproj/Localizable.strings index b7ced435a..3d222c6ea 100644 --- a/AlphaWallet/Localization/en.lproj/Localizable.strings +++ b/AlphaWallet/Localization/en.lproj/Localizable.strings @@ -14,6 +14,7 @@ "History" = "History"; "DuckDuckGo" = "DuckDuckGo"; "Google" = "Google"; +"error" = "Error"; "configureTransaction.error.gasFeeTooHigh" = "Gas Fee too high. Max available: %@"; "configureTransaction.error.gasLimitTooHigh" = "Gas Limit too high. Max available: %d"; "configureTransaction.error.nonceNotPositiveNumber" = "Nonce must be a positive number"; @@ -78,6 +79,8 @@ "settings.enabledNetworks.promptEnableTestnet.title" = "What is Testnet?"; "settings.enabledNetworks.promptEnableTestnet.description" = "Testnets tokens are like ‘Monopoly’ money. They have zero financial worth but are used by developers to try out new designs without needing to spend valuable coins."; "settings.enabledNetworks.promptEnableTestnet.button.title" = "Got it, enable Testnets"; +"settings.enabledNetworks.delete.title" = "Are you sure you would like to delete this server?"; +"settings.enabledNetworks.delete.message" = "This will restart the app"; "settings.language.button.title" = "Change Language"; "settings.backupWallet.button.title" = "Back up this Wallet"; "settings.showSeedPhrase.button.title" = "Show Seed Phrase"; @@ -622,3 +625,19 @@ You can check the latest gas price on gasnow.org"; "addCustomChain.switchToExisting" = "This site is requesting you to switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.enableExisting" = "This site is requesting you to enable and switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.addAndSwitch" = "This site is requesting you to add and switch to the %@ chain with chain ID: %d. This will reload the page."; +"addCustomChain.error.unknown" = "Unknown Error"; +"addCustomChain.error.chainIdNotMatch" = "Chain IDs returned by the RPC server do not match: %@ vs. %@"; +"addrpcServer.navigation.title" = "Add Custom RPC Network"; +"addrpcServer.saveButton.title" = "Add Network"; +"addrpcServer.networkName.title" = "Network Name"; +"addrpcServer.rpcUrl.title" = "RPC URL"; +"addrpcServer.blockExplorerUrl.title" = "Block Explorer URL"; +"addrpcServer.isTestnet.title" = "This is Testnet Network"; +"addrpcServer.networkName.error" = "Network Name is invalid"; +"addrpcServer.rpcUrl.error" = "RPC URL is invalid"; +"addrpcServer.chainID.error" = "Chain ID is invalid"; +"addrpcServer.symbol.error" = "Symbol is invalid"; +"addrpcServer.blockExplorerUrl.error" = "Block Explorer URL is invalid"; +"addrpcServer.chainIdAlreadySupported" = "Already supported. Enable this server instead"; +"addrpcServer.rpcUrl.placeholder" = "New RPC Network"; +"addrpcServer.blockExplorerUrl.placeholder" = "Block Explorer"; diff --git a/AlphaWallet/Localization/es.lproj/Localizable.strings b/AlphaWallet/Localization/es.lproj/Localizable.strings index aa672f4f2..a62a2f272 100644 --- a/AlphaWallet/Localization/es.lproj/Localizable.strings +++ b/AlphaWallet/Localization/es.lproj/Localizable.strings @@ -14,6 +14,7 @@ "History" = "Historial"; "DuckDuckGo" = "DuckDuckGo"; "Google" = "Google"; +"error" = "Error"; "configureTransaction.error.gasFeeTooHigh" = "Tarifa de gas demasiado alta. Máximo disponible: %@"; "configureTransaction.error.gasLimitTooHigh" = "Límite de gas demasiado alto. Máximo disponible: %d"; "configureTransaction.error.nonceNotPositiveNumber" = "El nonce debe ser un número positivo"; @@ -78,6 +79,8 @@ "settings.enabledNetworks.promptEnableTestnet.title" = "What is Testnet?"; "settings.enabledNetworks.promptEnableTestnet.description" = "Testnets tokens are like ‘Monopoly’ money. They have zero financial worth but are used by developers to try out new designs without needing to spend valuable coins."; "settings.enabledNetworks.promptEnableTestnet.button.title" = "Got it, enable Testnets"; +"settings.enabledNetworks.delete.title" = "Are you sure you would like to delete this server?"; +"settings.enabledNetworks.delete.message" = "This will restart the app"; "settings.language.button.title" = "Cambiar idioma"; "settings.backupWallet.button.title" = "Hacer una copia de seguridad de este monedero"; "settings.showSeedPhrase.button.title" = "Show Seed Phrase"; @@ -620,3 +623,19 @@ You can check the latest gas price on gasnow.org"; "addCustomChain.switchToExisting" = "This site is requesting you to switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.enableExisting" = "This site is requesting you to enable and switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.addAndSwitch" = "This site is requesting you to add and switch to the %@ chain with chain ID: %d. This will reload the page."; +"addCustomChain.error.unknown" = "Unknown Error"; +"addCustomChain.error.chainIdNotMatch" = "Chain IDs returned by the RPC server do not match: %@ vs. %@"; +"addrpcServer.navigation.title" = "Add Custom RPC Network"; +"addrpcServer.saveButton.title" = "Add Network"; +"addrpcServer.networkName.title" = "Network Name"; +"addrpcServer.rpcUrl.title" = "RPC URL"; +"addrpcServer.blockExplorerUrl.title" = "Block Explorer URL"; +"addrpcServer.isTestnet.title" = "This is Testnet Network"; +"addrpcServer.networkName.error" = "Network Name is invalid"; +"addrpcServer.rpcUrl.error" = "RPC URL is invalid"; +"addrpcServer.chainID.error" = "Chain ID is invalid"; +"addrpcServer.symbol.error" = "Symbol is invalid"; +"addrpcServer.blockExplorerUrl.error" = "Block Explorer URL is invalid"; +"addrpcServer.chainIdAlreadySupported" = "Already supported. Enable this server instead"; +"addrpcServer.rpcUrl.placeholder" = "New RPC Network"; +"addrpcServer.blockExplorerUrl.placeholder" = "Block Explorer"; diff --git a/AlphaWallet/Localization/ja.lproj/Localizable.strings b/AlphaWallet/Localization/ja.lproj/Localizable.strings index 224d356a3..28ae13259 100644 --- a/AlphaWallet/Localization/ja.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ja.lproj/Localizable.strings @@ -14,6 +14,7 @@ "History" = "履歴"; "DuckDuckGo" = "DuckDuckGo"; "Google" = "Google"; +"error" = "エラー"; "configureTransaction.error.gasFeeTooHigh" = "ガス料金が高すぎます。利用可能な最大: %@"; "configureTransaction.error.gasLimitTooHigh" = "ガス制限が高すぎます。利用可能な最大: %d"; "configureTransaction.error.nonceNotPositiveNumber" = "Nonce must be a positive number"; @@ -78,6 +79,8 @@ "settings.enabledNetworks.promptEnableTestnet.title" = "What is Testnet?"; "settings.enabledNetworks.promptEnableTestnet.description" = "Testnets tokens are like ‘Monopoly’ money. They have zero financial worth but are used by developers to try out new designs without needing to spend valuable coins."; "settings.enabledNetworks.promptEnableTestnet.button.title" = "Got it, enable Testnets"; +"settings.enabledNetworks.delete.title" = "Are you sure you would like to delete this server?"; +"settings.enabledNetworks.delete.message" = "This will restart the app"; "settings.language.button.title" = "Change Language"; "settings.backupWallet.button.title" = "Back up this Wallet"; "settings.showSeedPhrase.button.title" = "Show Seed Phrase"; @@ -620,3 +623,19 @@ You can check the latest gas price on gasnow.org"; "addCustomChain.switchToExisting" = "This site is requesting you to switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.enableExisting" = "This site is requesting you to enable and switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.addAndSwitch" = "This site is requesting you to add and switch to the %@ chain with chain ID: %d. This will reload the page."; +"addCustomChain.error.unknown" = "Unknown Error"; +"addCustomChain.error.chainIdNotMatch" = "Chain IDs returned by the RPC server do not match: %@ vs. %@"; +"addrpcServer.navigation.title" = "Add Custom RPC Network"; +"addrpcServer.saveButton.title" = "Add Network"; +"addrpcServer.networkName.title" = "Network Name"; +"addrpcServer.rpcUrl.title" = "RPC URL"; +"addrpcServer.blockExplorerUrl.title" = "Block Explorer URL"; +"addrpcServer.isTestnet.title" = "This is Testnet Network"; +"addrpcServer.networkName.error" = "Network Name is invalid"; +"addrpcServer.rpcUrl.error" = "RPC URL is invalid"; +"addrpcServer.chainID.error" = "Chain ID is invalid"; +"addrpcServer.symbol.error" = "Symbol is invalid"; +"addrpcServer.blockExplorerUrl.error" = "Block Explorer URL is invalid"; +"addrpcServer.chainIdAlreadySupported" = "Already supported. Enable this server instead"; +"addrpcServer.rpcUrl.placeholder" = "New RPC Network"; +"addrpcServer.blockExplorerUrl.placeholder" = "Block Explorer"; diff --git a/AlphaWallet/Localization/ko.lproj/Localizable.strings b/AlphaWallet/Localization/ko.lproj/Localizable.strings index 15614c8cb..1ceb4e005 100644 --- a/AlphaWallet/Localization/ko.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ko.lproj/Localizable.strings @@ -14,6 +14,7 @@ "History" = "기록"; "DuckDuckGo" = "DuckDuckGo"; "Google" = "Google"; +"error" = "오류"; "configureTransaction.error.gasFeeTooHigh" = "가스 수수료가 너무 높습니다. 최대 이용 한도: %@"; "configureTransaction.error.gasLimitTooHigh" = "가스 한도가 너무 높습니다. 최대 이용 한도: %d"; "configureTransaction.error.nonceNotPositiveNumber" = "Nonce must be a positive number"; @@ -78,6 +79,8 @@ "settings.enabledNetworks.promptEnableTestnet.title" = "What is Testnet?"; "settings.enabledNetworks.promptEnableTestnet.description" = "Testnets tokens are like ‘Monopoly’ money. They have zero financial worth but are used by developers to try out new designs without needing to spend valuable coins."; "settings.enabledNetworks.promptEnableTestnet.button.title" = "Got it, enable Testnets"; +"settings.enabledNetworks.delete.title" = "Are you sure you would like to delete this server?"; +"settings.enabledNetworks.delete.message" = "This will restart the app"; "settings.language.button.title" = "Change Language"; "settings.backupWallet.button.title" = "Back up this Wallet"; "settings.showSeedPhrase.button.title" = "Show Seed Phrase"; @@ -620,3 +623,19 @@ You can check the latest gas price on gasnow.org"; "addCustomChain.switchToExisting" = "This site is requesting you to switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.enableExisting" = "This site is requesting you to enable and switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.addAndSwitch" = "This site is requesting you to add and switch to the %@ chain with chain ID: %d. This will reload the page."; +"addCustomChain.error.unknown" = "Unknown Error"; +"addCustomChain.error.chainIdNotMatch" = "Chain IDs returned by the RPC server do not match: %@ vs. %@"; +"addrpcServer.navigation.title" = "Add Custom RPC Network"; +"addrpcServer.saveButton.title" = "Add Network"; +"addrpcServer.networkName.title" = "Network Name"; +"addrpcServer.rpcUrl.title" = "RPC URL"; +"addrpcServer.blockExplorerUrl.title" = "Block Explorer URL"; +"addrpcServer.isTestnet.title" = "This is Testnet Network"; +"addrpcServer.networkName.error" = "Network Name is invalid"; +"addrpcServer.rpcUrl.error" = "RPC URL is invalid"; +"addrpcServer.chainID.error" = "Chain ID is invalid"; +"addrpcServer.symbol.error" = "Symbol is invalid"; +"addrpcServer.blockExplorerUrl.error" = "Block Explorer URL is invalid"; +"addrpcServer.chainIdAlreadySupported" = "Already supported. Enable this server instead"; +"addrpcServer.rpcUrl.placeholder" = "New RPC Network"; +"addrpcServer.blockExplorerUrl.placeholder" = "Block Explorer"; diff --git a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings index 9cdd8e4b1..211de01d0 100644 --- a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings +++ b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings @@ -14,6 +14,7 @@ "History" = "历史记录"; "DuckDuckGo" = "DuckDuckGo"; "Google" = "Google"; +"error" = "错误"; "configureTransaction.error.gasFeeTooHigh" = "燃料费太高。可用上限:%@"; "configureTransaction.error.gasLimitTooHigh" = "燃料限制太高。 可用上限:%d"; "configureTransaction.error.nonceNotPositiveNumber" = "Nonce 必须是一个正值"; @@ -78,6 +79,8 @@ "settings.enabledNetworks.promptEnableTestnet.title" = "What is Testnet?"; "settings.enabledNetworks.promptEnableTestnet.description" = "Testnets tokens are like ‘Monopoly’ money. They have zero financial worth but are used by developers to try out new designs without needing to spend valuable coins."; "settings.enabledNetworks.promptEnableTestnet.button.title" = "Got it, enable Testnets"; +"settings.enabledNetworks.delete.title" = "Are you sure you would like to delete this server?"; +"settings.enabledNetworks.delete.message" = "This will restart the app"; "settings.language.button.title" = "更换语言"; "settings.backupWallet.button.title" = "备份此钱包"; "settings.showSeedPhrase.button.title" = "Show Seed Phrase"; @@ -620,3 +623,19 @@ You can check the latest gas price on gasnow.org"; "addCustomChain.switchToExisting" = "This site is requesting you to switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.enableExisting" = "This site is requesting you to enable and switch to the %@ chain with chain ID: %d. This will reload the page."; "addCustomChain.addAndSwitch" = "This site is requesting you to add and switch to the %@ chain with chain ID: %d. This will reload the page."; +"addCustomChain.error.unknown" = "Unknown Error"; +"addCustomChain.error.chainIdNotMatch" = "Chain IDs returned by the RPC server do not match: %@ vs. %@"; +"addrpcServer.navigation.title" = "Add Custom RPC Network"; +"addrpcServer.saveButton.title" = "Add Network"; +"addrpcServer.networkName.title" = "Network Name"; +"addrpcServer.rpcUrl.title" = "RPC URL"; +"addrpcServer.blockExplorerUrl.title" = "Block Explorer URL"; +"addrpcServer.isTestnet.title" = "This is Testnet Network"; +"addrpcServer.networkName.error" = "Network Name is invalid"; +"addrpcServer.rpcUrl.error" = "RPC URL is invalid"; +"addrpcServer.chainID.error" = "Chain ID is invalid"; +"addrpcServer.symbol.error" = "Symbol is invalid"; +"addrpcServer.blockExplorerUrl.error" = "Block Explorer URL is invalid"; +"addrpcServer.chainIdAlreadySupported" = "Already supported. Enable this server instead"; +"addrpcServer.rpcUrl.placeholder" = "New RPC Network"; +"addrpcServer.blockExplorerUrl.placeholder" = "Block Explorer"; diff --git a/AlphaWallet/Models/RestartTaskQueue.swift b/AlphaWallet/Models/RestartTaskQueue.swift index 5d6eade03..0f5ae2e98 100644 --- a/AlphaWallet/Models/RestartTaskQueue.swift +++ b/AlphaWallet/Models/RestartTaskQueue.swift @@ -6,7 +6,9 @@ class RestartTaskQueue { private (set) var queue: [Task] enum Task: Equatable { + //TODO make it unnecessary to restart UI after adding/removing a custom chain. At least have to start pricing fetching etc case addServer(CustomRPC) + case removeServer(CustomRPC) case enableServer(RPCServer) case switchDappServer(server: RPCServer) case loadUrlInDappBrowser(URL) diff --git a/AlphaWallet/Settings/Coordinators/AddRPCServerCoordinator.swift b/AlphaWallet/Settings/Coordinators/AddRPCServerCoordinator.swift new file mode 100644 index 000000000..7296b2b03 --- /dev/null +++ b/AlphaWallet/Settings/Coordinators/AddRPCServerCoordinator.swift @@ -0,0 +1,77 @@ +// +// AddRPCServerCoordinator.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 11.06.2021. +// + +import UIKit + +protocol AddRPCServerCoordinatorDelegate: class { + func didDismiss(in coordinator: AddRPCServerCoordinator) + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: AddRPCServerCoordinator) +} + +class AddRPCServerCoordinator: NSObject, Coordinator { + var coordinators: [Coordinator] = [] + + private let navigationController: UINavigationController + private let config: Config + private let restartQueue: RestartTaskQueue + weak var delegate: AddRPCServerCoordinatorDelegate? + + init(navigationController: UINavigationController, config: Config, restartQueue: RestartTaskQueue) { + self.navigationController = navigationController + self.config = config + self.restartQueue = restartQueue + } + + func start() { + let viewModel = AddrpcServerViewModel() + let viewController = AddRPCServerViewController(viewModel: viewModel, config: config) + viewController.delegate = self + viewController.navigationItem.largeTitleDisplayMode = .never + viewController.navigationItem.leftBarButtonItem = .backBarButton(self, selector: #selector(backSelected)) + + navigationController.pushViewController(viewController, animated: true) + } + + @objc private func backSelected(_ sender: UIBarButtonItem) { + navigationController.popViewController(animated: true) + + delegate?.didDismiss(in: self) + } + + private func addChain(_ customRpc: CustomRPC) { + let explorerEndpoints: [String]? + if let endpoint = customRpc.explorerEndpoint { + explorerEndpoints = [endpoint] + } else { + explorerEndpoints = nil + } + let defaultDecimals = 18 + let customChain = WalletAddEthereumChainObject(nativeCurrency: .init(name: customRpc.nativeCryptoTokenName ?? R.string.localizable.addCustomChainUnnamed(), symbol: customRpc.symbol ?? "", decimals: defaultDecimals), blockExplorerUrls: explorerEndpoints, chainName: customRpc.chainName, chainId: String(customRpc.chainID), rpcUrls: [customRpc.rpcEndpoint]) + let addCustomChain = AddCustomChain(customChain, isTestnet: customRpc.isTestnet, restartQueue: restartQueue, url: nil) + addCustomChain.delegate = self + addCustomChain.run() + } +} + +extension AddRPCServerCoordinator: AddRPCServerViewControllerDelegate { + func didFinish(in viewController: AddRPCServerViewController, rpc: CustomRPC) { + addChain(rpc) + } +} + +extension AddRPCServerCoordinator: AddCustomChainDelegate { + func notifyAddCustomChainQueuedSuccessfully(in addCustomChain: AddCustomChain) { + delegate?.restartToAddEnableAAndSwitchBrowserToServer(in: self) + //Note necessary to pop the navigation controller since we are restarting the UI + } + + func notifyAddCustomChainFailed(error: AddCustomChainError, in addCustomChain: AddCustomChain) { + let alertController = UIAlertController.alertController(title: R.string.localizable.error(), message: error.message, style: .alert, in: navigationController) + alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: nil)) + navigationController.present(alertController, animated: true, completion: nil) + } +} \ No newline at end of file diff --git a/AlphaWallet/Settings/Coordinators/EnabledServersCoordinator.swift b/AlphaWallet/Settings/Coordinators/EnabledServersCoordinator.swift index c89842396..b080a64de 100644 --- a/AlphaWallet/Settings/Coordinators/EnabledServersCoordinator.swift +++ b/AlphaWallet/Settings/Coordinators/EnabledServersCoordinator.swift @@ -5,6 +5,8 @@ import UIKit protocol EnabledServersCoordinatorDelegate: class { func didSelectServers(servers: [RPCServer], in coordinator: EnabledServersCoordinator) func didSelectDismiss(in coordinator: EnabledServersCoordinator) + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: EnabledServersCoordinator) + func restartToRemoveServer(in coordinator: EnabledServersCoordinator) } class EnabledServersCoordinator: Coordinator { @@ -16,21 +18,25 @@ class EnabledServersCoordinator: Coordinator { private let serverChoices = EnabledServersCoordinator.serversOrdered private let navigationController: UINavigationController private let selectedServers: [RPCServer] + private let restartQueue: RestartTaskQueue private lazy var enabledServersViewController: EnabledServersViewController = { let viewModel = EnabledServersViewModel(servers: serverChoices, selectedServers: selectedServers) - let controller = EnabledServersViewController(viewModel: viewModel) + let controller = EnabledServersViewController(viewModel: viewModel, restartQueue: restartQueue) controller.delegate = self controller.hidesBottomBarWhenPushed = true + controller.navigationItem.rightBarButtonItem = .addBarButton(self, selector: #selector(addRPCSelected)) + return controller }() var coordinators: [Coordinator] = [] weak var delegate: EnabledServersCoordinatorDelegate? - init(navigationController: UINavigationController, selectedServers: [RPCServer]) { + init(navigationController: UINavigationController, selectedServers: [RPCServer], restartQueue: RestartTaskQueue) { self.navigationController = navigationController self.selectedServers = selectedServers + self.restartQueue = restartQueue } func start() { @@ -41,6 +47,14 @@ class EnabledServersCoordinator: Coordinator { func stop() { navigationController.popViewController(animated: true) } + + @objc private func addRPCSelected() { + let coordinator = AddRPCServerCoordinator(navigationController: navigationController, config: Config(), restartQueue: restartQueue) + coordinator.delegate = self + addCoordinator(coordinator) + + coordinator.start() + } } extension EnabledServersCoordinator: EnabledServersViewControllerDelegate { @@ -51,4 +65,18 @@ extension EnabledServersCoordinator: EnabledServersViewControllerDelegate { func didDismiss(viewController: EnabledServersViewController) { delegate?.didSelectDismiss(in: self) } + + func notifyRemoveCustomChainQueued(in viewController: EnabledServersViewController) { + delegate?.restartToRemoveServer(in: self) + } } + +extension EnabledServersCoordinator: AddRPCServerCoordinatorDelegate { + func didDismiss(in coordinator: AddRPCServerCoordinator) { + removeCoordinator(coordinator) + } + + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: AddRPCServerCoordinator) { + delegate?.restartToAddEnableAAndSwitchBrowserToServer(in: self) + } +} \ No newline at end of file diff --git a/AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift b/AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift index 741aa7956..95c02209e 100644 --- a/AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift +++ b/AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift @@ -18,12 +18,15 @@ protocol SettingsCoordinatorDelegate: class, CanOpenURL { func assetDefinitionsOverrideViewController(for: SettingsCoordinator) -> UIViewController? func showConsole(in coordinator: SettingsCoordinator) func delete(account: Wallet, in coordinator: SettingsCoordinator) + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: SettingsCoordinator) + func restartToRemoveServer(in coordinator: SettingsCoordinator) } class SettingsCoordinator: Coordinator { private let keystore: Keystore private var config: Config private let sessions: ServerDictionary + private let restartQueue: RestartTaskQueue private let _promptBackupCoordinator: PromptBackupCoordinator private let analyticsCoordinator: AnalyticsCoordinator private let walletConnectCoordinator: WalletConnectCoordinator @@ -47,6 +50,7 @@ class SettingsCoordinator: Coordinator { keystore: Keystore, config: Config, sessions: ServerDictionary, + restartQueue: RestartTaskQueue, promptBackupCoordinator: PromptBackupCoordinator, analyticsCoordinator: AnalyticsCoordinator, walletConnectCoordinator: WalletConnectCoordinator @@ -56,6 +60,7 @@ class SettingsCoordinator: Coordinator { self.keystore = keystore self.config = config self.sessions = sessions + self.restartQueue = restartQueue self._promptBackupCoordinator = promptBackupCoordinator self.analyticsCoordinator = analyticsCoordinator self.walletConnectCoordinator = walletConnectCoordinator @@ -156,7 +161,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { } func settingsViewControllerActiveNetworksSelected(in controller: SettingsViewController) { - let coordinator = EnabledServersCoordinator(navigationController: navigationController, selectedServers: config.enabledServers) + let coordinator = EnabledServersCoordinator(navigationController: navigationController, selectedServers: config.enabledServers, restartQueue: restartQueue) coordinator.delegate = self coordinator.start() addCoordinator(coordinator) @@ -254,6 +259,16 @@ extension SettingsCoordinator: EnabledServersCoordinatorDelegate { coordinator.stop() removeCoordinator(coordinator) } + + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: EnabledServersCoordinator) { + delegate?.restartToAddEnableAAndSwitchBrowserToServer(in: self) + removeCoordinator(coordinator) + } + + func restartToRemoveServer(in coordinator: EnabledServersCoordinator) { + delegate?.restartToRemoveServer(in: self) + removeCoordinator(coordinator) + } } extension SettingsCoordinator: PromptBackupCoordinatorSubtlePromptDelegate { diff --git a/AlphaWallet/Settings/Models/AddCustomChain.swift b/AlphaWallet/Settings/Models/AddCustomChain.swift index c73b35ee0..6f2ffd41c 100644 --- a/AlphaWallet/Settings/Models/AddCustomChain.swift +++ b/AlphaWallet/Settings/Models/AddCustomChain.swift @@ -5,9 +5,24 @@ import APIKit import JSONRPCKit import PromiseKit +enum AddCustomChainError: Error { + case cancelled + case others(String) + + var message: String { + switch self { + case .cancelled: + //This is the default behavior, just keep it + return "\(self)" + case .others(let message): + return message + } + } +} + protocol AddCustomChainDelegate: class { func notifyAddCustomChainQueuedSuccessfully(in addCustomChain: AddCustomChain) - func notifyAddCustomChainFailed(error: DAppError, in addCustomChain: AddCustomChain) + func notifyAddCustomChainFailed(error: AddCustomChainError, in addCustomChain: AddCustomChain) } //TODO The detection and tests for various URLs are async so the UI might appear to do nothing to user as it is happening @@ -37,12 +52,12 @@ class AddCustomChain { }.then { chainId, rpcUrl, explorerType in self.queueAddCustomChain(self.customChain, chainId: chainId, rpcUrl: rpcUrl, etherscanCompatibleType: explorerType) }.done { - self.notifyAddCustomChainQueuedSuccessfully() + self.delegate?.notifyAddCustomChainQueuedSuccessfully(in: self) }.catch { - if let error = $0 as? DAppError { - self.informDappCustomChainAddingFailed(error) + if let error = $0 as? AddCustomChainError { + self.delegate?.notifyAddCustomChainFailed(error: error, in: self) } else { - self.informDappCustomChainAddingFailed(.nodeError("Unknown Error")) + self.delegate?.notifyAddCustomChainFailed(error: .others("\(R.string.localizable.addCustomChainErrorUnknown()) — \($0)"), in: self) } } } @@ -60,14 +75,6 @@ class AddCustomChain { seal.fulfill(()) } } - - private func notifyAddCustomChainQueuedSuccessfully() { - delegate?.notifyAddCustomChainQueuedSuccessfully(in: self) - } - - private func informDappCustomChainAddingFailed(_ error: DAppError) { - delegate?.notifyAddCustomChainFailed(error: error, in: self) - } } extension AddCustomChain { @@ -79,7 +86,7 @@ extension AddCustomChain { extension AddCustomChain.functional { static func checkChainId(_ customChain: WalletAddEthereumChainObject) -> Promise<(customChain: WalletAddEthereumChainObject, chainId: Int)> { guard let chainId = Int(chainId0xString: customChain.chainId) else { - return Promise(error: DAppError.nodeError(R.string.localizable.addCustomChainErrorInvalidChainId(customChain.chainId))) + return Promise(error: AddCustomChainError.others(R.string.localizable.addCustomChainErrorInvalidChainId(customChain.chainId))) } return .value((customChain: customChain, chainId: chainId)) } @@ -87,7 +94,7 @@ extension AddCustomChain.functional { static func checkAndDetectUrls(_ customChain: WalletAddEthereumChainObject, chainId: Int) -> Promise<(customChain: WalletAddEthereumChainObject, chainId: Int, rpcUrl: String)> { guard let rpcUrl = customChain.rpcUrls?.first else { //Not to spec since RPC URLs are optional according to EIP3085, but it is so much easier to assume it's needed, and quite useless if it isn't provided - return Promise(error: DAppError.nodeError(R.string.localizable.addCustomChainErrorNoRpcNodeUrl())) + return Promise(error: AddCustomChainError.others(R.string.localizable.addCustomChainErrorNoRpcNodeUrl())) } return firstly { @@ -108,14 +115,14 @@ extension AddCustomChain.functional { if let retrievedChainId = Int(chainId0xString: result), retrievedChainId == chainId { return (chainId: chainId, rpcUrl: rpcUrl) } else { - throw DAppError.nodeError("chainIds do not match: \(result) vs. \(customChain.chainId)") + throw AddCustomChainError.others(R.string.localizable.addCustomChainErrorChainIdNotMatch(result, customChain.chainId)) } } } private static func checkBlockchainExplorerApiHostname(customChain: WalletAddEthereumChainObject, chainId: Int, rpcUrl: String) -> Promise<(customChain: WalletAddEthereumChainObject, chainId: Int, rpcUrl: String)> { guard let urlString = customChain.blockExplorerUrls?.first else { - return Promise(error: DAppError.nodeError(R.string.localizable.addCustomChainErrorNoBlockchainExplorerUrl())) + return Promise(error: AddCustomChainError.others(R.string.localizable.addCustomChainErrorNoBlockchainExplorerUrl())) } return firstly { figureOutHostname(urlString) @@ -164,7 +171,7 @@ extension AddCustomChain.functional { //Careful to use `action=tokentx` and not `action=tokennfttx` because only the former works with both Etherscan and Blockscout guard let url = URL(string: "\(urlString)/api?module=account&action=tokentx&address=0x007bEe82BDd9e866b2bd114780a47f2261C684E3") else { - return Promise(error: DAppError.nodeError(R.string.localizable.addCustomChainErrorInvalidBlockchainExplorerUrl())) + return Promise(error: AddCustomChainError.others(R.string.localizable.addCustomChainErrorInvalidBlockchainExplorerUrl())) } return firstly { diff --git a/AlphaWallet/Settings/Types/RPCServers.swift b/AlphaWallet/Settings/Types/RPCServers.swift index 54b5b2322..2bf1a4c54 100644 --- a/AlphaWallet/Settings/Types/RPCServers.swift +++ b/AlphaWallet/Settings/Types/RPCServers.swift @@ -130,6 +130,19 @@ enum RPCServer: Hashable, CaseIterable { } } + var customRpc: CustomRPC? { + switch self { + case .xDai, .classic, .main, .poa, .callisto, .binance_smart_chain, .artis_sigma1, .heco, .fantom, .avalanche, .polygon, .optimistic, .kovan, .ropsten, .rinkeby, .sokol, .goerli, .artis_tau1, .binance_smart_chain_testnet, .heco_testnet, .fantom_testnet, .avalanche_testnet, .mumbai_testnet, .optimisticKovan: + return nil + case .custom(let custom): + return custom + } + } + + var isCustom: Bool { + customRpc != nil + } + var etherscanURLForGeneralTransactionHistory: URL? { switch self { case .main, .ropsten, .rinkeby, .kovan, .poa, .classic, .goerli, .xDai, .artis_sigma1, .artis_tau1, .polygon, .binance_smart_chain, .binance_smart_chain_testnet, .sokol, .callisto, .optimistic, .optimisticKovan, .custom: diff --git a/AlphaWallet/Settings/ViewControllers/AddRPCServerViewController.swift b/AlphaWallet/Settings/ViewControllers/AddRPCServerViewController.swift new file mode 100644 index 000000000..78c23e2bb --- /dev/null +++ b/AlphaWallet/Settings/ViewControllers/AddRPCServerViewController.swift @@ -0,0 +1,306 @@ +// +// AddRPCServerViewController.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 11.06.2021. +// + +import UIKit + +protocol AddRPCServerViewControllerDelegate: class { + func didFinish(in viewController: AddRPCServerViewController, rpc: CustomRPC) +} + +class AddRPCServerViewController: UIViewController { + + private let viewModel: AddrpcServerViewModel + private var config: Config + + private lazy var networkNameTextField: TextField = { + let textField = TextField() + textField.label.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textField.autocorrectionType = .no + textField.textField.autocapitalizationType = .none + textField.returnKeyType = .next + textField.placeholder = R.string.localizable.addrpcServerNetworkNameTitle() + + return textField + }() + + private lazy var rpcUrlTextField: TextField = { + let textField = TextField() + textField.label.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textField.autocorrectionType = .no + textField.textField.autocapitalizationType = .none + textField.returnKeyType = .next + textField.keyboardType = .URL + textField.placeholder = R.string.localizable.addrpcServerRpcUrlPlaceholder() + + return textField + }() + + private lazy var chainIDTextField: TextField = { + let textField = TextField() + textField.label.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textField.autocorrectionType = .no + textField.textField.autocapitalizationType = .none + textField.returnKeyType = .next + textField.keyboardType = .decimalPad + textField.placeholder = R.string.localizable.chainID() + + return textField + }() + + private lazy var symbolTextField: TextField = { + let textField = TextField() + textField.label.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textField.autocorrectionType = .no + textField.textField.autocapitalizationType = .none + textField.returnKeyType = .next + textField.placeholder = R.string.localizable.symbol() + + return textField + }() + + private lazy var blockExplorerURLTextField: TextField = { + let textField = TextField() + textField.label.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textField.autocorrectionType = .no + textField.textField.autocapitalizationType = .none + textField.returnKeyType = .done + textField.keyboardType = .URL + textField.placeholder = R.string.localizable.addrpcServerBlockExplorerUrlPlaceholder() + + return textField + }() + + private lazy var isTestNetworkView: SwitchView = { + let view = SwitchView() + view.delegate = self + + return view + }() + + private let buttonsBar = ButtonsBar(configuration: .green(buttons: 1)) + private var scrollViewBottomConstraint: NSLayoutConstraint! + private lazy var keyboardChecker = KeyboardChecker(self) + private let roundedBackground = RoundedBackground() + private let scrollView = UIScrollView() + + weak var delegate: AddRPCServerViewControllerDelegate? + + static func layoutSubviews(for textField: TextField) -> [UIView] { + [textField.label, .spacer(height: 4), textField, .spacer(height: 4), textField.statusLabel, .spacer(height: 24)] + } + + init(viewModel: AddrpcServerViewModel, config: Config) { + self.viewModel = viewModel + self.config = config + + super.init(nibName: nil, bundle: nil) + + scrollViewBottomConstraint = scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + scrollViewBottomConstraint.constant = -UIApplication.shared.bottomSafeAreaHeight + keyboardChecker.constraint = scrollViewBottomConstraint + + let stackView = ( + Self.layoutSubviews(for: networkNameTextField) + + Self.layoutSubviews(for: rpcUrlTextField) + + Self.layoutSubviews(for: chainIDTextField) + + Self.layoutSubviews(for: symbolTextField) + + Self.layoutSubviews(for: blockExplorerURLTextField) + + [ + isTestNetworkView, + .spacer(height: 40) + ] + ).asStackView(axis: .vertical) + stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stackView) + + roundedBackground.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(roundedBackground) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + roundedBackground.addSubview(scrollView) + + let footerBar = ButtonsBarBackgroundView(buttonsBar: buttonsBar, edgeInsets: .zero, separatorHeight: 0.0) + scrollView.addSubview(footerBar) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20), + stackView.leadingAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.trailingAnchor, constant: -20), + stackView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), + + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollViewBottomConstraint, + + footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + footerBar.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + ] + roundedBackground.createConstraintsWithContainer(view: view)) + + hidesBottomBarWhenPushed = true + } + + override func viewDidLoad() { + super.viewDidLoad() + + buttonsBar.configure() + networkNameTextField.configureOnce() + rpcUrlTextField.configureOnce() + chainIDTextField.configureOnce() + symbolTextField.configureOnce() + blockExplorerURLTextField.configureOnce() + isTestNetworkView.configure(viewModel: viewModel.enableServersHeaderViewModel) + + buttonsBar.buttons[0].addTarget(self, action: #selector(saveCustomRPC), for: .touchUpInside) + + configure(viewModel: viewModel) + + let tap = UITapGestureRecognizer(target: self, action: #selector(tapSelected)) + roundedBackground.addGestureRecognizer(tap) + } + + @objc private func tapSelected(_ sender: UITapGestureRecognizer) { + view.endEditing(true) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + keyboardChecker.viewWillAppear() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + keyboardChecker.viewWillDisappear() + } + + required init?(coder: NSCoder) { + return nil + } + + func configure(viewModel: AddrpcServerViewModel) { + navigationItem.title = viewModel.title + + networkNameTextField.label.text = viewModel.networkNameTitle + rpcUrlTextField.label.text = viewModel.rpcUrlTitle + chainIDTextField.label.text = viewModel.chainIDTitle + symbolTextField.label.text = viewModel.symbolTitle + blockExplorerURLTextField.label.text = viewModel.blockExplorerURLTitle + + buttonsBar.buttons[0].setTitle(viewModel.saverpcServerTitle, for: .normal) + } + + private func validateInputs() -> Bool { + var isValid: Bool = true + + if networkNameTextField.value.trimmed.isEmpty { + isValid = false + networkNameTextField.status = .error(R.string.localizable.addrpcServerNetworkNameError()) + } else { + networkNameTextField.status = .none + } + + if URL(string: rpcUrlTextField.value.trimmed) == nil { + isValid = false + rpcUrlTextField.status = .error(R.string.localizable.addrpcServerRpcUrlError()) + } else { + rpcUrlTextField.status = .none + } + + if let chainId = Int(chainId0xString: chainIDTextField.value.trimmed), chainId > 0 { + if config.enabledServers.contains(where: { $0.chainID == chainId }) { + isValid = false + //TODO maybe a prompt with button to enable it instead? + chainIDTextField.status = .error(R.string.localizable.addrpcServerChainIdAlreadySupported()) + } else { + chainIDTextField.status = .none + } + + } else { + isValid = false + chainIDTextField.status = .error(R.string.localizable.addrpcServerChainIDError()) + } + + if symbolTextField.value.trimmed.isEmpty { + isValid = false + symbolTextField.status = .error(R.string.localizable.addrpcServerSymbolError()) + } else { + symbolTextField.status = .none + } + + if URL(string: blockExplorerURLTextField.value.trimmed) == nil { + isValid = false + blockExplorerURLTextField.status = .error(R.string.localizable.addrpcServerBlockExplorerUrlError()) + } else { + blockExplorerURLTextField.status = .none + } + + return isValid + } + + @objc private func saveCustomRPC(_ sender: UIButton) { + guard validateInputs() else { return } + + let customRPC = CustomRPC( + chainID: Int(chainId0xString: chainIDTextField.value.trimmed)!, + nativeCryptoTokenName: nil, + chainName: networkNameTextField.value.trimmed, + symbol: symbolTextField.value.trimmed, + rpcEndpoint: rpcUrlTextField.value.trimmed, + explorerEndpoint: blockExplorerURLTextField.value.trimmed, + etherscanCompatibleType: .unknown, + isTestnet: isTestNetworkView.isOn + ) + + delegate?.didFinish(in: self, rpc: customRPC) + } +} + +extension AddRPCServerViewController: SwitchViewDelegate { + func toggledTo(_ newValue: Bool, headerView: SwitchView) { + //no-op + } +} + +extension AddRPCServerViewController: TextFieldDelegate { + + func shouldReturn(in textField: TextField) -> Bool { + switch textField { + case networkNameTextField: + rpcUrlTextField.becomeFirstResponder() + case rpcUrlTextField: + chainIDTextField.becomeFirstResponder() + case chainIDTextField: + symbolTextField.becomeFirstResponder() + case symbolTextField: + blockExplorerURLTextField.becomeFirstResponder() + case blockExplorerURLTextField: + view.endEditing(true) + default: + view.endEditing(true) + } + return true + } + + func doneButtonTapped(for textField: TextField) { + //no-op + } + + func nextButtonTapped(for textField: TextField) { + //no-op + } +} diff --git a/AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift b/AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift index ef688c4ce..b21ca29b9 100644 --- a/AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift +++ b/AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift @@ -4,6 +4,7 @@ import UIKit protocol EnabledServersViewControllerDelegate: class { func didSelectServers(servers: [RPCServer], in viewController: EnabledServersViewController) + func notifyRemoveCustomChainQueued(in viewController: EnabledServersViewController) } class EnabledServersViewController: UIViewController { @@ -26,17 +27,19 @@ class EnabledServersViewController: UIViewController { return tableView }() - private var viewModel: EnabledServersViewModel + private let restartQueue: RestartTaskQueue private let sections: [Section] = [.mainnet, .testnet] private var serversSelectedInPreviousMode: [RPCServer]? private var sectionIndices: IndexSet { IndexSet(integersIn: Range(uncheckedBounds: (lower: 0, sections.count))) } + var viewModel: EnabledServersViewModel weak var delegate: EnabledServersViewControllerDelegate? - init(viewModel: EnabledServersViewModel) { + init(viewModel: EnabledServersViewModel, restartQueue: RestartTaskQueue) { self.viewModel = viewModel + self.restartQueue = restartQueue super.init(nibName: nil, bundle: nil) view.backgroundColor = GroupedTable.Color.background @@ -79,6 +82,28 @@ class EnabledServersViewController: UIViewController { @objc private func done() { delegate?.didSelectServers(servers: viewModel.selectedServers, in: self) } + + private func confirmDelete(server: RPCServer) { + guard server.isCustom else { return } + guard !viewModel.isServerSelected(server) else { return } + //TODO make it possible to remove custom chains without restarting UI + confirm(title: R.string.localizable.settingsEnabledNetworksDeleteTitle(), message: R.string.localizable.settingsEnabledNetworksDeleteMessage(), okTitle: R.string.localizable.delete(), okStyle: .destructive) { [weak self] result in + guard let strongSelf = self else { return } + switch result { + case .success: + strongSelf.markForDeletion(server: server) + break + case .failure: + break + } + } + } + + private func markForDeletion(server: RPCServer) { + guard let customRpc = server.customRpc else { return } + restartQueue.add(.removeServer(customRpc)) + delegate?.notifyRemoveCustomChainQueued(in: self) + } } extension EnabledServersViewController: UITableViewDelegate, UITableViewDataSource { @@ -136,6 +161,24 @@ extension EnabledServersViewController: UITableViewDelegate, UITableViewDataSour tableView.reloadData() //Even if no servers is selected, we don't attempt to disable the back button here since calling code will take care of ignore the change server "request" when there are no servers selected. We don't want to disable the back button because users can't cancel the operation } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let server = viewModel.server(for: indexPath) + guard server.isCustom else { return nil } + guard !viewModel.isServerSelected(server) else { return nil } + let deleteAction = UIContextualAction(style: .normal, title: R.string.localizable.delete()) { _, _, complete in + self.confirmDelete(server: server) + complete(true) + } + + deleteAction.image = R.image.close()?.withRenderingMode(.alwaysTemplate) + deleteAction.backgroundColor = R.color.danger() + + let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) + configuration.performsFirstActionWithFullSwipe = true + + return configuration + } } extension EnabledServersViewController: EnableServersHeaderViewDelegate { @@ -184,4 +227,4 @@ extension EnabledServersViewController: PromptViewControllerDelegate { self.headers.testnet.configure(mode: .testnet, isEnabled: false) } } -} \ No newline at end of file +} diff --git a/AlphaWallet/Settings/ViewModels/AddRPCServerViewModel.swift b/AlphaWallet/Settings/ViewModels/AddRPCServerViewModel.swift new file mode 100644 index 000000000..12b378945 --- /dev/null +++ b/AlphaWallet/Settings/ViewModels/AddRPCServerViewModel.swift @@ -0,0 +1,41 @@ +// +// AddrpcServerViewModel.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 11.06.2021. +// + +import UIKit + +struct AddrpcServerViewModel { + + var title: String { + return R.string.localizable.addrpcServerNavigationTitle() + } + + var saverpcServerTitle: String { + return R.string.localizable.addrpcServerSaveButtonTitle() + } + + var networkNameTitle: String { + return R.string.localizable.addrpcServerNetworkNameTitle() + } + + var rpcUrlTitle: String { + return R.string.localizable.addrpcServerRpcUrlTitle() + } + + var chainIDTitle: String { + return R.string.localizable.chainID() + } + + var symbolTitle: String { + return R.string.localizable.symbol() + } + + var blockExplorerURLTitle: String { + return R.string.localizable.addrpcServerBlockExplorerUrlTitle() + } + + var enableServersHeaderViewModel = SwitchViewViewModel(text: R.string.localizable.addrpcServerIsTestnetTitle(), isOn: false) +} diff --git a/AlphaWallet/Settings/Views/SwitchView.swift b/AlphaWallet/Settings/Views/SwitchView.swift new file mode 100644 index 000000000..215c7d4bf --- /dev/null +++ b/AlphaWallet/Settings/Views/SwitchView.swift @@ -0,0 +1,75 @@ +// +// SwitchView.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 11.06.2021. +// + +import UIKit + +protocol SwitchViewDelegate: class { + func toggledTo(_ newValue: Bool, headerView: SwitchView) +} + +struct SwitchViewViewModel { + var backgroundColor = Colors.appWhite + var textColor = R.color.black() + var font = Fonts.regular(size: 17) + + var text: String + var isOn: Bool + + init(text: String, isOn: Bool) { + self.text = text + self.isOn = isOn + } +} + +class SwitchView: UIView { + private let label = UILabel() + private let toggle = UISwitch() + + var isOn: Bool { + toggle.isOn + } + + weak var delegate: SwitchViewDelegate? + + override init(frame: CGRect) { + super.init(frame: CGRect()) + + toggle.addTarget(self, action: #selector(toggled), for: .valueChanged) + + let stackView = [label, .spacer(), toggle].asStackView(axis: .horizontal, alignment: .center) + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.topAnchor.constraint(lessThanOrEqualTo: topAnchor), + stackView.bottomAnchor.constraint(greaterThanOrEqualTo: bottomAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + heightAnchor.constraint(equalToConstant: 40) + ]) + } + + required init?(coder aDecoder: NSCoder) { + return nil + } + + func configure(viewModel: SwitchViewViewModel) { + backgroundColor = viewModel.backgroundColor + + label.backgroundColor = viewModel.backgroundColor + label.textColor = viewModel.textColor + label.font = viewModel.font + label.text = viewModel.text + + toggle.isOn = viewModel.isOn + } + + @objc private func toggled() { + delegate?.toggledTo(toggle.isOn, headerView: self) + } +} diff --git a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift index 3c6ca466a..2c1f92eab 100644 --- a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift @@ -713,4 +713,8 @@ extension UIBarButtonItem { static func qrCodeBarButton(_ target: AnyObject, selector: Selector) -> UIBarButtonItem { return .init(image: R.image.qr_code_icon(), style: .plain, target: target, action: selector) } + + static func addBarButton(_ target: AnyObject, selector: Selector) -> UIBarButtonItem { + return .init(image: R.image.add_hide_tokens(), style: .plain, target: target, action: selector) + } } diff --git a/AlphaWallet/Tokens/Views/TextField.swift b/AlphaWallet/Tokens/Views/TextField.swift index 58401750d..687bb9561 100644 --- a/AlphaWallet/Tokens/Views/TextField.swift +++ b/AlphaWallet/Tokens/Views/TextField.swift @@ -150,6 +150,15 @@ class TextField: UIControl { } } + var placeholder: String? { + set { + textField.placeholder = newValue + } + get { + textField.placeholder + } + } + init() { super.init(frame: .zero) diff --git a/AlphaWalletTests/Extensions/IntExtensionsTests.swift b/AlphaWalletTests/Extensions/IntExtensionsTests.swift new file mode 100644 index 000000000..17f1f16d2 --- /dev/null +++ b/AlphaWalletTests/Extensions/IntExtensionsTests.swift @@ -0,0 +1,13 @@ +// Copyright © 2021 Stormbird PTE. LTD. + +import Foundation +import XCTest +@testable import AlphaWallet + +class IntExtensionsTests: XCTestCase { + func testChainId0xString() { + XCTAssertEqual(Int(chainId0xString: "12"), 12) + XCTAssertEqual(Int(chainId0xString: "0x80"), 128) + XCTAssertNil(Int(chainId0xString: "1xy")) + } +} \ No newline at end of file diff --git a/AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift b/AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift index 35dc22181..eddf5e9dc 100644 --- a/AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift +++ b/AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift @@ -28,6 +28,8 @@ class SettingsCoordinatorTests: XCTestCase { func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, server: RPCServer, in viewController: UIViewController) {} func didPressViewContractWebPage(_ url: URL, in viewController: UIViewController) {} func didPressOpenWebPage(_ url: URL, in viewController: UIViewController) {} + func restartToAddEnableAAndSwitchBrowserToServer(in coordinator: SettingsCoordinator) {} + func restartToRemoveServer(in coordinator: SettingsCoordinator) {} } let storage = FakeTransactionsStorage() @@ -40,6 +42,7 @@ class SettingsCoordinatorTests: XCTestCase { keystore: FakeEtherKeystore(), config: .make(), sessions: sessons, + restartQueue: .init(), promptBackupCoordinator: promptBackupCoordinator, analyticsCoordinator: FakeAnalyticsService(), walletConnectCoordinator: walletConnectCoordinator