From ff478d928790d5343f7761aa5f3424b1ea28d628 Mon Sep 17 00:00:00 2001 From: Vladyslav shepitko Date: Fri, 19 Feb 2021 09:42:22 +0200 Subject: [PATCH] Need to show WalletConnect is loading instead of not showing anything #2517 --- AlphaWallet.xcodeproj/project.pbxproj | 18 +- AlphaWallet/Extensions/String.swift | 11 + .../Localization/en.lproj/Localizable.strings | 2 + .../Localization/es.lproj/Localizable.strings | 2 + .../Localization/ja.lproj/Localizable.strings | 2 + .../Localization/ko.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../TokensViewController.swift | 8 +- ...InProgressCoordinatorBridgeToPromise.swift | 2 +- ...tToSessionCoordinatorBridgeToPromise.swift | 52 +++ .../WalletConnectCoordinator.swift | 179 ++++---- .../WalletConnectSessionCoordinator.swift | 2 + .../WalletConnectToSessionCoordinator.swift | 112 +++++ .../WalletConnectSessionsViewController.swift | 72 +++- ...WalletConnectToSessionViewController.swift | 395 ++++++++++++++++++ .../WalletConnectToSessionViewModel.swift | 84 ++++ .../WalletConnect/WalletConnectServer.swift | 5 + 17 files changed, 842 insertions(+), 108 deletions(-) create mode 100644 AlphaWallet/WalletConnect/Controllers/WalletConnectToSessionCoordinatorBridgeToPromise.swift create mode 100644 AlphaWallet/WalletConnect/Coordinator/WalletConnectToSessionCoordinator.swift rename AlphaWallet/WalletConnect/{Controllers => ViewController}/WalletConnectSessionsViewController.swift (50%) create mode 100644 AlphaWallet/WalletConnect/ViewController/WalletConnectToSessionViewController.swift create mode 100644 AlphaWallet/WalletConnect/ViewModel/WalletConnectToSessionViewModel.swift diff --git a/AlphaWallet.xcodeproj/project.pbxproj b/AlphaWallet.xcodeproj/project.pbxproj index b9f0619b9..7dacdadfb 100644 --- a/AlphaWallet.xcodeproj/project.pbxproj +++ b/AlphaWallet.xcodeproj/project.pbxproj @@ -696,6 +696,10 @@ 874DED1924C1BD2C006C8FCE /* SelectAssetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874DED1824C1BD2C006C8FCE /* SelectAssetViewModel.swift */; }; 8750F91724EE5AE100E19DFF /* RecipientResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8750F91624EE5AE100E19DFF /* RecipientResolver.swift */; }; 8750F91924EE66D700E19DFF /* GasSpeedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8750F91824EE66D700E19DFF /* GasSpeedTableViewCell.swift */; }; + 8757E5E025DE5A9D00812392 /* WalletConnectToSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8757E5DF25DE5A9D00812392 /* WalletConnectToSessionViewController.swift */; }; + 8757E5E225DE5ADB00812392 /* WalletConnectToSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8757E5E125DE5ADB00812392 /* WalletConnectToSessionViewModel.swift */; }; + 8757E5E425DE5EC400812392 /* WalletConnectToSessionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8757E5E325DE5EC400812392 /* WalletConnectToSessionCoordinator.swift */; }; + 8757E5E625DE676100812392 /* WalletConnectToSessionCoordinatorBridgeToPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8757E5E525DE676100812392 /* WalletConnectToSessionCoordinatorBridgeToPromise.swift */; }; 875B3C34250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */; }; 8769888D24C6ED04002BF62B /* TransactionInProgressCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */; }; 8769BCA8256D15BF0095EA5B /* BlockieImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */; }; @@ -1542,6 +1546,10 @@ 8750F91624EE5AE100E19DFF /* RecipientResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientResolver.swift; sourceTree = ""; }; 8750F91824EE66D700E19DFF /* GasSpeedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GasSpeedTableViewCell.swift; sourceTree = ""; }; 8750F91A24EFEC0300E19DFF /* Uniswap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniswap.swift; sourceTree = ""; }; + 8757E5DF25DE5A9D00812392 /* WalletConnectToSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectToSessionViewController.swift; sourceTree = ""; }; + 8757E5E125DE5ADB00812392 /* WalletConnectToSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectToSessionViewModel.swift; sourceTree = ""; }; + 8757E5E325DE5EC400812392 /* WalletConnectToSessionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectToSessionCoordinator.swift; sourceTree = ""; }; + 8757E5E525DE676100812392 /* WalletConnectToSessionCoordinatorBridgeToPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectToSessionCoordinatorBridgeToPromise.swift; sourceTree = ""; }; 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeResolutionCoordinator.swift; sourceTree = ""; }; 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressCoordinator.swift; sourceTree = ""; }; 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = ""; }; @@ -3863,6 +3871,7 @@ 87BBF96B2563DD7500FF4846 /* WalletConnectSessionDetailsViewModel.swift */, 87BBF96C2563DD7500FF4846 /* WallerConnectRawViewModel.swift */, 5E7C7DC4B06C1A623788EEED /* WalletConnectSessionCellViewModel.swift */, + 8757E5E125DE5ADB00812392 /* WalletConnectToSessionViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -3872,6 +3881,7 @@ children = ( 87BBF9722563DD7500FF4846 /* WalletConnectSessionCoordinator.swift */, 87BBF9732563DD7500FF4846 /* WalletConnectCoordinator.swift */, + 8757E5E325DE5EC400812392 /* WalletConnectToSessionCoordinator.swift */, ); path = Coordinator; sourceTree = ""; @@ -3903,7 +3913,7 @@ 87BBF97E2563DD7500FF4846 /* SignMessageCoordinatorBridgeToPromise.swift */, 87BBF97F2563DD7500FF4846 /* TransactionInProgressCoordinatorBridgeToPromise.swift */, 87BBF9802563DD7500FF4846 /* TransactionConfirmationCoordinatorBridgeToPromise.swift */, - 5E7C75F0DB84DB0F5449D6C1 /* WalletConnectSessionsViewController.swift */, + 8757E5E525DE676100812392 /* WalletConnectToSessionCoordinatorBridgeToPromise.swift */, ); path = Controllers; sourceTree = ""; @@ -3911,7 +3921,9 @@ 87BBF9812563DD7500FF4846 /* ViewController */ = { isa = PBXGroup; children = ( + 5E7C75F0DB84DB0F5449D6C1 /* WalletConnectSessionsViewController.swift */, 87BBF9822563DD7500FF4846 /* WalletConnectSessionDetailsViewController.swift */, + 8757E5DF25DE5A9D00812392 /* WalletConnectToSessionViewController.swift */, ); path = ViewController; sourceTree = ""; @@ -4543,6 +4555,7 @@ 87BBF9962563DD7600FF4846 /* SignMessageCoordinatorBridgeToPromise.swift in Sources */, AA26C62320412A4100318B9B /* UIViewInspectableEnhancements.swift in Sources */, 29FC9BC61F830899000209CD /* MigrationInitializerForOneChainPerDatabase.swift in Sources */, + 8757E5E425DE5EC400812392 /* WalletConnectToSessionCoordinator.swift in Sources */, 29AD8A091F93F8B2008E10E7 /* Session.swift in Sources */, 29BB94971F6FCD60009B09CC /* SendViewModel.swift in Sources */, 296106D01F778A8D0006164B /* TransactionType.swift in Sources */, @@ -4826,6 +4839,7 @@ 5E7C75E5C64619ABFD246183 /* TransferTokensCardViaWalletAddressViewController.swift in Sources */, 5E7C7EAEBB435F3909DA36FB /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift in Sources */, 5E7C7CCC8D376C6E5C245715 /* EthCurrencyHelper.swift in Sources */, + 8757E5E225DE5ADB00812392 /* WalletConnectToSessionViewModel.swift in Sources */, 5E7C7317533D24B6A292F88D /* UIStackView+Array.swift in Sources */, 5E7C78F1D29280E3FF4EAF5E /* RoundedBackground.swift in Sources */, 5E7C7E68425E20834B898D06 /* AppLocale.swift in Sources */, @@ -4854,6 +4868,7 @@ 5E7C741353DDF87133054FCC /* DeletedContract.swift in Sources */, 5E7C7788FA549A0402BB33CB /* HiddenContract.swift in Sources */, 5E7C7F60056FDD6ACC390400 /* UniversalLinkInPasteboardCoordinator.swift in Sources */, + 8757E5E625DE676100812392 /* WalletConnectToSessionCoordinatorBridgeToPromise.swift in Sources */, 5E7C7402B29A987B0AF7061D /* VerifiableStatusViewController.swift in Sources */, 76F1DBCA8BAAA42BAEB14719 /* GetERC721BalanceCoordinator.swift in Sources */, 87D175EB24AEF8B5002130D2 /* UITableView.swift in Sources */, @@ -5187,6 +5202,7 @@ 5E7C7B1689D3E1D51788FE26 /* Session+PromiseKit.swift in Sources */, 5E7C78C68AD1922796E6C88F /* WalletConnectSessionsViewController.swift in Sources */, 5E7C78C511CD388E882DBD83 /* WalletConnectSessionCell.swift in Sources */, + 8757E5E025DE5A9D00812392 /* WalletConnectToSessionViewController.swift in Sources */, 5E7C712B11D9D9AAA02C022F /* WalletConnectSessionCellViewModel.swift in Sources */, 5E7C7B3027F3A56EF3EE5B7F /* EthCalllRequest.swift in Sources */, 5E7C7E38EE25F8837CDF6875 /* TransactionRowViewModel.swift in Sources */, diff --git a/AlphaWallet/Extensions/String.swift b/AlphaWallet/Extensions/String.swift index e4f9ddc7d..84f4dfff7 100644 --- a/AlphaWallet/Extensions/String.swift +++ b/AlphaWallet/Extensions/String.swift @@ -3,6 +3,17 @@ import Foundation import UIKit +extension String { + + var toHexData: Data { + if self.hasPrefix("0x") { + return Data(hex: self) + } else { + return Data(hex: self.hex) + } + } +} + extension String { var hex: String { guard let data = self.data(using: .utf8) else { diff --git a/AlphaWallet/Localization/en.lproj/Localizable.strings b/AlphaWallet/Localization/en.lproj/Localizable.strings index 61a98c984..943b84c5e 100644 --- a/AlphaWallet/Localization/en.lproj/Localizable.strings +++ b/AlphaWallet/Localization/en.lproj/Localizable.strings @@ -29,6 +29,7 @@ "transaction.blockNumber.label.title" = "Block #"; "transaction.nonce.label.title" = "Nonce"; "confirmPayment.confirm.button.title" = "Confirm"; +"confirmPayment.reject.button.title" = "Reject"; "confirmPayment.from.label.title" = "From"; "confirmPayment.gasFee.label.title" = "Estimate Network Fee"; "confirmPayment.gasLimit.label.title" = "Gas Limit"; @@ -538,4 +539,5 @@ You can check the latest gas price on gasnow.org"; "walletConnect.sendRawTransaction.title" = "Send raw transaction"; "walletConnect.activeSessions" = "Active connection to Dapps"; "walletConnect.activeSessions.plural" = "Active connections to Dapps"; +"walletConnect.connection.title" = "Connect To Site?"; "send.allFunds" = "All Funds"; diff --git a/AlphaWallet/Localization/es.lproj/Localizable.strings b/AlphaWallet/Localization/es.lproj/Localizable.strings index 6e0ea9277..38a720d67 100644 --- a/AlphaWallet/Localization/es.lproj/Localizable.strings +++ b/AlphaWallet/Localization/es.lproj/Localizable.strings @@ -29,6 +29,7 @@ "transaction.blockNumber.label.title" = "Bloque n.º"; "transaction.nonce.label.title" = "Nonce"; "confirmPayment.confirm.button.title" = "Confirmar"; +"confirmPayment.reject.button.title" = "Reject"; "confirmPayment.from.label.title" = "De"; "confirmPayment.gasFee.label.title" = "Estimación de la tarifa de red"; "confirmPayment.gasLimit.label.title" = "Límite de gas"; @@ -538,4 +539,5 @@ You can check the latest gas price on gasnow.org"; "walletConnect.activeSessions" = "Active connection to Dapps"; "walletConnect.activeSessions.plural" = "Active connections to Dapps"; "walletConnect.sendRawTransaction.title" = "Send raw transaction"; +"walletConnect.connection.title" = "Connect To Site?"; "send.allFunds" = "All Funds"; diff --git a/AlphaWallet/Localization/ja.lproj/Localizable.strings b/AlphaWallet/Localization/ja.lproj/Localizable.strings index e2ffc42b1..c4939387a 100644 --- a/AlphaWallet/Localization/ja.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ja.lproj/Localizable.strings @@ -29,6 +29,7 @@ "transaction.blockNumber.label.title" = "ブロック番号"; "transaction.nonce.label.title" = "Nonce"; "confirmPayment.confirm.button.title" = "確認"; +"confirmPayment.reject.button.title" = "Reject"; "confirmPayment.from.label.title" = "送信元"; "confirmPayment.gasFee.label.title" = "Estimate Network Fee"; "confirmPayment.gasLimit.label.title" = "ガスの制限"; @@ -538,4 +539,5 @@ You can check the latest gas price on gasnow.org"; "walletConnect.sendRawTransaction.title" = "Send raw transaction"; "walletConnect.activeSessions" = "Active connection to Dapps"; "walletConnect.activeSessions.plural" = "Active connections to Dapps"; +"walletConnect.connection.title" = "Connect To Site?"; "send.allFunds" = "All Funds"; diff --git a/AlphaWallet/Localization/ko.lproj/Localizable.strings b/AlphaWallet/Localization/ko.lproj/Localizable.strings index 23d5a3a34..f24f96a6b 100644 --- a/AlphaWallet/Localization/ko.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ko.lproj/Localizable.strings @@ -29,6 +29,7 @@ "transaction.blockNumber.label.title" = "블록 번호"; "transaction.nonce.label.title" = "Nonce"; "confirmPayment.confirm.button.title" = "확인"; +"confirmPayment.reject.button.title" = "Reject"; "confirmPayment.from.label.title" = "발신인"; "confirmPayment.gasFee.label.title" = "Estimate Network Fee"; "confirmPayment.gasLimit.label.title" = "가스 한도"; @@ -538,4 +539,5 @@ You can check the latest gas price on gasnow.org"; "walletConnect.sendRawTransaction.title" = "Send raw transaction"; "walletConnect.activeSessions" = "Active connection to Dapps"; "walletConnect.activeSessions.plural" = "Active connections to Dapps"; +"walletConnect.connection.title" = "Connect To Site?"; "send.allFunds" = "All Funds"; diff --git a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings index e227b331f..f2c9a55b7 100644 --- a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings +++ b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings @@ -29,6 +29,7 @@ "transaction.blockNumber.label.title" = "区块 #"; "transaction.nonce.label.title" = "Nonce"; "confirmPayment.confirm.button.title" = "确认"; +"confirmPayment.reject.button.title" = "Reject"; "confirmPayment.from.label.title" = "来源地址"; "confirmPayment.gasFee.label.title" = "估计网络费用"; "confirmPayment.gasLimit.label.title" = "燃料限制"; @@ -538,4 +539,5 @@ You can check the latest gas price on gasnow.org"; "walletConnect.sendRawTransaction.title" = "Send raw transaction"; "walletConnect.activeSessions" = "Active connection to Dapps"; "walletConnect.activeSessions.plural" = "Active connections to Dapps"; +"walletConnect.connection.title" = "Connect To Site?"; "send.allFunds" = "All Funds"; diff --git a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift index 18adbf337..968aac618 100644 --- a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift @@ -240,12 +240,12 @@ class TokensViewController: UIViewController { navigationItem.rightBarButtonItem = UIBarButtonItem.qrCodeBarButton(self, selector: #selector(scanQRCodeButtonSelected)) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: blockieImageView) - walletConnectCoordinator.walletConnectSessions.subscribe { [weak self] sessions in - guard let strongSelf = self, let sessions = sessions else { return } - if sessions.isEmpty { + walletConnectCoordinator.sessionsToURLServersMap.subscribe { [weak self] value in + guard let strongSelf = self, let sessionsToURLServersMap = value else { return } + if sessionsToURLServersMap.sessions.isEmpty { strongSelf.sections = [.filters, .addHideToken, .tokens] } else { - strongSelf.sections = [.filters, .addHideToken, .activeWalletSession(count: sessions.count), .tokens] + strongSelf.sections = [.filters, .addHideToken, .activeWalletSession(count: sessionsToURLServersMap.sessions.count), .tokens] } strongSelf.tableView.reloadData() } diff --git a/AlphaWallet/WalletConnect/Controllers/TransactionInProgressCoordinatorBridgeToPromise.swift b/AlphaWallet/WalletConnect/Controllers/TransactionInProgressCoordinatorBridgeToPromise.swift index 9c8bf73c9..863f1781a 100644 --- a/AlphaWallet/WalletConnect/Controllers/TransactionInProgressCoordinatorBridgeToPromise.swift +++ b/AlphaWallet/WalletConnect/Controllers/TransactionInProgressCoordinatorBridgeToPromise.swift @@ -43,7 +43,7 @@ extension TransactionInProgressCoordinatorBridgeToPromise: TransactionInProgress extension TransactionInProgressCoordinator { - static func promise(navigationController: UINavigationController, coordinator: Coordinator) -> Promise { + static func promise(_ navigationController: UINavigationController, coordinator: Coordinator) -> Promise { return TransactionInProgressCoordinatorBridgeToPromise(navigationController: navigationController, coordinator: coordinator).promise } } diff --git a/AlphaWallet/WalletConnect/Controllers/WalletConnectToSessionCoordinatorBridgeToPromise.swift b/AlphaWallet/WalletConnect/Controllers/WalletConnectToSessionCoordinatorBridgeToPromise.swift new file mode 100644 index 000000000..59cd8610c --- /dev/null +++ b/AlphaWallet/WalletConnect/Controllers/WalletConnectToSessionCoordinatorBridgeToPromise.swift @@ -0,0 +1,52 @@ +// +// WalletConnectToSessionCoordinatorBridgeToPromise.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 18.02.2021. +// + +import PromiseKit + +private class WalletConnectToSessionCoordinatorBridgeToPromise { + + private let (promiseToReturn, seal) = Promise.pending() + private var retainCycle: WalletConnectToSessionCoordinatorBridgeToPromise? + + init(navigationController: UINavigationController, coordinator: Coordinator, connection: WalletConnectConnection, serverChoices: [RPCServer]) { + retainCycle = self + + let newCoordinator = WalletConnectToSessionCoordinator(connection: connection, navigationController: navigationController, serverChoices: serverChoices) + newCoordinator.delegate = self + coordinator.addCoordinator(newCoordinator) + + _ = promiseToReturn.ensure { + // ensure we break the retain cycle + self.retainCycle = nil + coordinator.removeCoordinator(newCoordinator) + } + + newCoordinator.start() + } + + var promise: Promise { + return promiseToReturn + } +} + +extension WalletConnectToSessionCoordinatorBridgeToPromise: WalletConnectToSessionCoordinatorDelegate { + func coordinator(_ coordinator: WalletConnectToSessionCoordinator, didCompleteWithConnection result: WalletConnectServer.ConnectionChoice) { + seal.fulfill(result) + } +} + +extension WalletConnectToSessionCoordinator { + + static func promise(_ navigationController: UINavigationController, coordinator: Coordinator, connection: WalletConnectConnection, serverChoices: [RPCServer]) -> Promise { + return WalletConnectToSessionCoordinatorBridgeToPromise( + navigationController: navigationController, + coordinator: coordinator, + connection: connection, + serverChoices: serverChoices + ).promise + } +} diff --git a/AlphaWallet/WalletConnect/Coordinator/WalletConnectCoordinator.swift b/AlphaWallet/WalletConnect/Coordinator/WalletConnectCoordinator.swift index 726113fc2..3755adedf 100644 --- a/AlphaWallet/WalletConnect/Coordinator/WalletConnectCoordinator.swift +++ b/AlphaWallet/WalletConnect/Coordinator/WalletConnectCoordinator.swift @@ -18,6 +18,8 @@ enum SessionsToDisconnect { case all } +typealias SessionsToURLServersMap = (sessions: [WalletConnectSession], urlToServer: [WCURL: RPCServer]) + class WalletConnectCoordinator: NSObject, Coordinator { private lazy var server: WalletConnectServer = { let server = WalletConnectServer(wallet: sessions.anyValue.account.address) @@ -28,9 +30,8 @@ class WalletConnectCoordinator: NSObject, Coordinator { private let navigationController: UINavigationController var coordinators: [Coordinator] = [] - var walletConnectSessions: Subscribable<[WalletConnectSession]> { - server.sessions - } + var sessionsToURLServersMap: Subscribable = .init(nil) + private let keystore: Keystore private let sessions: ServerDictionary private let analyticsCoordinator: AnalyticsCoordinator? @@ -40,6 +41,7 @@ class WalletConnectCoordinator: NSObject, Coordinator { private var serverChoices: [RPCServer] { ServersCoordinator.serversOrdered.filter { config.enabledServers.contains($0) } } + private weak var sessionsViewController: WalletConnectSessionsViewController? init(keystore: Keystore, sessions: ServerDictionary, navigationController: UINavigationController, analyticsCoordinator: AnalyticsCoordinator?, config: Config, nativeCryptoCurrencyPrices: ServerDictionary>) { self.config = config @@ -50,6 +52,12 @@ class WalletConnectCoordinator: NSObject, Coordinator { self.nativeCryptoCurrencyPrices = nativeCryptoCurrencyPrices super.init() start() + + server.sessions.subscribe { [weak self, weak server] sessions in + guard let strongSelf = self, let strongServer = server else { return } + + strongSelf.sessionsToURLServersMap.value = (sessions ?? [], strongServer.urlToServer) + } } //NOTE: we are using disconnection to notify dapp that we get disconnect, in other case dapp still stay connected @@ -86,23 +94,35 @@ class WalletConnectCoordinator: NSObject, Coordinator { } func openSession(url: WalletConnectURL) { - try? server.connect(url: url) + navigationController.setNavigationBarHidden(false, animated: true) + + showSessions(state: .loading, navigationController: navigationController) { + try? self.server.connect(url: url) + } } func showSessionDetails(inNavigationController navigationController: UINavigationController) { - guard let sessions = server.sessions.value else { return } - guard !sessions.isEmpty else { return } + guard let sessions = server.sessions.value, !sessions.isEmpty else { return } + if sessions.count == 1 { let session = sessions[0] display(session: session, withNavigationController: navigationController) } else { - let viewController = WalletConnectSessionsViewController(sessions: server.sessions, urlToServer: server.urlToServer) - viewController.delegate = self - viewController.configure() - navigationController.pushViewController(viewController, animated: true) + showSessions(state: .sessions, navigationController: navigationController) } } + private func showSessions(state: WalletConnectSessionsViewController.State, navigationController: UINavigationController, completion: @escaping (() -> Void) = {}) { + + let viewController = WalletConnectSessionsViewController(sessionsToURLServersMap: sessionsToURLServersMap) + viewController.delegate = self + viewController.configure(state: state) + + self.sessionsViewController = viewController + + navigationController.pushViewController(viewController, animated: true, completion: completion) + } + private func display(session: WalletConnectSession, withNavigationController navigationController: UINavigationController) { let coordinator = WalletConnectSessionCoordinator(navigationController: navigationController, server: server, session: session) coordinator.delegate = self @@ -118,44 +138,52 @@ extension WalletConnectCoordinator: WalletConnectSessionCoordinatorDelegate { } extension WalletConnectCoordinator: WalletConnectServerDelegate { - func server(_ server: WalletConnectServer, action: WalletConnectServer.Action, request: WalletConnectRequest) { - guard let rpcServer = server.urlToServer[request.url] else { - server.reject(request) - return + + func server(_ server: WalletConnectServer, didConnect session: WalletConnectSession) { + if let viewController = sessionsViewController { + viewController.set(state: .sessions) } - let session = sessions[rpcServer] - firstly { - Promise { seal in - switch session.account.type { - case .real: - seal.fulfill(()) - case .watch: - seal.reject(PMKError.cancelled) + } + + func server(_ server: WalletConnectServer, action: WalletConnectServer.Action, request: WalletConnectRequest) { + if let rpcServer = server.urlToServer[request.url] { + let session = sessions[rpcServer] + + firstly { + Promise { seal in + switch session.account.type { + case .real: + seal.fulfill(()) + case .watch: + seal.reject(PMKError.cancelled) + } } + }.then { _ -> Promise in + let account = session.account.address + switch action.type { + case .signTransaction(let transaction): + return self.executeTransaction(session: session, callbackID: action.id, url: action.url, transaction: transaction, type: .sign) + case .sendTransaction(let transaction): + return self.executeTransaction(session: session, callbackID: action.id, url: action.url, transaction: transaction, type: .signThenSend) + case .signMessage(let hexMessage): + return self.signMessage(with: .message(hexMessage.toHexData), account: account, callbackID: action.id, url: action.url) + case .signPersonalMessage(let hexMessage): + return self.signMessage(with: .personalMessage(hexMessage.toHexData), account: account, callbackID: action.id, url: action.url) + case .signTypedMessageV3(let typedData): + return self.signMessage(with: .eip712v3And4(typedData), account: account, callbackID: action.id, url: action.url) + case .sendRawTransaction(let raw): + return self.sendRawTransaction(session: session, rawTransaction: raw, callbackID: action.id, url: action.url) + case .getTransactionCount: + return self.getTransactionCount(session: session, callbackID: action.id, url: action.url) + case .unknown: + throw PMKError.cancelled + } + }.done { callback in + try? server.fulfill(callback, request: request) + }.catch { _ in + server.reject(request) } - }.then { _ -> Promise in - let account = session.account.address - switch action.type { - case .signTransaction(let transaction): - return self.executeTransaction(session: session, callbackID: action.id, url: action.url, transaction: transaction, type: .sign) - case .sendTransaction(let transaction): - return self.executeTransaction(session: session, callbackID: action.id, url: action.url, transaction: transaction, type: .signThenSend) - case .signMessage(let hexMessage): - return self.signMessage(with: .message(hexMessage.toHexData), account: account, callbackID: action.id, url: action.url) - case .signPersonalMessage(let hexMessage): - return self.signMessage(with: .personalMessage(hexMessage.toHexData), account: account, callbackID: action.id, url: action.url) - case .signTypedMessageV3(let typedData): - return self.signMessage(with: .eip712v3And4(typedData), account: account, callbackID: action.id, url: action.url) - case .sendRawTransaction(let raw): - return self.sendRawTransaction(session: session, rawTransaction: raw, callbackID: action.id, url: action.url) - case .getTransactionCount: - return self.getTransactionCount(session: session, callbackID: action.id, url: action.url) - case .unknown: - throw PMKError.cancelled - } - }.done { callback in - try? server.fulfill(callback, request: request) - }.catch { _ in + } else { server.reject(request) } } @@ -191,7 +219,7 @@ extension WalletConnectCoordinator: WalletConnectServerDelegate { case .sign: break case .signThenSend: - TransactionInProgressCoordinator.promise(navigationController: self.navigationController, coordinator: self).done { _ in + TransactionInProgressCoordinator.promise(self.navigationController, coordinator: self).done { _ in //no op }.cauterize() } @@ -220,7 +248,13 @@ extension WalletConnectCoordinator: WalletConnectServerDelegate { } func server(_ server: WalletConnectServer, shouldConnectFor connection: WalletConnectConnection, completion: @escaping (WalletConnectServer.ConnectionChoice) -> Void) { - showConnectToSession(connection: connection, completion: completion) + firstly { + WalletConnectToSessionCoordinator.promise(navigationController, coordinator: self, connection: connection, serverChoices: serverChoices) + }.done { choise in + completion(choise) + }.catch { _ in + completion(.cancel) + } } //TODO after we support sendRawTransaction in dapps (and hence a proper UI, be it the actionsheet for transaction confirmation or a simple prompt), let's modify this to use the same flow @@ -284,56 +318,21 @@ extension WalletConnectCoordinator: WalletConnectServerDelegate { } } } - - private func showConnectToSession(connection: WalletConnectConnection, completion: @escaping (WalletConnectServer.ConnectionChoice) -> Void) { - let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet - if let server = connection.server { - let alertViewController = UIAlertController(title: connection.name, message: connection.url.absoluteString, preferredStyle: style) - let startAction = UIAlertAction(title: R.string.localizable.walletConnectSessionConnect(server.name), style: .default) { _ in - completion(.connect(server)) - } - - let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel) { _ in - completion(.cancel) - } - - alertViewController.addAction(startAction) - alertViewController.addAction(cancelAction) - - navigationController.present(alertViewController, animated: true) - } else { - let alertViewController = UIAlertController(title: connection.name, message: R.string.localizable.walletConnectStart(connection.url.absoluteString), preferredStyle: style) - for each in serverChoices { - let action = UIAlertAction(title: each.name, style: .default) { _ in - completion(.connect(each)) - } - alertViewController.addAction(action) - } - - let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel) { _ in - completion(.cancel) - } - alertViewController.addAction(cancelAction) - - navigationController.present(alertViewController, animated: true) - } - } } -extension String { +extension WalletConnectCoordinator: WalletConnectSessionsViewControllerDelegate { + + func didClose(in viewController: WalletConnectSessionsViewController) { + //NOTE: even if we haven't sessions view controller pushed to navigation stack, we need to make sure that root NavigationBar will be hidden + navigationController.setNavigationBarHidden(true, animated: true) - var toHexData: Data { - if self.hasPrefix("0x") { - return Data(hex: self) - } else { - return Data(hex: self.hex) - } + guard let navigationController = viewController.navigationController else { return } + navigationController.popViewController(animated: true) } -} -extension WalletConnectCoordinator: WalletConnectSessionsViewControllerDelegate { func didSelect(session: WalletConnectSession, in viewController: WalletConnectSessionsViewController) { guard let navigationController = viewController.navigationController else { return } + display(session: session, withNavigationController: navigationController) } } diff --git a/AlphaWallet/WalletConnect/Coordinator/WalletConnectSessionCoordinator.swift b/AlphaWallet/WalletConnect/Coordinator/WalletConnectSessionCoordinator.swift index 9b8d1deb4..b45ead99e 100644 --- a/AlphaWallet/WalletConnect/Coordinator/WalletConnectSessionCoordinator.swift +++ b/AlphaWallet/WalletConnect/Coordinator/WalletConnectSessionCoordinator.swift @@ -42,6 +42,7 @@ class WalletConnectSessionCoordinator: Coordinator { } extension WalletConnectSessionCoordinator: WalletConnectSessionViewControllerDelegate { + func didDismiss(in controller: WalletConnectSessionViewController) { guard let delegate = delegate else { return } navigationController.popViewController(animated: true) @@ -50,6 +51,7 @@ extension WalletConnectSessionCoordinator: WalletConnectSessionViewControllerDel func controller(_ controller: WalletConnectSessionViewController, disconnectSelected sender: UIButton) { guard let delegate = delegate else { return } + do { try server.disconnect(session: session) } catch { diff --git a/AlphaWallet/WalletConnect/Coordinator/WalletConnectToSessionCoordinator.swift b/AlphaWallet/WalletConnect/Coordinator/WalletConnectToSessionCoordinator.swift new file mode 100644 index 000000000..4c9501261 --- /dev/null +++ b/AlphaWallet/WalletConnect/Coordinator/WalletConnectToSessionCoordinator.swift @@ -0,0 +1,112 @@ +// +// WalletConnectToSessionCoordinator.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 18.02.2021. +// + +import UIKit + +protocol WalletConnectToSessionCoordinatorDelegate: class { + func coordinator(_ coordinator: WalletConnectToSessionCoordinator, didCompleteWithConnection result: WalletConnectServer.ConnectionChoice) +} + +class WalletConnectToSessionCoordinator: Coordinator { + var coordinators: [Coordinator] = [] + + private let connection: WalletConnectConnection + private let presentationNavigationController: UINavigationController + private lazy var viewModel = WalletConnectToSessionViewModel(connection: connection, serverToConnect: serverToConnect) + private lazy var viewController: WalletConnectToSessionViewController = { + let viewController = WalletConnectToSessionViewController(viewModel: viewModel) + viewController.delegate = self + + return viewController + }() + private lazy var navigationController: UINavigationController = { + let controller = UINavigationController(rootViewController: viewController) + controller.modalPresentationStyle = .overFullScreen + controller.modalTransitionStyle = .crossDissolve + controller.view.backgroundColor = UIColor.black.withAlphaComponent(0.6) + + return controller + }() + private var serverToConnect: RPCServer + private let serverChoices: [RPCServer] + weak var delegate: WalletConnectToSessionCoordinatorDelegate? + + init(connection: WalletConnectConnection, navigationController: UINavigationController, serverChoices: [RPCServer]) { + self.connection = connection + self.serverToConnect = connection.server ?? .main + self.presentationNavigationController = navigationController + self.serverChoices = serverChoices + } + + func start() { + presentationNavigationController.present(navigationController, animated: false) + viewController.configure(for: viewModel) + viewController.reloadView() + } + + func dissmissAnimated(completion: @escaping () -> Void) { + viewController.dismissViewAnimated { + //Needs a strong self reference otherwise `self` might have been removed by its owner by the time animation completes and the `completion` block not called + self.navigationController.dismiss(animated: true, completion: completion) + } + } +} + +extension WalletConnectToSessionCoordinator: WalletConnectToSessionViewControllerDelegate { + + func changeConnectionServerSelected(in controller: WalletConnectToSessionViewController) { + showAvailableToConnectServers(completion: { [weak self] result in + guard let strongSelf = self else { return } + + switch result { + case .connect(let server): + strongSelf.serverToConnect = server + strongSelf.viewModel.set(serverToConnect: server) + case .cancel: + break + } + + strongSelf.viewController.configure(for: strongSelf.viewModel) + strongSelf.viewController.reloadView() + }) + } + + func controller(_ controller: WalletConnectToSessionViewController, continueButtonTapped sender: UIButton) { + dissmissAnimated(completion: { + guard let delegate = self.delegate else { return } + + delegate.coordinator(self, didCompleteWithConnection: .connect(self.serverToConnect)) + }) + } + + func didClose(in controller: WalletConnectToSessionViewController) { + navigationController.dismiss(animated: false) { [weak self] in + guard let strongSelf = self, let delegate = strongSelf.delegate else { return } + + delegate.coordinator(strongSelf, didCompleteWithConnection: .cancel) + } + } + + private func showAvailableToConnectServers(completion: @escaping (WalletConnectServer.ConnectionChoice) -> Void) { + let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet + let alertViewController = UIAlertController(title: connection.name, message: R.string.localizable.walletConnectStart(connection.url.absoluteString), preferredStyle: style) + for each in serverChoices { + let action = UIAlertAction(title: each.name, style: .default) { _ in + completion(.connect(each)) + } + alertViewController.addAction(action) + } + + let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel) { _ in + completion(.cancel) + } + alertViewController.addAction(cancelAction) + + navigationController.present(alertViewController, animated: true) + } + +} diff --git a/AlphaWallet/WalletConnect/Controllers/WalletConnectSessionsViewController.swift b/AlphaWallet/WalletConnect/ViewController/WalletConnectSessionsViewController.swift similarity index 50% rename from AlphaWallet/WalletConnect/Controllers/WalletConnectSessionsViewController.swift rename to AlphaWallet/WalletConnect/ViewController/WalletConnectSessionsViewController.swift index 6306be811..34f40e6fe 100644 --- a/AlphaWallet/WalletConnect/Controllers/WalletConnectSessionsViewController.swift +++ b/AlphaWallet/WalletConnect/ViewController/WalletConnectSessionsViewController.swift @@ -4,10 +4,25 @@ import UIKit protocol WalletConnectSessionsViewControllerDelegate: class { func didSelect(session: WalletConnectSession, in viewController: WalletConnectSessionsViewController) + func didClose(in viewController: WalletConnectSessionsViewController) +} + +extension WalletConnectSessionsViewController { + enum State { + case sessions + case loading + } } class WalletConnectSessionsViewController: UIViewController { - private let sessions: Subscribable<[WalletConnectSession]> + private var sessionsValue: [WalletConnectSession] { + return sessionsToURLServersMap.value?.sessions ?? [] + } + private var urlToServer: [WalletConnectURL: RPCServer] { + return sessionsToURLServersMap.value?.urlToServer ?? [:] + } + + private let sessionsToURLServersMap: Subscribable private lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.register(WalletConnectSessionCell.self) @@ -19,49 +34,82 @@ class WalletConnectSessionsViewController: UIViewController { tableView.translatesAutoresizingMaskIntoConstraints = false return tableView }() - private let urlToServer: [WalletConnectURL: RPCServer] weak var delegate: WalletConnectSessionsViewControllerDelegate? - init(sessions: Subscribable<[WalletConnectSession]>, urlToServer: [WalletConnectURL: RPCServer]) { - self.sessions = sessions - self.urlToServer = urlToServer + private lazy var spinner: UIActivityIndicatorView = { + let view = UIActivityIndicatorView(style: .gray) + view.translatesAutoresizingMaskIntoConstraints = false + view.hidesWhenStopped = true + view.tintColor = .red + return view + }() + + init(sessionsToURLServersMap: Subscribable) { + self.sessionsToURLServersMap = sessionsToURLServersMap +// self.urlToServer = urlToServer super.init(nibName: nil, bundle: nil) view.addSubview(tableView) + view.addSubview(spinner) - sessions.subscribe { _ in + sessionsToURLServersMap.subscribe { _ in self.tableView.reloadData() } NSLayoutConstraint.activate([ tableView.anchorsConstraint(to: view), + spinner.centerXAnchor.constraint(equalTo: tableView.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: tableView.centerYAnchor) ]) + navigationItem.leftBarButtonItem = UIBarButtonItem.backBarButton(self, selector: #selector(closeButtonSelected)) } required init?(coder aDecoder: NSCoder) { nil } - func configure() { + func configure(state: State) { navigationItem.largeTitleDisplayMode = .never hidesBottomBarWhenPushed = true title = R.string.localizable.walletConnectTitle() + + set(state: state) + } + +// func set(urlToServer: [WalletConnectURL: RPCServer]) { +// self.urlToServer = urlToServer +// } + + func set(state: State) { + switch state { + case .loading: + spinner.startAnimating() + case .sessions: + spinner.stopAnimating() + } + } + + @objc private func closeButtonSelected(_ sender: UIBarButtonItem) { + guard let delegate = self.delegate else { return } + + delegate.didClose(in: self) } } extension WalletConnectSessionsViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - guard let session = sessions.value?[indexPath.row] else { return } - delegate?.didSelect(session: session, in: self) + + delegate?.didSelect(session: sessionsValue[indexPath.row], in: self) } } extension WalletConnectSessionsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(for: indexPath) as WalletConnectSessionCell - guard let session = sessions.value?[indexPath.row] else { return cell } + let cell: WalletConnectSessionCell = tableView.dequeueReusableCell(for: indexPath) + + let session = sessionsValue[indexPath.row] if let server = urlToServer[session.url] { let viewModel = WalletConnectSessionCellViewModel(session: session, server: server) cell.configure(viewModel: viewModel) @@ -72,6 +120,6 @@ extension WalletConnectSessionsViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - sessions.value?.count ?? 0 + return sessionsValue.count } } diff --git a/AlphaWallet/WalletConnect/ViewController/WalletConnectToSessionViewController.swift b/AlphaWallet/WalletConnect/ViewController/WalletConnectToSessionViewController.swift new file mode 100644 index 000000000..a417fb740 --- /dev/null +++ b/AlphaWallet/WalletConnect/ViewController/WalletConnectToSessionViewController.swift @@ -0,0 +1,395 @@ +// +// WalletConnectToSessionViewController.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 17.02.2021. +// + +import UIKit + +protocol WalletConnectToSessionViewControllerDelegate: class { + func controller(_ controller: WalletConnectToSessionViewController, continueButtonTapped sender: UIButton) + func changeConnectionServerSelected(in controller: WalletConnectToSessionViewController) + + func didClose(in controller: WalletConnectToSessionViewController) +} + +class WalletConnectToSessionViewController: UIViewController { + private lazy var headerView: HeaderView = HeaderView(viewModel: .init(title: viewModel.navigationTitle)) + private let buttonsBar = ButtonsBar(configuration: .custom(types: [.green, .white])) + private var viewModel: WalletConnectToSessionViewModel + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 0 + stackView.translatesAutoresizingMaskIntoConstraints = false + + return stackView + }() + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stackView) + return scrollView + }() + + private let separatorLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = R.color.mercury() + + return view + }() + + private var contentSizeObservation: NSKeyValueObservation? + + private lazy var footerBar: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = viewModel.footerBackgroundColor + view.addSubview(buttonsBar) + + return view + }() + + private lazy var backgroundView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissViewController)) + view.isUserInteractionEnabled = true + view.addGestureRecognizer(tap) + + return view + }() + + private lazy var containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + + view.addSubview(scrollView) + view.addSubview(footerBar) + view.addSubview(headerView) + view.addSubview(separatorLine) + + return view + }() + + private lazy var heightConstraint: NSLayoutConstraint = { + return containerView.heightAnchor.constraint(equalToConstant: preferredContentSize.height) + }() + + private lazy var bottomConstraint: NSLayoutConstraint = { + containerView.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor) + }() + + private var allowPresentationAnimation: Bool = true + private var allowDismissalAnimation: Bool = true + + weak var delegate: WalletConnectToSessionViewControllerDelegate? + + init(viewModel: WalletConnectToSessionViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + + view.addSubview(backgroundView) + view.addSubview(containerView) + + NSLayoutConstraint.activate([ + backgroundView.bottomAnchor.constraint(equalTo: containerView.topAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + heightConstraint, + bottomConstraint, + containerView.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), + containerView.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + headerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + headerView.topAnchor.constraint(equalTo: containerView.topAnchor), + + scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: headerView.bottomAnchor), + scrollView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), + + stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + + separatorLine.heightAnchor.constraint(equalToConstant: DataEntry.Metric.TransactionConfirmation.separatorHeight), + separatorLine.bottomAnchor.constraint(equalTo: footerBar.topAnchor), + separatorLine.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), + + footerBar.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + footerBar.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + footerBar.heightAnchor.constraint(equalToConstant: DataEntry.Metric.TransactionConfirmation.footerHeight), + footerBar.bottomAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.bottomAnchor), + + buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor, constant: 20), + buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), + buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), + buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight), + ]) + headerView.closeButton.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside) + + contentSizeObservation = scrollView.observe(\.contentSize, options: [.new, .initial]) { [weak self] scrollView, _ in + guard let strongSelf = self, strongSelf.allowDismissalAnimation else { return } + + let statusBarHeight = UIApplication.shared.statusBarFrame.height + let contentHeight = scrollView.contentSize.height + DataEntry.Metric.TransactionConfirmation.footerHeight + DataEntry.Metric.TransactionConfirmation.headerHeight + UIApplication.shared.bottomSafeAreaHeight + let newHeight = min(UIScreen.main.bounds.height - statusBarHeight, contentHeight) + + let fillScreenPercentage = strongSelf.heightConstraint.constant / strongSelf.view.bounds.height + + if fillScreenPercentage >= 0.9 { + strongSelf.heightConstraint.constant = strongSelf.containerView.bounds.height + } else { + strongSelf.heightConstraint.constant = newHeight + } + } + + generateSubviews() + } + + override func viewDidLoad() { + super.viewDidLoad() + + configure(for: viewModel) + + //NOTE: to display animation correctly we can take 'view.frame.height' and bottom view will smoothly slide up from button ;) + bottomConstraint.constant = view.frame.height + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let navigationController = navigationController { + navigationController.setNavigationBarHidden(true, animated: false) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + presentViewAnimated() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let navigationController = navigationController { + navigationController.setNavigationBarHidden(false, animated: false) + } + } + + private func presentViewAnimated() { + guard allowPresentationAnimation else { return } + allowPresentationAnimation = false + + bottomConstraint.constant = 0 + + UIView.animate(withDuration: 0.4) { + self.view.layoutIfNeeded() + } + } + + func dismissViewAnimated(with completion: @escaping () -> Void) { + guard allowDismissalAnimation else { return } + allowDismissalAnimation = false + + bottomConstraint.constant = heightConstraint.constant + + UIView.animate(withDuration: 0.4, animations: { + self.view.layoutIfNeeded() + }, completion: { _ in + completion() + }) + } + + @objc private func dismissViewController() { + dismissViewAnimated(with: { + //NOTE: strong reff is required + self.delegate?.didClose(in: self) + }) + } + + func reloadView() { + generateSubviews() + } + + func configure(for viewModel: WalletConnectToSessionViewModel) { + self.viewModel = viewModel + + scrollView.backgroundColor = viewModel.backgroundColor + view.backgroundColor = viewModel.backgroundColor + navigationItem.title = viewModel.title + + buttonsBar.configure() + + let button1 = buttonsBar.buttons[0] + button1.shrinkBorderColor = Colors.loadingIndicatorBorder + button1.setTitle(viewModel.confirmationButtonTitle, for: .normal) + button1.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) + + let button2 = buttonsBar.buttons[1] + button2.shrinkBorderColor = Colors.loadingIndicatorBorder + button2.setTitle(viewModel.rejectionButtonTitle, for: .normal) + button2.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside) + } + + @objc private func confirmButtonTapped(_ sender: UIButton) { + delegate?.controller(self, continueButtonTapped: sender) + } + + required init?(coder aDecoder: NSCoder) { + return nil + } +} + +fileprivate struct HeaderViewModel { + let title: String + var backgroundColor: UIColor { + Colors.appBackground + } + var icon: UIImage? { + return R.image.awLogoSmall() + } + var attributedTitle: NSAttributedString { + let style = NSMutableParagraphStyle() + style.alignment = .center + + return .init(string: title, attributes: [ + .font: DataEntry.Font.text as Any, + .paragraphStyle: style, + .foregroundColor: Colors.darkGray + ]) + } +} + +fileprivate class HeaderView: UIView { + private let separatorLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = R.color.mercury() + + return view + }() + + private let titleLabel: UILabel = { + let titleLabel = UILabel(frame: .zero) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + return titleLabel + }() + + private let iconImageView: UIImageView = { + let imageView = UIImageView(frame: .zero) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + let closeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.contentMode = .scaleAspectFit + button.setImage(R.image.close(), for: .normal) + + return button + }() + + init(viewModel: HeaderViewModel) { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + + addSubview(separatorLine) + addSubview(titleLabel) + addSubview(iconImageView) + addSubview(closeButton) + + NSLayoutConstraint.activate([ + separatorLine.heightAnchor.constraint(equalToConstant: DataEntry.Metric.TransactionConfirmation.separatorHeight), + separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor), + titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor), + + iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), + iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 30), + iconImageView.heightAnchor.constraint(equalToConstant: 30), + + closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), + closeButton.centerYAnchor.constraint(equalTo: centerYAnchor), + closeButton.widthAnchor.constraint(equalToConstant: 30), + closeButton.heightAnchor.constraint(equalToConstant: 30), + + heightAnchor.constraint(equalToConstant: DataEntry.Metric.TransactionConfirmation.headerHeight) + ]) + + titleLabel.attributedText = viewModel.attributedTitle + iconImageView.image = viewModel.icon + backgroundColor = viewModel.backgroundColor + } + + required init?(coder: NSCoder) { + return nil + } +} + +extension WalletConnectToSessionViewController { + + private func generateSubviews() { + stackView.removeAllArrangedSubviews() + + var views: [UIView] = [] + for (sectionIndex, section) in viewModel.sections.enumerated() { + let header = TransactionConfirmationHeaderView(viewModel: viewModel.headerViewModel(section: sectionIndex)) + header.delegate = self + + switch section { + case .name, .url: + break + case .network: + if viewModel.allowChangeConnectionServer { + header.enableTapAction(title: "Edit") + } + } + views.append(header) + } + + stackView.addArrangedSubviews(views) + } +} + +extension WalletConnectToSessionViewController: TransactionConfirmationHeaderViewDelegate { + + func headerView(_ header: TransactionConfirmationHeaderView, shouldHideChildren section: Int, index: Int) -> Bool { + return true + } + + func headerView(_ header: TransactionConfirmationHeaderView, shouldShowChildren section: Int, index: Int) -> Bool { + return false + } + + func headerView(_ header: TransactionConfirmationHeaderView, openStateChanged section: Int) { + //no-op + } + + func headerView(_ header: TransactionConfirmationHeaderView, tappedSection section: Int) { + delegate?.changeConnectionServerSelected(in: self) + } +} diff --git a/AlphaWallet/WalletConnect/ViewModel/WalletConnectToSessionViewModel.swift b/AlphaWallet/WalletConnect/ViewModel/WalletConnectToSessionViewModel.swift new file mode 100644 index 000000000..9ea588bca --- /dev/null +++ b/AlphaWallet/WalletConnect/ViewModel/WalletConnectToSessionViewModel.swift @@ -0,0 +1,84 @@ +// +// SignatureConfirmationConfirmationViewModel.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 17.02.2021. +// + +import UIKit + +struct WalletConnectToSessionViewModel { + + private let connection: WalletConnectConnection + private var serverToConnect: RPCServer + + init(connection: WalletConnectConnection, serverToConnect: RPCServer) { + self.connection = connection + self.serverToConnect = serverToConnect + } + + mutating func set(serverToConnect: RPCServer) { + self.serverToConnect = serverToConnect + } + + var navigationTitle: String { + return R.string.localizable.walletConnectConnectionTitle() + } + + var title: String { + return R.string.localizable.confirmPaymentConfirmButtonTitle() + } + + var confirmationButtonTitle: String { + return R.string.localizable.confirmPaymentConfirmButtonTitle() + } + + var rejectionButtonTitle: String { + return R.string.localizable.confirmPaymentRejectButtonTitle() + } + + var backgroundColor: UIColor { + return UIColor.clear + } + + var footerBackgroundColor: UIColor { + return R.color.white()! + } + + var sections: [Section] { + Section.allCases + } + + enum Section: CaseIterable { + case name + case network + case url + + var title: String { + switch self { + case .name: + return "Name" + case .network: + return "Network" + case .url: + return "Connected To" + } + } + } + + var allowChangeConnectionServer: Bool { + return connection.server == nil + } + + func headerViewModel(section: Int) -> TransactionConfirmationHeaderViewModel { + let headerName = sections[section].title + switch sections[section] { + case .name: + return .init(title: .normal(connection.name), headerName: headerName, configuration: .init(section: section)) + case .network: + return .init(title: .normal(serverToConnect.displayName), headerName: headerName, configuration: .init(section: section)) + case .url: + return .init(title: .normal(connection.url.absoluteString), headerName: headerName, configuration: .init(section: section)) + } + } +} diff --git a/AlphaWallet/WalletConnect/WalletConnectServer.swift b/AlphaWallet/WalletConnect/WalletConnectServer.swift index d14157837..acfc11307 100644 --- a/AlphaWallet/WalletConnect/WalletConnectServer.swift +++ b/AlphaWallet/WalletConnect/WalletConnectServer.swift @@ -17,6 +17,7 @@ enum WalletConnectError: Error { } protocol WalletConnectServerDelegate: class { + func server(_ server: WalletConnectServer, didConnect session: WalletConnectSession) func server(_ server: WalletConnectServer, shouldConnectFor connection: WalletConnectConnection, completion: @escaping (WalletConnectServer.ConnectionChoice) -> Void) func server(_ server: WalletConnectServer, action: WalletConnectServer.Action, request: WalletConnectRequest) func server(_ server: WalletConnectServer, didFail error: Error) @@ -289,6 +290,10 @@ extension WalletConnectServer: ServerDelegate { UserDefaults.standard.walletConnectSessions = sessions self.refresh(sessions: sessions) + + if let delegate = self.delegate { + delegate.server(self, didConnect: session) + } } }