diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index b62a66148..3b5d06115 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 2657A0CD2C707D800021E7E6 /* short-success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57E28C8B834009337F2 /* short-success.mp3 */; }; 2657A0CE2C707D830021E7E6 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; 265AA1622B74E6B900CF98B0 /* ChatPreservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */; }; + 26843D6A2CDD29760010F047 /* NodeAvailabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26843D692CDD29710010F047 /* NodeAvailabilityService.swift */; }; 269B83102C74A2FF002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; 269B83112C74A34F002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; 269B831E2C74B4EC002AA1D7 /* handoff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83122C74B4EA002AA1D7 /* handoff.mp3 */; }; @@ -706,6 +707,7 @@ 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewModel.swift; sourceTree = ""; }; 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsFactory.swift; sourceTree = ""; }; 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservation.swift; sourceTree = ""; }; + 26843D692CDD29710010F047 /* NodeAvailabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAvailabilityService.swift; sourceTree = ""; }; 269B830F2C74A2FF002AA1D7 /* note.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = note.mp3; sourceTree = ""; }; 269B83122C74B4EA002AA1D7 /* handoff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = handoff.mp3; sourceTree = ""; }; 269B83132C74B4EA002AA1D7 /* portal.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = portal.mp3; sourceTree = ""; }; @@ -2297,6 +2299,7 @@ 3AA2D5F8280EAF49000ED971 /* SocketService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, + 26843D692CDD29710010F047 /* NodeAvailabilityService.swift */, 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */, E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */, E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */, @@ -3302,6 +3305,7 @@ 3A26D93D2C3C1CC3003AD832 /* KlyNodeApiService.swift in Sources */, 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */, 93760BD72C656CF8002507C3 /* DefaultNodesProvider.swift in Sources */, + 26843D6A2CDD29760010F047 /* NodeAvailabilityService.swift in Sources */, 3A26D93B2C3C1C97003AD832 /* KlyApiCore.swift in Sources */, 2621AB372C60E74A00046D7A /* NotificationsView.swift in Sources */, 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */, diff --git a/Adamant/Modules/Account/AccountFactory.swift b/Adamant/Modules/Account/AccountFactory.swift index 2c8638580..667f61564 100644 --- a/Adamant/Modules/Account/AccountFactory.swift +++ b/Adamant/Modules/Account/AccountFactory.swift @@ -26,7 +26,12 @@ struct AccountFactory { currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, languageService: assembler.resolve(LanguageStorageProtocol.self)!, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) + ) } } diff --git a/Adamant/Modules/Account/AccountViewController.swift b/Adamant/Modules/Account/AccountViewController.swift index cd9b06907..19e471650 100644 --- a/Adamant/Modules/Account/AccountViewController.swift +++ b/Adamant/Modules/Account/AccountViewController.swift @@ -155,6 +155,7 @@ final class AccountViewController: FormViewController { private let languageService: LanguageStorageProtocol private let walletServiceCompose: WalletServiceCompose private let apiServiceCompose: ApiServiceComposeProtocol + private let nodeAvailabilityService: NodeAvailabilityProtocol let accountService: AccountService let dialogService: DialogService @@ -219,7 +220,8 @@ final class AccountViewController: FormViewController { currencyInfoService: InfoServiceProtocol, languageService: LanguageStorageProtocol, walletServiceCompose: WalletServiceCompose, - apiServiceCompose: ApiServiceComposeProtocol + apiServiceCompose: ApiServiceComposeProtocol, + nodeAvailabilityService: NodeAvailabilityProtocol ) { self.visibleWalletsService = visibleWalletsService self.accountService = accountService @@ -233,6 +235,7 @@ final class AccountViewController: FormViewController { self.languageService = languageService self.walletServiceCompose = walletServiceCompose self.apiServiceCompose = apiServiceCompose + self.nodeAvailabilityService = nodeAvailabilityService super.init(style: .insetGrouped) } @@ -1111,19 +1114,12 @@ final class AccountViewController: FormViewController { } @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { - let disabledGroup = NodeGroup.allCases.first { - apiServiceCompose.get($0)?.hasEnabledNode != true - } - - if let disabledGroup { - dialogService.showWarning( - withMessage: ApiServiceError.noEndpointsAvailable( - nodeGroupName: disabledGroup.name - ).localizedDescription - ) - } - - refreshControl.endRefreshing() + defer { refreshControl.endRefreshing() } + guard nodeAvailabilityService.checkNodeAvailability( + in: .adm, + vc: self + ) else { return } + DispatchQueue.background.async { [accountService] in accountService.reloadWallets() } diff --git a/Adamant/Modules/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift index 35d8399a7..294bbdaec 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -477,6 +477,20 @@ private extension ChatViewController { self?.didTapSelectText(text: text) } .store(in: &subscriptions) + + viewModel.presentNodeListVC + .sink { [weak self] node in + guard let self = self else { return } + + let vc = node == .adm + ? screensFactory.makeNodesList() + : screensFactory.makeCoinsNodesList(context: .menu) + + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .pageSheet + self.present(nav, animated: true, completion: nil) + } + .store(in: &subscriptions) } } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index 2d3fcc368..13afe3310 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -25,7 +25,8 @@ final class ChatDialogManager { typealias DidSelectEmojiAction = ((_ emoji: String, _ messageId: String) -> Void)? typealias ContextMenuAction = ((_ messageId: String) -> Void)? - + typealias NoActiveNodesAction = (() -> Void) + init( viewModel: ChatViewModel, dialogService: DialogService, @@ -108,6 +109,8 @@ private extension ChatDialogManager { showRenameAlert() case .actionMenu: showActionMenu() + case .noActiveNodesAlert(let name, let action): + dialogService.showNoActiveNodesAlert(nodeName: name, completion: action) } } diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index 3d0ac0587..22dc7f7a9 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -97,6 +97,7 @@ final class ChatViewModel: NSObject { let presentDocumentPickerVC = ObservableSender() let presentDocumentViewerVC = ObservableSender<([FileResult], Int)>() let presentDropView = ObservableSender() + let presentNodeListVC = ObservableSender() @ObservableValue private(set) var isHeaderLoading = false @ObservableValue private(set) var fullscreenLoading = false @@ -295,9 +296,13 @@ final class ChatViewModel: NSObject { Task { guard apiServiceCompose.get(.adm)?.hasEnabledNode == true else { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription)) + dialog.send(.noActiveNodesAlert( + nodeName: NodeGroup.adm.name, + action: { [weak self] in + guard let self = self else { return } + self.presentNodeListVC.send(.adm) + } + )) return } @@ -707,12 +712,15 @@ final class ChatViewModel: NSObject { } guard apiServiceCompose.get(.adm)?.hasEnabledNode == true else { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription)) + dialog.send(.noActiveNodesAlert( + nodeName: NodeGroup.adm.name, + action: { [weak self] in + guard let self = self else { return } + self.presentNodeListVC.send(.adm) + } + )) return false } - return true } @@ -1061,9 +1069,15 @@ extension ChatViewModel: NSFetchedResultsControllerDelegate { private extension ChatViewModel { func sendFiles(with text: String) async throws { guard apiServiceCompose.get(.ipfs)?.hasEnabledNode == true else { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.ipfs.name - ).localizedDescription)) + dialog.send( + .noActiveNodesAlert( + nodeName: NodeGroup.adm.name, + action: { [weak self] in + guard let self = self else { return } + self.presentNodeListVC.send(.ipfs) + } + ) + ) return } diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift index f572003a8..1c810733d 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift @@ -36,4 +36,5 @@ enum ChatDialog { case dismissMenu case renameAlert case actionMenu + case noActiveNodesAlert(nodeName: String, action: ChatDialogManager.NoActiveNodesAction) } diff --git a/Adamant/Modules/ChatsList/ChatListFactory.swift b/Adamant/Modules/ChatsList/ChatListFactory.swift index c830143a5..a3750d3d2 100644 --- a/Adamant/Modules/ChatsList/ChatListFactory.swift +++ b/Adamant/Modules/ChatsList/ChatListFactory.swift @@ -24,7 +24,11 @@ struct ChatListFactory { dialogService: assembler.resolve(DialogService.self)!, addressBook: assembler.resolve(AddressBookService.self)!, avatarService: assembler.resolve(AvatarService.self)!, - walletServiceCompose: assembler.resolve(WalletServiceCompose.self)! + walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } @@ -42,8 +46,14 @@ struct ChatListFactory { visibleWalletsService: assembler.resolve(VisibleWalletsService.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, screensFactory: screensFactory, - walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, + nodesStorage: assembler.resolve(NodesStorageProtocol.self)!, + dialogService: assembler.resolve(DialogService.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index c18e87dcf..9cad44af6 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -57,6 +57,7 @@ final class ChatListViewController: KeyboardObservingViewController { private let addressBook: AddressBookService private let avatarService: AvatarService private let walletServiceCompose: WalletServiceCompose + private let nodeAvailabilityService: NodeAvailabilityProtocol // MARK: IBOutlet @IBOutlet weak var tableView: UITableView! @@ -149,7 +150,8 @@ final class ChatListViewController: KeyboardObservingViewController { dialogService: DialogService, addressBook: AddressBookService, avatarService: AvatarService, - walletServiceCompose: WalletServiceCompose + walletServiceCompose: WalletServiceCompose, + nodeAvailabilityService: NodeAvailabilityProtocol ) { self.accountService = accountService self.chatsProvider = chatsProvider @@ -160,6 +162,7 @@ final class ChatListViewController: KeyboardObservingViewController { self.addressBook = addressBook self.avatarService = avatarService self.walletServiceCompose = walletServiceCompose + self.nodeAvailabilityService = nodeAvailabilityService super.init(nibName: "ChatListViewController", bundle: nil) } @@ -467,9 +470,9 @@ final class ChatListViewController: KeyboardObservingViewController { @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { Task { let result = await chatsProvider.update(notifyState: true) + defer { refreshControl.endRefreshing() } guard let result = result else { - refreshControl.endRefreshing() return } @@ -477,11 +480,12 @@ final class ChatListViewController: KeyboardObservingViewController { case .success: tableView.reloadData() - case .failure(let error): - dialogService.showRichError(error: error) + case .failure: + guard nodeAvailabilityService.checkNodeAvailability( + in: .adm, + vc: self + ) else { return } } - - refreshControl.endRefreshing() } } diff --git a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift index c65750ca8..e1dfaae99 100644 --- a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift @@ -24,6 +24,8 @@ final class ComplexTransferViewController: UIViewController { private let screensFactory: ScreensFactory private let walletServiceCompose: WalletServiceCompose private let nodesStorage: NodesStorageProtocol + private let dialogService: DialogService + private let nodeAvailabilityService: NodeAvailabilityProtocol // MARK: - Properties var pagingViewController: PagingViewController! @@ -44,13 +46,17 @@ final class ComplexTransferViewController: UIViewController { addressBookService: AddressBookService, screensFactory: ScreensFactory, walletServiceCompose: WalletServiceCompose, - nodesStorage: NodesStorageProtocol + nodesStorage: NodesStorageProtocol, + dialogService: DialogService, + nodeAvailabilityService: NodeAvailabilityProtocol ) { self.visibleWalletsService = visibleWalletsService self.addressBookService = addressBookService self.screensFactory = screensFactory self.walletServiceCompose = walletServiceCompose self.nodesStorage = nodesStorage + self.dialogService = dialogService + self.nodeAvailabilityService = nodeAvailabilityService super.init(nibName: nil, bundle: nil) } @@ -144,15 +150,10 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { vc.showProgressView(animated: false) Task { - guard service.core.hasEnabledNode else { - vc.showAlertView( - message: ApiServiceError.noEndpointsAvailable( - nodeGroupName: service.core.tokenName - ).errorDescription ?? .adamant.sharedErrors.unknownError, - animated: true - ) - return - } + guard nodeAvailabilityService.checkNodeAvailability( + in: .adm, + vc: self + ) else { return } guard admService?.core.hasEnabledNode ?? false else { vc.showAlertView( diff --git a/Adamant/Modules/Login/LoginFactory.swift b/Adamant/Modules/Login/LoginFactory.swift index f949a7edb..8bd0f46f1 100644 --- a/Adamant/Modules/Login/LoginFactory.swift +++ b/Adamant/Modules/Login/LoginFactory.swift @@ -21,7 +21,12 @@ struct LoginFactory { dialogService: assembler.resolve(DialogService.self)!, localAuth: assembler.resolve(LocalAuthentication.self)!, screensFactory: screenFactory, - apiService: assembler.resolve(AdamantApiServiceProtocol.self)! + apiService: assembler.resolve(AdamantApiServiceProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screenFactory + ) ) } } diff --git a/Adamant/Modules/Login/LoginViewController+Pinpad.swift b/Adamant/Modules/Login/LoginViewController+Pinpad.swift index 7a634aad1..c98ce9d99 100644 --- a/Adamant/Modules/Login/LoginViewController+Pinpad.swift +++ b/Adamant/Modules/Login/LoginViewController+Pinpad.swift @@ -67,16 +67,8 @@ extension LoginViewController { dialogService.showProgress(withMessage: String.adamant.login.loggingInProgressMessage, userInteractionEnable: false) Task { - do { - let result = try await accountService.loginWithStoredAccount() - handleSavedAccountLoginResult(result) - } catch { - dialogService.showRichError(error: error) - - if let pinpad = presentedViewController as? PinpadViewController { - pinpad.clearPin() - } - } + let result = await accountService.loginWithStoredAccount() + handleSavedAccountLoginResult(result) } } @@ -104,13 +96,24 @@ extension LoginViewController { } case .failure(let error): - dialogService.showRichError(error: error) + handleError(error) if let pinpad = presentedViewController as? PinpadViewController { pinpad.clearPin() } } } + + func handleError(_ error: AccountServiceError) { + guard case .apiError(let error) = error else { + dialogService.showRichError(error: error) + return + } + + dismiss(animated: true) { [weak self] in + self?.handleError(error) + } + } } // MARK: - PinpadViewControllerDelegate diff --git a/Adamant/Modules/Login/LoginViewController.swift b/Adamant/Modules/Login/LoginViewController.swift index 373f76e2b..38a122e58 100644 --- a/Adamant/Modules/Login/LoginViewController.swift +++ b/Adamant/Modules/Login/LoginViewController.swift @@ -141,6 +141,7 @@ final class LoginViewController: FormViewController { let screensFactory: ScreensFactory let apiService: AdamantApiServiceProtocol let dialogService: DialogService + let nodeAvailabilityService: NodeAvailabilityProtocol // MARK: Properties private var hideNewPassphrase: Bool = true @@ -160,7 +161,8 @@ final class LoginViewController: FormViewController { dialogService: DialogService, localAuth: LocalAuthentication, screensFactory: ScreensFactory, - apiService: AdamantApiServiceProtocol + apiService: AdamantApiServiceProtocol, + nodeAvailabilityService: NodeAvailabilityProtocol ) { self.accountService = accountService self.adamantCore = adamantCore @@ -168,6 +170,7 @@ final class LoginViewController: FormViewController { self.localAuth = localAuth self.screensFactory = screensFactory self.apiService = apiService + self.nodeAvailabilityService = nodeAvailabilityService super.init(style: .insetGrouped) } @@ -446,11 +449,23 @@ extension LoginViewController { loginIntoExistingAccount(passphrase: passphrase) case .failure(let error): - dialogService.showRichError(error: error) + handleError(error) } } } + func handleError(_ error: ApiServiceError) { + guard case .noEndpointsAvailable = error else { + dialogService.showRichError(error: error) + return + } + + guard nodeAvailabilityService.checkNodeAvailability( + in: .adm, + vc: self + ) else { return } + } + func generateNewPassphrase() { let passphrase = (try? Mnemonic.generate().joined(separator: " ")) ?? .empty diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift index 71c147234..8c862892f 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift @@ -53,7 +53,12 @@ struct AdmWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift index a575d21f9..3b1cdb33f 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift @@ -49,7 +49,12 @@ struct BtcWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift index 254644865..f8b8cf973 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift @@ -48,7 +48,12 @@ struct DashWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift index 0ece9a4e8..5649f9a67 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift @@ -48,7 +48,12 @@ struct DogeWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift index f77913123..af0dc3863 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift @@ -48,7 +48,12 @@ struct ERC20WalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift index 51b00bc72..2d70c16b5 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift @@ -48,7 +48,12 @@ struct EthWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift index f09d2701a..c7b23c72f 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift @@ -49,7 +49,12 @@ struct KlyWalletFactory: WalletFactory { vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + nodeAvailabilityService: NodeAvailabilityService( + dialogService: assembler.resolve(DialogService.self)!, + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + screensFactory: screensFactory + ) ) } diff --git a/Adamant/Modules/Wallets/TransferViewControllerBase.swift b/Adamant/Modules/Wallets/TransferViewControllerBase.swift index f300c08cc..790144830 100644 --- a/Adamant/Modules/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase.swift @@ -188,6 +188,7 @@ class TransferViewControllerBase: FormViewController { let walletCore: WalletCoreProtocol let reachabilityMonitor: ReachabilityMonitor let apiServiceCompose: ApiServiceComposeProtocol + let nodeAvailabilityService: NodeAvailabilityProtocol // MARK: - Properties @@ -319,7 +320,8 @@ class TransferViewControllerBase: FormViewController { vibroService: VibroService, walletService: WalletService, reachabilityMonitor: ReachabilityMonitor, - apiServiceCompose: ApiServiceComposeProtocol + apiServiceCompose: ApiServiceComposeProtocol, + nodeAvailabilityService: NodeAvailabilityProtocol ) { self.accountService = accountService self.accountsProvider = accountsProvider @@ -333,6 +335,7 @@ class TransferViewControllerBase: FormViewController { self.walletCore = walletService.core self.reachabilityMonitor = reachabilityMonitor self.apiServiceCompose = apiServiceCompose + self.nodeAvailabilityService = nodeAvailabilityService super.init(style: .insetGrouped) } @@ -796,25 +799,17 @@ class TransferViewControllerBase: FormViewController { return } - guard - apiServiceCompose.get(.adm)?.hasEnabledNode == true || admReportRecipient == nil - else { - dialogService.showWarning( - withMessage: ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription - ) - return + if admReportRecipient != nil || walletCore.nodeGroups == [.adm] { + guard nodeAvailabilityService.checkNodeAvailability( + in: .adm, + vc: self + )else { return } } - guard walletCore.hasEnabledNode else { - dialogService.showWarning( - withMessage: ApiServiceError.noEndpointsAvailable( - nodeGroupName: walletCore.tokenName - ).localizedDescription - ) - return - } + guard nodeAvailabilityService.checkNodeAvailability( + in: walletCore, + vc: self + ) else { return } let recipient: String if let recipientName = recipientName { diff --git a/Adamant/ServiceProtocols/AccountService.swift b/Adamant/ServiceProtocols/AccountService.swift index edf5c780c..c29f3ebf0 100644 --- a/Adamant/ServiceProtocols/AccountService.swift +++ b/Adamant/ServiceProtocols/AccountService.swift @@ -162,10 +162,10 @@ protocol AccountService: AnyObject, Sendable { func update(_ completion: (@Sendable (AccountServiceResult) -> Void)?) /// Login into Adamant using passphrase. - func loginWith(passphrase: String) async throws -> AccountServiceResult + func loginWith(passphrase: String) async -> AccountServiceResult /// Login into Adamant using previously logged account - func loginWithStoredAccount() async throws -> AccountServiceResult + func loginWithStoredAccount() async -> AccountServiceResult /// Logout func logout() diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index 85676546b..5a2a4496b 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -232,4 +232,8 @@ protocol DialogService: AnyObject { func showAlert(title: String?, message: String?, style: AdamantAlertStyle, actions: [AdamantAlertAction]?, from: UIAlertController.SourceView?) func selectAllTextFields(in alert: UIAlertController) + func showNoActiveNodesAlert( + nodeName: String, + completion: @escaping () -> Void + ) } diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 73e83523e..aa4720f53 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -283,16 +283,16 @@ extension AdamantAccountService { extension AdamantAccountService { // MARK: Passphrase @MainActor - func loginWith(passphrase: String) async throws -> AccountServiceResult { + func loginWith(passphrase: String) async -> AccountServiceResult { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { - throw AccountServiceError.invalidPassphrase + return .failure(.invalidPassphrase) } guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { - throw AccountServiceError.internalError(message: "Failed to generate keypair for passphrase", error: nil) + return .failure(.internalError(message: "Failed to generate keypair for passphrase", error: nil)) } - let account = try await loginWith(keypair: keypair) + let account = await loginWith(keypair: keypair) // MARK: Drop saved accs if let storedPassphrase = self.getSavedPassphrase(), @@ -310,32 +310,36 @@ extension AdamantAccountService { _ = await initWallets() - return .success(account: account, alert: nil) + return account } // MARK: Pincode - func loginWith(pincode: String) async throws -> AccountServiceResult { + func loginWith(pincode: String) async -> AccountServiceResult { guard let storePin = securedStore.get(.pin) else { - throw AccountServiceError.invalidPassphrase + return .failure(.invalidPassphrase) } guard storePin == pincode else { - throw AccountServiceError.invalidPassphrase + return .failure(.invalidPassphrase) } - return try await loginWithStoredAccount() + return await loginWithStoredAccount() } // MARK: Biometry @MainActor - func loginWithStoredAccount() async throws -> AccountServiceResult { + func loginWithStoredAccount() async -> AccountServiceResult { if let passphrase = getSavedPassphrase() { - let account = try await loginWith(passphrase: passphrase) + let account = await loginWith(passphrase: passphrase) return account } if let keypair = getSavedKeypair() { - let account = try await loginWith(keypair: keypair) + let account = await loginWith(keypair: keypair) + + guard case .success(let account, _) = account else { + return account + } let alert: (title: String, message: String)? if securedStore.get(.showedV12) != nil { @@ -353,14 +357,14 @@ extension AdamantAccountService { return .success(account: account, alert: alert) } - throw AccountServiceError.invalidPassphrase + return .failure(.invalidPassphrase) } // MARK: Keypair - private func loginWith(keypair: Keypair) async throws -> AdamantAccount { + private func loginWith(keypair: Keypair) async -> AccountServiceResult { switch state { case .isLoggingIn: - throw AccountServiceError.internalError(message: "Service is busy", error: nil) + return .failure(.internalError(message: "Service is busy", error: nil)) case .updating: fallthrough @@ -389,19 +393,19 @@ extension AdamantAccountService { ) self.state = .loggedIn - return account + return .success(account: account, alert: nil) } catch let error as ApiServiceError { self.state = .notLogged switch error { case .accountNotFound: - throw AccountServiceError.wrongPassphrase + return .failure(AccountServiceError.wrongPassphrase) default: - throw AccountServiceError.apiError(error: error) + return .failure(AccountServiceError.apiError(error: error)) } } catch { - throw AccountServiceError.internalError(message: error.localizedDescription, error: error) + return .failure(.internalError(message: error.localizedDescription, error: error)) } } diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdamantDialogService.swift index 476a849be..c853837e6 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -282,6 +282,34 @@ extension AdamantDialogService { present(alert, animated: animated, completion: completion) } + func showNoActiveNodesAlert( + nodeName: String, + completion: @escaping () -> Void + ) { + dismissProgress() + let alert = UIAlertController( + title: "", + message: ApiServiceError.noEndpointsAvailable( + nodeGroupName: nodeName).localizedDescription, + preferredStyle: .alert + ) + + let action = UIAlertAction( + title: .adamant.sharedErrors.reviewNodeListButtonTitle(nodeName), + style: .default, + handler: { _ in completion() } + ) + + alert.addAction(action) + alert.addAction(UIAlertAction( + title: String.adamant.alert.cancel, + style: .cancel, + handler: nil) + ) + + self.present(alert, animated: true, completion: nil) + } + func presentShareAlertFor( string: String, types: [ShareType], diff --git a/Adamant/Services/NodeAvailabilityService.swift b/Adamant/Services/NodeAvailabilityService.swift new file mode 100644 index 000000000..ce25e7745 --- /dev/null +++ b/Adamant/Services/NodeAvailabilityService.swift @@ -0,0 +1,107 @@ +// +// NodeAvailabilityService.swift +// Adamant +// +// Created by Yana Silosieva on 07.11.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import UIKit +import CommonKit + +@MainActor +protocol NodeAvailabilityProtocol { + func checkNodeAvailability( + in walletCore: WalletCoreProtocol, + vc: UIViewController + ) -> Bool + + func checkNodeAvailability( + in nodeGroup: NodeGroup, + vc: UIViewController + ) -> Bool +} + +@MainActor +final class NodeAvailabilityService: NodeAvailabilityProtocol { + + // MARK: Dependencies + + private let dialogService: DialogService + private let apiServiceCompose: ApiServiceComposeProtocol + private let screensFactory: ScreensFactory + + init( + dialogService: DialogService, + apiServiceCompose: ApiServiceComposeProtocol, + screensFactory: ScreensFactory + ) { + self.dialogService = dialogService + self.apiServiceCompose = apiServiceCompose + self.screensFactory = screensFactory + } + + func checkNodeAvailability( + in nodeGroup: NodeGroup, + vc: UIViewController + ) -> Bool { + guard apiServiceCompose.get(nodeGroup)?.hasEnabledNode == true + else { + dialogService.showNoActiveNodesAlert( + nodeName: NodeGroup.adm.name + ) { [weak self] in + guard let self = self else { return } + + self.presentNodeListVC( + screensFactory: self.screensFactory, + node: nodeGroup, + rootVC: vc + ) + } + + return false + } + + return true + } + + func checkNodeAvailability( + in walletCore: WalletCoreProtocol, + vc: UIViewController + ) -> Bool { + guard walletCore.hasEnabledNode else { + let network = type(of: walletCore).tokenNetworkSymbol + dialogService.showNoActiveNodesAlert( + nodeName: network + ) { [weak self] in + guard let self = self, + let nodeGroup = walletCore.nodeGroups.first else { return } + + self.presentNodeListVC( + screensFactory: self.screensFactory, + node: nodeGroup, + rootVC: vc + ) + } + return false + } + + return true + } +} + +private extension NodeAvailabilityService { + func presentNodeListVC( + screensFactory: ScreensFactory, + node: NodeGroup, + rootVC: UIViewController + ) { + let vc = node == .adm + ? screensFactory.makeNodesList() + : screensFactory.makeCoinsNodesList(context: .menu) + + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .pageSheet + rootVC.present(nav, animated: true, completion: nil) + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index db1bd67eb..b96f6b259 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -296,11 +296,14 @@ "ApiService.InternalError.ParsingFailed" = "Parsing fehlgeschlagen. Bericht senden"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "Keine aktiven %@ Knoten. Überprüfen Sie die Knotenliste."; +"ApiService.InternalError.NoNodesAvailable" = "Neue Nachrichten können nicht angefordert werden — keine aktiven %@ Blockchain-Knoten. Da Sie einige davon deaktiviert haben, sollten Sie die Knotenliste überprüfen."; /* Serious internal error: No ADM nodes available */ "ApiService.InternalError.NoAdmNodesAvailable" = "Keine aktiven ADM Knoten zum Abrufen der %@ Adresse des Partners."; +/* Button title for alert when all ADM nodes are inactive */ +"AlertButton.ReviewNodeList" = "Überprüfe %@-Knotenliste"; + /* Eureka forms Cancel button */ "Cancel" = "Abbrechen"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings index d6a39a650..205b45af1 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -293,11 +293,14 @@ "ApiService.InternalError.ParsingFailed" = "Parsing failed. Report a bug"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "No active %@ nodes. Review the node list"; +"ApiService.InternalError.NoNodesAvailable" = "Unable to request new messages — No active %@ blockchain nodes. As you’ve deactivated some of them, consider reviewing the node list."; /* Serious internal error: No ADM nodes available */ "ApiService.InternalError.NoAdmNodesAvailable" = "No active ADM nodes to fetch the partner's %@ address"; +/* Button title for alert when all ADM nodes are inactive */ +"AlertButton.ReviewNodeList" = "Review %@ node list"; + /* Eureka forms Cancel button */ "Cancel" = "Cancel"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings index d7941cc36..2f60cdc1b 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -293,11 +293,14 @@ "ApiService.InternalError.ParsingFailed" = "Не удалось разобрать ответ узла блокчена. Сообщите разработчикам"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "Нет доступных %@ нод. Просмотрите список узлов"; +"ApiService.InternalError.NoNodesAvailable" = "Не удается получить новые сообщения — нет активных узлов блокчейна %@. Поскольку вы отключили некоторые из них, посмотрите список узлов еще раз."; /* Serious internal error: No ADM nodes available */ "ApiService.InternalError.NoAdmNodesAvailable" = "Нет доступных ADM нод для получения адреса партнера %@"; +/* Button title for alert when all ADM nodes are inactive */ +"AlertButton.ReviewNodeList" = "К списку узлов %@"; + /* Eureka forms Cancel button */ "Cancel" = "Отмена"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings index 1c993af94..214df6b39 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings @@ -293,11 +293,14 @@ "ApiService.InternalError.PassingFailed" = "分析失败。报告错误"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "没有活动的%@节点。查看节点列表"; +"ApiService.InternalError.NoNodesAvailable" = "无法请求新消息 — 没有活跃的%@区块链节点。由于您已停用了一些节点,请考虑检查节点列表."; /* Serious internal error: No ADM nodes available */ "ApiService.InternalError.NoAdmNodesAvailable" = "没有活动的 ADM 节点来获取合作伙伴的 %@ 地址"; +/* Button title for alert when all ADM nodes are inactive */ +"AlertButton.ReviewNodeList" = "审核%@节点列表"; + /* Eureka forms Cancel button */ "Cancel" = "取消"; diff --git a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift index bd4768254..fbb01e1dd 100644 --- a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift +++ b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift @@ -88,6 +88,10 @@ public extension String.adamant { String.localizedStringWithFormat(.localized("ApiService.InternalError.NoAdmNodesAvailable", comment: "No active ADM nodes to fetch the partner's %@ address"), coin) } + public static func reviewNodeListButtonTitle(_ coin: String) -> String { + String.localizedStringWithFormat(.localized("AlertButton.ReviewNodeList", comment: "Button title for alert when all ADM nodes are inactive"), coin) + } + public static var notEnoughMoney: String { String.localized("WalletServices.SharedErrors.notEnoughMoney", comment: "Wallet Services: Shared error, user do not have enought money.") }