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