Merge pull request #2884 from vladyslav-iosdev/#2826
Let users add a custom chain #2826pull/2914/head
commit
ff6b673bd1
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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")) |
||||
} |
||||
} |
Loading…
Reference in new issue