From cc9d0088ca0c5a078f93be0e8e7badafe12cad2d Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Thu, 1 Sep 2022 00:39:19 -0300 Subject: [PATCH] User friendly error --- .../Controllers/SingleChatController.swift | 14 +- Sources/LaunchFeature/LaunchViewModel.swift | 34 ++++- ...OnboardingEmailConfirmationViewModel.swift | 3 +- .../ViewModels/OnboardingEmailViewModel.swift | 5 +- ...OnboardingPhoneConfirmationViewModel.swift | 3 +- .../ViewModels/OnboardingPhoneViewModel.swift | 3 +- .../OnboardingUsernameViewModel.swift | 3 +- .../ViewModels/ProfileEmailViewModel.swift | 3 +- .../ViewModels/ProfilePhoneViewModel.swift | 3 +- .../ViewModels/ProfileViewModel.swift | 3 +- .../Controllers/ScanController.swift | 3 +- .../ViewModels/ScanViewModel.swift | 136 ++++++++---------- 12 files changed, 118 insertions(+), 95 deletions(-) diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 4b419317..2028fc2d 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -530,12 +530,14 @@ extension SingleChatController: KeyboardListenerDelegate { } func keyboardWillChangeFrame(info: KeyboardInfo) { - let keyWindow: UIWindow? = UIApplication.shared.connectedScenes - .filter { $0.activationState == .foregroundActive } - .compactMap { $0 as? UIWindowScene } - .first? - .windows - .first(where: \.isKeyWindow) + let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first + +// let keyWindow: UIWindow? = UIApplication.shared.connectedScenes +// .filter { $0.activationState == .foregroundActive } +// .compactMap { $0 as? UIWindowScene } +// .first? +// .windows +// .first(where: \.isKeyWindow) guard let keyWindow = keyWindow else { fatalError("[keyboardWillChangeFrame]: Couldn't get key window") diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 43ae7a6f..6c27c1de 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -81,7 +81,11 @@ final class LaunchViewModel { self.versionChecker().sink { [unowned self] in switch $0 { case .upToDate: - self.updateBannedList { self.continueWithInitialization() } + self.updateBannedList { + self.updateErrors { + self.continueWithInitialization() + } + } case .failure(let error): self.versionFailed(error: error) case .updateRequired(let info): @@ -97,7 +101,7 @@ final class LaunchViewModel { do { try self.setupDatabase() - try SetLogLevel.live(.trace) + _ = try SetLogLevel.live(.trace) guard let certPath = Bundle.module.path(forResource: "cmix.rip", ofType: "crt"), let contactFilePath = Bundle.module.path(forResource: "udContact", ofType: "bin") else { @@ -326,6 +330,32 @@ final class LaunchViewModel { } } + private func updateErrors(completion: @escaping () -> Void) { + let errorsURLString = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" + + URLSession.shared.dataTask(with: URL(string: errorsURLString)!) { [weak self] data, _, error in + guard let self = self else { return } + + guard error == nil else { + print(">>> Issue when trying to download errors json: \(error!.localizedDescription)") + self.updateErrors(completion: completion) + return + } + + guard let data = data, let json = String(data: data, encoding: .utf8) else { + print(">>> Issue when trying to unwrap errors json") + return + } + + do { + try UpdateCommonErrors.live(jsonFile: json) + completion() + } catch { + print(">>> Issue when trying to update common errors: \(error.localizedDescription)") + } + }.resume() + } + private func updateBannedList(completion: @escaping () -> Void) { fetchBannedList { result in switch result { diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift index d379b9cc..756479e2 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift @@ -78,7 +78,8 @@ final class OnboardingEmailConfirmationViewModel { self.hudRelay.send(.none) self.completionRelay.send(self.confirmation) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index 98262328..cf21d93b 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -4,8 +4,8 @@ import Models import Shared import Combine import Defaults -import InputField import XXClient +import InputField import CombineSchedulers import DependencyInjection import XXMessengerClient @@ -56,7 +56,8 @@ final class OnboardingEmailViewModel { confirmationId: confirmationId ) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift index 49e15708..ba335ba8 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift @@ -78,7 +78,8 @@ final class OnboardingPhoneConfirmationViewModel { self.hudRelay.send(.none) self.completionRelay.send(self.confirmation) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index a3141d2c..f226397f 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -67,7 +67,8 @@ final class OnboardingPhoneViewModel { confirmationId: confirmationId ) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index 39d71f1a..b042341c 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -4,6 +4,7 @@ import Models import Combine import Defaults import XXModels +import XXClient import InputField import Foundation import XXMessengerClient @@ -75,7 +76,7 @@ final class OnboardingUsernameViewModel { self.greenRelay.send() } catch { self.hudRelay.send(.none) - self.stateRelay.value.status = .invalid(error.localizedDescription) + self.stateRelay.value.status = .invalid(CreateUserFriendlyErrorMessage.live(error.localizedDescription)) } } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index 8edf40aa..4a192c30 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -59,7 +59,8 @@ final class ProfileEmailViewModel { confirmationId: confirmationId ) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index e5504140..bbf580a2 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -68,7 +68,8 @@ final class ProfilePhoneViewModel { ) } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index 57334dcf..36006fa5 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -109,7 +109,8 @@ final class ProfileViewModel { self.hudRelay.send(.none) self.refresh() } catch { - self.hudRelay.send(.error(.init(with: error))) + let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) + self.hudRelay.send(.error(.init(content: xxError))) } } } diff --git a/Sources/ScanFeature/Controllers/ScanController.swift b/Sources/ScanFeature/Controllers/ScanController.swift index 2a1b99aa..52055ac4 100644 --- a/Sources/ScanFeature/Controllers/ScanController.swift +++ b/Sources/ScanFeature/Controllers/ScanController.swift @@ -83,8 +83,7 @@ final class ScanController: UIViewController { .sink { [unowned self] in coordinator.toContact($0, from: self) } .store(in: &cancellables) - viewModel.state - .map(\.status) + viewModel.statePublisher .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [unowned self] in diff --git a/Sources/ScanFeature/ViewModels/ScanViewModel.swift b/Sources/ScanFeature/ViewModels/ScanViewModel.swift index 365533cd..b58546fb 100644 --- a/Sources/ScanFeature/ViewModels/ScanViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanViewModel.swift @@ -2,9 +2,9 @@ import Shared import Models import Combine import XXModels -import Foundation import XXClient -import CombineSchedulers +import Foundation +import ReportingFeature import DependencyInjection enum ScanStatus: Equatable { @@ -21,93 +21,77 @@ enum ScanError: Equatable { case alreadyFriends(String) } -struct ScanViewState: Equatable { - var status: ScanStatus = .reading -} - final class ScanViewModel { @Dependency var database: Database - @Dependency var getFactsFromContact: GetFactsFromContact + @Dependency var reportingStatus: ReportingStatus - var backgroundScheduler: AnySchedulerOf<DispatchQueue> - = DispatchQueue.global().eraseToAnyScheduler() + var contactPublisher: AnyPublisher<XXModels.Contact, Never> { + contactSubject.eraseToAnyPublisher() + } - var contactPublisher: AnyPublisher<XXModels.Contact, Never> { contactRelay.eraseToAnyPublisher() } - private let contactRelay = PassthroughSubject<XXModels.Contact, Never>() + var statePublisher: AnyPublisher<ScanStatus, Never> { + stateSubject.eraseToAnyPublisher() + } - var state: AnyPublisher<ScanViewState, Never> { stateRelay.eraseToAnyPublisher() } - private let stateRelay = CurrentValueSubject<ScanViewState, Never>(.init()) + private let contactSubject = PassthroughSubject<XXModels.Contact, Never>() + private let stateSubject = CurrentValueSubject<ScanStatus, Never>(.reading) func resetScanner() { - stateRelay.value.status = .reading + stateSubject.send(.reading) } func didScanData(_ data: Data) { - guard stateRelay.value.status == .reading else { return } - stateRelay.value.status = .processing - - backgroundScheduler.schedule { [weak self] in - guard let self = self else { return } - - do { - guard let usernameAndId = try self.verifyScanned(data) else { - self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.general)) - return - } - - - - if let previouslyAdded = try? self.database.fetchContacts(.init(id: [usernameAndId.1])).first { - var error = ScanError.unknown(Localized.Scan.Error.general) - - switch previouslyAdded.authStatus { - case .friend: - error = .alreadyFriends(usernameAndId.0) - case .requested, .verified: - error = .requestOpened - default: - break - } - - self.stateRelay.value.status = .failed(error) - return - } - - let facts = try? self.getFactsFromContact(data) - let contactEmail = facts?.first(where: { $0.type == FactType.email.rawValue })?.fact - let contactPhone = facts?.first(where: { $0.type == FactType.phone.rawValue })?.fact - - let contact = Contact( - id: usernameAndId.1, - marshaled: data, - username: usernameAndId.0, - email: contactEmail, - phone: contactPhone, - nickname: nil, - photo: nil, - authStatus: .stranger, - isRecent: false, - createdAt: Date() - ) - - self.succeed(with: contact) - } catch { - self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.invalid)) - } + guard stateSubject.value == .reading else { return } + stateSubject.send(.processing) + + let user = XXClient.Contact.live(data) + + guard let uid = try? user.getId(), + let facts = try? user.getFacts(), + let username = facts.first(where: { $0.type == FactType.username.rawValue })?.fact else { + let errorTitle = Localized.Scan.Error.invalid + stateSubject.send(.failed(.unknown(errorTitle))) + return } - } - private func verifyScanned(_ data: Data) throws -> (String, Data)? { - let id = try? GetIdFromContact.live(data) - let facts = try? getFactsFromContact(data) - let username = facts?.first(where: { $0.type == FactType.username.rawValue })?.fact + let email = facts.first { $0.type == FactType.email.rawValue }?.fact + let phone = facts.first { $0.type == FactType.phone.rawValue }?.fact - guard let id = id, let username = username else { return nil } - return (username, id) - } + if let alreadyContact = try? database.fetchContacts(.init(id: [uid])).first { + if alreadyContact.isBlocked, reportingStatus.isEnabled() { + stateSubject.send(.failed(.unknown("You previously blocked this user."))) + return + } + + if alreadyContact.isBanned, reportingStatus.isEnabled() { + stateSubject.send(.failed(.unknown("This user was banned."))) + return + } + + if alreadyContact.authStatus == .friend { + stateSubject.send(.failed(.alreadyFriends(username))) + } else if [.requested, .verified].contains(alreadyContact.authStatus) { + stateSubject.send(.failed(.requestOpened)) + } else { + let generalErrorTitle = Localized.Scan.Error.general + stateSubject.send(.failed(.unknown(generalErrorTitle))) + } + + return + } - private func succeed(with contact: XXModels.Contact) { - stateRelay.value.status = .success - contactRelay.send(contact) + stateSubject.send(.success) + contactSubject.send(.init( + id: uid, + marshaled: data, + username: username, + email: email, + phone: phone, + nickname: nil, + photo: nil, + authStatus: .stranger, + isRecent: false, + createdAt: Date() + )) } } -- GitLab