diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 19cfcd91d..1aee0ca86 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 */; }; @@ -712,6 +713,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 = ""; }; @@ -2331,6 +2333,7 @@ 3AA2D5F8280EAF49000ED971 /* SocketService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, + 26843D692CDD29710010F047 /* NodeAvailabilityService.swift */, 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */, E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */, E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */, @@ -3336,6 +3339,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 5cd1c6d55..1b746402e 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(nibName: nil, bundle: nil) } @@ -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 b309fc4d0..1259dbd27 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -476,6 +476,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 612dccc74..558b97e74 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -99,6 +99,7 @@ final class ChatViewModel: NSObject { let presentDocumentViewerVC = ObservableSender<([FileResult], Int)>() let presentDropView = ObservableSender() let enableScroll = ObservableSender() + let presentNodeListVC = ObservableSender() @ObservableValue private(set) var swipeableMessage: ChatSwipeWrapperModel = .default @ObservableValue private(set) var isHeaderLoading = false @@ -291,10 +292,15 @@ final class ChatViewModel: NSObject { } Task { - if apiServiceCompose.get(.adm)?.hasEnabledNode == false { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription)) + guard apiServiceCompose.get(.adm)?.hasEnabledNode == true else { + dialog.send(.noActiveNodesAlert( + nodeName: NodeGroup.adm.name, + action: { [weak self] in + guard let self = self else { return } + self.presentNodeListVC.send(.adm) + } + )) + return } if !(filesPicked?.isEmpty ?? true) { @@ -703,12 +709,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 } @@ -1071,9 +1080,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 a60a84cec..efbd4e3d2 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(nibName: nil, bundle: nil) } @@ -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 a4b82550a..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,27 +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 } } - for group in walletCore.nodeGroups { - guard apiServiceCompose.get(group)?.hasEnabledNode == true else { - dialogService.showWarning( - withMessage: ApiServiceError.noEndpointsAvailable( - nodeGroupName: group.name - ).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 61932b587..9682b5b11 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 @@ -390,19 +394,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..9da00bf02 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -282,6 +282,35 @@ 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) + vibroService.applyVibration(.heavy) + } + func presentShareAlertFor( string: String, types: [ShareType], diff --git a/Adamant/Services/NodeAvailabilityService.swift b/Adamant/Services/NodeAvailabilityService.swift new file mode 100644 index 000000000..1802937e0 --- /dev/null +++ b/Adamant/Services/NodeAvailabilityService.swift @@ -0,0 +1,127 @@ +// +// 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 + } + + guard apiServiceCompose.get(nodeGroup)?.hasActiveNode == true + else { + dialogService.showError( + withMessage: noActiveNodesError(for: nodeGroup.name), + supportEmail: false, + error: nil + ) + 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) + } +} + +private func noActiveNodesError(for nodeGroupName: String) -> String { + .localizedStringWithFormat( + .localized( + "ApiService.InternalError.NoActiveNodesAvailable", + comment: "Serious internal error: No active nodes available" + ), + nodeGroupName + ).localized +} diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index 98ad666b7..61e991855 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -296,11 +296,17 @@ "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.NoEnabledNodesAvailable" = "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 active nodes available */ +"ApiService.InternalError.NoActiveNodesAvailable" = "Keine aktiven %@ Knoten. Überprüfen Sie die Knotenliste."; /* 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 0d17be684..719ac5915 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -293,11 +293,17 @@ "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.NoEnabledNodesAvailable" = "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 active nodes available */ +"ApiService.InternalError.NoActiveNodesAvailable" = "No active %@ nodes. Review 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 a50a3f859..97772ee19 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -293,11 +293,17 @@ "ApiService.InternalError.ParsingFailed" = "Не удалось разобрать ответ узла блокчена. Сообщите разработчикам"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "Нет доступных %@ нод. Просмотрите список узлов"; +"ApiService.InternalError.NoEnabledNodesAvailable" = "Не удается получить новые сообщения — нет активных узлов блокчейна %@. Поскольку вы отключили некоторые из них, посмотрите список узлов еще раз."; + +/* Serious internal error: No active nodes available */ +"ApiService.InternalError.NoActiveNodesAvailable" = "Нет доступных %@ нод. Просмотрите список узлов"; /* 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 105a23fa6..08ff9bb94 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings @@ -293,11 +293,17 @@ "ApiService.InternalError.PassingFailed" = "分析失败。报告错误"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "没有活动的%@节点。查看节点列表"; +"ApiService.InternalError.NoEnabledNodesAvailable" = "无法请求新消息 — 没有活跃的%@区块链节点。由于您已停用了一些节点,请考虑检查节点列表."; + +/* Serious internal error: No active nodes available */ +"ApiService.InternalError.NoActiveNodesAvailable" = "没有活动的%@节点。查看节点列表"; /* 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.") } diff --git a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift index d63aff4a1..2bfb2dd92 100644 --- a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift +++ b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift @@ -45,7 +45,7 @@ public enum ApiServiceError: LocalizedError, Sendable { case let .noEndpointsAvailable(nodeGroupName): return .localizedStringWithFormat( .localized( - "ApiService.InternalError.NoNodesAvailable", + "ApiService.InternalError.NoEnabledNodesAvailable", comment: "Serious internal error: No nodes available" ), nodeGroupName diff --git a/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift index 8b2b1338f..d4a8f46aa 100644 --- a/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift @@ -31,4 +31,9 @@ public extension ApiServiceProtocol { var hasEnabledNode: Bool { nodesInfo.nodes.contains { $0.isEnabled } } + + @MainActor + var hasActiveNode: Bool { + nodesInfo.nodes.contains { $0.connectionStatus == .allowed } + } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift index aa300c340..75aeefe07 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift @@ -31,8 +31,12 @@ struct NotificationPresenterView: View { isTextLimited: $isTextLimited, model: model ) - .padding([.leading, .trailing], 15) + .padding([.leading, .trailing], 10) .padding([.top, .bottom], 10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.init(uiColor:.adamant.chatInputBarBorderColor), lineWidth: 1) + ) .background(GeometryReader(content: processGeometry)) .expanded(axes: .horizontal) .offset(y: verticalDragTranslation < .zero ? verticalDragTranslation : .zero) diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift index 7b759090f..d30848285 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift @@ -13,12 +13,18 @@ struct NotificationView: View { let model: NotificationModel var body: some View { - HStack(alignment: .top, spacing: 8) { - if let icon = model.icon { - makeIcon(image: icon) + VStack(alignment: .center, spacing: 5) { + HStack(alignment: .top, spacing: 10) { + if let icon = model.icon { + makeIcon(image: icon) + } + textStack + Spacer(minLength: .zero) } - textStack - Spacer(minLength: .zero) + + Image(systemName: isTextLimited ? pullDownIcon : pullUpIcon) + .font(.title) + .foregroundColor(.gray) } } } @@ -49,3 +55,6 @@ private extension NotificationView { } } } + +private let pullDownIcon = "chevron.compact.down" +private let pullUpIcon = "chevron.compact.up"