From 6e220b88eca8b6ba42dc2cbd73f1c2b99a73bd99 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 00:15:46 +0200
Subject: [PATCH 01/24] Restore contacts from backup

---
 .../RestoreFeature/RestoreFeature.swift       |  41 ++++-
 .../Sources/RestoreFeature/RestoreView.swift  | 162 +++++++++++-------
 .../RestoreFeatureTests.swift                 |  84 +++++++--
 3 files changed, 202 insertions(+), 85 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
index 66d7df2f..7179264a 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
@@ -24,7 +24,7 @@ public struct RestoreState: Equatable {
   public init(
     file: File? = nil,
     fileImportFailure: String? = nil,
-    restoreFailure: String? = nil,
+    restoreFailures: [String] = [],
     focusedField: Field? = nil,
     isImportingFile: Bool = false,
     passphrase: String = "",
@@ -32,7 +32,7 @@ public struct RestoreState: Equatable {
   ) {
     self.file = file
     self.fileImportFailure = fileImportFailure
-    self.restoreFailure = restoreFailure
+    self.restoreFailures = restoreFailures
     self.focusedField = focusedField
     self.isImportingFile = isImportingFile
     self.passphrase = passphrase
@@ -41,7 +41,7 @@ public struct RestoreState: Equatable {
 
   public var file: File?
   public var fileImportFailure: String?
-  public var restoreFailure: String?
+  public var restoreFailures: [String]
   @BindableState public var focusedField: Field?
   @BindableState public var isImportingFile: Bool
   @BindableState public var passphrase: String
@@ -53,7 +53,7 @@ public enum RestoreAction: Equatable, BindableAction {
   case fileImport(Result<URL, NSError>)
   case restoreTapped
   case finished
-  case failed(NSError)
+  case failed([NSError])
   case binding(BindingAction<RestoreState>)
 }
 
@@ -125,7 +125,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
     guard let backupData = state.file?.data, backupData.count > 0 else { return .none }
     let backupPassphrase = state.passphrase
     state.isRestoring = true
-    state.restoreFailure = nil
+    state.restoreFailures = []
     return Effect.result {
       do {
         let result = try env.messenger.restoreBackup(
@@ -140,10 +140,33 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
           phone: facts.get(.phone)?.value,
           createdAt: env.now()
         ))
+        let lookupResult = try env.messenger.lookupContacts.callAsFunction(
+          ids: result.restoredContacts
+        )
+        try lookupResult.contacts.forEach { contact in
+          let facts = try contact.getFacts()
+          try env.db().saveContact(XXModels.Contact(
+            id: try contact.getId(),
+            marshaled: contact.data,
+            username: facts.get(.username)?.value,
+            email: facts.get(.email)?.value,
+            phone: facts.get(.phone)?.value,
+            authStatus: .friend,
+            createdAt: env.now()
+          ))
+        }
+        guard lookupResult.errors.isEmpty else {
+          return .success(.failed(lookupResult.errors))
+        }
         return .success(.finished)
       } catch {
-        try? env.messenger.destroy()
-        return .success(.failed(error as NSError))
+        var errors = [error as NSError]
+        do {
+          try env.messenger.destroy()
+        } catch {
+          errors.append(error as NSError)
+        }
+        return .success(.failed(errors))
       }
     }
     .subscribe(on: env.bgQueue)
@@ -154,9 +177,9 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
     state.isRestoring = false
     return .none
 
-  case .failed(let error):
+  case .failed(let errors):
     state.isRestoring = false
-    state.restoreFailure = error.localizedDescription
+    state.restoreFailures = errors.map(\.localizedDescription)
     return .none
 
   case .binding(_):
diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
index 06f7b915..4dcaa5dc 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
@@ -21,7 +21,7 @@ public struct RestoreView: View {
     var isRestoring: Bool
     var focusedField: RestoreState.Field?
     var fileImportFailure: String?
-    var restoreFailure: String?
+    var restoreFailures: [String]
 
     init(state: RestoreState) {
       file = state.file.map { .init(name: $0.name, size: $0.data.count) }
@@ -30,7 +30,7 @@ public struct RestoreView: View {
       isRestoring = state.isRestoring
       focusedField = state.focusedField
       fileImportFailure = state.fileImportFailure
-      restoreFailure = state.restoreFailure
+      restoreFailures = state.restoreFailures
     }
   }
 
@@ -38,69 +38,9 @@ public struct RestoreView: View {
     WithViewStore(store, observe: ViewState.init) { viewStore in
       NavigationView {
         Form {
-          Section {
-            if let file = viewStore.file {
-              HStack(alignment: .bottom) {
-                Text(file.name)
-                Spacer()
-                Text(format(byteCount: file.size))
-              }
-            }
-
-            Button {
-              viewStore.send(.importFileTapped)
-            } label: {
-              Text("Import backup file")
-            }
-            .fileImporter(
-              isPresented: viewStore.binding(
-                get: \.isImportingFile,
-                send: { .set(\.$isImportingFile, $0) }
-              ),
-              allowedContentTypes: [.data],
-              onCompletion: { result in
-                viewStore.send(.fileImport(result.mapError { $0 as NSError }))
-              }
-            )
-
-            if let failure = viewStore.fileImportFailure {
-              Text("Error: \(failure)")
-            }
-          } header: {
-            Text("File")
-          }
-          .disabled(viewStore.isRestoring)
-
+          fileSection(viewStore)
           if viewStore.file != nil {
-            Section {
-              SecureField("Passphrase", text: viewStore.binding(
-                get: \.passphrase,
-                send: { .set(\.$passphrase, $0) }
-              ))
-              .textContentType(.password)
-              .textInputAutocapitalization(.never)
-              .disableAutocorrection(true)
-              .focused($focusedField, equals: .passphrase)
-
-              Button {
-                viewStore.send(.restoreTapped)
-              } label: {
-                HStack {
-                  Text("Restore")
-                  Spacer()
-                  if viewStore.isRestoring {
-                    ProgressView()
-                  }
-                }
-              }
-
-              if let failure = viewStore.restoreFailure {
-                Text("Error: \(failure)")
-              }
-            } header: {
-              Text("Backup")
-            }
-            .disabled(viewStore.isRestoring)
+            restoreSection(viewStore)
           }
         }
         .toolbar {
@@ -110,7 +50,7 @@ public struct RestoreView: View {
             } label: {
               Text("Cancel")
             }
-            .disabled(viewStore.isRestoring)
+            .disabled(viewStore.isImportingFile || viewStore.isRestoring)
           }
         }
         .navigationTitle("Restore")
@@ -121,6 +61,84 @@ public struct RestoreView: View {
     }
   }
 
+  @ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View {
+    Section {
+      if let file = viewStore.file {
+        HStack(alignment: .bottom) {
+          Text(file.name)
+          Spacer()
+          Text(format(byteCount: file.size))
+        }
+      } else {
+        Button {
+          viewStore.send(.importFileTapped)
+        } label: {
+          Text("Import backup file")
+        }
+        .fileImporter(
+          isPresented: viewStore.binding(
+            get: \.isImportingFile,
+            send: { .set(\.$isImportingFile, $0) }
+          ),
+          allowedContentTypes: [.data],
+          onCompletion: { result in
+            viewStore.send(.fileImport(result.mapError { $0 as NSError }))
+          }
+        )
+        .disabled(viewStore.isRestoring)
+      }
+    } header: {
+      Text("File")
+    }
+
+    if let failure = viewStore.fileImportFailure {
+      Section {
+        Text(failure)
+      } header: {
+        Text("Error")
+      }
+    }
+  }
+
+  @ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View {
+    Section {
+      SecureField("Passphrase", text: viewStore.binding(
+        get: \.passphrase,
+        send: { .set(\.$passphrase, $0) }
+      ))
+      .textContentType(.password)
+      .textInputAutocapitalization(.never)
+      .disableAutocorrection(true)
+      .focused($focusedField, equals: .passphrase)
+      .disabled(viewStore.isRestoring)
+
+      Button {
+        viewStore.send(.restoreTapped)
+      } label: {
+        HStack {
+          Text("Restore")
+          Spacer()
+          if viewStore.isRestoring {
+            ProgressView()
+          }
+        }
+      }
+    } header: {
+      Text("Restore")
+    }
+
+    if !viewStore.restoreFailures.isEmpty {
+      Section {
+        ForEach(Array(viewStore.restoreFailures.enumerated()), id: \.offset) { _, failure in
+          Text(failure)
+        }
+        .font(.footnote)
+      } header: {
+        Text("Error")
+      }
+    }
+  }
+
   func format(byteCount: Int) -> String {
     let formatter = ByteCountFormatter()
     formatter.allowedUnits = [.useMB, .useKB, .useBytes]
@@ -133,7 +151,19 @@ public struct RestoreView: View {
 public struct RestoreView_Previews: PreviewProvider {
   public static var previews: some View {
     RestoreView(store: Store(
-      initialState: RestoreState(),
+      initialState: RestoreState(
+        file: .init(name: "preview", data: Data()),
+        fileImportFailure: nil,
+        restoreFailures: [
+          "Preview failure 1",
+          "Preview failure 2",
+          "Preview failure 3",
+        ],
+        focusedField: nil,
+        isImportingFile: false,
+        passphrase: "",
+        isRestoring: false
+      ),
       reducer: .empty,
       environment: ()
     ))
diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
index 9b0a7b87..5ec8409e 100644
--- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
@@ -85,15 +85,22 @@ final class RestoreFeatureTests: XCTestCase {
     ]
     let restoreResult = MessengerRestoreBackup.Result(
       restoredParams: BackupParams(username: "restored-username"),
-      restoredContacts: []
+      restoredContacts: [
+        "contact-1-id".data(using: .utf8)!,
+        "contact-2-id".data(using: .utf8)!,
+        "contact-3-id".data(using: .utf8)!,
+      ]
     )
+    let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)]
     let now = Date()
     let contactId = "contact-id".data(using: .utf8)!
 
+
     var udFacts: [Fact] = []
     var didRestoreWithData: [Data] = []
     var didRestoreWithPassphrase: [String] = []
     var didSaveContact: [XXModels.Contact] = []
+    var didLookupContactIds: [[Data]] = []
 
     let store = TestStore(
       initialState: RestoreState(
@@ -126,6 +133,14 @@ final class RestoreFeatureTests: XCTestCase {
       ud.getFacts.run = { udFacts }
       return ud
     }
+    store.environment.messenger.lookupContacts.run = { contactIds in
+      didLookupContactIds.append(contactIds)
+      return .init(
+        contacts: lookedUpContacts,
+        failedIds: [],
+        errors: []
+      )
+    }
     store.environment.db.run = {
       var db: Database = .unimplemented
       db.saveContact.run = { contact in
@@ -145,13 +160,43 @@ final class RestoreFeatureTests: XCTestCase {
 
     XCTAssertNoDifference(didRestoreWithData, [backupData])
     XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase])
-    XCTAssertNoDifference(didSaveContact, [Contact(
-      id: contactId,
-      username: restoreResult.restoredParams.username,
-      email: restoredFacts.get(.email)?.value,
-      phone: restoredFacts.get(.phone)?.value,
-      createdAt: now
-    )])
+    XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts])
+    XCTAssertNoDifference(didSaveContact, [
+      Contact(
+        id: contactId,
+        username: restoreResult.restoredParams.username,
+        email: restoredFacts.get(.email)?.value,
+        phone: restoredFacts.get(.phone)?.value,
+        createdAt: now
+      ),
+      Contact(
+        id: "contact-\(1)-id".data(using: .utf8)!,
+        marshaled: "contact-\(1)-data".data(using: .utf8)!,
+        username: "contact-\(1)-username",
+        email: "contact-\(1)-email",
+        phone: "contact-\(1)-phone",
+        authStatus: .friend,
+        createdAt: now
+      ),
+      Contact(
+        id: "contact-\(2)-id".data(using: .utf8)!,
+        marshaled: "contact-\(2)-data".data(using: .utf8)!,
+        username: "contact-\(2)-username",
+        email: "contact-\(2)-email",
+        phone: "contact-\(2)-phone",
+        authStatus: .friend,
+        createdAt: now
+      ),
+      Contact(
+        id: "contact-\(3)-id".data(using: .utf8)!,
+        marshaled: "contact-\(3)-data".data(using: .utf8)!,
+        username: "contact-\(3)-username",
+        email: "contact-\(3)-email",
+        phone: "contact-\(3)-phone",
+        authStatus: .friend,
+        createdAt: now
+      ),
+    ])
 
     store.receive(.finished) {
       $0.isRestoring = false
@@ -195,9 +240,28 @@ final class RestoreFeatureTests: XCTestCase {
 
     XCTAssertEqual(didDestroyMessenger, 1)
 
-    store.receive(.failed(failure as NSError)) {
+    store.receive(.failed([failure as NSError])) {
       $0.isRestoring = false
-      $0.restoreFailure = failure.localizedDescription
+      $0.restoreFailures = [failure.localizedDescription]
+    }
+  }
+}
+
+private extension XXClient.Contact {
+  static func stub(_ id: Int) -> XXClient.Contact {
+    var contact = XXClient.Contact.unimplemented(
+      "contact-\(id)-data".data(using: .utf8)!
+    )
+    contact.getIdFromContact.run = { _ in
+      "contact-\(id)-id".data(using: .utf8)!
+    }
+    contact.getFactsFromContact.run = { _ in
+      [
+        Fact(type: .username, value: "contact-\(id)-username"),
+        Fact(type: .email, value: "contact-\(id)-email"),
+        Fact(type: .phone, value: "contact-\(id)-phone"),
+      ]
     }
+    return contact
   }
 }
-- 
GitLab


From 62c8a894f698b39772faaf712e50eadf26d20e44 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 00:39:04 +0200
Subject: [PATCH 02/24] Update UdMultiLookupCallback

Improve error handling
---
 .../Callbacks/UdMultiLookupCallback.swift     | 24 +++++++++----------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
index 1782c01e..cc40b606 100644
--- a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
+++ b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift
@@ -49,21 +49,21 @@ extension UdMultiLookupCallback {
         if let err = err {
           result.errors.append(err as NSError)
         }
-        if let contactListJSON = contactListJSON {
-          do {
-            result.contacts = try JSONDecoder()
-              .decode([Data].self, from: contactListJSON)
-              .map { Contact.live($0) }
-          } catch {
-            result.errors.append(error as NSError)
+        do {
+          if let data = contactListJSON,
+             let contactListJSON = try JSONDecoder().decode([Data]?.self, from: data) {
+            result.contacts = contactListJSON.map { Contact.live($0) }
           }
+        } catch {
+          result.errors.append(error as NSError)
         }
-        if let failedIDs = failedIDs {
-          do {
-            result.failedIds = try JSONDecoder().decode([Data].self, from: failedIDs)
-          } catch {
-            result.errors.append(error as NSError)
+        do {
+          if let data = failedIDs,
+             let failedIDs = try JSONDecoder().decode([Data]?.self, from: data) {
+            result.failedIds = failedIDs
           }
+        } catch {
+            result.errors.append(error as NSError)
         }
         callback.handle(result)
       }
-- 
GitLab


From 8b436846f1f61309450e9239531358b9aa8f1245 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 00:39:26 +0200
Subject: [PATCH 03/24] Update RestoreView

---
 Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
index 4dcaa5dc..281f3f06 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift
@@ -126,6 +126,7 @@ public struct RestoreView: View {
     } header: {
       Text("Restore")
     }
+    .disabled(viewStore.isRestoring)
 
     if !viewStore.restoreFailures.isEmpty {
       Section {
@@ -162,7 +163,7 @@ public struct RestoreView_Previews: PreviewProvider {
         focusedField: nil,
         isImportingFile: false,
         passphrase: "",
-        isRestoring: false
+        isRestoring: true
       ),
       reducer: .empty,
       environment: ()
-- 
GitLab


From f83db6a781efc4e38bd19d6f15fcbfa3332bd161 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 01:03:05 +0200
Subject: [PATCH 04/24] Improve error handling in RestoreFeature

---
 .../RestoreFeature/RestoreFeature.swift       |   8 +-
 .../RestoreFeatureTests.swift                 | 101 ++++++++++++++++--
 2 files changed, 97 insertions(+), 12 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
index 7179264a..b11895fb 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
@@ -156,7 +156,13 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
           ))
         }
         guard lookupResult.errors.isEmpty else {
-          return .success(.failed(lookupResult.errors))
+          var errors = lookupResult.errors
+          do {
+            try env.messenger.destroy()
+          } catch {
+            errors.append(error as NSError)
+          }
+          return .success(.failed(errors))
         }
         return .success(.finished)
       } catch {
diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
index 5ec8409e..a103013b 100644
--- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
@@ -95,7 +95,6 @@ final class RestoreFeatureTests: XCTestCase {
     let now = Date()
     let contactId = "contact-id".data(using: .utf8)!
 
-
     var udFacts: [Fact] = []
     var didRestoreWithData: [Data] = []
     var didRestoreWithPassphrase: [String] = []
@@ -203,6 +202,85 @@ final class RestoreFeatureTests: XCTestCase {
     }
   }
 
+  func testRestoreLookupFailure() {
+    struct FailureA: Error {}
+    struct FailureB: Error {}
+    struct DestroyFailure: Error {}
+
+    let restoreResult = MessengerRestoreBackup.Result(
+      restoredParams: BackupParams(username: "restored-username"),
+      restoredContacts: [
+        "contact-1-id".data(using: .utf8)!,
+        "contact-2-id".data(using: .utf8)!,
+        "contact-3-id".data(using: .utf8)!,
+      ]
+    )
+    let now = Date()
+    let contactId = "contact-id".data(using: .utf8)!
+
+    let store = TestStore(
+      initialState: RestoreState(
+        file: .init(name: "name", data: "data".data(using: .utf8)!)
+      ),
+      reducer: restoreReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.bgQueue = .immediate
+    store.environment.mainQueue = .immediate
+    store.environment.now = { now }
+    store.environment.messenger.restoreBackup.run = { _, _ in restoreResult }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactId }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getFacts.run = { [] }
+      return ud
+    }
+    store.environment.messenger.lookupContacts.run = { contactIds in
+      .init(
+        contacts: [],
+        failedIds: [],
+        errors: [
+          FailureA() as NSError,
+          FailureB() as NSError,
+        ]
+      )
+    }
+    store.environment.messenger.destroy.run = {
+      throw DestroyFailure()
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.saveContact.run = { $0 }
+      return db
+    }
+
+    store.send(.restoreTapped) {
+      $0.isRestoring = true
+    }
+
+    store.receive(.failed([
+      FailureA() as NSError,
+      FailureB() as NSError,
+      DestroyFailure() as NSError
+    ])) {
+      $0.isRestoring = false
+      $0.restoreFailures = [
+        FailureA().localizedDescription,
+        FailureB().localizedDescription,
+        DestroyFailure().localizedDescription,
+      ]
+    }
+  }
+
   func testRestoreWithoutFile() {
     let store = TestStore(
       initialState: RestoreState(
@@ -216,10 +294,10 @@ final class RestoreFeatureTests: XCTestCase {
   }
 
   func testRestoreFailure() {
-    struct Failure: Error {}
-    let failure = Failure()
-
-    var didDestroyMessenger = 0
+    enum Failure: Error {
+      case restore
+      case destroy
+    }
 
     let store = TestStore(
       initialState: RestoreState(
@@ -231,18 +309,19 @@ final class RestoreFeatureTests: XCTestCase {
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
-    store.environment.messenger.restoreBackup.run = { _, _ in throw failure }
-    store.environment.messenger.destroy.run = { didDestroyMessenger += 1 }
+    store.environment.messenger.restoreBackup.run = { _, _ in throw Failure.restore }
+    store.environment.messenger.destroy.run = { throw Failure.destroy }
 
     store.send(.restoreTapped) {
       $0.isRestoring = true
     }
 
-    XCTAssertEqual(didDestroyMessenger, 1)
-
-    store.receive(.failed([failure as NSError])) {
+    store.receive(.failed([Failure.restore as NSError, Failure.destroy as NSError])) {
       $0.isRestoring = false
-      $0.restoreFailures = [failure.localizedDescription]
+      $0.restoreFailures = [
+        Failure.restore.localizedDescription,
+        Failure.destroy.localizedDescription,
+      ]
     }
   }
 }
-- 
GitLab


From f89c6fce67a70264bbb230252cad86edaabfd295 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 01:35:42 +0200
Subject: [PATCH 05/24] Don't lookup restored contacts

---
 .../RestoreFeature/RestoreFeature.swift       |  28 +---
 .../RestoreFeatureTests.swift                 | 140 ++----------------
 2 files changed, 19 insertions(+), 149 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
index b11895fb..455f8989 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
@@ -140,29 +140,13 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
           phone: facts.get(.phone)?.value,
           createdAt: env.now()
         ))
-        let lookupResult = try env.messenger.lookupContacts.callAsFunction(
-          ids: result.restoredContacts
-        )
-        try lookupResult.contacts.forEach { contact in
-          let facts = try contact.getFacts()
-          try env.db().saveContact(XXModels.Contact(
-            id: try contact.getId(),
-            marshaled: contact.data,
-            username: facts.get(.username)?.value,
-            email: facts.get(.email)?.value,
-            phone: facts.get(.phone)?.value,
-            authStatus: .friend,
-            createdAt: env.now()
-          ))
-        }
-        guard lookupResult.errors.isEmpty else {
-          var errors = lookupResult.errors
-          do {
-            try env.messenger.destroy()
-          } catch {
-            errors.append(error as NSError)
+        try result.restoredContacts.forEach { contactId in
+          if try env.db().fetchContacts(.init(id: [contactId])).isEmpty {
+            try env.db().saveContact(Contact(
+              id: contactId,
+              createdAt: env.now()
+            ))
           }
-          return .success(.failed(errors))
         }
         return .success(.finished)
       } catch {
diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
index a103013b..8cfdb3d6 100644
--- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
@@ -91,15 +91,14 @@ final class RestoreFeatureTests: XCTestCase {
         "contact-3-id".data(using: .utf8)!,
       ]
     )
-    let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)]
     let now = Date()
     let contactId = "contact-id".data(using: .utf8)!
 
     var udFacts: [Fact] = []
     var didRestoreWithData: [Data] = []
     var didRestoreWithPassphrase: [String] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
     var didSaveContact: [XXModels.Contact] = []
-    var didLookupContactIds: [[Data]] = []
 
     let store = TestStore(
       initialState: RestoreState(
@@ -132,16 +131,12 @@ final class RestoreFeatureTests: XCTestCase {
       ud.getFacts.run = { udFacts }
       return ud
     }
-    store.environment.messenger.lookupContacts.run = { contactIds in
-      didLookupContactIds.append(contactIds)
-      return .init(
-        contacts: lookedUpContacts,
-        failedIds: [],
-        errors: []
-      )
-    }
     store.environment.db.run = {
       var db: Database = .unimplemented
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return []
+      }
       db.saveContact.run = { contact in
         didSaveContact.append(contact)
         return contact
@@ -159,7 +154,11 @@ final class RestoreFeatureTests: XCTestCase {
 
     XCTAssertNoDifference(didRestoreWithData, [backupData])
     XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase])
-    XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts])
+    XCTAssertNoDifference(didFetchContacts, [
+      .init(id: [restoreResult.restoredContacts[0]]),
+      .init(id: [restoreResult.restoredContacts[1]]),
+      .init(id: [restoreResult.restoredContacts[2]]),
+    ])
     XCTAssertNoDifference(didSaveContact, [
       Contact(
         id: contactId,
@@ -169,30 +168,15 @@ final class RestoreFeatureTests: XCTestCase {
         createdAt: now
       ),
       Contact(
-        id: "contact-\(1)-id".data(using: .utf8)!,
-        marshaled: "contact-\(1)-data".data(using: .utf8)!,
-        username: "contact-\(1)-username",
-        email: "contact-\(1)-email",
-        phone: "contact-\(1)-phone",
-        authStatus: .friend,
+        id: restoreResult.restoredContacts[0],
         createdAt: now
       ),
       Contact(
-        id: "contact-\(2)-id".data(using: .utf8)!,
-        marshaled: "contact-\(2)-data".data(using: .utf8)!,
-        username: "contact-\(2)-username",
-        email: "contact-\(2)-email",
-        phone: "contact-\(2)-phone",
-        authStatus: .friend,
+        id: restoreResult.restoredContacts[1],
         createdAt: now
       ),
       Contact(
-        id: "contact-\(3)-id".data(using: .utf8)!,
-        marshaled: "contact-\(3)-data".data(using: .utf8)!,
-        username: "contact-\(3)-username",
-        email: "contact-\(3)-email",
-        phone: "contact-\(3)-phone",
-        authStatus: .friend,
+        id: restoreResult.restoredContacts[2],
         createdAt: now
       ),
     ])
@@ -202,85 +186,6 @@ final class RestoreFeatureTests: XCTestCase {
     }
   }
 
-  func testRestoreLookupFailure() {
-    struct FailureA: Error {}
-    struct FailureB: Error {}
-    struct DestroyFailure: Error {}
-
-    let restoreResult = MessengerRestoreBackup.Result(
-      restoredParams: BackupParams(username: "restored-username"),
-      restoredContacts: [
-        "contact-1-id".data(using: .utf8)!,
-        "contact-2-id".data(using: .utf8)!,
-        "contact-3-id".data(using: .utf8)!,
-      ]
-    )
-    let now = Date()
-    let contactId = "contact-id".data(using: .utf8)!
-
-    let store = TestStore(
-      initialState: RestoreState(
-        file: .init(name: "name", data: "data".data(using: .utf8)!)
-      ),
-      reducer: restoreReducer,
-      environment: .unimplemented
-    )
-
-    store.environment.bgQueue = .immediate
-    store.environment.mainQueue = .immediate
-    store.environment.now = { now }
-    store.environment.messenger.restoreBackup.run = { _, _ in restoreResult }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented(Data())
-        contact.getIdFromContact.run = { _ in contactId }
-        return contact
-      }
-      return e2e
-    }
-    store.environment.messenger.ud.get = {
-      var ud: UserDiscovery = .unimplemented
-      ud.getFacts.run = { [] }
-      return ud
-    }
-    store.environment.messenger.lookupContacts.run = { contactIds in
-      .init(
-        contacts: [],
-        failedIds: [],
-        errors: [
-          FailureA() as NSError,
-          FailureB() as NSError,
-        ]
-      )
-    }
-    store.environment.messenger.destroy.run = {
-      throw DestroyFailure()
-    }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.saveContact.run = { $0 }
-      return db
-    }
-
-    store.send(.restoreTapped) {
-      $0.isRestoring = true
-    }
-
-    store.receive(.failed([
-      FailureA() as NSError,
-      FailureB() as NSError,
-      DestroyFailure() as NSError
-    ])) {
-      $0.isRestoring = false
-      $0.restoreFailures = [
-        FailureA().localizedDescription,
-        FailureB().localizedDescription,
-        DestroyFailure().localizedDescription,
-      ]
-    }
-  }
-
   func testRestoreWithoutFile() {
     let store = TestStore(
       initialState: RestoreState(
@@ -325,22 +230,3 @@ final class RestoreFeatureTests: XCTestCase {
     }
   }
 }
-
-private extension XXClient.Contact {
-  static func stub(_ id: Int) -> XXClient.Contact {
-    var contact = XXClient.Contact.unimplemented(
-      "contact-\(id)-data".data(using: .utf8)!
-    )
-    contact.getIdFromContact.run = { _ in
-      "contact-\(id)-id".data(using: .utf8)!
-    }
-    contact.getFactsFromContact.run = { _ in
-      [
-        Fact(type: .username, value: "contact-\(id)-username"),
-        Fact(type: .email, value: "contact-\(id)-email"),
-        Fact(type: .phone, value: "contact-\(id)-phone"),
-      ]
-    }
-    return contact
-  }
-}
-- 
GitLab


From 7a004c2a5e0719488cd9073d6b64e1a2ce037f65 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 01:55:41 +0200
Subject: [PATCH 06/24] Improve UI in ContactView

---
 .../Sources/ContactFeature/ContactView.swift     | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index d775df01..a9611140 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -26,6 +26,10 @@ public struct ContactView: View {
     var importUsername: Bool
     var importEmail: Bool
     var importPhone: Bool
+    var canSendRequest: Bool
+    var canVerifyContact: Bool
+    var canConfirmRequest: Bool
+    var canCheckAuthorization: Bool
 
     init(state: ContactState) {
       dbContact = state.dbContact
@@ -36,6 +40,10 @@ public struct ContactView: View {
       importUsername = state.importUsername
       importEmail = state.importEmail
       importPhone = state.importPhone
+      canSendRequest = state.xxContact != nil || state.dbContact?.marshaled != nil
+      canVerifyContact = state.dbContact?.marshaled != nil
+      canConfirmRequest = state.dbContact?.marshaled != nil
+      canCheckAuthorization = state.dbContact?.marshaled != nil
     }
   }
 
@@ -109,6 +117,7 @@ public struct ContactView: View {
 
           Section {
             ContactAuthStatusView(dbContact.authStatus)
+
             Button {
               viewStore.send(.sendRequestTapped)
             } label: {
@@ -118,6 +127,8 @@ public struct ContactView: View {
                 Image(systemName: "chevron.forward")
               }
             }
+            .disabled(!viewStore.canSendRequest)
+
             Button {
               viewStore.send(.verifyContactTapped)
             } label: {
@@ -127,6 +138,8 @@ public struct ContactView: View {
                 Image(systemName: "chevron.forward")
               }
             }
+            .disabled(!viewStore.canVerifyContact)
+
             Button {
               viewStore.send(.confirmRequestTapped)
             } label: {
@@ -136,6 +149,8 @@ public struct ContactView: View {
                 Image(systemName: "chevron.forward")
               }
             }
+            .disabled(!viewStore.canConfirmRequest)
+
             Button {
               viewStore.send(.checkAuthTapped)
             } label: {
@@ -145,6 +160,7 @@ public struct ContactView: View {
                 Image(systemName: "chevron.forward")
               }
             }
+            .disabled(!viewStore.canCheckAuthorization)
           } header: {
             Text("Auth")
           }
-- 
GitLab


From 13b626fab9591849bda352a3cceccc5b679d145a Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 02:15:00 +0200
Subject: [PATCH 07/24] Add contact ID to ContactView

---
 .../Sources/AppCore/SharedUI/Data+hexString.swift          | 7 +++++++
 .../xx-messenger/Sources/ContactFeature/ContactView.swift  | 2 ++
 2 files changed, 9 insertions(+)
 create mode 100644 Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift

diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift
new file mode 100644
index 00000000..05755e40
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension Data {
+  public var hexString: String {
+    map { String(format: "%02hhx ", $0) }.joined()
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index a9611140..b8ab18a7 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -108,6 +108,8 @@ public struct ContactView: View {
 
         if let dbContact = viewStore.dbContact {
           Section {
+            Label(dbContact.id.hexString, systemImage: "number")
+              .font(.footnote.monospaced())
             Label(dbContact.username ?? "", systemImage: "person")
             Label(dbContact.email ?? "", systemImage: "envelope")
             Label(dbContact.phone ?? "", systemImage: "phone")
-- 
GitLab


From 13bc70acb679dceba2bf830090c488ba0baf574b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 02:17:09 +0200
Subject: [PATCH 08/24] Allow test selection in ContactView

---
 Examples/xx-messenger/Sources/ContactFeature/ContactView.swift | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index b8ab18a7..2afc08f5 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -116,6 +116,7 @@ public struct ContactView: View {
           } header: {
             Text("Contact")
           }
+          .textSelection(.enabled)
 
           Section {
             ContactAuthStatusView(dbContact.authStatus)
-- 
GitLab


From c1952b00c10e35878d9811afc22382f51b4161ef Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 02:22:15 +0200
Subject: [PATCH 09/24] Add contact ID to MyContactView

---
 .../Sources/MyContactFeature/MyContactView.swift     | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index f32af242..eca55d62 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -1,3 +1,4 @@
+import AppCore
 import ComposableArchitecture
 import SwiftUI
 import XXModels
@@ -49,8 +50,17 @@ public struct MyContactView: View {
   public var body: some View {
     WithViewStore(store, observe: ViewState.init) { viewStore in
       Form {
+        Section {
+          Text(viewStore.contact?.id.hexString ?? "")
+            .font(.footnote.monospaced())
+            .textSelection(.enabled)
+        } header: {
+          Label("ID", systemImage: "number")
+        }
+
         Section {
           Text(viewStore.contact?.username ?? "")
+            .textSelection(.enabled)
         } header: {
           Label("Username", systemImage: "person")
         }
@@ -59,6 +69,7 @@ public struct MyContactView: View {
           if let contact = viewStore.contact {
             if let email = contact.email {
               Text(email)
+                .textSelection(.enabled)
               Button(role: .destructive) {
                 viewStore.send(.unregisterEmailTapped)
               } label: {
@@ -135,6 +146,7 @@ public struct MyContactView: View {
           if let contact = viewStore.contact {
             if let phone = contact.phone {
               Text(phone)
+                .textSelection(.enabled)
               Button(role: .destructive) {
                 viewStore.send(.unregisterPhoneTapped)
               } label: {
-- 
GitLab


From cc1864ab1d3c8757edf89bc012fc1cac6fd4dc15 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 02:53:44 +0200
Subject: [PATCH 10/24] Add ContactLookupFeature library

---
 .../xcschemes/ContactLookupFeature.xcscheme   | 78 +++++++++++++++++++
 Examples/xx-messenger/Package.swift           | 19 +++++
 .../xcschemes/XXMessenger.xcscheme            | 10 +++
 .../ContactLookupFeature.swift                | 46 +++++++++++
 .../ContactLookupView.swift                   | 69 ++++++++++++++++
 .../ContactLookupFeatureTests.swift           | 31 ++++++++
 6 files changed, 253 insertions(+)
 create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme
 create mode 100644 Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
 create mode 100644 Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift

diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme
new file mode 100644
index 00000000..e0070c64
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1400"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactLookupFeature"
+               BuildableName = "ContactLookupFeature"
+               BlueprintName = "ContactLookupFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactLookupFeatureTests"
+               BuildableName = "ContactLookupFeatureTests"
+               BlueprintName = "ContactLookupFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "ContactLookupFeature"
+            BuildableName = "ContactLookupFeature"
+            BlueprintName = "ContactLookupFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 4668d629..1cab487e 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -20,6 +20,7 @@ let package = Package(
     .library(name: "CheckContactAuthFeature", targets: ["CheckContactAuthFeature"]),
     .library(name: "ConfirmRequestFeature", targets: ["ConfirmRequestFeature"]),
     .library(name: "ContactFeature", targets: ["ContactFeature"]),
+    .library(name: "ContactLookupFeature", targets: ["ContactLookupFeature"]),
     .library(name: "ContactsFeature", targets: ["ContactsFeature"]),
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "MyContactFeature", targets: ["MyContactFeature"]),
@@ -208,6 +209,24 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "ContactLookupFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "ContactLookupFeatureTests",
+      dependencies: [
+        .target(name: "ContactLookupFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "ContactsFeature",
       dependencies: [
diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
index b50f1c10..42a3cede 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -99,6 +99,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactLookupFeatureTests"
+               BuildableName = "ContactLookupFeatureTests"
+               BlueprintName = "ContactLookupFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
new file mode 100644
index 00000000..7fd2fc8e
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
@@ -0,0 +1,46 @@
+import ComposableArchitecture
+import Foundation
+import XCTestDynamicOverlay
+
+public struct ContactLookupState: Equatable {
+  public init(
+    id: Data,
+    isLookingUp: Bool = false
+  ) {
+    self.id = id
+    self.isLookingUp = isLookingUp
+  }
+
+  public var id: Data
+  public var isLookingUp: Bool
+}
+
+public enum ContactLookupAction: Equatable {
+  case task
+  case cancelTask
+  case lookupTapped
+}
+
+public struct ContactLookupEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension ContactLookupEnvironment {
+  public static let unimplemented = ContactLookupEnvironment()
+}
+#endif
+
+public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment>
+{ state, action, env in
+  switch action {
+  case .task:
+    return .none
+
+  case .cancelTask:
+    return .none
+
+  case .lookupTapped:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
new file mode 100644
index 00000000..1d1e0208
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
@@ -0,0 +1,69 @@
+import AppCore
+import ComposableArchitecture
+import SwiftUI
+
+public struct ContactLookupView: View {
+  public init(store: Store<ContactLookupState, ContactLookupAction>) {
+    self.store = store
+  }
+
+  let store: Store<ContactLookupState, ContactLookupAction>
+
+  struct ViewState: Equatable {
+    init(state: ContactLookupState) {
+      id = state.id
+      isLookingUp = state.isLookingUp
+    }
+
+    var id: Data
+    var isLookingUp: Bool
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+        Section {
+          Label(viewStore.id.hexString, systemImage: "number")
+            .font(.footnote.monospaced())
+
+          Button {
+            viewStore.send(.lookupTapped)
+          } label: {
+            HStack {
+              Text("Lookup")
+              Spacer()
+              if viewStore.isLookingUp {
+                ProgressView()
+              } else {
+                Image(systemName: "magnifyingglass")
+              }
+            }
+          }
+          .disabled(viewStore.isLookingUp)
+        } header: {
+          Text("Contact ID")
+        }
+      }
+      .navigationTitle("Lookup")
+      .task {
+        await viewStore.send(.task).finish()
+      }
+    }
+  }
+}
+
+#if DEBUG
+public struct ContactLookupView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      ContactLookupView(store: Store(
+        initialState: ContactLookupState(
+          id: "1234".data(using: .utf8)!
+        ),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
new file mode 100644
index 00000000..70847105
--- /dev/null
+++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
@@ -0,0 +1,31 @@
+import ComposableArchitecture
+import XCTest
+@testable import ContactLookupFeature
+
+final class ContactLookupFeatureTests: XCTestCase {
+  func testTask() {
+    let store = TestStore(
+      initialState: ContactLookupState(
+        id: "1234".data(using: .utf8)!
+      ),
+      reducer: contactLookupReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.task)
+
+    store.send(.cancelTask)
+  }
+
+  func testLookup() {
+    let store = TestStore(
+      initialState: ContactLookupState(
+        id: "1234".data(using: .utf8)!
+      ),
+      reducer: contactLookupReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.lookupTapped)
+  }
+}
-- 
GitLab


From 89c7eab8a2fead8e6cb3cd28b4195f47445ed8fb Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 03:07:40 +0200
Subject: [PATCH 11/24] Present Lookup from Contact

---
 Examples/xx-messenger/Package.swift           |  2 ++
 .../AppFeature/AppEnvironment+Live.swift      |  3 ++
 .../ContactFeature/ContactFeature.swift       | 30 ++++++++++++++++-
 .../Sources/ContactFeature/ContactView.swift  | 23 +++++++++++++
 .../ContactFeatureTests.swift                 | 32 +++++++++++++++++++
 5 files changed, 89 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 1cab487e..4dad2c2b 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -90,6 +90,7 @@ let package = Package(
         .target(name: "CheckContactAuthFeature"),
         .target(name: "ConfirmRequestFeature"),
         .target(name: "ContactFeature"),
+        .target(name: "ContactLookupFeature"),
         .target(name: "ContactsFeature"),
         .target(name: "HomeFeature"),
         .target(name: "MyContactFeature"),
@@ -192,6 +193,7 @@ let package = Package(
         .target(name: "ChatFeature"),
         .target(name: "CheckContactAuthFeature"),
         .target(name: "ConfirmRequestFeature"),
+        .target(name: "ContactLookupFeature"),
         .target(name: "SendRequestFeature"),
         .target(name: "VerifyContactFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 3cf2cb0b..34d9f806 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -37,6 +37,9 @@ extension AppEnvironment {
       db: dbManager.getDB,
       mainQueue: mainQueue,
       bgQueue: bgQueue,
+      lookup: {
+        ContactLookupEnvironment()
+      },
       sendRequest: {
         SendRequestEnvironment(
           messenger: messenger,
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 993796e4..16c6a9a6 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -4,6 +4,7 @@ import CheckContactAuthFeature
 import ComposableArchitecture
 import ComposablePresentation
 import ConfirmRequestFeature
+import ContactLookupFeature
 import Foundation
 import SendRequestFeature
 import VerifyContactFeature
@@ -20,6 +21,7 @@ public struct ContactState: Equatable {
     importUsername: Bool = true,
     importEmail: Bool = true,
     importPhone: Bool = true,
+    lookup: ContactLookupState? = nil,
     sendRequest: SendRequestState? = nil,
     verifyContact: VerifyContactState? = nil,
     confirmRequest: ConfirmRequestState? = nil,
@@ -32,6 +34,7 @@ public struct ContactState: Equatable {
     self.importUsername = importUsername
     self.importEmail = importEmail
     self.importPhone = importPhone
+    self.lookup = lookup
     self.sendRequest = sendRequest
     self.verifyContact = verifyContact
     self.confirmRequest = confirmRequest
@@ -45,6 +48,7 @@ public struct ContactState: Equatable {
   @BindableState public var importUsername: Bool
   @BindableState public var importEmail: Bool
   @BindableState public var importPhone: Bool
+  public var lookup: ContactLookupState?
   public var sendRequest: SendRequestState?
   public var verifyContact: VerifyContactState?
   public var confirmRequest: ConfirmRequestState?
@@ -56,6 +60,9 @@ public enum ContactAction: Equatable, BindableAction {
   case start
   case dbContactFetched(XXModels.Contact?)
   case importFactsTapped
+  case lookupTapped
+  case lookupDismissed
+  case lookup(ContactLookupAction)
   case sendRequestTapped
   case sendRequestDismissed
   case sendRequest(SendRequestAction)
@@ -80,6 +87,7 @@ public struct ContactEnvironment {
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
+    lookup: @escaping () -> ContactLookupEnvironment,
     sendRequest: @escaping () -> SendRequestEnvironment,
     verifyContact: @escaping () -> VerifyContactEnvironment,
     confirmRequest: @escaping () -> ConfirmRequestEnvironment,
@@ -90,6 +98,7 @@ public struct ContactEnvironment {
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
+    self.lookup = lookup
     self.sendRequest = sendRequest
     self.verifyContact = verifyContact
     self.confirmRequest = confirmRequest
@@ -101,6 +110,7 @@ public struct ContactEnvironment {
   public var db: DBManagerGetDB
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
+  public var lookup: () -> ContactLookupEnvironment
   public var sendRequest: () -> SendRequestEnvironment
   public var verifyContact: () -> VerifyContactEnvironment
   public var confirmRequest: () -> ConfirmRequestEnvironment
@@ -115,6 +125,7 @@ extension ContactEnvironment {
     db: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
+    lookup: { .unimplemented },
     sendRequest: { .unimplemented },
     verifyContact: { .unimplemented },
     confirmRequest: { .unimplemented },
@@ -163,6 +174,14 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     .receive(on: env.mainQueue)
     .eraseToEffect()
 
+  case .lookupTapped:
+    state.lookup = ContactLookupState(id: state.id)
+    return .none
+
+  case .lookupDismissed:
+    state.lookup = nil
+    return .none
+
   case .sendRequestTapped:
     if let xxContact = state.xxContact {
       state.sendRequest = SendRequestState(contact: xxContact)
@@ -223,11 +242,20 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     state.chat = nil
     return .none
 
-  case .binding(_), .sendRequest(_), .verifyContact(_), .confirmRequest(_), .checkAuth(_), .chat(_):
+  case .binding(_), .lookup(_), .sendRequest(_),
+      .verifyContact(_), .confirmRequest(_),
+      .checkAuth(_), .chat(_):
     return .none
   }
 }
 .binding()
+.presenting(
+  contactLookupReducer,
+  state: .keyPath(\.lookup),
+  id: .notNil(),
+  action: /ContactAction.lookup,
+  environment: { $0.lookup() }
+)
 .presenting(
   sendRequestReducer,
   state: .keyPath(\.sendRequest),
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 2afc08f5..5bfe475b 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -4,6 +4,7 @@ import CheckContactAuthFeature
 import ComposableArchitecture
 import ComposablePresentation
 import ConfirmRequestFeature
+import ContactLookupFeature
 import SendRequestFeature
 import SwiftUI
 import VerifyContactFeature
@@ -26,6 +27,7 @@ public struct ContactView: View {
     var importUsername: Bool
     var importEmail: Bool
     var importPhone: Bool
+    var canLookup: Bool
     var canSendRequest: Bool
     var canVerifyContact: Bool
     var canConfirmRequest: Bool
@@ -40,6 +42,7 @@ public struct ContactView: View {
       importUsername = state.importUsername
       importEmail = state.importEmail
       importPhone = state.importPhone
+      canLookup = state.dbContact?.id != nil
       canSendRequest = state.xxContact != nil || state.dbContact?.marshaled != nil
       canVerifyContact = state.dbContact?.marshaled != nil
       canConfirmRequest = state.dbContact?.marshaled != nil
@@ -121,6 +124,17 @@ public struct ContactView: View {
           Section {
             ContactAuthStatusView(dbContact.authStatus)
 
+            Button {
+              viewStore.send(.lookupTapped)
+            } label: {
+              HStack {
+                Text("Lookup")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
+            .disabled(!viewStore.canLookup)
+
             Button {
               viewStore.send(.sendRequestTapped)
             } label: {
@@ -186,6 +200,15 @@ public struct ContactView: View {
       }
       .navigationTitle("Contact")
       .task { viewStore.send(.start) }
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.lookup,
+          action: ContactAction.lookup
+        ),
+        mapState: replayNonNil(),
+        onDeactivate: { viewStore.send(.lookupDismissed) },
+        destination: ContactLookupView.init(store:)
+      ))
       .background(NavigationLinkWithStore(
         store.scope(
           state: \.sendRequest,
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index feeccc98..a8f9f144 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -3,6 +3,7 @@ import CheckContactAuthFeature
 import Combine
 import ComposableArchitecture
 import ConfirmRequestFeature
+import ContactLookupFeature
 import CustomDump
 import SendRequestFeature
 import VerifyContactFeature
@@ -99,6 +100,37 @@ final class ContactFeatureTests: XCTestCase {
     XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact])
   }
 
+  func testLookupTapped() {
+    let contactId = "contact-id".data(using: .utf8)!
+    let store = TestStore(
+      initialState: ContactState(
+        id: contactId
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.lookupTapped) {
+      $0.lookup = ContactLookupState(id: contactId)
+    }
+  }
+
+  func testLookupDismissed() {
+    let contactId = "contact-id".data(using: .utf8)!
+    let store = TestStore(
+      initialState: ContactState(
+        id: contactId,
+        lookup: ContactLookupState(id: contactId)
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.lookupDismissed) {
+      $0.lookup = nil
+    }
+  }
+
   func testSendRequestWithDBContact() {
     var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
     dbContact.marshaled = "contact-data".data(using: .utf8)!
-- 
GitLab


From 72fdc4358ce5cb1892532e2a0bd5ecec3a2e173b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 11:08:07 +0200
Subject: [PATCH 12/24] Implement lookup

---
 .../AppFeature/AppEnvironment+Live.swift      |  7 ++-
 .../ContactLookupFeature.swift                | 53 +++++++++++++++----
 .../ContactLookupView.swift                   |  3 --
 .../ContactLookupFeatureTests.swift           | 48 +++++++++++++----
 4 files changed, 87 insertions(+), 24 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 34d9f806..cd5d752e 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -4,6 +4,7 @@ import ChatFeature
 import CheckContactAuthFeature
 import ConfirmRequestFeature
 import ContactFeature
+import ContactLookupFeature
 import ContactsFeature
 import Foundation
 import HomeFeature
@@ -38,7 +39,11 @@ extension AppEnvironment {
       mainQueue: mainQueue,
       bgQueue: bgQueue,
       lookup: {
-        ContactLookupEnvironment()
+        ContactLookupEnvironment(
+          messenger: messenger,
+          mainQueue: mainQueue,
+          bgQueue: bgQueue
+        )
       },
       sendRequest: {
         SendRequestEnvironment(
diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
index 7fd2fc8e..82a775dd 100644
--- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
@@ -1,46 +1,81 @@
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
 
 public struct ContactLookupState: Equatable {
   public init(
     id: Data,
-    isLookingUp: Bool = false
+    isLookingUp: Bool = false,
+    failure: String? = nil
   ) {
     self.id = id
     self.isLookingUp = isLookingUp
+    self.failure = failure
   }
 
   public var id: Data
   public var isLookingUp: Bool
+  public var failure: String?
 }
 
 public enum ContactLookupAction: Equatable {
-  case task
-  case cancelTask
   case lookupTapped
+  case didLookup(XXClient.Contact)
+  case didFail(NSError)
 }
 
 public struct ContactLookupEnvironment {
-  public init() {}
+  public init(
+    messenger: Messenger,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension ContactLookupEnvironment {
-  public static let unimplemented = ContactLookupEnvironment()
+  public static let unimplemented = ContactLookupEnvironment(
+    messenger: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
 public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment>
 { state, action, env in
   switch action {
-  case .task:
-    return .none
+  case .lookupTapped:
+    state.isLookingUp = true
+    return Effect.result { [state] in
+      do {
+        let contact = try env.messenger.lookupContact(id: state.id)
+        return .success(.didLookup(contact))
+      } catch {
+        return .success(.didFail(error as NSError))
+      }
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
-  case .cancelTask:
+  case .didLookup(_):
+    state.isLookingUp = false
     return .none
 
-  case .lookupTapped:
+  case .didFail(let error):
+    state.failure = error.localizedDescription
+    state.isLookingUp = false
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
index 1d1e0208..a8bcd339 100644
--- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
@@ -45,9 +45,6 @@ public struct ContactLookupView: View {
         }
       }
       .navigationTitle("Lookup")
-      .task {
-        await viewStore.send(.task).finish()
-      }
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
index 70847105..34ecabc3 100644
--- a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
@@ -1,31 +1,57 @@
 import ComposableArchitecture
 import XCTest
+import XXClient
 @testable import ContactLookupFeature
 
 final class ContactLookupFeatureTests: XCTestCase {
-  func testTask() {
+  func testLookup() {
+    let id: Data = "1234".data(using: .utf8)!
+    var didLookupId: [Data] = []
+    let lookedUpContact = Contact.unimplemented("123data".data(using: .utf8)!)
+
     let store = TestStore(
-      initialState: ContactLookupState(
-        id: "1234".data(using: .utf8)!
-      ),
+      initialState: ContactLookupState(id: id),
       reducer: contactLookupReducer,
       environment: .unimplemented
     )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.lookupContact.run = { id in
+      didLookupId.append(id)
+      return lookedUpContact
+    }
+
+    store.send(.lookupTapped) {
+      $0.isLookingUp = true
+    }
 
-    store.send(.task)
+    XCTAssertEqual(didLookupId, [id])
 
-    store.send(.cancelTask)
+    store.receive(.didLookup(lookedUpContact)) {
+      $0.isLookingUp = false
+    }
   }
 
-  func testLookup() {
+  func testLookupFailure() {
+    let id: Data = "1234".data(using: .utf8)!
+    let failure = NSError(domain: "test", code: 0)
+
     let store = TestStore(
-      initialState: ContactLookupState(
-        id: "1234".data(using: .utf8)!
-      ),
+      initialState: ContactLookupState(id: id),
       reducer: contactLookupReducer,
       environment: .unimplemented
     )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.lookupContact.run = { _ in throw failure }
+
+    store.send(.lookupTapped) {
+      $0.isLookingUp = true
+    }
 
-    store.send(.lookupTapped)
+    store.receive(.didFail(failure)) {
+      $0.failure = failure.localizedDescription
+      $0.isLookingUp = false
+    }
   }
 }
-- 
GitLab


From f7f7def9c886402dbd54a33257413ee7c7803087 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 11:13:28 +0200
Subject: [PATCH 13/24] Handle looked up contact in ContactFeature

---
 .../Sources/ContactFeature/ContactFeature.swift |  4 ++++
 .../ContactFeatureTests.swift                   | 17 +++++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 16c6a9a6..1113b489 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -182,6 +182,10 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     state.lookup = nil
     return .none
 
+  case .lookup(.didLookup(let xxContact)):
+    state.xxContact = xxContact
+    return .none
+
   case .sendRequestTapped:
     if let xxContact = state.xxContact {
       state.sendRequest = SendRequestState(contact: xxContact)
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index a8f9f144..b16d7884 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -131,6 +131,23 @@ final class ContactFeatureTests: XCTestCase {
     }
   }
 
+  func testLookupDidLookup() {
+    let contactId = "contact-id".data(using: .utf8)!
+    let contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
+    let store = TestStore(
+      initialState: ContactState(
+        id: contactId,
+        lookup: ContactLookupState(id: contactId)
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.lookup(.didLookup(contact))) {
+      $0.xxContact = contact
+    }
+  }
+
   func testSendRequestWithDBContact() {
     var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
     dbContact.marshaled = "contact-data".data(using: .utf8)!
-- 
GitLab


From 618eae3102d18c50a2d3be605e552b8d3381bcf9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 11:16:03 +0200
Subject: [PATCH 14/24] Show failure in ContactLookupView

---
 .../ContactLookupFeature/ContactLookupView.swift       | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
index a8bcd339..d4a759d5 100644
--- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
@@ -13,10 +13,12 @@ public struct ContactLookupView: View {
     init(state: ContactLookupState) {
       id = state.id
       isLookingUp = state.isLookingUp
+      failure = state.failure
     }
 
     var id: Data
     var isLookingUp: Bool
+    var failure: String?
   }
 
   public var body: some View {
@@ -43,6 +45,14 @@ public struct ContactLookupView: View {
         } header: {
           Text("Contact ID")
         }
+
+        if let failure = viewStore.failure {
+          Section {
+            Text(failure)
+          } header: {
+            Text("Error")
+          }
+        }
       }
       .navigationTitle("Lookup")
     }
-- 
GitLab


From 4843a17f8dbac0843cc25502cb0389bd7e26bf1d Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 11:19:54 +0200
Subject: [PATCH 15/24] Dismiss lookup on success

---
 .../xx-messenger/Sources/ContactFeature/ContactFeature.swift     | 1 +
 .../Tests/ContactFeatureTests/ContactFeatureTests.swift          | 1 +
 2 files changed, 2 insertions(+)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 1113b489..dc166e4e 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -184,6 +184,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
 
   case .lookup(.didLookup(let xxContact)):
     state.xxContact = xxContact
+    state.lookup = nil
     return .none
 
   case .sendRequestTapped:
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index b16d7884..c200094f 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -145,6 +145,7 @@ final class ContactFeatureTests: XCTestCase {
 
     store.send(.lookup(.didLookup(contact))) {
       $0.xxContact = contact
+      $0.lookup = nil
     }
   }
 
-- 
GitLab


From 7c1394e8a42c767ebe89c8b638aa9bf87376f865 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 11:34:30 +0200
Subject: [PATCH 16/24] Refactor

---
 .../Sources/AppCore/SharedUI/Data+hexString.swift             | 4 ++--
 .../xx-messenger/Sources/ContactFeature/ContactView.swift     | 2 +-
 .../Sources/ContactLookupFeature/ContactLookupView.swift      | 2 +-
 .../xx-messenger/Sources/MyContactFeature/MyContactView.swift | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift
index 05755e40..e8010b95 100644
--- a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift
+++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift
@@ -1,7 +1,7 @@
 import Foundation
 
 extension Data {
-  public var hexString: String {
-    map { String(format: "%02hhx ", $0) }.joined()
+  public func hexString(bytesSeparator: String = " ") -> String {
+    map { String(format: "%02hhx\(bytesSeparator)", $0) }.joined()
   }
 }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 5bfe475b..daa84903 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -111,7 +111,7 @@ public struct ContactView: View {
 
         if let dbContact = viewStore.dbContact {
           Section {
-            Label(dbContact.id.hexString, systemImage: "number")
+            Label(dbContact.id.hexString(), systemImage: "number")
               .font(.footnote.monospaced())
             Label(dbContact.username ?? "", systemImage: "person")
             Label(dbContact.email ?? "", systemImage: "envelope")
diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
index d4a759d5..6ce83eda 100644
--- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift
@@ -25,7 +25,7 @@ public struct ContactLookupView: View {
     WithViewStore(store, observe: ViewState.init) { viewStore in
       Form {
         Section {
-          Label(viewStore.id.hexString, systemImage: "number")
+          Label(viewStore.id.hexString(), systemImage: "number")
             .font(.footnote.monospaced())
 
           Button {
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index eca55d62..d32a6f68 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -51,7 +51,7 @@ public struct MyContactView: View {
     WithViewStore(store, observe: ViewState.init) { viewStore in
       Form {
         Section {
-          Text(viewStore.contact?.id.hexString ?? "")
+          Text(viewStore.contact?.id.hexString() ?? "")
             .font(.footnote.monospaced())
             .textSelection(.enabled)
         } header: {
-- 
GitLab


From 5eda3f487f97cc5fd22fe5454ebd3fc7811a9691 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 12:08:24 +0200
Subject: [PATCH 17/24] Improve failure handling in ContactLookupFeature

---
 .../Sources/ContactLookupFeature/ContactLookupFeature.swift  | 4 +++-
 .../ContactLookupFeatureTests.swift                          | 5 ++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
index 82a775dd..0b6f92dd 100644
--- a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift
@@ -57,6 +57,7 @@ public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupActio
   switch action {
   case .lookupTapped:
     state.isLookingUp = true
+    state.failure = nil
     return Effect.result { [state] in
       do {
         let contact = try env.messenger.lookupContact(id: state.id)
@@ -71,11 +72,12 @@ public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupActio
 
   case .didLookup(_):
     state.isLookingUp = false
+    state.failure = nil
     return .none
 
   case .didFail(let error):
-    state.failure = error.localizedDescription
     state.isLookingUp = false
+    state.failure = error.localizedDescription
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
index 34ecabc3..76dde8d0 100644
--- a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift
@@ -23,12 +23,14 @@ final class ContactLookupFeatureTests: XCTestCase {
 
     store.send(.lookupTapped) {
       $0.isLookingUp = true
+      $0.failure = nil
     }
 
     XCTAssertEqual(didLookupId, [id])
 
     store.receive(.didLookup(lookedUpContact)) {
       $0.isLookingUp = false
+      $0.failure = nil
     }
   }
 
@@ -47,11 +49,12 @@ final class ContactLookupFeatureTests: XCTestCase {
 
     store.send(.lookupTapped) {
       $0.isLookingUp = true
+      $0.failure = nil
     }
 
     store.receive(.didFail(failure)) {
-      $0.failure = failure.localizedDescription
       $0.isLookingUp = false
+      $0.failure = failure.localizedDescription
     }
   }
 }
-- 
GitLab


From 9f37c6eac49d16fffaa140fd49182f64b6fae65d Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 12:36:02 +0200
Subject: [PATCH 18/24] Improve send request feature

Use client to retrieve my facts, instead of database
---
 .../SendRequestFeature.swift                  | 39 ++++++----
 .../SendRequestFeatureTests.swift             | 77 ++++++++++++-------
 2 files changed, 75 insertions(+), 41 deletions(-)

diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index f2625b91..4204a5ed 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -1,4 +1,5 @@
 import AppCore
+import Combine
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
@@ -40,7 +41,8 @@ public enum SendRequestAction: Equatable, BindableAction {
   case sendSucceeded
   case sendFailed(String)
   case binding(BindingAction<SendRequestState>)
-  case myContactFetched(XXClient.Contact?)
+  case myContactFetched(XXClient.Contact)
+  case myContactFetchFailed(NSError)
 }
 
 public struct SendRequestEnvironment {
@@ -75,25 +77,32 @@ extension SendRequestEnvironment {
 
 public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment>
 { state, action, env in
-  enum DBFetchEffectID {}
-
   switch action {
   case .start:
-    return Effect
-      .catching { try env.messenger.e2e.tryGet().getContact().getId() }
-      .tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) }
-      .flatMap { $0 }
-      .assertNoFailure()
-      .map(\.first)
-      .map { $0?.marshaled.map { XXClient.Contact.live($0) } }
-      .map(SendRequestAction.myContactFetched)
-      .subscribe(on: env.bgQueue)
-      .receive(on: env.mainQueue)
-      .eraseToEffect()
-      .cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
+    return Effect.run { subscriber in
+      do {
+        var contact = try env.messenger.e2e.tryGet().getContact()
+        let facts = try env.messenger.ud.tryGet().getFacts()
+        try contact.setFacts(facts)
+        subscriber.send(.myContactFetched(contact))
+      } catch {
+        subscriber.send(.myContactFetchFailed(error as NSError))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .receive(on: env.mainQueue)
+    .subscribe(on: env.bgQueue)
+    .eraseToEffect()
 
   case .myContactFetched(let contact):
     state.myContact = contact
+    state.failure = nil
+    return .none
+
+  case .myContactFetchFailed(let failure):
+    state.myContact = nil
+    state.failure = failure.localizedDescription
     return .none
 
   case .sendTapped:
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index 9b15f6cd..76812896 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -8,6 +8,22 @@ import XXModels
 
 final class SendRequestFeatureTests: XCTestCase {
   func testStart() {
+    var didSetFactsOnE2EContact: [[Fact]] = []
+    let e2eContactData = "e2e-contact-data".data(using: .utf8)!
+    let e2eContactDataWithFacts = "e2e-contact-data-with-facts".data(using: .utf8)!
+    let e2eContact: XXClient.Contact = {
+      var contact = XXClient.Contact.unimplemented(e2eContactData)
+      contact.setFactsOnContact.run = { data, facts in
+        didSetFactsOnE2EContact.append(facts)
+        return e2eContactDataWithFacts
+      }
+      return contact
+    }()
+    let udFacts = [
+      Fact(type: .username, value: "ud-username"),
+      Fact(type: .email, value: "ud-email"),
+      Fact(type: .phone, value: "ud-phone"),
+    ]
     let store = TestStore(
       initialState: SendRequestState(
         contact: .unimplemented("contact-data".data(using: .utf8)!)
@@ -15,47 +31,56 @@ final class SendRequestFeatureTests: XCTestCase {
       reducer: sendRequestReducer,
       environment: .unimplemented
     )
-
-    var dbDidFetchContacts: [XXModels.Contact.Query] = []
-    let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
-
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
     store.environment.messenger.e2e.get = {
       var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!)
-        contact.getIdFromContact.run = { _ in "my-contact-id".data(using: .utf8)! }
-        return contact
-      }
+      e2e.getContact.run = { e2eContact }
       return e2e
     }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.fetchContactsPublisher.run = { query in
-        dbDidFetchContacts.append(query)
-        return dbContactsPublisher.eraseToAnyPublisher()
-      }
-      return db
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getFacts.run = { udFacts }
+      return ud
     }
 
     store.send(.start)
 
-    XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["my-contact-id".data(using: .utf8)!])])
+    store.receive(.myContactFetched(.unimplemented(e2eContactDataWithFacts))) {
+      $0.myContact = .unimplemented(e2eContactDataWithFacts)
+    }
+  }
 
-    dbContactsPublisher.send([])
+  func testMyContactFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
 
-    store.receive(.myContactFetched(nil))
+    let store = TestStore(
+      initialState: SendRequestState(
+        contact: .unimplemented("contact-data".data(using: .utf8)!)
+      ),
+      reducer: sendRequestReducer,
+      environment: .unimplemented
+    )
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = { .unimplemented(Data()) }
+      return e2e
+    }
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getFacts.run = { throw failure }
+      return ud
+    }
 
-    var myDbContact = XXModels.Contact(id: "my-contact-id".data(using: .utf8)!)
-    myDbContact.marshaled = "my-contact-data".data(using: .utf8)!
-    dbContactsPublisher.send([myDbContact])
+    store.send(.start)
 
-    store.receive(.myContactFetched(.live("my-contact-data".data(using: .utf8)!))) {
-      $0.myContact = .live("my-contact-data".data(using: .utf8)!)
+    store.receive(.myContactFetchFailed(failure as NSError)) {
+      $0.myContact = nil
+      $0.failure = failure.localizedDescription
     }
-
-    dbContactsPublisher.send(completion: .finished)
   }
 
   func testSendRequest() {
-- 
GitLab


From 3b6d4db726a7c753de4c8113e5ba61a35ac52da9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 12:44:30 +0200
Subject: [PATCH 19/24] Improve form on BackupView

---
 .../Sources/BackupFeature/BackupFeature.swift     |  8 ++++++++
 .../Sources/BackupFeature/BackupView.swift        | 15 ++++++++++++---
 .../BackupFeatureTests/BackupFeatureTests.swift   |  4 ++++
 3 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
index f672b312..431dd8ea 100644
--- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
@@ -7,6 +7,10 @@ import XXMessengerClient
 import XXModels
 
 public struct BackupState: Equatable {
+  public enum Field: String, Hashable {
+    case passphrase
+  }
+
   public enum Error: String, Swift.Error, Equatable {
     case dbContactNotFound
     case dbContactUsernameMissing
@@ -19,6 +23,7 @@ public struct BackupState: Equatable {
     isStopping: Bool = false,
     backup: BackupStorage.Backup? = nil,
     alert: AlertState<BackupAction>? = nil,
+    focusedField: Field? = nil,
     passphrase: String = "",
     isExporting: Bool = false,
     exportData: Data? = nil
@@ -29,6 +34,7 @@ public struct BackupState: Equatable {
     self.isStopping = isStopping
     self.backup = backup
     self.alert = alert
+    self.focusedField = focusedField
     self.passphrase = passphrase
     self.isExporting = isExporting
     self.exportData = exportData
@@ -40,6 +46,7 @@ public struct BackupState: Equatable {
   public var isStopping: Bool
   public var backup: BackupStorage.Backup?
   public var alert: AlertState<BackupAction>?
+  @BindableState public var focusedField: Field?
   @BindableState public var passphrase: String
   @BindableState public var isExporting: Bool
   public var exportData: Data?
@@ -119,6 +126,7 @@ public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment>
 
   case .startTapped:
     state.isStarting = true
+    state.focusedField = nil
     return Effect.run { [state] subscriber in
       do {
         let e2e: E2E = try env.messenger.e2e.tryGet()
diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
index 54fc6567..89510b2f 100644
--- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift
@@ -8,6 +8,7 @@ public struct BackupView: View {
   }
 
   let store: Store<BackupState, BackupAction>
+  @FocusState var focusedField: BackupState.Field?
 
   struct ViewState: Equatable {
     struct Backup: Equatable {
@@ -23,6 +24,7 @@ public struct BackupView: View {
       backup = state.backup.map { backup in
         Backup(date: backup.date, size: backup.data.count)
       }
+      focusedField = state.focusedField
       passphrase = state.passphrase
       isExporting = state.isExporting
       exportData = state.exportData
@@ -34,6 +36,7 @@ public struct BackupView: View {
     var isStopping: Bool
     var isLoading: Bool { isStarting || isResuming || isStopping }
     var backup: Backup?
+    var focusedField: BackupState.Field?
     var passphrase: String
     var isExporting: Bool
     var exportData: Data?
@@ -57,9 +60,9 @@ public struct BackupView: View {
         )
       }
       .navigationTitle("Backup")
-      .task {
-        await viewStore.send(.task).finish()
-      }
+      .task { await viewStore.send(.task).finish() }
+      .onChange(of: viewStore.focusedField) { focusedField = $0 }
+      .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
     }
   }
 
@@ -75,6 +78,11 @@ public struct BackupView: View {
         prompt: Text("Backup passphrase"),
         label: { Text("Backup passphrase") }
       )
+      .textContentType(.password)
+      .textInputAutocapitalization(.never)
+      .disableAutocorrection(true)
+      .focused($focusedField, equals: .passphrase)
+
       Button {
         viewStore.send(.startTapped)
       } label: {
@@ -91,6 +99,7 @@ public struct BackupView: View {
     } header: {
       Text("New backup")
     }
+    .disabled(viewStore.isStarting)
   }
 
   @ViewBuilder func backupSection(
diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
index 21afbd54..b916ddc0 100644
--- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
@@ -101,6 +101,9 @@ final class BackupFeatureTests: XCTestCase {
     }
 
     actions = []
+    store.send(.set(\.$focusedField, .passphrase)) {
+      $0.focusedField = .passphrase
+    }
     store.send(.set(\.$passphrase, passphrase)) {
       $0.passphrase = passphrase
     }
@@ -110,6 +113,7 @@ final class BackupFeatureTests: XCTestCase {
     actions = []
     store.send(.startTapped) {
       $0.isStarting = true
+      $0.focusedField = nil
     }
 
     XCTAssertNoDifference(actions, [
-- 
GitLab


From 8718aa66750592c3efb69294c9a1851394c86d23 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 12:47:36 +0200
Subject: [PATCH 20/24] Restore username from facts, not from params

---
 .../Sources/RestoreFeature/RestoreFeature.swift          | 2 +-
 .../Tests/RestoreFeatureTests/RestoreFeatureTests.swift  | 9 +++++----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
index 455f8989..4d372202 100644
--- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
+++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift
@@ -135,7 +135,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
         let facts = try env.messenger.ud.tryGet().getFacts()
         try env.db().saveContact(Contact(
           id: try env.messenger.e2e.tryGet().getContact().getId(),
-          username: result.restoredParams.username,
+          username: facts.get(.username)?.value,
           email: facts.get(.email)?.value,
           phone: facts.get(.phone)?.value,
           createdAt: env.now()
diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
index 8cfdb3d6..716d7650 100644
--- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift
@@ -80,11 +80,12 @@ final class RestoreFeatureTests: XCTestCase {
     let backupData = "backup-data".data(using: .utf8)!
     let backupPassphrase = "backup-passphrase"
     let restoredFacts = [
-      Fact(type: .email, value: "restored-email"),
-      Fact(type: .phone, value: "restored-phone"),
+      Fact(type: .username, value: "restored-fact-username"),
+      Fact(type: .email, value: "restored-fact-email"),
+      Fact(type: .phone, value: "restored-fact-phone"),
     ]
     let restoreResult = MessengerRestoreBackup.Result(
-      restoredParams: BackupParams(username: "restored-username"),
+      restoredParams: BackupParams(username: "restored-param-username"),
       restoredContacts: [
         "contact-1-id".data(using: .utf8)!,
         "contact-2-id".data(using: .utf8)!,
@@ -162,7 +163,7 @@ final class RestoreFeatureTests: XCTestCase {
     XCTAssertNoDifference(didSaveContact, [
       Contact(
         id: contactId,
-        username: restoreResult.restoredParams.username,
+        username: restoredFacts.get(.username)?.value,
         email: restoredFacts.get(.email)?.value,
         phone: restoredFacts.get(.phone)?.value,
         createdAt: now
-- 
GitLab


From b783ad91e7323598ce6918970a52c340e06dde10 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 15:44:36 +0200
Subject: [PATCH 21/24] Use MessengerMyContact in BackupFeature

---
 Examples/xx-messenger/Package.swift           |   2 -
 .../AppFeature/AppEnvironment+Live.swift      |   1 -
 .../Sources/BackupFeature/BackupFeature.swift |  21 +---
 .../BackupFeatureTests.swift                  | 109 +++++-------------
 4 files changed, 33 insertions(+), 100 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 4dad2c2b..d863e016 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -119,10 +119,8 @@ let package = Package(
     .target(
       name: "BackupFeature",
       dependencies: [
-        .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
-        .product(name: "XXModels", package: "client-ios-db"),
       ],
       swiftSettings: swiftSettings
     ),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index cd5d752e..ce52387f 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -164,7 +164,6 @@ extension AppEnvironment {
           backup: {
             BackupEnvironment(
               messenger: messenger,
-              db: dbManager.getDB,
               backupStorage: backupStorage,
               mainQueue: mainQueue,
               bgQueue: bgQueue
diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
index 431dd8ea..8010f17d 100644
--- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
+++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift
@@ -1,10 +1,8 @@
-import AppCore
 import Combine
 import ComposableArchitecture
 import Foundation
 import XXClient
 import XXMessengerClient
-import XXModels
 
 public struct BackupState: Equatable {
   public enum Field: String, Hashable {
@@ -12,8 +10,7 @@ public struct BackupState: Equatable {
   }
 
   public enum Error: String, Swift.Error, Equatable {
-    case dbContactNotFound
-    case dbContactUsernameMissing
+    case contactUsernameMissing
   }
 
   public init(
@@ -71,20 +68,17 @@ public enum BackupAction: Equatable, BindableAction {
 public struct BackupEnvironment {
   public init(
     messenger: Messenger,
-    db: DBManagerGetDB,
     backupStorage: BackupStorage,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>
   ) {
     self.messenger = messenger
-    self.db = db
     self.backupStorage = backupStorage
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
   }
 
   public var messenger: Messenger
-  public var db: DBManagerGetDB
   public var backupStorage: BackupStorage
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
@@ -94,7 +88,6 @@ public struct BackupEnvironment {
 extension BackupEnvironment {
   public static let unimplemented = BackupEnvironment(
     messenger: .unimplemented,
-    db: .unimplemented,
     backupStorage: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented
@@ -129,15 +122,9 @@ public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment>
     state.focusedField = nil
     return Effect.run { [state] subscriber in
       do {
-        let e2e: E2E = try env.messenger.e2e.tryGet()
-        let contactID = try e2e.getContact().getId()
-        let db = try env.db()
-        let query = XXModels.Contact.Query(id: [contactID])
-        guard let contact = try db.fetchContacts(query).first else {
-          throw BackupState.Error.dbContactNotFound
-        }
-        guard let username = contact.username else {
-          throw BackupState.Error.dbContactUsernameMissing
+        let contact = try env.messenger.myContact(includeFacts: .types([.username]))
+        guard let username = try contact.getFact(.username)?.value else {
+          throw BackupState.Error.contactUsernameMissing
         }
         try env.messenger.startBackup(
           password: state.passphrase,
diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
index b916ddc0..d0c69eb8 100644
--- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift
@@ -2,7 +2,6 @@ import ComposableArchitecture
 import XCTest
 import XXClient
 import XXMessengerClient
-import XXModels
 @testable import BackupFeature
 
 final class BackupFeatureTests: XCTestCase {
@@ -65,11 +64,7 @@ final class BackupFeatureTests: XCTestCase {
   func testStartBackup() {
     var actions: [Action]!
     var isBackupRunning: [Bool] = [true]
-    let contactID = "contact-id".data(using: .utf8)!
-    let dbContact = XXModels.Contact(
-      id: contactID,
-      username: "db-contact-username"
-    )
+    let username = "test-username"
     let passphrase = "backup-password"
 
     let store = TestStore(
@@ -79,26 +74,18 @@ final class BackupFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
+    store.environment.messenger.myContact.run = { includeFacts in
+      actions.append(.didGetMyContact(includingFacts: includeFacts))
+      var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
+      contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: username)] }
+      return contact
+    }
     store.environment.messenger.startBackup.run = { passphrase, params in
       actions.append(.didStartBackup(passphrase: passphrase, params: params))
     }
     store.environment.messenger.isBackupRunning.run = {
       isBackupRunning.removeFirst()
     }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented(Data())
-        contact.getIdFromContact.run = { _ in contactID }
-        return contact
-      }
-      return e2e
-    }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.fetchContacts.run = { _ in return [dbContact] }
-      return db
-    }
 
     actions = []
     store.send(.set(\.$focusedField, .passphrase)) {
@@ -117,9 +104,12 @@ final class BackupFeatureTests: XCTestCase {
     }
 
     XCTAssertNoDifference(actions, [
+      .didGetMyContact(
+        includingFacts: .types([.username])
+      ),
       .didStartBackup(
         passphrase: passphrase,
-        params: .init(username: dbContact.username!)
+        params: .init(username: username)
       )
     ])
 
@@ -130,9 +120,8 @@ final class BackupFeatureTests: XCTestCase {
     }
   }
 
-  func testStartBackupWithoutDbContact() {
+  func testStartBackupWithoutContactUsername() {
     var isBackupRunning: [Bool] = [false]
-    let contactID = "contact-id".data(using: .utf8)!
 
     let store = TestStore(
       initialState: BackupState(
@@ -143,29 +132,20 @@ final class BackupFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
+    store.environment.messenger.myContact.run = { _ in
+      var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
+      contact.getFactsFromContact.run = { _ in [] }
+      return contact
+    }
     store.environment.messenger.isBackupRunning.run = {
       isBackupRunning.removeFirst()
     }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented(Data())
-        contact.getIdFromContact.run = { _ in contactID }
-        return contact
-      }
-      return e2e
-    }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.fetchContacts.run = { _ in [] }
-      return db
-    }
 
     store.send(.startTapped) {
       $0.isStarting = true
     }
 
-    let failure = BackupState.Error.dbContactNotFound
+    let failure = BackupState.Error.contactUsernameMissing
     store.receive(.didStart(failure: failure as NSError)) {
       $0.isRunning = false
       $0.isStarting = false
@@ -173,13 +153,10 @@ final class BackupFeatureTests: XCTestCase {
     }
   }
 
-  func testStartBackupWithoutDbContactUsername() {
+  func testStartBackupMyContactFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
     var isBackupRunning: [Bool] = [false]
-    let contactID = "contact-id".data(using: .utf8)!
-    let dbContact = XXModels.Contact(
-      id: contactID,
-      username: nil
-    )
 
     let store = TestStore(
       initialState: BackupState(
@@ -190,29 +167,15 @@ final class BackupFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
+    store.environment.messenger.myContact.run = { _ in throw failure }
     store.environment.messenger.isBackupRunning.run = {
       isBackupRunning.removeFirst()
     }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented(Data())
-        contact.getIdFromContact.run = { _ in contactID }
-        return contact
-      }
-      return e2e
-    }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.fetchContacts.run = { _ in [dbContact] }
-      return db
-    }
 
     store.send(.startTapped) {
       $0.isStarting = true
     }
 
-    let failure = BackupState.Error.dbContactUsernameMissing
     store.receive(.didStart(failure: failure as NSError)) {
       $0.isRunning = false
       $0.isStarting = false
@@ -220,15 +183,10 @@ final class BackupFeatureTests: XCTestCase {
     }
   }
 
-  func testStartBackupFailure() {
+  func testStartBackupStartFailure() {
     struct Failure: Error {}
     let failure = Failure()
     var isBackupRunning: [Bool] = [false]
-    let contactID = "contact-id".data(using: .utf8)!
-    let dbContact = XXModels.Contact(
-      id: contactID,
-      username: "db-contact-username"
-    )
 
     let store = TestStore(
       initialState: BackupState(
@@ -239,26 +197,17 @@ final class BackupFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
+    store.environment.messenger.myContact.run = { _ in
+      var contact = Contact.unimplemented("data".data(using: .utf8)!)
+      contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: "username")] }
+      return contact
+    }
     store.environment.messenger.startBackup.run = { _, _ in
       throw failure
     }
     store.environment.messenger.isBackupRunning.run = {
       isBackupRunning.removeFirst()
     }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact: XXClient.Contact = .unimplemented(Data())
-        contact.getIdFromContact.run = { _ in contactID }
-        return contact
-      }
-      return e2e
-    }
-    store.environment.db.run = {
-      var db: Database = .unimplemented
-      db.fetchContacts.run = { _ in return [dbContact] }
-      return db
-    }
 
     store.send(.startTapped) {
       $0.isStarting = true
@@ -463,5 +412,5 @@ private enum Action: Equatable {
   case didResumeBackup
   case didStopBackup
   case didRemoveBackup
-  case didFetchContacts(XXModels.Contact.Query)
+  case didGetMyContact(includingFacts: MessengerMyContact.IncludeFacts?)
 }
-- 
GitLab


From 20e23f555e33a5f38e77cfe9201e6f8afe2e826b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 16:10:53 +0200
Subject: [PATCH 22/24] Use MessengerMyContact in RegisterFeature

---
 .../RegisterFeature/RegisterFeature.swift     |  19 ++-
 .../RegisterFeatureTests.swift                | 153 +++++++++++++-----
 2 files changed, 131 insertions(+), 41 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
index cb43c430..9b9754c6 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
@@ -7,6 +7,10 @@ import XXMessengerClient
 import XXModels
 
 public struct RegisterState: Equatable {
+  public enum Error: Swift.Error, Equatable {
+    case usernameMismatch(registering: String, registered: String?)
+  }
+
   public enum Field: String, Hashable {
     case username
   }
@@ -82,14 +86,22 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
       do {
         let db = try env.db()
         try env.messenger.register(username: username)
-        var contact = try env.messenger.e2e.tryGet().getContact()
-        try contact.setFact(.username, username)
+        let contact = try env.messenger.myContact()
+        let facts = try contact.getFacts()
         try db.saveContact(Contact(
           id: try contact.getId(),
           marshaled: contact.data,
-          username: username,
+          username: facts.get(.username)?.value,
+          email: facts.get(.email)?.value,
+          phone: facts.get(.phone)?.value,
           createdAt: env.now()
         ))
+        guard facts.get(.username)?.value == username else {
+          throw RegisterState.Error.usernameMismatch(
+            registering: username,
+            registered: facts.get(.username)?.value
+          )
+        }
         fulfill(.success(.finished))
       }
       catch {
@@ -106,6 +118,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
     return .none
 
   case .finished:
+    state.isRegistering = false
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
index b156335b..e0ffc5ef 100644
--- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
@@ -8,38 +8,40 @@ import XXModels
 
 final class RegisterFeatureTests: XCTestCase {
   func testRegister() throws {
+    let now = Date()
+    let username = "registering-username"
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let myContactData = "my-contact-data".data(using: .utf8)!
+    let myContactUsername = username
+    let myContactEmail = "my-contact-email"
+    let myContactPhone = "my-contact-phone"
+    let myContactFacts = [
+      Fact(type: .username, value: myContactUsername),
+      Fact(type: .email, value: myContactEmail),
+      Fact(type: .phone, value: myContactPhone),
+    ]
+
+    var messengerDidRegisterUsername: [String] = []
+    var didGetMyContact: [MessengerMyContact.IncludeFacts?] = []
+    var dbDidSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
       initialState: RegisterState(),
       reducer: registerReducer,
       environment: .unimplemented
     )
-
-    let now = Date()
-    let mainQueue = DispatchQueue.test
-    let bgQueue = DispatchQueue.test
-    var didSetFactsOnContact: [[XXClient.Fact]] = []
-    var dbDidSaveContact: [XXModels.Contact] = []
-    var messengerDidRegisterUsername: [String] = []
-
     store.environment.now = { now }
-    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
-    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
     store.environment.messenger.register.run = { username in
       messengerDidRegisterUsername.append(username)
     }
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = {
-        var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!)
-        contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
-        contact.getFactsFromContact.run = { _ in [] }
-        contact.setFactsOnContact.run = { data, facts in
-          didSetFactsOnContact.append(facts)
-          return data
-        }
-        return contact
-      }
-      return e2e
+    store.environment.messenger.myContact.run = { includeFacts in
+      didGetMyContact.append(includeFacts)
+      var contact = XXClient.Contact.unimplemented(myContactData)
+      contact.getIdFromContact.run = { _ in myContactId }
+      contact.getFactsFromContact.run = { _ in myContactFacts }
+      return contact
     }
     store.environment.db.run = {
       var db: Database = .unimplemented
@@ -50,33 +52,34 @@ final class RegisterFeatureTests: XCTestCase {
       return db
     }
 
-    store.send(.set(\.$username, "NewUser")) {
-      $0.username = "NewUser"
+    store.send(.set(\.$focusedField, .username)) {
+      $0.focusedField = .username
+    }
+
+    store.send(.set(\.$username, myContactUsername)) {
+      $0.username = myContactUsername
     }
 
     store.send(.registerTapped) {
+      $0.focusedField = nil
       $0.isRegistering = true
     }
 
-    XCTAssertNoDifference(messengerDidRegisterUsername, [])
-    XCTAssertNoDifference(dbDidSaveContact, [])
-
-    bgQueue.advance()
-
-    XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"])
-    XCTAssertNoDifference(didSetFactsOnContact, [[Fact(type: .username, value: "NewUser")]])
+    XCTAssertNoDifference(messengerDidRegisterUsername, [username])
     XCTAssertNoDifference(dbDidSaveContact, [
       XXModels.Contact(
-        id: "contact-id".data(using: .utf8)!,
-        marshaled: "contact-data".data(using: .utf8)!,
-        username: "NewUser",
+        id: myContactId,
+        marshaled: myContactData,
+        username: myContactUsername,
+        email: myContactEmail,
+        phone: myContactPhone,
         createdAt: now
       )
     ])
 
-    mainQueue.advance()
-
-    store.receive(.finished)
+    store.receive(.finished) {
+      $0.isRegistering = false
+    }
   }
 
   func testGetDbFailure() throws {
@@ -139,4 +142,78 @@ final class RegisterFeatureTests: XCTestCase {
       $0.failure = error.localizedDescription
     }
   }
+
+  func testRegisterUsernameMismatchFailure() throws {
+    let now = Date()
+    let username = "registering-username"
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let myContactData = "my-contact-data".data(using: .utf8)!
+    let myContactUsername = "my-contact-username"
+    let myContactEmail = "my-contact-email"
+    let myContactPhone = "my-contact-phone"
+    let myContactFacts = [
+      Fact(type: .username, value: myContactUsername),
+      Fact(type: .email, value: myContactEmail),
+      Fact(type: .phone, value: myContactPhone),
+    ]
+
+    var messengerDidRegisterUsername: [String] = []
+    var didGetMyContact: [MessengerMyContact.IncludeFacts?] = []
+    var dbDidSaveContact: [XXModels.Contact] = []
+
+    let store = TestStore(
+      initialState: RegisterState(
+        username: username
+      ),
+      reducer: registerReducer,
+      environment: .unimplemented
+    )
+    store.environment.now = { now }
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.register.run = { username in
+      messengerDidRegisterUsername.append(username)
+    }
+    store.environment.messenger.myContact.run = { includeFacts in
+      didGetMyContact.append(includeFacts)
+      var contact = XXClient.Contact.unimplemented(myContactData)
+      contact.getIdFromContact.run = { _ in myContactId }
+      contact.getFactsFromContact.run = { _ in myContactFacts }
+      return contact
+    }
+    store.environment.db.run = {
+      var db: Database = .unimplemented
+      db.saveContact.run = { contact in
+        dbDidSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.registerTapped) {
+      $0.focusedField = nil
+      $0.isRegistering = true
+    }
+
+    XCTAssertNoDifference(messengerDidRegisterUsername, [username])
+    XCTAssertNoDifference(dbDidSaveContact, [
+      XXModels.Contact(
+        id: myContactId,
+        marshaled: myContactData,
+        username: myContactUsername,
+        email: myContactEmail,
+        phone: myContactPhone,
+        createdAt: now
+      )
+    ])
+
+    let failure = RegisterState.Error.usernameMismatch(
+      registering: username,
+      registered: myContactUsername
+    )
+    store.receive(.failed(failure.localizedDescription)) {
+      $0.isRegistering = false
+      $0.failure = failure.localizedDescription
+    }
+  }
 }
-- 
GitLab


From bb1e81e4f005e7c7263f4585f5b58e4b11a973b5 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 16:17:45 +0200
Subject: [PATCH 23/24] Use MessengerMyContact in SendRequestFeature

---
 .../SendRequestFeature.swift                  |  4 +-
 .../SendRequestFeatureTests.swift             | 48 +++++--------------
 2 files changed, 12 insertions(+), 40 deletions(-)

diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index 4204a5ed..18075179 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -81,9 +81,7 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen
   case .start:
     return Effect.run { subscriber in
       do {
-        var contact = try env.messenger.e2e.tryGet().getContact()
-        let facts = try env.messenger.ud.tryGet().getFacts()
-        try contact.setFacts(facts)
+        let contact = try env.messenger.myContact()
         subscriber.send(.myContactFetched(contact))
       } catch {
         subscriber.send(.myContactFetchFailed(error as NSError))
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index 76812896..4c68abab 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -3,27 +3,16 @@ import ComposableArchitecture
 import CustomDump
 import XCTest
 import XXClient
+import XXMessengerClient
 import XXModels
 @testable import SendRequestFeature
 
 final class SendRequestFeatureTests: XCTestCase {
   func testStart() {
-    var didSetFactsOnE2EContact: [[Fact]] = []
-    let e2eContactData = "e2e-contact-data".data(using: .utf8)!
-    let e2eContactDataWithFacts = "e2e-contact-data-with-facts".data(using: .utf8)!
-    let e2eContact: XXClient.Contact = {
-      var contact = XXClient.Contact.unimplemented(e2eContactData)
-      contact.setFactsOnContact.run = { data, facts in
-        didSetFactsOnE2EContact.append(facts)
-        return e2eContactDataWithFacts
-      }
-      return contact
-    }()
-    let udFacts = [
-      Fact(type: .username, value: "ud-username"),
-      Fact(type: .email, value: "ud-email"),
-      Fact(type: .phone, value: "ud-phone"),
-    ]
+    let myContact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!)
+
+    var didGetMyContact: [MessengerMyContact.IncludeFacts?] = []
+
     let store = TestStore(
       initialState: SendRequestState(
         contact: .unimplemented("contact-data".data(using: .utf8)!)
@@ -33,21 +22,15 @@ final class SendRequestFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = { e2eContact }
-      return e2e
-    }
-    store.environment.messenger.ud.get = {
-      var ud: UserDiscovery = .unimplemented
-      ud.getFacts.run = { udFacts }
-      return ud
+    store.environment.messenger.myContact.run = { includeFacts in
+      didGetMyContact.append(includeFacts)
+      return myContact
     }
 
     store.send(.start)
 
-    store.receive(.myContactFetched(.unimplemented(e2eContactDataWithFacts))) {
-      $0.myContact = .unimplemented(e2eContactDataWithFacts)
+    store.receive(.myContactFetched(myContact)) {
+      $0.myContact = myContact
     }
   }
 
@@ -64,16 +47,7 @@ final class SendRequestFeatureTests: XCTestCase {
     )
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
-    store.environment.messenger.e2e.get = {
-      var e2e: E2E = .unimplemented
-      e2e.getContact.run = { .unimplemented(Data()) }
-      return e2e
-    }
-    store.environment.messenger.ud.get = {
-      var ud: UserDiscovery = .unimplemented
-      ud.getFacts.run = { throw failure }
-      return ud
-    }
+    store.environment.messenger.myContact.run = { _ in throw failure }
 
     store.send(.start)
 
-- 
GitLab


From dcef4f39c15953cd0e035539d48daf10100b6f7d Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Fri, 30 Sep 2022 16:27:18 +0200
Subject: [PATCH 24/24] Load username fact in MyContactFeature

---
 .../Sources/MyContactFeature/MyContactFeature.swift            | 1 +
 .../Tests/MyContactFeatureTests/MyContactFeatureTests.swift    | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index b25d0212..434a1aca 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -285,6 +285,7 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
         let contactId = try env.messenger.e2e.tryGet().getContact().getId()
         if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
           let facts = try env.messenger.ud.tryGet().getFacts()
+          dbContact.username = facts.get(.username)?.value
           dbContact.email = facts.get(.email)?.value
           dbContact.phone = facts.get(.phone)?.value
           try env.db().saveContact(dbContact)
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 0e553296..830e9715 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -645,6 +645,7 @@ final class MyContactFeatureTests: XCTestCase {
   func testLoadFactsFromClient() {
     let contactId = "contact-id".data(using: .utf8)!
     let dbContact = XXModels.Contact(id: contactId)
+    let username = "user234"
     let email = "test@email.com"
     let phone = "123456789"
 
@@ -672,6 +673,7 @@ final class MyContactFeatureTests: XCTestCase {
       var ud: UserDiscovery = .unimplemented
       ud.getFacts.run = {
         [
+          Fact(type: .username, value: username),
           Fact(type: .email, value: email),
           Fact(type: .phone, value: phone),
         ]
@@ -697,6 +699,7 @@ final class MyContactFeatureTests: XCTestCase {
 
     XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])])
     var expectedSavedContact = dbContact
+    expectedSavedContact.username = username
     expectedSavedContact.email = email
     expectedSavedContact.phone = phone
     XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
-- 
GitLab