Support UI for user to add custom RPC server

pull/2884/head
Vladyslav shepitko 3 years ago committed by Hwee-Boon Yar
parent 8e07fd24b3
commit 83dbe85a85
  1. 28
      AlphaWallet.xcodeproj/project.pbxproj
  2. 3
      AlphaWallet/Assets.xcassets/add_hide_tokens.imageset/Contents.json
  3. 20
      AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift
  4. 37
      AlphaWallet/Browser/Coordinators/DappRequestSwitchCustomChainCoordinator.swift
  5. 1
      AlphaWallet/Browser/Types/DappCommand.swift
  6. 40
      AlphaWallet/Extensions/Error.swift
  7. 34
      AlphaWallet/InCoordinator.swift
  8. 19
      AlphaWallet/Localization/en.lproj/Localizable.strings
  9. 19
      AlphaWallet/Localization/es.lproj/Localizable.strings
  10. 19
      AlphaWallet/Localization/ja.lproj/Localizable.strings
  11. 19
      AlphaWallet/Localization/ko.lproj/Localizable.strings
  12. 19
      AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings
  13. 2
      AlphaWallet/Models/RestartTaskQueue.swift
  14. 77
      AlphaWallet/Settings/Coordinators/AddRPCServerCoordinator.swift
  15. 32
      AlphaWallet/Settings/Coordinators/EnabledServersCoordinator.swift
  16. 17
      AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift
  17. 43
      AlphaWallet/Settings/Models/AddCustomChain.swift
  18. 13
      AlphaWallet/Settings/Types/RPCServers.swift
  19. 306
      AlphaWallet/Settings/ViewControllers/AddRPCServerViewController.swift
  20. 49
      AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift
  21. 41
      AlphaWallet/Settings/ViewModels/AddRPCServerViewModel.swift
  22. 75
      AlphaWallet/Settings/Views/SwitchView.swift
  23. 4
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  24. 9
      AlphaWallet/Tokens/Views/TextField.swift
  25. 13
      AlphaWalletTests/Extensions/IntExtensionsTests.swift
  26. 3
      AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift

@ -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 = "<group>"; };
5E7C7C0AC267283F3F2A6E37 /* EmptyDapps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDapps.swift; sourceTree = "<group>"; };
5E7C7C0CFD047ED7C488FB45 /* OpenSea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSea.swift; sourceTree = "<group>"; };
5E7C7C11AF59E7CA26B3A2BB /* SwitchCustomChainCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCustomChainCoordinator.swift; sourceTree = "<group>"; };
5E7C7C11AF59E7CA26B3A2BB /* DappRequestSwitchCustomChainCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappRequestSwitchCustomChainCoordinator.swift; sourceTree = "<group>"; };
5E7C7C12E88EB0B73AA1E562 /* TokenCardRowViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCardRowViewModelProtocol.swift; sourceTree = "<group>"; };
5E7C7C1720AD49046D2B4023 /* TransactionDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewModel.swift; sourceTree = "<group>"; };
5E7C7C34A7BDCFE17CEF8F79 /* OpenSeaNonFungibleTokenAttributeCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenAttributeCellViewModel.swift; sourceTree = "<group>"; };
@ -1520,6 +1525,7 @@
5E7C7F1F965C69B80A234F1F /* EditedTransactionConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditedTransactionConfiguration.swift; sourceTree = "<group>"; };
5E7C7F21FA7A02F6341FB58D /* AssetDefinitionsOverridesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionsOverridesViewController.swift; sourceTree = "<group>"; };
5E7C7F3DD81D44A996789FC4 /* UniversalLinkInPasteboardCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalLinkInPasteboardCoordinator.swift; sourceTree = "<group>"; };
5E7C7F4F209C6EE3828E18EC /* IntExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntExtensionsTests.swift; sourceTree = "<group>"; };
5E7C7F54FD247553A7427881 /* TokensViewControllerCollectiblesCollectionViewHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewControllerCollectiblesCollectionViewHeader.swift; sourceTree = "<group>"; };
5E7C7F55495A6095B3E86248 /* EditMyDappViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMyDappViewController.swift; sourceTree = "<group>"; };
5E7C7F5A42365FDA22AEE6F7 /* ActivityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityViewModel.swift; sourceTree = "<group>"; };
@ -1672,6 +1678,10 @@
8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressCoordinator.swift; sourceTree = "<group>"; };
8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = "<group>"; };
876C80C82673349300B16595 /* CoinTickersFetcherCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinTickersFetcherCache.swift; sourceTree = "<group>"; };
876C80CA267386E300B16595 /* AddRPCServerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerCoordinator.swift; sourceTree = "<group>"; };
876C80CC2673940B00B16595 /* SwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchView.swift; sourceTree = "<group>"; };
876C80CE267398F900B16595 /* AddRPCServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerViewController.swift; sourceTree = "<group>"; };
876C80D02673992300B16595 /* AddRPCServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRPCServerViewModel.swift; sourceTree = "<group>"; };
87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodedFunctionCall+Decode.swift"; sourceTree = "<group>"; };
877D00AE25ADF60A008E22CC /* TransactionConfiguratorTransactionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConfiguratorTransactionsTests.swift; sourceTree = "<group>"; };
8782035C2431E66600792F12 /* FilterTokensCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterTokensCoordinator.swift; sourceTree = "<group>"; };
@ -2254,6 +2264,7 @@
5E7C7C7CB95B7EE4B2547585 /* EnabledServersCoordinator.swift */,
5E7C7C51CEC4AAFDFBD75482 /* ConsoleCoordinator.swift */,
5E7C72571AB0FECB26FEB1B1 /* ClearDappBrowserCacheCoordinator.swift */,
876C80CA267386E300B16595 /* AddRPCServerCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -2512,6 +2523,7 @@
87ED8F98248541380005C69B /* AdvancedSettingsViewModel.swift */,
87ED8F9C248647C80005C69B /* SettingViewHeaderViewModel.swift */,
87ED8FAE2488EA9C0005C69B /* SupportViewModel.swift */,
876C80D02673992300B16595 /* AddRPCServerViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -2838,7 +2850,7 @@
5E7C7721E0E4D4EFDD35E196 /* ScanQRCodeCoordinator.swift */,
76F1D4E689C3ECFD38CBBC47 /* DappBrowserCoordinator.swift */,
875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */,
5E7C7C11AF59E7CA26B3A2BB /* SwitchCustomChainCoordinator.swift */,
5E7C7C11AF59E7CA26B3A2BB /* DappRequestSwitchCustomChainCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -2972,6 +2984,7 @@
5E7C7C468EC03C073E0EEA03 /* SettingsViewController.swift */,
87ED8F96248540F90005C69B /* AdvancedSettingsViewController.swift */,
87ED8FAC2488EA610005C69B /* SupportViewController.swift */,
876C80CE267398F900B16595 /* AddRPCServerViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -3583,6 +3596,7 @@
5E7C7553BB089397B7E74BE0 /* WalletSecurityLevelIndicator.swift */,
87ED8F92248534E30005C69B /* SwitchTableViewCell.swift */,
5E7C7DC3AEBE4049927B7625 /* EnableServersHeaderView.swift */,
876C80CC2673940B00B16595 /* SwitchView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -3671,6 +3685,7 @@
isa = PBXGroup;
children = (
5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */,
5E7C7F4F209C6EE3828E18EC /* IntExtensionsTests.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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;
};

@ -17,5 +17,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

@ -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)
}
}

@ -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)
}
}

@ -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]?
}

@ -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()
}
}
}
}

@ -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 {

@ -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";

@ -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";

@ -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";

@ -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";

@ -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";

@ -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)

@ -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)
}
}

@ -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)
}
}

@ -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<WalletSession>
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<WalletSession>,
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 {

@ -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 {

@ -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:

@ -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
}
}

@ -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)
}
}
}
}

@ -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)
}

@ -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)
}
}

@ -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)
}
}

@ -150,6 +150,15 @@ class TextField: UIControl {
}
}
var placeholder: String? {
set {
textField.placeholder = newValue
}
get {
textField.placeholder
}
}
init() {
super.init(frame: .zero)

@ -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"))
}
}

@ -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

Loading…
Cancel
Save