From 2ee4cb82542c25e4b4fffc7a2946e7a6fef0ea56 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 19 Jul 2023 14:32:36 +0600 Subject: [PATCH] Improve error handling and showing for Coinzix login and verify codes --- .../CoinzixVerifyViewController.swift | 33 +++++++++++---- .../CoinzixVerifyViewModel.swift | 4 +- .../RestoreCoinzixService.swift | 36 ++++++---------- .../RestoreCoinzixViewModel.swift | 2 +- .../Modules/Wallet/CoinzixCexProvider.swift | 41 +++++++++++++++---- .../UserInterface/Cells/PasteInputCell.swift | 5 +++ .../Cells/ResendPastInputCell.swift | 8 +++- .../Components/PasteInputView.swift | 5 +++ .../Components/ResendPasteInputView.swift | 5 +++ .../en.lproj/Localizable.strings | 19 ++++----- 10 files changed, 99 insertions(+), 59 deletions(-) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewController.swift index 3e279bb092..666395edc6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewController.swift @@ -39,7 +39,7 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { override func viewDidLoad() { super.viewDidLoad() - title = "coinzix_verify_withdraw.title".localized + title = "coinzix_verify.title".localized navigationItem.largeTitleDisplayMode = .never navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.cancel".localized, style: .plain, target: self, action: #selector(onTapCancel)) @@ -53,7 +53,8 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { tableView.backgroundColor = .clear tableView.separatorStyle = .none - emailPinInputCell.inputPlaceholder = "coinzix_verify_withdraw.email_pin".localized + emailPinInputCell.inputPlaceholder = "coinzix_verify.email_pin".localized + emailPinInputCell.keyboardType = .numberPad emailPinInputCell.onChangeHeight = { [weak self] in self?.reloadHeights() } emailPinInputCell.onChangeText = { [weak self] in self?.viewModel.onChange(emailPin: $0 ?? "") } emailPinInputCell.onFetchText = { [weak self] in @@ -62,7 +63,8 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { } emailPinInputCell.onResend = { [weak self] in self?.viewModel.onTapResend() } - googlePinInputCell.inputPlaceholder = "coinzix_verify_withdraw.google_pin".localized + googlePinInputCell.inputPlaceholder = "coinzix_verify.google_pin".localized + googlePinInputCell.keyboardType = .numberPad googlePinInputCell.onChangeHeight = { [weak self] in self?.reloadHeights() } googlePinInputCell.onChangeText = { [weak self] in self?.viewModel.onChange(googlePin: $0 ?? "") } googlePinInputCell.onFetchText = { [weak self] in @@ -87,13 +89,13 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { stackView.addArrangedSubview(submitButton) submitButton.set(style: .yellow) - submitButton.setTitle("coinzix_verify_withdraw.submit".localized, for: .normal) + submitButton.setTitle("coinzix_verify.submit".localized, for: .normal) submitButton.addTarget(self, action: #selector(onTapSubmit), for: .touchUpInside) stackView.addArrangedSubview(submittingButton) submittingButton.set(style: .gray, accessoryType: .spinner) submittingButton.isEnabled = false - submittingButton.setTitle("coinzix_verify_withdraw.submit".localized, for: .normal) + submittingButton.setTitle("coinzix_verify.submit".localized, for: .normal) viewModel.$submitButtonState .receive(on: DispatchQueue.main) @@ -112,7 +114,7 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { viewModel.errorPublisher .receive(on: DispatchQueue.main) - .sink { text in HudHelper.instance.showErrorBanner(title: text) } + .sink { [weak self] in self?.show(error: $0) } .store(in: &cancellables) tableView.buildSections() @@ -167,6 +169,21 @@ class CoinzixVerifyViewController: KeyboardAwareViewController { viewModel.onTapSubmit() } + private func show(error: String) { + let viewController = BottomSheetModule.viewController( + image: .local(image: UIImage(named: "warning_2_24")?.withTintColor(.themeLucian)), + title: "coinzix_verify.failed".localized, + items: [ + .highlightedDescription(text: error, style: .red) + ], + buttons: [ + .init(style: .yellow, title: "button.ok".localized) + ] + ) + + present(viewController, animated: true) + } + } extension CoinzixVerifyViewController: SectionsDataSource { @@ -196,7 +213,7 @@ extension CoinzixVerifyViewController: SectionsDataSource { rows: [ tableView.descriptionRow( id: "email-pin-description", - text: "coinzix_verify_withdraw.email_pin.description".localized, + text: "coinzix_verify.email_pin.description".localized, font: .subhead2, textColor: .themeGray, ignoreBottomMargin: true @@ -226,7 +243,7 @@ extension CoinzixVerifyViewController: SectionsDataSource { rows: [ tableView.descriptionRow( id: "google-pin-description", - text: "coinzix_verify_withdraw.google_pin.description".localized, + text: "coinzix_verify.google_pin.description".localized, font: .subhead2, textColor: .themeGray, ignoreBottomMargin: true diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewModel.swift index 403ebca4a1..70360e3bce 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/CoinzixVerify/CoinzixVerifyViewModel.swift @@ -47,9 +47,7 @@ extension CoinzixVerifyViewModel { } var errorPublisher: AnyPublisher { - service.errorPublisher - .map { _ in "coinzix_verify_withdraw.failed".localized } - .eraseToAnyPublisher() + service.errorPublisher.map { $0.smartDescription }.eraseToAnyPublisher() } var twoFactorTypes: [CoinzixCexProvider.TwoFactorType] { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixService.swift index 47c84a06b2..da6e0a64a7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixService.swift @@ -22,7 +22,7 @@ class RestoreCoinzixService { @PostPublished private(set) var state: State = .notReady private let verifySubject = PassthroughSubject<(CoinzixVerifyModule.Mode, [CoinzixCexProvider.TwoFactorType]), Never>() - private let errorSubject = PassthroughSubject() + private let errorSubject = PassthroughSubject() init(networkManager: NetworkManager) { self.networkManager = networkManager @@ -32,27 +32,15 @@ class RestoreCoinzixService { state = username.trimmingCharacters(in: .whitespaces).isEmpty || password.trimmingCharacters(in: .whitespaces).isEmpty ? .notReady : .ready } - private func handle(loginResult: CoinzixCexProvider.LoginResult) { - switch loginResult { - case .success(let token, let secret, let twoFactorType): - let type: CoinzixCexProvider.TwoFactorType + private func handle(loginData: CoinzixCexProvider.LoginData) { + let type: CoinzixCexProvider.TwoFactorType - switch twoFactorType { - case .email: type = .email - case .authenticator: type = .authenticator - } - - verifySubject.send((.login(token: token, secret: secret), [type])) - case .failed(let reason): - switch reason { - case .invalidCredentials(let attemptsLeft): - errorSubject.send("Invalid login credentials. Attempts left: \(attemptsLeft).") - case .tooManyAttempts(let unlockDate): - errorSubject.send("Too many invalid login attempts were made. Login is locked until \(DateHelper.instance.formatFullTime(from: unlockDate)).") - case .unknown(let message): - errorSubject.send(message) - } + switch loginData.twoFactorType { + case .email: type = .email + case .authenticator: type = .authenticator } + + verifySubject.send((.login(token: loginData.token, secret: loginData.secret), [type])) } } @@ -63,7 +51,7 @@ extension RestoreCoinzixService { verifySubject.eraseToAnyPublisher() } - var errorPublisher: AnyPublisher { + var errorPublisher: AnyPublisher { errorSubject.eraseToAnyPublisher() } @@ -72,10 +60,10 @@ extension RestoreCoinzixService { Task { [weak self, username, password, networkManager] in do { - let loginResult = try await CoinzixCexProvider.login(username: username, password: password, networkManager: networkManager) - self?.handle(loginResult: loginResult) + let loginData = try await CoinzixCexProvider.login(username: username, password: password, networkManager: networkManager) + self?.handle(loginData: loginData) } catch { - self?.errorSubject.send(error.smartDescription) + self?.errorSubject.send(error) } self?.state = .ready diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixViewModel.swift index 05ea04800b..7658456b69 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/RestoreAccount/RestoreCoinzix/RestoreCoinzixViewModel.swift @@ -39,7 +39,7 @@ class RestoreCoinzixViewModel { extension RestoreCoinzixViewModel { var errorPublisher: AnyPublisher { - service.errorPublisher + service.errorPublisher.map { $0.smartDescription }.eraseToAnyPublisher() } var verifyPublisher: AnyPublisher<(CoinzixVerifyModule.Mode, [CoinzixCexProvider.TwoFactorType]), Never> { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/CoinzixCexProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/CoinzixCexProvider.swift index fc41ed4c9c..4f6de1725d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/CoinzixCexProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/CoinzixCexProvider.swift @@ -323,7 +323,7 @@ extension CoinzixCexProvider { extension CoinzixCexProvider { - static func login(username: String, password: String, networkManager: NetworkManager) async throws -> LoginResult { + static func login(username: String, password: String, networkManager: NetworkManager) async throws -> LoginData { let parameters: Parameters = [ "username": username, "password": password, @@ -338,19 +338,19 @@ extension CoinzixCexProvider { if response.status { if let token = response.token, let secret = response.secret, let twoFactorTypeRaw = response.twoFactorTypeRaw, let twoFactorType = TwoFactorType(rawValue: twoFactorTypeRaw) { - return .success(token: token, secret: secret, twoFactorType: twoFactorType) + return LoginData(token: token, secret: secret, twoFactorType: twoFactorType) } } else { if let leftAttempts = response.leftAttempts { - return .failed(reason: .invalidCredentials(attemptsLeft: leftAttempts)) + throw LoginError.invalidCredentials(attemptsLeft: leftAttempts) } if let timeExpire = response.timeExpire { - return .failed(reason: .tooManyAttempts(unlockDate: Date(timeIntervalSince1970: TimeInterval(timeExpire)))) + throw LoginError.tooManyAttempts(unlockDate: Date(timeIntervalSince1970: TimeInterval(timeExpire))) } } - return .failed(reason: .unknown(message: response.errors.map { $0.joined(separator: "\n") } ?? "Unknown error")) + throw LoginError.unknown(message: response.errors.map { $0.joined(separator: "\n") } ?? "Unknown error") } static func validateCode(code: String, token: String, networkManager: NetworkManager) async throws { @@ -367,6 +367,10 @@ extension CoinzixCexProvider { ) guard response.status else { + if let errors = response.errors { + throw VerifyError(messages: errors) + } + throw RequestError.negativeStatus } } @@ -375,15 +379,32 @@ extension CoinzixCexProvider { extension CoinzixCexProvider { - enum LoginResult { - case success(token: String, secret: String, twoFactorType: TwoFactorType) - case failed(reason: LoginFailureReason) + struct LoginData { + let token: String + let secret: String + let twoFactorType: TwoFactorType } - enum LoginFailureReason { + enum LoginError: LocalizedError { case invalidCredentials(attemptsLeft: Int) case tooManyAttempts(unlockDate: Date) case unknown(message: String) + + var errorDescription: String? { + switch self { + case .invalidCredentials(let attemptsLeft): return "Invalid login credentials. Attempts left: \(attemptsLeft)." + case .tooManyAttempts(let unlockDate): return "Too many invalid login attempts were made. Login is locked until \(DateHelper.instance.formatFullTime(from: unlockDate))." + case .unknown(let message): return message + } + } + } + + struct VerifyError: LocalizedError { + let messages: [String] + + var errorDescription: String? { + messages.joined(separator: "\n") + } } enum TwoFactorType: Int { @@ -531,9 +552,11 @@ extension CoinzixCexProvider { private struct StatusResponse: ImmutableMappable { let status: Bool + let errors: [String]? init(map: Map) throws { status = try map.value("status") + errors = try? map.value("errors") } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/PasteInputCell.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/PasteInputCell.swift index c987271e8d..74ed76e0f5 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/PasteInputCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/PasteInputCell.swift @@ -30,6 +30,11 @@ extension PasteInputCell { set { pasteInputView.inputPlaceholder = newValue } } + var keyboardType: UIKeyboardType { + get { pasteInputView.keyboardType } + set { pasteInputView.keyboardType = newValue } + } + var inputText: String? { get { pasteInputView.inputText } set { pasteInputView.inputText = newValue } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/ResendPastInputCell.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/ResendPastInputCell.swift index c36585c516..6396e3bd7c 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/ResendPastInputCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/ResendPastInputCell.swift @@ -3,10 +3,9 @@ import ThemeKit import SnapKit class ResendPasteInputCell: UITableViewCell { - private let view: ResendPasteInputView + private let view = ResendPasteInputView() init() { - self.view = ResendPasteInputView() super.init(style: .default, reuseIdentifier: nil) backgroundColor = .clear @@ -31,6 +30,11 @@ extension ResendPasteInputCell { set { view.inputPlaceholder = newValue } } + var keyboardType: UIKeyboardType { + get { view.keyboardType } + set { view.keyboardType = newValue } + } + var inputText: String? { get { view.inputText } set { view.inputText = newValue } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/PasteInputView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/PasteInputView.swift index 1b56a61eb2..ba01a84eb6 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/PasteInputView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/PasteInputView.swift @@ -100,6 +100,11 @@ extension PasteInputView { set { inputStackView.placeholder = newValue } } + var keyboardType: UIKeyboardType { + get { inputStackView.keyboardType } + set { inputStackView.keyboardType = newValue } + } + var inputText: String? { get { inputStackView.text } set { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/ResendPasteInputView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/ResendPasteInputView.swift index fd55cd302b..fda5a5f711 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/ResendPasteInputView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/ResendPasteInputView.swift @@ -113,6 +113,11 @@ extension ResendPasteInputView { set { inputStackView.placeholder = newValue } } + var keyboardType: UIKeyboardType { + get { inputStackView.keyboardType } + set { inputStackView.keyboardType = newValue } + } + var inputText: String? { get { inputStackView.text } set { diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 8316ac240c..efee92beb3 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1658,15 +1658,10 @@ Go to Settings - > Unstoppable and allow access to the camera."; // Coinzix Verify Withdraw -"coinzix_verify_withdraw.title" = "Security Verification"; -"coinzix_verify_withdraw.email.title" = "Email Verification Code"; -"coinzix_verify_withdraw.email.description" = "Enter verification code sent to %@"; -"coinzix_verify_withdraw.google.title" = "Google Authentication Code"; -"coinzix_verify_withdraw.google.description" = "Enter Google authentication code from your Google Authenticator App"; -"coinzix_verify_withdraw.get_code" = "Get Code"; -"coinzix_verify_withdraw.failed" = "Failed to verify"; -"coinzix_verify_withdraw.submit" = "Submit"; -"coinzix_verify_withdraw.email_pin" = "Verification Code"; -"coinzix_verify_withdraw.email_pin.description" = "Enter verification code sent to test@gmail.com"; -"coinzix_verify_withdraw.google_pin" = "Google Authentication Code"; -"coinzix_verify_withdraw.google_pin.description" = "Enter google authentication code from your Google Authenticator App"; +"coinzix_verify.title" = "Security Verification"; +"coinzix_verify.failed" = "Failed to verify"; +"coinzix_verify.submit" = "Submit"; +"coinzix_verify.email_pin" = "Verification Code"; +"coinzix_verify.email_pin.description" = "Enter verification code sent to your email"; +"coinzix_verify.google_pin" = "Google Authentication Code"; +"coinzix_verify.google_pin.description" = "Enter google authentication code from your Google Authenticator App";