From 2e50e9c88e80c549d88e2bdf56fffd495f8c1d11 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 6 Sep 2022 23:51:12 +0200
Subject: [PATCH 01/29] Add UserSearchResultFeature

---
 .../UserSearchResultFeature.swift             | 52 +++++++++++++++
 .../UserSearchResultView.swift                | 63 +++++++++++++++++++
 .../UserSearchResultFeatureTests.swift        | 32 ++++++++++
 3 files changed, 147 insertions(+)
 create mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
 create mode 100644 Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift

diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
new file mode 100644
index 00000000..a224384b
--- /dev/null
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -0,0 +1,52 @@
+import ComposableArchitecture
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+
+public struct UserSearchResultState: Equatable, Identifiable {
+  public init(
+    id: Data,
+    contact: Contact,
+    username: String? = nil,
+    email: String? = nil,
+    phone: String? = nil
+  ) {
+    self.id = id
+    self.contact = contact
+    self.username = username
+    self.email = email
+    self.phone = phone
+  }
+
+  public var id: Data
+  public var contact: XXClient.Contact
+  public var username: String?
+  public var email: String?
+  public var phone: String?
+}
+
+public enum UserSearchResultAction: Equatable {
+  case start
+}
+
+public struct UserSearchResultEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension UserSearchResultEnvironment {
+  public static let unimplemented = UserSearchResultEnvironment()
+}
+#endif
+
+public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    let facts = (try? state.contact.getFacts()) ?? []
+    state.username = facts.first(where: { $0.type == 0 })?.fact
+    state.email = facts.first(where: { $0.type == 1 })?.fact
+    state.phone = facts.first(where: { $0.type == 2 })?.fact
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
new file mode 100644
index 00000000..b1a6e267
--- /dev/null
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
@@ -0,0 +1,63 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct UserSearchResultView: View {
+  public init(store: Store<UserSearchResultState, UserSearchResultAction>) {
+    self.store = store
+  }
+
+  let store: Store<UserSearchResultState, UserSearchResultAction>
+
+  struct ViewState: Equatable {
+    var username: String?
+    var email: String?
+    var phone: String?
+
+    init(state: UserSearchResultState) {
+      username = state.username
+      email = state.email
+      phone = state.phone
+    }
+
+    var isEmpty: Bool {
+      username == nil && email == nil && phone == nil
+    }
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Section {
+        if viewStore.isEmpty {
+          Image(systemName: "questionmark")
+            .frame(maxWidth: .infinity)
+        } else {
+          if let username = viewStore.username {
+            Text(username)
+          }
+          if let email = viewStore.email {
+            Text(email)
+          }
+          if let phone = viewStore.phone {
+            Text(phone)
+          }
+        }
+      }
+      .task { viewStore.send(.start) }
+    }
+  }
+}
+
+#if DEBUG
+public struct UserSearchResultView_Previews: PreviewProvider {
+  public static var previews: some View {
+    UserSearchResultView(store: Store(
+      initialState: UserSearchResultState(
+        id: "contact-id".data(using: .utf8)!,
+        contact: .unimplemented("contact-data".data(using: .utf8)!)
+      ),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
new file mode 100644
index 00000000..b4f68452
--- /dev/null
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
@@ -0,0 +1,32 @@
+import ComposableArchitecture
+import XCTest
+import XXClient
+@testable import UserSearchFeature
+
+final class UserSearchResultFeatureTests: XCTestCase {
+  func testStart() {
+    var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
+    contact.getFactsFromContact.run = { _ in
+      [
+        Fact(fact: "contact-username", type: 0),
+        Fact(fact: "contact-email", type: 1),
+        Fact(fact: "contact-phone", type: 2),
+      ]
+    }
+
+    let store = TestStore(
+      initialState: UserSearchResultState(
+        id: "contact-id".data(using: .utf8)!,
+        contact: contact
+      ),
+      reducer: userSearchResultReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start) {
+      $0.username = "contact-username"
+      $0.email = "contact-email"
+      $0.phone = "contact-phone"
+    }
+  }
+}
-- 
GitLab


From 0c2d1aca81121fd603f76715f5e2b7f21858a17c Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 00:07:54 +0200
Subject: [PATCH 02/29] Embed UserSearchResultFeature in UserSearchFeature

---
 Examples/xx-messenger/Package.swift           |  1 +
 .../AppFeature/AppEnvironment+Live.swift      |  5 +-
 .../UserSearchFeature/UserSearchFeature.swift | 53 +++++++------------
 .../UserSearchFeature/UserSearchView.swift    | 26 +++------
 .../UserSearchFeatureTests.swift              | 35 ++----------
 5 files changed, 33 insertions(+), 87 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index f9ceb75f..565dccc6 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -141,6 +141,7 @@ let package = Package(
       name: "UserSearchFeature",
       dependencies: [
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
       ],
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 5d777780..00415b43 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -50,7 +50,10 @@ extension AppEnvironment {
             UserSearchEnvironment(
               messenger: messenger,
               mainQueue: mainQueue,
-              bgQueue: bgQueue
+              bgQueue: bgQueue,
+              result: {
+                UserSearchResultEnvironment()
+              }
             )
           }
         )
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 86a13c0e..9d16d8f5 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -1,4 +1,5 @@
 import ComposableArchitecture
+import ComposablePresentation
 import Foundation
 import XCTestDynamicOverlay
 import XXClient
@@ -11,34 +12,12 @@ public struct UserSearchState: Equatable {
     case phone
   }
 
-  public struct Result: Equatable, Identifiable {
-    public init(
-      id: Data,
-      contact: Contact,
-      username: String? = nil,
-      email: String? = nil,
-      phone: String? = nil
-    ) {
-      self.id = id
-      self.contact = contact
-      self.username = username
-      self.email = email
-      self.phone = phone
-    }
-
-    public var id: Data
-    public var contact: XXClient.Contact
-    public var username: String?
-    public var email: String?
-    public var phone: String?
-  }
-
   public init(
     focusedField: Field? = nil,
     query: MessengerSearchUsers.Query = .init(),
     isSearching: Bool = false,
     failure: String? = nil,
-    results: IdentifiedArrayOf<Result> = []
+    results: IdentifiedArrayOf<UserSearchResultState> = []
   ) {
     self.focusedField = focusedField
     self.query = query
@@ -51,7 +30,7 @@ public struct UserSearchState: Equatable {
   @BindableState public var query: MessengerSearchUsers.Query
   public var isSearching: Bool
   public var failure: String?
-  public var results: IdentifiedArrayOf<Result>
+  public var results: IdentifiedArrayOf<UserSearchResultState>
 }
 
 public enum UserSearchAction: Equatable, BindableAction {
@@ -59,22 +38,26 @@ public enum UserSearchAction: Equatable, BindableAction {
   case didFail(String)
   case didSucceed([Contact])
   case binding(BindingAction<UserSearchState>)
+  case result(id: UserSearchResultState.ID, action: UserSearchResultAction)
 }
 
 public struct UserSearchEnvironment {
   public init(
     messenger: Messenger,
     mainQueue: AnySchedulerOf<DispatchQueue>,
-    bgQueue: AnySchedulerOf<DispatchQueue>
+    bgQueue: AnySchedulerOf<DispatchQueue>,
+    result: @escaping () -> UserSearchResultEnvironment
   ) {
     self.messenger = messenger
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
+    self.result = result
   }
 
   public var messenger: Messenger
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
+  public var result: () -> UserSearchResultEnvironment
 }
 
 #if DEBUG
@@ -82,7 +65,8 @@ extension UserSearchEnvironment {
   public static let unimplemented = UserSearchEnvironment(
     messenger: .unimplemented,
     mainQueue: .unimplemented,
-    bgQueue: .unimplemented
+    bgQueue: .unimplemented,
+    result: { .unimplemented }
   )
 }
 #endif
@@ -111,14 +95,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     state.failure = nil
     state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in
       guard let id = try? contact.getId() else { return nil }
-      let facts = (try? contact.getFacts()) ?? []
-      return UserSearchState.Result(
-        id: id,
-        contact: contact,
-        username: facts.first(where: { $0.type == 0 })?.fact,
-        email: facts.first(where: { $0.type == 1 })?.fact,
-        phone: facts.first(where: { $0.type == 2 })?.fact
-      )
+      return UserSearchResultState(id: id, contact: contact)
     })
     return .none
 
@@ -128,8 +105,14 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     state.results = []
     return .none
 
-  case .binding(_):
+  case .binding(_), .result(_, _):
     return .none
   }
 }
 .binding()
+.presenting(
+  forEach: userSearchResultReducer,
+  state: \.results,
+  action: /UserSearchAction.result(id:action:),
+  environment: { $0.result() }
+)
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
index 266938a2..e149ac2a 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
@@ -15,14 +15,12 @@ public struct UserSearchView: View {
     var query: MessengerSearchUsers.Query
     var isSearching: Bool
     var failure: String?
-    var results: IdentifiedArrayOf<UserSearchState.Result>
 
     init(state: UserSearchState) {
       focusedField = state.focusedField
       query = state.query
       isSearching = state.isSearching
       failure = state.failure
-      results = state.results
     }
   }
 
@@ -87,23 +85,13 @@ public struct UserSearchView: View {
           }
         }
 
-        ForEach(viewStore.results) { result in
-          Section {
-            if let username = result.username {
-              Text(username)
-            }
-            if let email = result.email {
-              Text(email)
-            }
-            if let phone = result.phone {
-              Text(phone)
-            }
-            if result.username == nil, result.email == nil, result.phone == nil {
-              Image(systemName: "questionmark")
-                .frame(maxWidth: .infinity)
-            }
-          }
-        }
+        ForEachStore(
+          store.scope(
+            state: \.results,
+            action: UserSearchAction.result(id:action:)
+          ),
+          content: UserSearchResultView.init(store:)
+        )
       }
       .onChange(of: viewStore.focusedField) { focusedField = $0 }
       .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index 8d2f414a..a96afd95 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -19,23 +19,12 @@ final class UserSearchFeatureTests: XCTestCase {
 
     var contact1 = Contact.unimplemented("contact-1".data(using: .utf8)!)
     contact1.getIdFromContact.run = { _ in "contact-1-id".data(using: .utf8)! }
-    contact1.getFactsFromContact.run = { _ in
-      [Fact(fact: "contact-1-username", type: 0),
-       Fact(fact: "contact-1-email", type: 1),
-       Fact(fact: "contact-1-phone", type: 2)]
-    }
     var contact2 = Contact.unimplemented("contact-1".data(using: .utf8)!)
     contact2.getIdFromContact.run = { _ in "contact-2-id".data(using: .utf8)! }
-    contact2.getFactsFromContact.run = { _ in
-      [Fact(fact: "contact-2-username", type: 0),
-       Fact(fact: "contact-2-email", type: 1),
-       Fact(fact: "contact-2-phone", type: 2)]
-    }
     var contact3 = Contact.unimplemented("contact-3".data(using: .utf8)!)
     contact3.getIdFromContact.run = { _ in throw GetIdFromContactError() }
     var contact4 = Contact.unimplemented("contact-4".data(using: .utf8)!)
     contact4.getIdFromContact.run = { _ in "contact-4-id".data(using: .utf8)! }
-    contact4.getFactsFromContact.run = { _ in throw GetFactsFromContactError() }
     let contacts = [contact1, contact2, contact3, contact4]
 
     store.environment.bgQueue = .immediate
@@ -64,27 +53,9 @@ final class UserSearchFeatureTests: XCTestCase {
       $0.isSearching = false
       $0.failure = nil
       $0.results = [
-        .init(
-          id: "contact-1-id".data(using: .utf8)!,
-          contact: contact1,
-          username: "contact-1-username",
-          email: "contact-1-email",
-          phone: "contact-1-phone"
-        ),
-        .init(
-          id: "contact-2-id".data(using: .utf8)!,
-          contact: contact2,
-          username: "contact-2-username",
-          email: "contact-2-email",
-          phone: "contact-2-phone"
-        ),
-        .init(
-          id: "contact-4-id".data(using: .utf8)!,
-          contact: contact4,
-          username: nil,
-          email: nil,
-          phone: nil
-        )
+        .init(id: "contact-1-id".data(using: .utf8)!, contact: contact1),
+        .init(id: "contact-2-id".data(using: .utf8)!, contact: contact2),
+        .init(id: "contact-4-id".data(using: .utf8)!, contact: contact4)
       ]
     }
   }
-- 
GitLab


From a35d5b95fce807f9b5dd0b48068d5c3372bca3f9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 00:10:33 +0200
Subject: [PATCH 03/29] Refactor

---
 .../Sources/UserSearchFeature/UserSearchFeature.swift     | 2 +-
 .../UserSearchFeature/UserSearchResultFeature.swift       | 8 ++++----
 .../Sources/UserSearchFeature/UserSearchResultView.swift  | 2 +-
 .../UserSearchFeatureTests/UserSearchFeatureTests.swift   | 6 +++---
 .../UserSearchResultFeatureTests.swift                    | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 9d16d8f5..84c78195 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -95,7 +95,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     state.failure = nil
     state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in
       guard let id = try? contact.getId() else { return nil }
-      return UserSearchResultState(id: id, contact: contact)
+      return UserSearchResultState(id: id, xxContact: contact)
     })
     return .none
 
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
index a224384b..f4c0f856 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -6,20 +6,20 @@ import XXClient
 public struct UserSearchResultState: Equatable, Identifiable {
   public init(
     id: Data,
-    contact: Contact,
+    xxContact: Contact,
     username: String? = nil,
     email: String? = nil,
     phone: String? = nil
   ) {
     self.id = id
-    self.contact = contact
+    self.xxContact = xxContact
     self.username = username
     self.email = email
     self.phone = phone
   }
 
   public var id: Data
-  public var contact: XXClient.Contact
+  public var xxContact: XXClient.Contact
   public var username: String?
   public var email: String?
   public var phone: String?
@@ -43,7 +43,7 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe
 { state, action, env in
   switch action {
   case .start:
-    let facts = (try? state.contact.getFacts()) ?? []
+    let facts = (try? state.xxContact.getFacts()) ?? []
     state.username = facts.first(where: { $0.type == 0 })?.fact
     state.email = facts.first(where: { $0.type == 1 })?.fact
     state.phone = facts.first(where: { $0.type == 2 })?.fact
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
index b1a6e267..129081fe 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
@@ -53,7 +53,7 @@ public struct UserSearchResultView_Previews: PreviewProvider {
     UserSearchResultView(store: Store(
       initialState: UserSearchResultState(
         id: "contact-id".data(using: .utf8)!,
-        contact: .unimplemented("contact-data".data(using: .utf8)!)
+        xxContact: .unimplemented("contact-data".data(using: .utf8)!)
       ),
       reducer: .empty,
       environment: ()
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index a96afd95..4311b515 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -53,9 +53,9 @@ final class UserSearchFeatureTests: XCTestCase {
       $0.isSearching = false
       $0.failure = nil
       $0.results = [
-        .init(id: "contact-1-id".data(using: .utf8)!, contact: contact1),
-        .init(id: "contact-2-id".data(using: .utf8)!, contact: contact2),
-        .init(id: "contact-4-id".data(using: .utf8)!, contact: contact4)
+        .init(id: "contact-1-id".data(using: .utf8)!, xxContact: contact1),
+        .init(id: "contact-2-id".data(using: .utf8)!, xxContact: contact2),
+        .init(id: "contact-4-id".data(using: .utf8)!, xxContact: contact4)
       ]
     }
   }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
index b4f68452..c9efe0c7 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
@@ -17,7 +17,7 @@ final class UserSearchResultFeatureTests: XCTestCase {
     let store = TestStore(
       initialState: UserSearchResultState(
         id: "contact-id".data(using: .utf8)!,
-        contact: contact
+        xxContact: contact
       ),
       reducer: userSearchResultReducer,
       environment: .unimplemented
-- 
GitLab


From 976cf766afe60447f72706e4f721ed7a17599623 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 00:47:08 +0200
Subject: [PATCH 04/29] Fetch db contact and update UI

---
 Examples/xx-messenger/Package.swift           |  2 +
 .../AppFeature/AppEnvironment+Live.swift      |  6 +-
 .../UserSearchResultFeature.swift             | 45 +++++++++-
 .../UserSearchResultView.swift                | 88 +++++++++++++++++++
 .../UserSearchResultFeatureTests.swift        | 43 +++++++++
 5 files changed, 180 insertions(+), 4 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 565dccc6..8070952e 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -140,10 +140,12 @@ let package = Package(
     .target(
       name: "UserSearchFeature",
       dependencies: [
+        .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
         .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 00415b43..d8761a65 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -52,7 +52,11 @@ extension AppEnvironment {
               mainQueue: mainQueue,
               bgQueue: bgQueue,
               result: {
-                UserSearchResultEnvironment()
+                UserSearchResultEnvironment(
+                  db: dbManager.getDB,
+                  mainQueue: mainQueue,
+                  bgQueue: bgQueue
+                )
               }
             )
           }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
index f4c0f856..f13c4b10 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -1,18 +1,22 @@
+import AppCore
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
 import XXClient
+import XXModels
 
 public struct UserSearchResultState: Equatable, Identifiable {
   public init(
     id: Data,
-    xxContact: Contact,
+    xxContact: XXClient.Contact,
+    dbContact: XXModels.Contact? = nil,
     username: String? = nil,
     email: String? = nil,
     phone: String? = nil
   ) {
     self.id = id
     self.xxContact = xxContact
+    self.dbContact = dbContact
     self.username = username
     self.email = email
     self.phone = phone
@@ -20,6 +24,7 @@ public struct UserSearchResultState: Equatable, Identifiable {
 
   public var id: Data
   public var xxContact: XXClient.Contact
+  public var dbContact: XXModels.Contact?
   public var username: String?
   public var email: String?
   public var phone: String?
@@ -27,26 +32,60 @@ public struct UserSearchResultState: Equatable, Identifiable {
 
 public enum UserSearchResultAction: Equatable {
   case start
+  case didUpdateContact(XXModels.Contact?)
+  case sendRequestButtonTapped
 }
 
 public struct UserSearchResultEnvironment {
-  public init() {}
+  public init(
+    db: DBManagerGetDB,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.db = db
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var db: DBManagerGetDB
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension UserSearchResultEnvironment {
-  public static let unimplemented = UserSearchResultEnvironment()
+  public static let unimplemented = UserSearchResultEnvironment(
+    db: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
 public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment>
 { state, action, env in
+  enum DBFetchEffectID {}
+
   switch action {
   case .start:
     let facts = (try? state.xxContact.getFacts()) ?? []
     state.username = facts.first(where: { $0.type == 0 })?.fact
     state.email = facts.first(where: { $0.type == 1 })?.fact
     state.phone = facts.first(where: { $0.type == 2 })?.fact
+    return try! env.db().fetchContactsPublisher(.init(id: [state.id]))
+      .assertNoFailure()
+      .map(\.first)
+      .map(UserSearchResultAction.didUpdateContact)
+      .subscribe(on: env.bgQueue)
+      .receive(on: env.mainQueue)
+      .eraseToEffect()
+      .cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
+
+  case .didUpdateContact(let contact):
+    state.dbContact = contact
+    return .none
+
+  case .sendRequestButtonTapped:
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
index 129081fe..4eb4ca8b 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import SwiftUI
+import XXModels
 
 public struct UserSearchResultView: View {
   public init(store: Store<UserSearchResultState, UserSearchResultAction>) {
@@ -12,11 +13,13 @@ public struct UserSearchResultView: View {
     var username: String?
     var email: String?
     var phone: String?
+    var dbContactAuth: XXModels.Contact.AuthStatus?
 
     init(state: UserSearchResultState) {
       username = state.username
       email = state.email
       phone = state.phone
+      dbContactAuth = state.dbContact?.authStatus
     }
 
     var isEmpty: Bool {
@@ -41,6 +44,91 @@ public struct UserSearchResultView: View {
             Text(phone)
           }
         }
+        switch viewStore.dbContactAuth {
+        case .none, .stranger:
+          Button {
+            viewStore.send(.sendRequestButtonTapped)
+          } label: {
+            HStack {
+              Text("Send request")
+              Spacer()
+              Image(systemName: "person.badge.plus")
+            }
+          }
+
+        case .requesting:
+          HStack {
+            Text("Sending auth request")
+            Spacer()
+            ProgressView()
+          }
+
+        case .requested:
+          HStack {
+            Text("Request sent")
+            Spacer()
+            Image(systemName: "paperplane")
+          }
+
+        case .requestFailed:
+          HStack {
+            Text("Sending request failed")
+            Spacer()
+            Image(systemName: "xmark.diamond.fill")
+              .foregroundColor(.red)
+          }
+
+        case .verificationInProgress:
+          HStack {
+            Text("Verification is progress")
+            Spacer()
+            ProgressView()
+          }
+
+        case .verified:
+          HStack {
+            Text("Verified")
+            Spacer()
+            Image(systemName: "person.fill.checkmark")
+          }
+
+        case .verificationFailed:
+          HStack {
+            Text("Verification failed")
+            Spacer()
+            Image(systemName: "xmark.diamond.fill")
+              .foregroundColor(.red)
+          }
+
+        case .confirming:
+          HStack {
+            Text("Confirming auth request")
+            Spacer()
+            ProgressView()
+          }
+
+        case .confirmationFailed:
+          HStack {
+            Text("Confirmation failed")
+            Spacer()
+            Image(systemName: "xmark.diamond.fill")
+              .foregroundColor(.red)
+          }
+
+        case .friend:
+          HStack {
+            Text("Friend")
+            Spacer()
+            Image(systemName: "person.fill.checkmark")
+          }
+
+        case .hidden:
+          HStack {
+            Text("Hidden")
+            Spacer()
+            Image(systemName: "eye.slash")
+          }
+        }
       }
       .task { viewStore.send(.start) }
     }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
index c9efe0c7..e776f803 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
@@ -1,6 +1,9 @@
+import Combine
 import ComposableArchitecture
 import XCTest
+import XCTestDynamicOverlay
 import XXClient
+import XXModels
 @testable import UserSearchFeature
 
 final class UserSearchResultFeatureTests: XCTestCase {
@@ -23,10 +26,50 @@ final class UserSearchResultFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    var dbDidFetchContacts: [XXModels.Contact.Query] = []
+    let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContactsPublisher.run = { query in
+        dbDidFetchContacts.append(query)
+        return dbContactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
     store.send(.start) {
       $0.username = "contact-username"
       $0.email = "contact-email"
       $0.phone = "contact-phone"
     }
+
+    XCTAssertNoDifference(dbDidFetchContacts, [
+      .init(id: ["contact-id".data(using: .utf8)!])
+    ])
+
+    let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
+    dbContactsPublisher.send([dbContact])
+
+    store.receive(.didUpdateContact(dbContact)) {
+      $0.dbContact = dbContact
+    }
+
+    dbContactsPublisher.send(completion: .finished)
+  }
+
+  func testSendRequest() {
+    let store = TestStore(
+      initialState: UserSearchResultState(
+        id: "contact-id".data(using: .utf8)!,
+        xxContact: .unimplemented("contact-data".data(using: .utf8)!)
+      ),
+      reducer: userSearchResultReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.sendRequestButtonTapped)
   }
 }
-- 
GitLab


From 20f14b6af264204b75fab1254bef710920a85b82 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 10:57:56 +0200
Subject: [PATCH 05/29] Add ContactFeature library

---
 .../xcschemes/ContactFeature.xcscheme         | 78 +++++++++++++++++++
 Examples/xx-messenger/Package.swift           | 18 +++++
 .../xcschemes/XXMessenger.xcscheme            | 10 +++
 .../ContactFeature/ContactFeature.swift       | 28 +++++++
 .../Sources/ContactFeature/ContactView.swift  | 35 +++++++++
 .../ContactFeatureTests.swift                 | 15 ++++
 6 files changed, 184 insertions(+)
 create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme
 create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
 create mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift

diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme
new file mode 100644
index 00000000..f4e87fe1
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactFeature"
+               BuildableName = "ContactFeature"
+               BlueprintName = "ContactFeature"
+               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 = "ContactFeatureTests"
+               BuildableName = "ContactFeatureTests"
+               BlueprintName = "ContactFeatureTests"
+               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 = "ContactFeature"
+            BuildableName = "ContactFeature"
+            BlueprintName = "ContactFeature"
+            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 8070952e..5ce26cb9 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -20,6 +20,7 @@ let package = Package(
   products: [
     .library(name: "AppCore", targets: ["AppCore"]),
     .library(name: "AppFeature", targets: ["AppFeature"]),
+    .library(name: "ContactFeature", targets: ["ContactFeature"]),
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
@@ -87,6 +88,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "ContactFeature",
+      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: "ContactFeatureTests",
+      dependencies: [
+        .target(name: "ContactFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "HomeFeature",
       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 041cf3f7..0bf8b441 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -49,6 +49,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ContactFeatureTests"
+               BuildableName = "ContactFeatureTests"
+               BlueprintName = "ContactFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
new file mode 100644
index 00000000..98344db8
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -0,0 +1,28 @@
+import ComposableArchitecture
+import XCTestDynamicOverlay
+
+public struct ContactState: Equatable {
+  public init() {}
+}
+
+public enum ContactAction: Equatable {
+  case start
+}
+
+public struct ContactEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension ContactEnvironment {
+  public static let unimplemented = ContactEnvironment()
+}
+#endif
+
+public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
new file mode 100644
index 00000000..2b403a21
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -0,0 +1,35 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct ContactView: View {
+  public init(store: Store<ContactState, ContactAction>) {
+    self.store = store
+  }
+
+  let store: Store<ContactState, ContactAction>
+
+  struct ViewState: Equatable {
+    init(state: ContactState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Form {
+
+      }
+      .navigationTitle("Contact")
+    }
+  }
+}
+
+#if DEBUG
+public struct ContactView_Previews: PreviewProvider {
+  public static var previews: some View {
+    ContactView(store: Store(
+      initialState: ContactState(),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
new file mode 100644
index 00000000..e22cd95b
--- /dev/null
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import ContactFeature
+
+final class ContactFeatureTests: XCTestCase {
+  func testStart() {
+    let store = TestStore(
+      initialState: ContactState(),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start)
+  }
+}
-- 
GitLab


From b5bbc0ca1addb287aa42d7e9d923efe5c9ec90a6 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 11:07:16 +0200
Subject: [PATCH 06/29] Update UserSearchResultFeature

---
 Examples/xx-messenger/Package.swift           |   1 -
 .../UserSearchResultFeature.swift             |  43 +------
 .../UserSearchResultView.swift                | 117 +++---------------
 3 files changed, 24 insertions(+), 137 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 5ce26cb9..1254c0e0 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -158,7 +158,6 @@ let package = Package(
     .target(
       name: "UserSearchFeature",
       dependencies: [
-        .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
index f13c4b10..ece68742 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -1,22 +1,18 @@
-import AppCore
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
 import XXClient
-import XXModels
 
 public struct UserSearchResultState: Equatable, Identifiable {
   public init(
     id: Data,
     xxContact: XXClient.Contact,
-    dbContact: XXModels.Contact? = nil,
     username: String? = nil,
     email: String? = nil,
     phone: String? = nil
   ) {
     self.id = id
     self.xxContact = xxContact
-    self.dbContact = dbContact
     self.username = username
     self.email = email
     self.phone = phone
@@ -24,7 +20,6 @@ public struct UserSearchResultState: Equatable, Identifiable {
 
   public var id: Data
   public var xxContact: XXClient.Contact
-  public var dbContact: XXModels.Contact?
   public var username: String?
   public var email: String?
   public var phone: String?
@@ -32,60 +27,30 @@ public struct UserSearchResultState: Equatable, Identifiable {
 
 public enum UserSearchResultAction: Equatable {
   case start
-  case didUpdateContact(XXModels.Contact?)
-  case sendRequestButtonTapped
+  case tapped
 }
 
 public struct UserSearchResultEnvironment {
-  public init(
-    db: DBManagerGetDB,
-    mainQueue: AnySchedulerOf<DispatchQueue>,
-    bgQueue: AnySchedulerOf<DispatchQueue>
-  ) {
-    self.db = db
-    self.mainQueue = mainQueue
-    self.bgQueue = bgQueue
-  }
-
-  public var db: DBManagerGetDB
-  public var mainQueue: AnySchedulerOf<DispatchQueue>
-  public var bgQueue: AnySchedulerOf<DispatchQueue>
+  public init() {}
 }
 
 #if DEBUG
 extension UserSearchResultEnvironment {
-  public static let unimplemented = UserSearchResultEnvironment(
-    db: .unimplemented,
-    mainQueue: .unimplemented,
-    bgQueue: .unimplemented
-  )
+  public static let unimplemented = UserSearchResultEnvironment()
 }
 #endif
 
 public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment>
 { state, action, env in
-  enum DBFetchEffectID {}
-
   switch action {
   case .start:
     let facts = (try? state.xxContact.getFacts()) ?? []
     state.username = facts.first(where: { $0.type == 0 })?.fact
     state.email = facts.first(where: { $0.type == 1 })?.fact
     state.phone = facts.first(where: { $0.type == 2 })?.fact
-    return try! env.db().fetchContactsPublisher(.init(id: [state.id]))
-      .assertNoFailure()
-      .map(\.first)
-      .map(UserSearchResultAction.didUpdateContact)
-      .subscribe(on: env.bgQueue)
-      .receive(on: env.mainQueue)
-      .eraseToEffect()
-      .cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
-
-  case .didUpdateContact(let contact):
-    state.dbContact = contact
     return .none
 
-  case .sendRequestButtonTapped:
+  case .tapped:
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
index 4eb4ca8b..fd29a84f 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultView.swift
@@ -13,13 +13,11 @@ public struct UserSearchResultView: View {
     var username: String?
     var email: String?
     var phone: String?
-    var dbContactAuth: XXModels.Contact.AuthStatus?
 
     init(state: UserSearchResultState) {
       username = state.username
       email = state.email
       phone = state.phone
-      dbContactAuth = state.dbContact?.authStatus
     }
 
     var isEmpty: Bool {
@@ -30,103 +28,28 @@ public struct UserSearchResultView: View {
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Section {
-        if viewStore.isEmpty {
-          Image(systemName: "questionmark")
-            .frame(maxWidth: .infinity)
-        } else {
-          if let username = viewStore.username {
-            Text(username)
-          }
-          if let email = viewStore.email {
-            Text(email)
-          }
-          if let phone = viewStore.phone {
-            Text(phone)
-          }
-        }
-        switch viewStore.dbContactAuth {
-        case .none, .stranger:
-          Button {
-            viewStore.send(.sendRequestButtonTapped)
-          } label: {
-            HStack {
-              Text("Send request")
-              Spacer()
-              Image(systemName: "person.badge.plus")
-            }
-          }
-
-        case .requesting:
-          HStack {
-            Text("Sending auth request")
-            Spacer()
-            ProgressView()
-          }
-
-        case .requested:
-          HStack {
-            Text("Request sent")
-            Spacer()
-            Image(systemName: "paperplane")
-          }
-
-        case .requestFailed:
-          HStack {
-            Text("Sending request failed")
-            Spacer()
-            Image(systemName: "xmark.diamond.fill")
-              .foregroundColor(.red)
-          }
-
-        case .verificationInProgress:
-          HStack {
-            Text("Verification is progress")
-            Spacer()
-            ProgressView()
-          }
-
-        case .verified:
-          HStack {
-            Text("Verified")
-            Spacer()
-            Image(systemName: "person.fill.checkmark")
-          }
-
-        case .verificationFailed:
-          HStack {
-            Text("Verification failed")
-            Spacer()
-            Image(systemName: "xmark.diamond.fill")
-              .foregroundColor(.red)
-          }
-
-        case .confirming:
-          HStack {
-            Text("Confirming auth request")
-            Spacer()
-            ProgressView()
-          }
-
-        case .confirmationFailed:
-          HStack {
-            Text("Confirmation failed")
-            Spacer()
-            Image(systemName: "xmark.diamond.fill")
-              .foregroundColor(.red)
-          }
-
-        case .friend:
+        Button {
+          viewStore.send(.tapped)
+        } label: {
           HStack {
-            Text("Friend")
-            Spacer()
-            Image(systemName: "person.fill.checkmark")
-          }
-
-        case .hidden:
-          HStack {
-            Text("Hidden")
+            VStack {
+              if viewStore.isEmpty {
+                Image(systemName: "questionmark")
+                  .frame(maxWidth: .infinity)
+              } else {
+                if let username = viewStore.username {
+                  Text(username)
+                }
+                if let email = viewStore.email {
+                  Text(email)
+                }
+                if let phone = viewStore.phone {
+                  Text(phone)
+                }
+              }
+            }
             Spacer()
-            Image(systemName: "eye.slash")
+            Image(systemName: "chevron.forward")
           }
         }
       }
-- 
GitLab


From 94b81c4ce869724070113864a73627f38ba91051 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 11:20:07 +0200
Subject: [PATCH 07/29] Present contact from search results

---
 Examples/xx-messenger/Package.swift           |  2 ++
 .../AppFeature/AppEnvironment+Live.swift      |  9 +++--
 .../UserSearchFeature/UserSearchFeature.swift | 33 +++++++++++++++---
 .../UserSearchFeature/UserSearchView.swift    | 10 ++++++
 .../UserSearchFeatureTests.swift              | 34 +++++++++++++++++++
 .../UserSearchResultFeatureTests.swift        | 33 ++----------------
 6 files changed, 81 insertions(+), 40 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 1254c0e0..95a87202 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -69,6 +69,7 @@ let package = Package(
       name: "AppFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "ContactFeature"),
         .target(name: "HomeFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
@@ -158,6 +159,7 @@ let package = Package(
     .target(
       name: "UserSearchFeature",
       dependencies: [
+        .target(name: "ContactFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index d8761a65..03c8442c 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -52,11 +52,10 @@ extension AppEnvironment {
               mainQueue: mainQueue,
               bgQueue: bgQueue,
               result: {
-                UserSearchResultEnvironment(
-                  db: dbManager.getDB,
-                  mainQueue: mainQueue,
-                  bgQueue: bgQueue
-                )
+                UserSearchResultEnvironment()
+              },
+              contact: {
+                ContactEnvironment()
               }
             )
           }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 84c78195..425832e2 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import ComposablePresentation
+import ContactFeature
 import Foundation
 import XCTestDynamicOverlay
 import XXClient
@@ -17,13 +18,15 @@ public struct UserSearchState: Equatable {
     query: MessengerSearchUsers.Query = .init(),
     isSearching: Bool = false,
     failure: String? = nil,
-    results: IdentifiedArrayOf<UserSearchResultState> = []
+    results: IdentifiedArrayOf<UserSearchResultState> = [],
+    contact: ContactState? = nil
   ) {
     self.focusedField = focusedField
     self.query = query
     self.isSearching = isSearching
     self.failure = failure
     self.results = results
+    self.contact = contact
   }
 
   @BindableState public var focusedField: Field?
@@ -31,14 +34,17 @@ public struct UserSearchState: Equatable {
   public var isSearching: Bool
   public var failure: String?
   public var results: IdentifiedArrayOf<UserSearchResultState>
+  public var contact: ContactState?
 }
 
 public enum UserSearchAction: Equatable, BindableAction {
   case searchTapped
   case didFail(String)
   case didSucceed([Contact])
+  case didDismissContact
   case binding(BindingAction<UserSearchState>)
   case result(id: UserSearchResultState.ID, action: UserSearchResultAction)
+  case contact(ContactAction)
 }
 
 public struct UserSearchEnvironment {
@@ -46,18 +52,21 @@ public struct UserSearchEnvironment {
     messenger: Messenger,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
-    result: @escaping () -> UserSearchResultEnvironment
+    result: @escaping () -> UserSearchResultEnvironment,
+    contact: @escaping () -> ContactEnvironment
   ) {
     self.messenger = messenger
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.result = result
+    self.contact = contact
   }
 
   public var messenger: Messenger
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var result: () -> UserSearchResultEnvironment
+  public var contact: () -> ContactEnvironment
 }
 
 #if DEBUG
@@ -66,7 +75,8 @@ extension UserSearchEnvironment {
     messenger: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
-    result: { .unimplemented }
+    result: { .unimplemented },
+    contact: { .unimplemented }
   )
 }
 #endif
@@ -105,7 +115,15 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     state.results = []
     return .none
 
-  case .binding(_), .result(_, _):
+  case .didDismissContact:
+    state.contact = nil
+    return .none
+
+  case .result(let id, action: .tapped):
+    state.contact = ContactState()
+    return .none
+
+  case .binding(_), .result(_, _), .contact(_):
     return .none
   }
 }
@@ -116,3 +134,10 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
   action: /UserSearchAction.result(id:action:),
   environment: { $0.result() }
 )
+.presenting(
+  contactReducer,
+  state: .keyPath(\.contact),
+  id: .notNil(), // TODO: use Contact.ID
+  action: /UserSearchAction.contact,
+  environment: { $0.contact() }
+)
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
index e149ac2a..f0416b3a 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
@@ -1,4 +1,6 @@
 import ComposableArchitecture
+import ComposablePresentation
+import ContactFeature
 import SwiftUI
 import XXMessengerClient
 
@@ -96,6 +98,14 @@ public struct UserSearchView: View {
       .onChange(of: viewStore.focusedField) { focusedField = $0 }
       .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
       .navigationTitle("User Search")
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.contact,
+          action: UserSearchAction.contact
+        ),
+        onDeactivate: { viewStore.send(.didDismissContact) },
+        destination: ContactView.init(store:)
+      ))
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index 4311b515..285d359e 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -1,4 +1,5 @@
 import ComposableArchitecture
+import ContactFeature
 import XCTest
 import XXClient
 import XXMessengerClient
@@ -87,4 +88,37 @@ final class UserSearchFeatureTests: XCTestCase {
       $0.results = []
     }
   }
+
+  func testResultTapped() {
+    let store = TestStore(
+      initialState: UserSearchState(
+        results: [
+          .init(
+            id: "contact-id".data(using: .utf8)!,
+            xxContact: .unimplemented("contact-data".data(using: .utf8)!)
+          )
+        ]
+      ),
+      reducer: userSearchReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.result(id: "contact-id".data(using: .utf8)!, action: .tapped)) {
+      $0.contact = ContactState()
+    }
+  }
+
+  func testDismissingContact() {
+    let store = TestStore(
+      initialState: UserSearchState(
+        contact: ContactState()
+      ),
+      reducer: userSearchReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.didDismissContact) {
+      $0.contact = nil
+    }
+  }
 }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
index e776f803..c8f2a99b 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchResultFeatureTests.swift
@@ -1,9 +1,7 @@
-import Combine
 import ComposableArchitecture
 import XCTest
 import XCTestDynamicOverlay
 import XXClient
-import XXModels
 @testable import UserSearchFeature
 
 final class UserSearchResultFeatureTests: XCTestCase {
@@ -26,41 +24,14 @@ final class UserSearchResultFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
-    var dbDidFetchContacts: [XXModels.Contact.Query] = []
-    let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
-
-    store.environment.mainQueue = .immediate
-    store.environment.bgQueue = .immediate
-    store.environment.db.run = {
-      var db: Database = .failing
-      db.fetchContactsPublisher.run = { query in
-        dbDidFetchContacts.append(query)
-        return dbContactsPublisher.eraseToAnyPublisher()
-      }
-      return db
-    }
-
     store.send(.start) {
       $0.username = "contact-username"
       $0.email = "contact-email"
       $0.phone = "contact-phone"
     }
-
-    XCTAssertNoDifference(dbDidFetchContacts, [
-      .init(id: ["contact-id".data(using: .utf8)!])
-    ])
-
-    let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
-    dbContactsPublisher.send([dbContact])
-
-    store.receive(.didUpdateContact(dbContact)) {
-      $0.dbContact = dbContact
-    }
-
-    dbContactsPublisher.send(completion: .finished)
   }
 
-  func testSendRequest() {
+  func testTapped() {
     let store = TestStore(
       initialState: UserSearchResultState(
         id: "contact-id".data(using: .utf8)!,
@@ -70,6 +41,6 @@ final class UserSearchResultFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
-    store.send(.sendRequestButtonTapped)
+    store.send(.tapped)
   }
 }
-- 
GitLab


From f97470fb78aa4fe2048ca2cb3e6fd45f05edc460 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 11:37:58 +0200
Subject: [PATCH 08/29] Update ContactFeature

---
 .../AppFeature/AppEnvironment+Live.swift      |  8 +++-
 .../ContactFeature/ContactFeature.swift       | 43 +++++++++++++++++--
 .../Sources/ContactFeature/ContactView.swift  |  4 +-
 .../UserSearchFeature/UserSearchFeature.swift |  5 ++-
 .../ContactFeatureTests.swift                 |  4 +-
 .../UserSearchFeatureTests.swift              |  9 +++-
 6 files changed, 64 insertions(+), 9 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 03c8442c..8e3f48dc 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -1,4 +1,5 @@
 import AppCore
+import ContactFeature
 import Foundation
 import HomeFeature
 import RegisterFeature
@@ -55,7 +56,12 @@ extension AppEnvironment {
                 UserSearchResultEnvironment()
               },
               contact: {
-                ContactEnvironment()
+                ContactEnvironment(
+                  messenger: messenger,
+                  db: dbManager.getDB,
+                  mainQueue: mainQueue,
+                  bgQueue: bgQueue
+                )
               }
             )
           }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 98344db8..80fbcc81 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -1,8 +1,25 @@
+import AppCore
 import ComposableArchitecture
+import Foundation
 import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
 
 public struct ContactState: Equatable {
-  public init() {}
+  public init(
+    id: Data,
+    dbContact: XXModels.Contact? = nil,
+    xxContact: XXClient.Contact? = nil
+  ) {
+    self.id = id
+    self.dbContact = dbContact
+    self.xxContact = xxContact
+  }
+
+  public var id: Data
+  public var dbContact: XXModels.Contact?
+  public var xxContact: XXClient.Contact?
 }
 
 public enum ContactAction: Equatable {
@@ -10,12 +27,32 @@ public enum ContactAction: Equatable {
 }
 
 public struct ContactEnvironment {
-  public init() {}
+  public init(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.db = db
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var db: DBManagerGetDB
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension ContactEnvironment {
-  public static let unimplemented = ContactEnvironment()
+  public static let unimplemented = ContactEnvironment(
+    messenger: .unimplemented,
+    db: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 2b403a21..a050747e 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -26,7 +26,9 @@ public struct ContactView: View {
 public struct ContactView_Previews: PreviewProvider {
   public static var previews: some View {
     ContactView(store: Store(
-      initialState: ContactState(),
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!
+      ),
       reducer: .empty,
       environment: ()
     ))
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 425832e2..e8cb92c0 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -120,7 +120,10 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
     return .none
 
   case .result(let id, action: .tapped):
-    state.contact = ContactState()
+    state.contact = ContactState(
+      id: id,
+      xxContact: state.results[id: id]?.xxContact
+    )
     return .none
 
   case .binding(_), .result(_, _), .contact(_):
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index e22cd95b..4711a132 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -5,7 +5,9 @@ import XCTest
 final class ContactFeatureTests: XCTestCase {
   func testStart() {
     let store = TestStore(
-      initialState: ContactState(),
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!
+      ),
       reducer: contactReducer,
       environment: .unimplemented
     )
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index 285d359e..c457327c 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -104,14 +104,19 @@ final class UserSearchFeatureTests: XCTestCase {
     )
 
     store.send(.result(id: "contact-id".data(using: .utf8)!, action: .tapped)) {
-      $0.contact = ContactState()
+      $0.contact = ContactState(
+        id: "contact-id".data(using: .utf8)!,
+        xxContact: .unimplemented("contact-data".data(using: .utf8)!)
+      )
     }
   }
 
   func testDismissingContact() {
     let store = TestStore(
       initialState: UserSearchState(
-        contact: ContactState()
+        contact: ContactState(
+          id: "contact-id".data(using: .utf8)!
+        )
       ),
       reducer: userSearchReducer,
       environment: .unimplemented
-- 
GitLab


From d0b2595b96d82eaabedfa33f3761b4e02deb4424 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 11:58:38 +0200
Subject: [PATCH 09/29] Add XXClient.Contact helpers

---
 Examples/xx-messenger/Package.swift               |  1 +
 .../XXClientHelpers/XXContact+Helpers.swift       | 15 +++++++++++++++
 2 files changed, 16 insertions(+)
 create mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 95a87202..e991313f 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -53,6 +53,7 @@ let package = Package(
       name: "AppCore",
       dependencies: [
         .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXDatabase", package: "client-ios-db"),
         .product(name: "XXModels", package: "client-ios-db"),
       ],
diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift
new file mode 100644
index 00000000..d2c93683
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift
@@ -0,0 +1,15 @@
+import XXClient
+
+extension Contact {
+  public var username: String? {
+    try? getFacts().first(where: { $0.type == 0 })?.fact
+  }
+
+  public var email: String? {
+    try? getFacts().first(where: { $0.type == 1 })?.fact
+  }
+
+  public var phone: String? {
+    try? getFacts().first(where: { $0.type == 2 })?.fact
+  }
+}
-- 
GitLab


From aad544cd6ca3591117c33306ca6f4b1525bcf8d9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 12:04:13 +0200
Subject: [PATCH 10/29] Fetch db contact in ContactFeature

---
 .../ContactFeature/ContactFeature.swift       |  14 +++
 .../Sources/ContactFeature/ContactView.swift  | 115 +++++++++++++++++-
 .../ContactFeatureTests.swift                 |  30 +++++
 3 files changed, 158 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 80fbcc81..6624bb2c 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -24,6 +24,7 @@ public struct ContactState: Equatable {
 
 public enum ContactAction: Equatable {
   case start
+  case dbContactFetched(XXModels.Contact?)
 }
 
 public struct ContactEnvironment {
@@ -58,8 +59,21 @@ extension ContactEnvironment {
 
 public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironment>
 { state, action, env in
+  enum DBFetchEffectID {}
+
   switch action {
   case .start:
+    return try! env.db().fetchContactsPublisher(.init(id: [state.id]))
+      .assertNoFailure()
+      .map(\.first)
+      .map(ContactAction.dbContactFetched)
+      .subscribe(on: env.bgQueue)
+      .receive(on: env.mainQueue)
+      .eraseToEffect()
+      .cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
+
+  case .dbContactFetched(let contact):
+    state.dbContact = contact
     return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index a050747e..0d985c62 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -1,5 +1,8 @@
+import AppCore
 import ComposableArchitecture
 import SwiftUI
+import XXClient
+import XXModels
 
 public struct ContactView: View {
   public init(store: Store<ContactState, ContactAction>) {
@@ -9,13 +12,123 @@ public struct ContactView: View {
   let store: Store<ContactState, ContactAction>
 
   struct ViewState: Equatable {
-    init(state: ContactState) {}
+    var dbContact: XXModels.Contact?
+    var xxContact: XXClient.Contact?
+
+    init(state: ContactState) {
+      dbContact = state.dbContact
+      xxContact = state.xxContact
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
+        Section {
+          if let dbContact = viewStore.dbContact {
+            Label(dbContact.username ?? "", systemImage: "person")
+            Label(dbContact.email ?? "", systemImage: "envelope")
+            Label(dbContact.phone ?? "", systemImage: "phone")
+          } else {
+            Text("Contact not saved locally")
+          }
+        } header: {
+          Text("Local data")
+        }
+
+        Section {
+          Label(viewStore.xxContact?.username ?? "", systemImage: "person")
+          Label(viewStore.xxContact?.email ?? "", systemImage: "envelope")
+          Label(viewStore.xxContact?.phone ?? "", systemImage: "phone")
+        } header: {
+          Text("Facts")
+        }
+
+        Section {
+          switch viewStore.dbContact?.authStatus {
+          case .none, .stranger:
+            HStack {
+              Text("Stranger")
+              Spacer()
+              Image(systemName: "person.fill.questionmark")
+            }
+
+          case .requesting:
+            HStack {
+              Text("Sending auth request")
+              Spacer()
+              ProgressView()
+            }
+
+          case .requested:
+            HStack {
+              Text("Request sent")
+              Spacer()
+              Image(systemName: "paperplane")
+            }
+
+          case .requestFailed:
+            HStack {
+              Text("Sending request failed")
+              Spacer()
+              Image(systemName: "xmark.diamond.fill")
+                .foregroundColor(.red)
+            }
+
+          case .verificationInProgress:
+            HStack {
+              Text("Verification is progress")
+              Spacer()
+              ProgressView()
+            }
+
+          case .verified:
+            HStack {
+              Text("Verified")
+              Spacer()
+              Image(systemName: "person.fill.checkmark")
+            }
+
+          case .verificationFailed:
+            HStack {
+              Text("Verification failed")
+              Spacer()
+              Image(systemName: "xmark.diamond.fill")
+                .foregroundColor(.red)
+            }
+
+          case .confirming:
+            HStack {
+              Text("Confirming auth request")
+              Spacer()
+              ProgressView()
+            }
+
+          case .confirmationFailed:
+            HStack {
+              Text("Confirmation failed")
+              Spacer()
+              Image(systemName: "xmark.diamond.fill")
+                .foregroundColor(.red)
+            }
+
+          case .friend:
+            HStack {
+              Text("Friend")
+              Spacer()
+              Image(systemName: "person.fill.checkmark")
+            }
 
+          case .hidden:
+            HStack {
+              Text("Hidden")
+              Spacer()
+              Image(systemName: "eye.slash")
+            }
+          }
+        } header: {
+          Text("Auth status")
+        }
       }
       .navigationTitle("Contact")
     }
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index 4711a132..5145acce 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -1,5 +1,8 @@
+import Combine
 import ComposableArchitecture
+import CustomDump
 import XCTest
+import XXModels
 @testable import ContactFeature
 
 final class ContactFeatureTests: XCTestCase {
@@ -12,6 +15,33 @@ final class ContactFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    var dbDidFetchContacts: [XXModels.Contact.Query] = []
+    let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContactsPublisher.run = { query in
+        dbDidFetchContacts.append(query)
+        return dbContactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
     store.send(.start)
+
+    XCTAssertNoDifference(dbDidFetchContacts, [
+      .init(id: ["contact-id".data(using: .utf8)!])
+    ])
+
+    let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
+    dbContactsPublisher.send([dbContact])
+
+    store.receive(.dbContactFetched(dbContact)) {
+      $0.dbContact = dbContact
+    }
+
+    dbContactsPublisher.send(completion: .finished)
   }
 }
-- 
GitLab


From 4fbe3b68b073a6659083eb34c51eec2549d35f87 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 12:57:20 +0200
Subject: [PATCH 11/29] Update ContactFeature

---
 .../ContactFeature/ContactFeature.swift       |   8 +
 .../Sources/ContactFeature/ContactView.swift  | 222 ++++++++++--------
 .../ContactFeatureTests.swift                 |  24 ++
 3 files changed, 157 insertions(+), 97 deletions(-)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 6624bb2c..37388359 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -25,6 +25,8 @@ public struct ContactState: Equatable {
 public enum ContactAction: Equatable {
   case start
   case dbContactFetched(XXModels.Contact?)
+  case saveFactsTapped
+  case sendRequestTapped
 }
 
 public struct ContactEnvironment {
@@ -75,5 +77,11 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
   case .dbContactFetched(let contact):
     state.dbContact = contact
     return .none
+
+  case .saveFactsTapped:
+    return .none
+
+  case .sendRequestTapped:
+    return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 0d985c62..ca5eb328 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -24,110 +24,138 @@ public struct ContactView: View {
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
-        Section {
-          if let dbContact = viewStore.dbContact {
+        if let xxContact = viewStore.xxContact {
+          Section {
+            Label(xxContact.username ?? "", systemImage: "person")
+            Label(xxContact.email ?? "", systemImage: "envelope")
+            Label(xxContact.phone ?? "", systemImage: "phone")
+            Button {
+              viewStore.send(.saveFactsTapped)
+            } label: {
+              if viewStore.dbContact == nil {
+                Text("Save contact")
+              } else {
+                Text("Update contact")
+              }
+            }
+          } header: {
+            Text("Facts")
+          }
+        }
+
+        if let dbContact = viewStore.dbContact {
+          Section {
             Label(dbContact.username ?? "", systemImage: "person")
             Label(dbContact.email ?? "", systemImage: "envelope")
             Label(dbContact.phone ?? "", systemImage: "phone")
-          } else {
-            Text("Contact not saved locally")
+          } header: {
+            Text("Contact")
           }
-        } header: {
-          Text("Local data")
-        }
-
-        Section {
-          Label(viewStore.xxContact?.username ?? "", systemImage: "person")
-          Label(viewStore.xxContact?.email ?? "", systemImage: "envelope")
-          Label(viewStore.xxContact?.phone ?? "", systemImage: "phone")
-        } header: {
-          Text("Facts")
-        }
-
-        Section {
-          switch viewStore.dbContact?.authStatus {
-          case .none, .stranger:
-            HStack {
-              Text("Stranger")
-              Spacer()
-              Image(systemName: "person.fill.questionmark")
-            }
-
-          case .requesting:
-            HStack {
-              Text("Sending auth request")
-              Spacer()
-              ProgressView()
-            }
-
-          case .requested:
-            HStack {
-              Text("Request sent")
-              Spacer()
-              Image(systemName: "paperplane")
-            }
-
-          case .requestFailed:
-            HStack {
-              Text("Sending request failed")
-              Spacer()
-              Image(systemName: "xmark.diamond.fill")
-                .foregroundColor(.red)
-            }
-
-          case .verificationInProgress:
-            HStack {
-              Text("Verification is progress")
-              Spacer()
-              ProgressView()
-            }
-
-          case .verified:
-            HStack {
-              Text("Verified")
-              Spacer()
-              Image(systemName: "person.fill.checkmark")
-            }
-
-          case .verificationFailed:
-            HStack {
-              Text("Verification failed")
-              Spacer()
-              Image(systemName: "xmark.diamond.fill")
-                .foregroundColor(.red)
-            }
-
-          case .confirming:
-            HStack {
-              Text("Confirming auth request")
-              Spacer()
-              ProgressView()
-            }
-
-          case .confirmationFailed:
-            HStack {
-              Text("Confirmation failed")
-              Spacer()
-              Image(systemName: "xmark.diamond.fill")
-                .foregroundColor(.red)
-            }
-
-          case .friend:
-            HStack {
-              Text("Friend")
-              Spacer()
-              Image(systemName: "person.fill.checkmark")
-            }
 
-          case .hidden:
-            HStack {
-              Text("Hidden")
-              Spacer()
-              Image(systemName: "eye.slash")
+          Section {
+            switch dbContact.authStatus {
+            case .stranger:
+              HStack {
+                Text("Stranger")
+                Spacer()
+                Image(systemName: "person.fill.questionmark")
+              }
+              Button {
+                viewStore.send(.sendRequestTapped)
+              } label: {
+                HStack {
+                  Text("Send request")
+                  Spacer()
+                  Image(systemName: "chevron.forward")
+                }
+              }
+
+            case .requesting:
+              HStack {
+                Text("Sending auth request")
+                Spacer()
+                ProgressView()
+              }
+
+            case .requested:
+              HStack {
+                Text("Request sent")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+
+            case .requestFailed:
+              HStack {
+                Text("Sending request failed")
+                Spacer()
+                Image(systemName: "xmark.diamond.fill")
+                  .foregroundColor(.red)
+              }
+              Button {
+                viewStore.send(.sendRequestTapped)
+              } label: {
+                HStack {
+                  Text("Resend request")
+                  Spacer()
+                  Image(systemName: "paperplane")
+                }
+              }
+
+            case .verificationInProgress:
+              HStack {
+                Text("Verification is progress")
+                Spacer()
+                ProgressView()
+              }
+
+            case .verified:
+              HStack {
+                Text("Verified")
+                Spacer()
+                Image(systemName: "person.fill.checkmark")
+              }
+
+            case .verificationFailed:
+              HStack {
+                Text("Verification failed")
+                Spacer()
+                Image(systemName: "xmark.diamond.fill")
+                  .foregroundColor(.red)
+              }
+
+            case .confirming:
+              HStack {
+                Text("Confirming auth request")
+                Spacer()
+                ProgressView()
+              }
+
+            case .confirmationFailed:
+              HStack {
+                Text("Confirmation failed")
+                Spacer()
+                Image(systemName: "xmark.diamond.fill")
+                  .foregroundColor(.red)
+              }
+
+            case .friend:
+              HStack {
+                Text("Friend")
+                Spacer()
+                Image(systemName: "person.fill.checkmark")
+              }
+
+            case .hidden:
+              HStack {
+                Text("Hidden")
+                Spacer()
+                Image(systemName: "eye.slash")
+              }
             }
+          } header: {
+            Text("Auth status")
           }
-        } header: {
-          Text("Auth status")
+          .animation(.default, value: viewStore.dbContact?.authStatus)
         }
       }
       .navigationTitle("Contact")
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index 5145acce..b0d3e8b6 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -44,4 +44,28 @@ final class ContactFeatureTests: XCTestCase {
 
     dbContactsPublisher.send(completion: .finished)
   }
+
+  func testSaveFacts() {
+    let store = TestStore(
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.saveFactsTapped)
+  }
+
+  func testSendRequest() {
+    let store = TestStore(
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.sendRequestTapped)
+  }
 }
-- 
GitLab


From a05510e2255fc40f2652697420e9bb44b52f391e Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 13:14:16 +0200
Subject: [PATCH 12/29] Save contact

---
 .../ContactFeature/ContactFeature.swift       | 13 ++++++-
 .../Sources/ContactFeature/ContactView.swift  |  1 +
 .../ContactFeatureTests.swift                 | 39 ++++++++++++++++++-
 3 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 37388359..7bbb5f3c 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -79,7 +79,18 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     return .none
 
   case .saveFactsTapped:
-    return .none
+    guard let xxContact = state.xxContact else { return .none }
+    return .fireAndForget { [state] in
+      var dbContact = state.dbContact ?? XXModels.Contact(id: state.id)
+      dbContact.marshaled = xxContact.data
+      dbContact.username = xxContact.username
+      dbContact.email = xxContact.email
+      dbContact.phone = xxContact.phone
+      _ = try! env.db().saveContact(dbContact)
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .sendRequestTapped:
     return .none
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index ca5eb328..6bf50150 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -159,6 +159,7 @@ public struct ContactView: View {
         }
       }
       .navigationTitle("Contact")
+      .task { viewStore.send(.start) }
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index b0d3e8b6..f14c9e39 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -2,6 +2,7 @@ import Combine
 import ComposableArchitecture
 import CustomDump
 import XCTest
+import XXClient
 import XXModels
 @testable import ContactFeature
 
@@ -46,15 +47,51 @@ final class ContactFeatureTests: XCTestCase {
   }
 
   func testSaveFacts() {
+    let dbContact: XXModels.Contact = .init(
+      id: "contact-id".data(using: .utf8)!
+    )
+
+    var xxContact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!)
+    xxContact.getFactsFromContact.run = { _ in
+      [
+        Fact(fact: "contact-username", type: 0),
+        Fact(fact: "contact-email", type: 1),
+        Fact(fact: "contact-phone", type: 2),
+      ]
+    }
+
     let store = TestStore(
       initialState: ContactState(
-        id: "contact-id".data(using: .utf8)!
+        id: "contact-id".data(using: .utf8)!,
+        dbContact: dbContact,
+        xxContact: xxContact
       ),
       reducer: contactReducer,
       environment: .unimplemented
     )
 
+    var dbDidSaveContact: [XXModels.Contact] = []
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.saveContact.run = { contact in
+        dbDidSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
     store.send(.saveFactsTapped)
+
+    var expectedSavedContact = dbContact
+    expectedSavedContact.marshaled = xxContact.data
+    expectedSavedContact.username = "contact-username"
+    expectedSavedContact.email = "contact-email"
+    expectedSavedContact.phone = "contact-phone"
+
+    XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact])
   }
 
   func testSendRequest() {
-- 
GitLab


From b2b508d289d05d78eeb756ec3dc700f3b9626edd Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 13:26:26 +0200
Subject: [PATCH 13/29] Add Send Request screen

---
 Examples/xx-messenger/Package.swift           |  1 +
 .../AppFeature/AppEnvironment+Live.swift      |  5 ++-
 .../ContactFeature/ContactFeature.swift       | 31 ++++++++++++++--
 .../ContactSendRequestFeature.swift           | 28 +++++++++++++++
 .../ContactSendRequestView.swift              | 36 +++++++++++++++++++
 .../Sources/ContactFeature/ContactView.swift  |  9 +++++
 .../ContactFeatureTests.swift                 |  8 ++++-
 .../ContactSendRequestFeatureTests.swift      | 15 ++++++++
 8 files changed, 128 insertions(+), 5 deletions(-)
 create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift
 create mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index e991313f..79123e05 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -95,6 +95,7 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXModels", package: "client-ios-db"),
       ],
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 8e3f48dc..514f56bd 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -60,7 +60,10 @@ extension AppEnvironment {
                   messenger: messenger,
                   db: dbManager.getDB,
                   mainQueue: mainQueue,
-                  bgQueue: bgQueue
+                  bgQueue: bgQueue,
+                  sendRequest: {
+                    ContactSendRequestEnvironment()
+                  }
                 )
               }
             )
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 7bbb5f3c..76715592 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -1,5 +1,6 @@
 import AppCore
 import ComposableArchitecture
+import ComposablePresentation
 import Foundation
 import XCTestDynamicOverlay
 import XXClient
@@ -10,16 +11,19 @@ public struct ContactState: Equatable {
   public init(
     id: Data,
     dbContact: XXModels.Contact? = nil,
-    xxContact: XXClient.Contact? = nil
+    xxContact: XXClient.Contact? = nil,
+    sendRequest: ContactSendRequestState? = nil
   ) {
     self.id = id
     self.dbContact = dbContact
     self.xxContact = xxContact
+    self.sendRequest = sendRequest
   }
 
   public var id: Data
   public var dbContact: XXModels.Contact?
   public var xxContact: XXClient.Contact?
+  public var sendRequest: ContactSendRequestState?
 }
 
 public enum ContactAction: Equatable {
@@ -27,6 +31,8 @@ public enum ContactAction: Equatable {
   case dbContactFetched(XXModels.Contact?)
   case saveFactsTapped
   case sendRequestTapped
+  case sendRequestDismissed
+  case sendRequest(ContactSendRequestAction)
 }
 
 public struct ContactEnvironment {
@@ -34,18 +40,21 @@ public struct ContactEnvironment {
     messenger: Messenger,
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
-    bgQueue: AnySchedulerOf<DispatchQueue>
+    bgQueue: AnySchedulerOf<DispatchQueue>,
+    sendRequest: @escaping () -> ContactSendRequestEnvironment
   ) {
     self.messenger = messenger
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
+    self.sendRequest = sendRequest
   }
 
   public var messenger: Messenger
   public var db: DBManagerGetDB
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
+  public var sendRequest: () -> ContactSendRequestEnvironment
 }
 
 #if DEBUG
@@ -54,7 +63,8 @@ extension ContactEnvironment {
     messenger: .unimplemented,
     db: .unimplemented,
     mainQueue: .unimplemented,
-    bgQueue: .unimplemented
+    bgQueue: .unimplemented,
+    sendRequest: { .unimplemented }
   )
 }
 #endif
@@ -93,6 +103,21 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     .eraseToEffect()
 
   case .sendRequestTapped:
+    state.sendRequest = ContactSendRequestState()
+    return .none
+
+  case .sendRequestDismissed:
+    state.sendRequest = nil
+    return .none
+
+  case .sendRequest(_):
     return .none
   }
 }
+.presenting(
+  contactSendRequestReducer,
+  state: .keyPath(\.sendRequest),
+  id: .notNil(),
+  action: /ContactAction.sendRequest,
+  environment: { $0.sendRequest() }
+)
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
new file mode 100644
index 00000000..a0a474e5
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
@@ -0,0 +1,28 @@
+import ComposableArchitecture
+import XCTestDynamicOverlay
+
+public struct ContactSendRequestState: Equatable {
+  public init() {}
+}
+
+public enum ContactSendRequestAction: Equatable {
+  case start
+}
+
+public struct ContactSendRequestEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension ContactSendRequestEnvironment {
+  public static let unimplemented = ContactSendRequestEnvironment()
+}
+#endif
+
+public let contactSendRequestReducer = Reducer<ContactSendRequestState, ContactSendRequestAction, ContactSendRequestEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift
new file mode 100644
index 00000000..4f5048d3
--- /dev/null
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift
@@ -0,0 +1,36 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct ContactSendRequestView: View {
+  public init(store: Store<ContactSendRequestState, ContactSendRequestAction>) {
+    self.store = store
+  }
+
+  let store: Store<ContactSendRequestState, ContactSendRequestAction>
+
+  struct ViewState: Equatable {
+    init(state: ContactSendRequestState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Form {
+
+      }
+      .navigationTitle("Send Request")
+      .task { viewStore.send(.start) }
+    }
+  }
+}
+
+#if DEBUG
+public struct ContactSendRequestView_Previews: PreviewProvider {
+  public static var previews: some View {
+    ContactSendRequestView(store: Store(
+      initialState: ContactSendRequestState(),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 6bf50150..44a46c56 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -1,5 +1,6 @@
 import AppCore
 import ComposableArchitecture
+import ComposablePresentation
 import SwiftUI
 import XXClient
 import XXModels
@@ -160,6 +161,14 @@ public struct ContactView: View {
       }
       .navigationTitle("Contact")
       .task { viewStore.send(.start) }
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.sendRequest,
+          action: ContactAction.sendRequest
+        ),
+        onDeactivate: { viewStore.send(.sendRequestDismissed) },
+        destination: ContactSendRequestView.init(store:)
+      ))
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index f14c9e39..64d0d72a 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -103,6 +103,12 @@ final class ContactFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
-    store.send(.sendRequestTapped)
+    store.send(.sendRequestTapped) {
+      $0.sendRequest = ContactSendRequestState()
+    }
+
+    store.send(.sendRequestDismissed) {
+      $0.sendRequest = nil
+    }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift
new file mode 100644
index 00000000..8d4eb040
--- /dev/null
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import ContactFeature
+
+final class ContactSendRequestFeatureTests: XCTestCase {
+  func testStart() {
+    let store = TestStore(
+      initialState: ContactSendRequestState(),
+      reducer: contactSendRequestReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start)
+  }
+}
-- 
GitLab


From 4f378901dfa92a0eca8fccd327e671504933c3f9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 13:42:33 +0200
Subject: [PATCH 14/29] Extract SendRequestFeature to separate library

---
 .../xcschemes/SendRequestFeature.xcscheme     | 78 +++++++++++++++++++
 Examples/xx-messenger/Package.swift           | 17 ++++
 .../xcschemes/XXMessenger.xcscheme            | 10 +++
 .../AppFeature/AppEnvironment+Live.swift      |  2 +-
 .../ContactFeature/ContactFeature.swift       | 15 ++--
 .../ContactSendRequestFeature.swift           | 28 -------
 .../Sources/ContactFeature/ContactView.swift  |  3 +-
 .../SendRequestFeature.swift                  | 28 +++++++
 .../SendRequestView.swift}                    | 14 ++--
 .../ContactFeatureTests.swift                 |  3 +-
 .../ContactSendRequestFeatureTests.swift      | 15 ----
 .../SendRequestFeatureTests.swift             | 15 ++++
 12 files changed, 168 insertions(+), 60 deletions(-)
 create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme
 delete mode 100644 Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
 rename Examples/xx-messenger/Sources/{ContactFeature/ContactSendRequestView.swift => SendRequestFeature/SendRequestView.swift} (52%)
 delete mode 100644 Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift
 create mode 100644 Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift

diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme
new file mode 100644
index 00000000..2f70b385
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/SendRequestFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SendRequestFeature"
+               BuildableName = "SendRequestFeature"
+               BlueprintName = "SendRequestFeature"
+               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 = "SendRequestFeatureTests"
+               BuildableName = "SendRequestFeatureTests"
+               BlueprintName = "SendRequestFeatureTests"
+               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 = "SendRequestFeature"
+            BuildableName = "SendRequestFeature"
+            BlueprintName = "SendRequestFeature"
+            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 79123e05..bbd8afa7 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -24,6 +24,7 @@ let package = Package(
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
+    .library(name: "SendRequestFeature", targets: ["SendRequestFeature"]),
     .library(name: "UserSearchFeature", targets: ["UserSearchFeature"]),
     .library(name: "WelcomeFeature", targets: ["WelcomeFeature"]),
   ],
@@ -74,6 +75,7 @@ let package = Package(
         .target(name: "HomeFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
+        .target(name: "SendRequestFeature"),
         .target(name: "UserSearchFeature"),
         .target(name: "WelcomeFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
@@ -94,6 +96,7 @@ let package = Package(
       name: "ContactFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "SendRequestFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
@@ -158,6 +161,20 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "SendRequestFeature",
+      dependencies: [
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "SendRequestFeatureTests",
+      dependencies: [
+        .target(name: "SendRequestFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "UserSearchFeature",
       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 0bf8b441..0d7cb106 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -89,6 +89,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SendRequestFeatureTests"
+               BuildableName = "SendRequestFeatureTests"
+               BlueprintName = "SendRequestFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 514f56bd..a060ad48 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -62,7 +62,7 @@ extension AppEnvironment {
                   mainQueue: mainQueue,
                   bgQueue: bgQueue,
                   sendRequest: {
-                    ContactSendRequestEnvironment()
+                    SendRequestEnvironment()
                   }
                 )
               }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 76715592..99b7b2bc 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -2,6 +2,7 @@ import AppCore
 import ComposableArchitecture
 import ComposablePresentation
 import Foundation
+import SendRequestFeature
 import XCTestDynamicOverlay
 import XXClient
 import XXMessengerClient
@@ -12,7 +13,7 @@ public struct ContactState: Equatable {
     id: Data,
     dbContact: XXModels.Contact? = nil,
     xxContact: XXClient.Contact? = nil,
-    sendRequest: ContactSendRequestState? = nil
+    sendRequest: SendRequestState? = nil
   ) {
     self.id = id
     self.dbContact = dbContact
@@ -23,7 +24,7 @@ public struct ContactState: Equatable {
   public var id: Data
   public var dbContact: XXModels.Contact?
   public var xxContact: XXClient.Contact?
-  public var sendRequest: ContactSendRequestState?
+  public var sendRequest: SendRequestState?
 }
 
 public enum ContactAction: Equatable {
@@ -32,7 +33,7 @@ public enum ContactAction: Equatable {
   case saveFactsTapped
   case sendRequestTapped
   case sendRequestDismissed
-  case sendRequest(ContactSendRequestAction)
+  case sendRequest(SendRequestAction)
 }
 
 public struct ContactEnvironment {
@@ -41,7 +42,7 @@ public struct ContactEnvironment {
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
-    sendRequest: @escaping () -> ContactSendRequestEnvironment
+    sendRequest: @escaping () -> SendRequestEnvironment
   ) {
     self.messenger = messenger
     self.db = db
@@ -54,7 +55,7 @@ public struct ContactEnvironment {
   public var db: DBManagerGetDB
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
-  public var sendRequest: () -> ContactSendRequestEnvironment
+  public var sendRequest: () -> SendRequestEnvironment
 }
 
 #if DEBUG
@@ -103,7 +104,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     .eraseToEffect()
 
   case .sendRequestTapped:
-    state.sendRequest = ContactSendRequestState()
+    state.sendRequest = SendRequestState()
     return .none
 
   case .sendRequestDismissed:
@@ -115,7 +116,7 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
   }
 }
 .presenting(
-  contactSendRequestReducer,
+  sendRequestReducer,
   state: .keyPath(\.sendRequest),
   id: .notNil(),
   action: /ContactAction.sendRequest,
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
deleted file mode 100644
index a0a474e5..00000000
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestFeature.swift
+++ /dev/null
@@ -1,28 +0,0 @@
-import ComposableArchitecture
-import XCTestDynamicOverlay
-
-public struct ContactSendRequestState: Equatable {
-  public init() {}
-}
-
-public enum ContactSendRequestAction: Equatable {
-  case start
-}
-
-public struct ContactSendRequestEnvironment {
-  public init() {}
-}
-
-#if DEBUG
-extension ContactSendRequestEnvironment {
-  public static let unimplemented = ContactSendRequestEnvironment()
-}
-#endif
-
-public let contactSendRequestReducer = Reducer<ContactSendRequestState, ContactSendRequestAction, ContactSendRequestEnvironment>
-{ state, action, env in
-  switch action {
-  case .start:
-    return .none
-  }
-}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 44a46c56..02f12d62 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -1,6 +1,7 @@
 import AppCore
 import ComposableArchitecture
 import ComposablePresentation
+import SendRequestFeature
 import SwiftUI
 import XXClient
 import XXModels
@@ -167,7 +168,7 @@ public struct ContactView: View {
           action: ContactAction.sendRequest
         ),
         onDeactivate: { viewStore.send(.sendRequestDismissed) },
-        destination: ContactSendRequestView.init(store:)
+        destination: SendRequestView.init(store:)
       ))
     }
   }
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
new file mode 100644
index 00000000..a6683c29
--- /dev/null
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -0,0 +1,28 @@
+import ComposableArchitecture
+import XCTestDynamicOverlay
+
+public struct SendRequestState: Equatable {
+  public init() {}
+}
+
+public enum SendRequestAction: Equatable {
+  case start
+}
+
+public struct SendRequestEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension SendRequestEnvironment {
+  public static let unimplemented = SendRequestEnvironment()
+}
+#endif
+
+public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
similarity index 52%
rename from Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift
rename to Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
index 4f5048d3..40042466 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactSendRequestView.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
@@ -1,15 +1,15 @@
 import ComposableArchitecture
 import SwiftUI
 
-public struct ContactSendRequestView: View {
-  public init(store: Store<ContactSendRequestState, ContactSendRequestAction>) {
+public struct SendRequestView: View {
+  public init(store: Store<SendRequestState, SendRequestAction>) {
     self.store = store
   }
 
-  let store: Store<ContactSendRequestState, ContactSendRequestAction>
+  let store: Store<SendRequestState, SendRequestAction>
 
   struct ViewState: Equatable {
-    init(state: ContactSendRequestState) {}
+    init(state: SendRequestState) {}
   }
 
   public var body: some View {
@@ -24,10 +24,10 @@ public struct ContactSendRequestView: View {
 }
 
 #if DEBUG
-public struct ContactSendRequestView_Previews: PreviewProvider {
+public struct SendRequestView_Previews: PreviewProvider {
   public static var previews: some View {
-    ContactSendRequestView(store: Store(
-      initialState: ContactSendRequestState(),
+    SendRequestView(store: Store(
+      initialState: SendRequestState(),
       reducer: .empty,
       environment: ()
     ))
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index 64d0d72a..9f323870 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -1,6 +1,7 @@
 import Combine
 import ComposableArchitecture
 import CustomDump
+import SendRequestFeature
 import XCTest
 import XXClient
 import XXModels
@@ -104,7 +105,7 @@ final class ContactFeatureTests: XCTestCase {
     )
 
     store.send(.sendRequestTapped) {
-      $0.sendRequest = ContactSendRequestState()
+      $0.sendRequest = SendRequestState()
     }
 
     store.send(.sendRequestDismissed) {
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift
deleted file mode 100644
index 8d4eb040..00000000
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactSendRequestFeatureTests.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import ComposableArchitecture
-import XCTest
-@testable import ContactFeature
-
-final class ContactSendRequestFeatureTests: XCTestCase {
-  func testStart() {
-    let store = TestStore(
-      initialState: ContactSendRequestState(),
-      reducer: contactSendRequestReducer,
-      environment: .unimplemented
-    )
-
-    store.send(.start)
-  }
-}
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
new file mode 100644
index 00000000..c64665d2
--- /dev/null
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import SendRequestFeature
+
+final class SendRequestFeatureTests: XCTestCase {
+  func testStart() {
+    let store = TestStore(
+      initialState: SendRequestState(),
+      reducer: sendRequestReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start)
+  }
+}
-- 
GitLab


From adce3dd232c6a63a08e21c6b74752109cd168018 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 14:21:42 +0200
Subject: [PATCH 15/29] Implement SendRequest UI

---
 Examples/xx-messenger/Package.swift           |   4 +
 .../AppFeature/AppEnvironment+Live.swift      |   1 +
 .../ContactFeature/ContactFeature.swift       |   6 +-
 .../SendRequestFeature.swift                  |  38 +++++-
 .../SendRequestFeature/SendRequestView.swift  | 129 +++++++++++++++++-
 .../ContactFeatureTests.swift                 |  40 +++++-
 .../SendRequestFeatureTests.swift             |   4 +-
 7 files changed, 209 insertions(+), 13 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index bbd8afa7..7a0357bd 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -164,7 +164,11 @@ let package = Package(
     .target(
       name: "SendRequestFeature",
       dependencies: [
+        .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+        .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 a060ad48..49ea8087 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -4,6 +4,7 @@ import Foundation
 import HomeFeature
 import RegisterFeature
 import RestoreFeature
+import SendRequestFeature
 import UserSearchFeature
 import WelcomeFeature
 import XXMessengerClient
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index 99b7b2bc..0bf0e29b 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -104,7 +104,11 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     .eraseToEffect()
 
   case .sendRequestTapped:
-    state.sendRequest = SendRequestState()
+    if let xxContact = state.xxContact {
+      state.sendRequest = SendRequestState(contact: xxContact)
+    } else if let marshaled = state.dbContact?.marshaled {
+      state.sendRequest = SendRequestState(contact: .live(marshaled))
+    }
     return .none
 
   case .sendRequestDismissed:
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index a6683c29..cefd80d3 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -1,12 +1,40 @@
 import ComposableArchitecture
 import XCTestDynamicOverlay
+import XXClient
+import XXModels
 
 public struct SendRequestState: Equatable {
-  public init() {}
+  public init(
+    contact: XXClient.Contact,
+    myContact: XXClient.Contact? = nil,
+    sendUsername: Bool = true,
+    sendEmail: Bool = true,
+    sendPhone: Bool = true,
+    isSending: Bool = false,
+    failure: String? = nil
+  ) {
+    self.contact = contact
+    self.myContact = myContact
+    self.sendUsername = sendUsername
+    self.sendEmail = sendEmail
+    self.sendPhone = sendPhone
+    self.isSending = isSending
+    self.failure = failure
+  }
+
+  public var contact: XXClient.Contact
+  public var myContact: XXClient.Contact?
+  @BindableState public var sendUsername: Bool
+  @BindableState public var sendEmail: Bool
+  @BindableState public var sendPhone: Bool
+  public var isSending: Bool
+  public var failure: String?
 }
 
-public enum SendRequestAction: Equatable {
+public enum SendRequestAction: Equatable, BindableAction {
   case start
+  case sendTapped
+  case binding(BindingAction<SendRequestState>)
 }
 
 public struct SendRequestEnvironment {
@@ -24,5 +52,11 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen
   switch action {
   case .start:
     return .none
+
+  case .sendTapped:
+    return .none
+
+  case .binding(_):
+    return .none
   }
 }
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
index 40042466..f1bd8ff5 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
@@ -1,5 +1,7 @@
+import AppCore
 import ComposableArchitecture
 import SwiftUI
+import XXClient
 
 public struct SendRequestView: View {
   public init(store: Store<SendRequestState, SendRequestAction>) {
@@ -9,13 +11,101 @@ public struct SendRequestView: View {
   let store: Store<SendRequestState, SendRequestAction>
 
   struct ViewState: Equatable {
-    init(state: SendRequestState) {}
+    var contact: XXClient.Contact
+    var myContact: XXClient.Contact?
+    var sendUsername: Bool
+    var sendEmail: Bool
+    var sendPhone: Bool
+    var isSending: Bool
+    var failure: String?
+
+    init(state: SendRequestState) {
+      contact = state.contact
+      myContact = state.myContact
+      sendUsername = state.sendUsername
+      sendEmail = state.sendEmail
+      sendPhone = state.sendPhone
+      isSending = state.isSending
+      failure = state.failure
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
+        Section {
+          HStack {
+            Label(viewStore.myContact?.username ?? "", systemImage: "person")
+            Spacer()
+            Toggle(
+              isOn: viewStore.binding(
+                get: \.sendUsername,
+                send: { SendRequestAction.set(\.$sendUsername, $0) }
+              ),
+              label: EmptyView.init
+            )
+          }
+
+          HStack {
+            Label(viewStore.myContact?.email ?? "", systemImage: "envelope")
+            Spacer()
+            Toggle(
+              isOn: viewStore.binding(
+                get: \.sendEmail,
+                send: { SendRequestAction.set(\.$sendEmail, $0) }
+              ),
+              label: EmptyView.init
+            )
+          }
+
+          HStack {
+            Label(viewStore.myContact?.phone ?? "", systemImage: "phone")
+            Spacer()
+            Toggle(
+              isOn: viewStore.binding(
+                get: \.sendPhone,
+                send: { SendRequestAction.set(\.$sendPhone, $0) }
+              ),
+              label: EmptyView.init
+            )
+          }
+        } header: {
+          Text("My facts")
+        }
+        .disabled(viewStore.isSending)
 
+        Section {
+          Label(viewStore.contact.username ?? "", systemImage: "person")
+          Label(viewStore.contact.email ?? "", systemImage: "envelope")
+          Label(viewStore.contact.phone ?? "", systemImage: "phone")
+        } header: {
+          Text("Contact")
+        }
+
+        Section {
+          Button {
+            viewStore.send(.sendTapped)
+          } label: {
+            HStack {
+              Text("Send request")
+              Spacer()
+              if viewStore.isSending {
+                ProgressView()
+              } else {
+                Image(systemName: "paperplane")
+              }
+            }
+          }
+        }
+        .disabled(viewStore.isSending)
+
+        if let failure = viewStore.failure {
+          Section {
+            Text(failure)
+          } header: {
+            Text("Error")
+          }
+        }
       }
       .navigationTitle("Send Request")
       .task { viewStore.send(.start) }
@@ -26,11 +116,38 @@ public struct SendRequestView: View {
 #if DEBUG
 public struct SendRequestView_Previews: PreviewProvider {
   public static var previews: some View {
-    SendRequestView(store: Store(
-      initialState: SendRequestState(),
-      reducer: .empty,
-      environment: ()
-    ))
+    NavigationView {
+      SendRequestView(store: Store(
+        initialState: SendRequestState(
+          contact: {
+            var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!)
+            contact.getFactsFromContact.run = { _ in
+              [
+                Fact(fact: "contact-username", type: 0),
+                Fact(fact: "contact-email", type: 1),
+                Fact(fact: "contact-phone", type: 2),
+              ]
+            }
+            return contact
+          }(),
+          myContact: {
+            var contact = XXClient.Contact.unimplemented("my-data".data(using: .utf8)!)
+            contact.getFactsFromContact.run = { _ in
+              [
+                Fact(fact: "my-username", type: 0),
+                Fact(fact: "my-email", type: 1),
+                Fact(fact: "my-phone", type: 2),
+              ]
+            }
+            return contact
+          }(),
+          isSending: true,
+          failure: "Something went wrong"
+        ),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
   }
 }
 #endif
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index 9f323870..e629b3f0 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -95,18 +95,52 @@ final class ContactFeatureTests: XCTestCase {
     XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact])
   }
 
-  func testSendRequest() {
+  func testSendRequestWithDBContact() {
+    var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
+    dbContact.marshaled = "contact-data".data(using: .utf8)!
+
     let store = TestStore(
       initialState: ContactState(
-        id: "contact-id".data(using: .utf8)!
+        id: dbContact.id,
+        dbContact: dbContact
       ),
       reducer: contactReducer,
       environment: .unimplemented
     )
 
     store.send(.sendRequestTapped) {
-      $0.sendRequest = SendRequestState()
+      $0.sendRequest = SendRequestState(contact: .live(dbContact.marshaled!))
     }
+  }
+
+  func testSendRequestWithXXContact() {
+    let xxContact = XXClient.Contact.unimplemented("contact-id".data(using: .utf8)!)
+
+    let store = TestStore(
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!,
+        xxContact: xxContact
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.sendRequestTapped) {
+      $0.sendRequest = SendRequestState(contact: xxContact)
+    }
+  }
+
+  func testSendRequestDismissed() {
+    let store = TestStore(
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!,
+        sendRequest: SendRequestState(
+          contact: .unimplemented("contact-id".data(using: .utf8)!)
+        )
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
 
     store.send(.sendRequestDismissed) {
       $0.sendRequest = nil
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index c64665d2..a67a241c 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -5,7 +5,9 @@ import XCTest
 final class SendRequestFeatureTests: XCTestCase {
   func testStart() {
     let store = TestStore(
-      initialState: SendRequestState(),
+      initialState: SendRequestState(
+        contact: .unimplemented("contact-data".data(using: .utf8)!)
+      ),
       reducer: sendRequestReducer,
       environment: .unimplemented
     )
-- 
GitLab


From 7a15a34db018b6c76d58cf308ce2738b2e6c35f7 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 14:53:12 +0200
Subject: [PATCH 16/29] Fix saving contact when registering

---
 Examples/xx-messenger/Package.swift                        | 1 +
 .../Sources/RegisterFeature/RegisterFeature.swift          | 7 ++++++-
 .../Tests/RegisterFeatureTests/RegisterFeatureTests.swift  | 7 +++++++
 3 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 7a0357bd..77b97a0f 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -135,6 +135,7 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXModels", package: "client-ios-db"),
       ],
diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
index 8e1f3541..b55cae7d 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
@@ -2,6 +2,7 @@ import AppCore
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
+import XXClient
 import XXMessengerClient
 import XXModels
 
@@ -81,7 +82,11 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
       do {
         let db = try env.db()
         try env.messenger.register(username: username)
-        let contact = env.messenger.e2e()!.getContact()
+        var contact = try env.messenger.e2e.tryGet().getContact()
+        var facts: [Fact] = (try? contact.getFacts()) ?? []
+        facts.removeAll(where: { $0.type == 0 })
+        facts.append(Fact(fact: username, type: 0))
+        try contact.setFacts(facts)
         try db.saveContact(Contact(
           id: try contact.getId(),
           marshaled: contact.data,
diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
index a2aad5bd..12addba7 100644
--- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
@@ -16,6 +16,7 @@ final class RegisterFeatureTests: XCTestCase {
     let now = Date()
     let mainQueue = DispatchQueue.test
     let bgQueue = DispatchQueue.test
+    var didSetFactsOnContact: [[XXClient.Fact]] = []
     var dbDidSaveContact: [XXModels.Contact] = []
     var messengerDidRegisterUsername: [String] = []
 
@@ -30,6 +31,11 @@ final class RegisterFeatureTests: XCTestCase {
       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
@@ -57,6 +63,7 @@ final class RegisterFeatureTests: XCTestCase {
     bgQueue.advance()
 
     XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"])
+    XCTAssertNoDifference(didSetFactsOnContact, [[Fact(fact: "NewUser", type: 0)]])
     XCTAssertNoDifference(dbDidSaveContact, [
       XXModels.Contact(
         id: "contact-id".data(using: .utf8)!,
-- 
GitLab


From f7ff8b5949868a7cfbf368433dbff212d8465b8b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 15:03:00 +0200
Subject: [PATCH 17/29] Add DBManagerRemoveDB function

---
 .../Sources/AppCore/DBManager/DBManager.swift |  7 +++--
 .../AppCore/DBManager/DBManagerRemoveDB.swift | 30 +++++++++++++++++++
 2 files changed, 35 insertions(+), 2 deletions(-)
 create mode 100644 Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift

diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift
index 178e1078..f591dc1c 100644
--- a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift
+++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift
@@ -4,6 +4,7 @@ public struct DBManager {
   public var hasDB: DBManagerHasDB
   public var makeDB: DBManagerMakeDB
   public var getDB: DBManagerGetDB
+  public var removeDB: DBManagerRemoveDB
 }
 
 extension DBManager {
@@ -17,7 +18,8 @@ extension DBManager {
     return DBManager(
       hasDB: .init { container.db != nil },
       makeDB: .live(setDB: { container.db = $0 }),
-      getDB: .live(getDB: { container.db })
+      getDB: .live(getDB: { container.db }),
+      removeDB: .live(getDB: { container.db }, unsetDB: { container.db = nil })
     )
   }
 }
@@ -26,6 +28,7 @@ extension DBManager {
   public static let unimplemented = DBManager(
     hasDB: .unimplemented,
     makeDB: .unimplemented,
-    getDB: .unimplemented
+    getDB: .unimplemented,
+    removeDB: .unimplemented
   )
 }
diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift
new file mode 100644
index 00000000..69ab6e02
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift
@@ -0,0 +1,30 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXDatabase
+import XXModels
+
+public struct DBManagerRemoveDB {
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws -> Void {
+    try run()
+  }
+}
+
+extension DBManagerRemoveDB {
+  public static func live(
+    getDB: @escaping () -> Database?,
+    unsetDB: @escaping () -> Void
+  ) -> DBManagerRemoveDB {
+    DBManagerRemoveDB {
+      try getDB()?.drop()
+      unsetDB()
+    }
+  }
+}
+
+extension DBManagerRemoveDB {
+  public static let unimplemented = DBManagerRemoveDB(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
-- 
GitLab


From 557dadbc709a4a3d5b8b0be20926de01b9f18f90 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 15:03:44 +0200
Subject: [PATCH 18/29] Remove db on account deletion

---
 .../Sources/AppFeature/AppEnvironment+Live.swift     |  2 +-
 .../Sources/HomeFeature/HomeFeature.swift            | 12 ++++++------
 .../Tests/HomeFeatureTests/HomeFeatureTests.swift    | 12 ++++++------
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 49ea8087..9b298c38 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -36,7 +36,7 @@ extension AppEnvironment {
       home: {
         HomeEnvironment(
           messenger: messenger,
-          db: dbManager.getDB,
+          dbManager: dbManager,
           mainQueue: mainQueue,
           bgQueue: bgQueue,
           register: {
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
index 51083ea0..712855ba 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
@@ -71,14 +71,14 @@ public enum HomeAction: Equatable {
 public struct HomeEnvironment {
   public init(
     messenger: Messenger,
-    db: DBManagerGetDB,
+    dbManager: DBManager,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
     register: @escaping () -> RegisterEnvironment,
     userSearch: @escaping () -> UserSearchEnvironment
   ) {
     self.messenger = messenger
-    self.db = db
+    self.dbManager = dbManager
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.register = register
@@ -86,7 +86,7 @@ public struct HomeEnvironment {
   }
 
   public var messenger: Messenger
-  public var db: DBManagerGetDB
+  public var dbManager: DBManager
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var register: () -> RegisterEnvironment
@@ -96,7 +96,7 @@ public struct HomeEnvironment {
 extension HomeEnvironment {
   public static let unimplemented = HomeEnvironment(
     messenger: .unimplemented,
-    db: .unimplemented,
+    dbManager: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
     register: { .unimplemented },
@@ -197,13 +197,13 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
     return .result {
       do {
         let contactId = try env.messenger.e2e.tryGet().getContact().getId()
-        let contact = try env.db().fetchContacts(.init(id: [contactId])).first
+        let contact = try env.dbManager.getDB().fetchContacts(.init(id: [contactId])).first
         if let username = contact?.username {
           let ud = try env.messenger.ud.tryGet()
           try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0))
         }
         try env.messenger.destroy()
-        try env.db().drop()
+        try env.dbManager.removeDB()
         return .success(.deleteAccount(.success))
       } catch {
         return .success(.deleteAccount(.failure(error as NSError)))
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index aa309016..cbc261ce 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -316,7 +316,7 @@ final class HomeFeatureTests: XCTestCase {
     var dbDidFetchContacts: [XXModels.Contact.Query] = []
     var udDidPermanentDeleteAccount: [Fact] = []
     var messengerDidDestroy = 0
-    var dbDidDrop = 0
+    var didRemoveDB = 0
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
@@ -329,7 +329,7 @@ final class HomeFeatureTests: XCTestCase {
       }
       return e2e
     }
-    store.environment.db.run = {
+    store.environment.dbManager.getDB.run = {
       var db: Database = .failing
       db.fetchContacts.run = { query in
         dbDidFetchContacts.append(query)
@@ -341,11 +341,11 @@ final class HomeFeatureTests: XCTestCase {
           )
         ]
       }
-      db.drop.run = {
-        dbDidDrop += 1
-      }
       return db
     }
+    store.environment.dbManager.removeDB.run = {
+      didRemoveDB += 1
+    }
     store.environment.messenger.ud.get = {
       var ud: UserDiscovery = .unimplemented
       ud.permanentDeleteAccount.run = { usernameFact in
@@ -372,7 +372,7 @@ final class HomeFeatureTests: XCTestCase {
     XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])])
     XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)])
     XCTAssertNoDifference(messengerDidDestroy, 1)
-    XCTAssertNoDifference(dbDidDrop, 1)
+    XCTAssertNoDifference(didRemoveDB, 1)
 
     store.receive(.deleteAccount(.success)) {
       $0.isDeletingAccount = false
-- 
GitLab


From 15798ff9a952591d190a899230c8bb4a560e4058 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 15:18:40 +0200
Subject: [PATCH 19/29] Fetch my facts in SendRequestFeature

---
 .../AppFeature/AppEnvironment+Live.swift      |  7 +-
 .../SendRequestFeature.swift                  | 46 ++++++++++++-
 .../SendRequestFeatureTests.swift             | 68 +++++++++++++++++++
 3 files changed, 118 insertions(+), 3 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 9b298c38..298cb9f6 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -63,7 +63,12 @@ extension AppEnvironment {
                   mainQueue: mainQueue,
                   bgQueue: bgQueue,
                   sendRequest: {
-                    SendRequestEnvironment()
+                    SendRequestEnvironment(
+                      messenger: messenger,
+                      db: dbManager.getDB,
+                      mainQueue: mainQueue,
+                      bgQueue: bgQueue
+                    )
                   }
                 )
               }
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index cefd80d3..8740c744 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -1,6 +1,9 @@
+import AppCore
 import ComposableArchitecture
+import Foundation
 import XCTestDynamicOverlay
 import XXClient
+import XXMessengerClient
 import XXModels
 
 public struct SendRequestState: Equatable {
@@ -35,22 +38,60 @@ public enum SendRequestAction: Equatable, BindableAction {
   case start
   case sendTapped
   case binding(BindingAction<SendRequestState>)
+  case myContactFetched(XXClient.Contact?)
 }
 
 public struct SendRequestEnvironment {
-  public init() {}
+  public init(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.db = db
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var db: DBManagerGetDB
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension SendRequestEnvironment {
-  public static let unimplemented = SendRequestEnvironment()
+  public static let unimplemented = SendRequestEnvironment(
+    messenger: .unimplemented,
+    db: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
 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)
+
+  case .myContactFetched(let contact):
+    state.myContact = contact
     return .none
 
   case .sendTapped:
@@ -60,3 +101,4 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen
     return .none
   }
 }
+.binding()
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index a67a241c..a0a7750f 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -1,5 +1,8 @@
+import Combine
 import ComposableArchitecture
 import XCTest
+import XXClient
+import XXModels
 @testable import SendRequestFeature
 
 final class SendRequestFeatureTests: XCTestCase {
@@ -12,6 +15,71 @@ final class SendRequestFeatureTests: XCTestCase {
       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
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContactsPublisher.run = { query in
+        dbDidFetchContacts.append(query)
+        return dbContactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
     store.send(.start)
+
+    XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["my-contact-id".data(using: .utf8)!])])
+
+    dbContactsPublisher.send([])
+
+    store.receive(.myContactFetched(nil))
+
+    var myDbContact = XXModels.Contact(id: "my-contact-id".data(using: .utf8)!)
+    myDbContact.marshaled = "my-contact-data".data(using: .utf8)!
+    dbContactsPublisher.send([myDbContact])
+
+    store.receive(.myContactFetched(.live("my-contact-data".data(using: .utf8)!))) {
+      $0.myContact = .live("my-contact-data".data(using: .utf8)!)
+    }
+
+    dbContactsPublisher.send(completion: .finished)
+  }
+
+  func testSendRequest() {
+    var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!)
+    myContact.getFactsFromContact.run = { _ in
+      [
+        Fact(fact: "my-username", type: 0),
+        Fact(fact: "my-email", type: 1),
+        Fact(fact: "my-phone", type: 2),
+      ]
+    }
+
+    let store = TestStore(
+      initialState: SendRequestState(
+        contact: .unimplemented("contact-data".data(using: .utf8)!),
+        myContact: myContact
+      ),
+      reducer: sendRequestReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.set(\.$sendPhone, false)) {
+      $0.sendPhone = false
+    }
+
+    store.send(.sendTapped)
   }
 }
-- 
GitLab


From 4f55b8b49e9281e1a7b05fb7e0f4ef8c0f2337e9 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 15:37:11 +0200
Subject: [PATCH 20/29] Fix icon

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

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index 02f12d62..a5f07bc0 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -83,7 +83,7 @@ public struct ContactView: View {
               HStack {
                 Text("Request sent")
                 Spacer()
-                Image(systemName: "chevron.forward")
+                Image(systemName: "paperplane")
               }
 
             case .requestFailed:
-- 
GitLab


From b65e5aeac07a46effe203028ca824415642d8dac Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 15:50:03 +0200
Subject: [PATCH 21/29] Send auth request

---
 .../SendRequestFeature.swift                  |  47 +++++++
 .../SendRequestFeatureTests.swift             | 120 ++++++++++++++++--
 2 files changed, 157 insertions(+), 10 deletions(-)

diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index 8740c744..684e344a 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -37,6 +37,8 @@ public struct SendRequestState: Equatable {
 public enum SendRequestAction: Equatable, BindableAction {
   case start
   case sendTapped
+  case sendSucceeded
+  case sendFailed(String)
   case binding(BindingAction<SendRequestState>)
   case myContactFetched(XXClient.Contact?)
 }
@@ -95,6 +97,51 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen
     return .none
 
   case .sendTapped:
+    state.isSending = true
+    state.failure = nil
+    return .result { [state] in
+      func updateAuthStatus(_ authStatus: XXModels.Contact.AuthStatus) throws {
+        try env.db().bulkUpdateContacts(
+          .init(id: [try state.contact.getId()]),
+          .init(authStatus: authStatus)
+        )
+      }
+      do {
+        try updateAuthStatus(.requesting)
+        let myFacts = try state.myContact?.getFacts() ?? []
+        var includedFacts: [Fact] = []
+        if state.sendUsername, let fact = myFacts.first(where: { $0.type == 0 }) {
+          includedFacts.append(fact)
+        }
+        if state.sendEmail, let fact = myFacts.first(where: { $0.type == 1 }) {
+          includedFacts.append(fact)
+        }
+        if state.sendPhone, let fact = myFacts.first(where: { $0.type == 2 }) {
+          includedFacts.append(fact)
+        }
+        _ = try env.messenger.e2e.tryGet().requestAuthenticatedChannel(
+          partner: state.contact,
+          myFacts: includedFacts
+        )
+        try updateAuthStatus(.requested)
+        return .success(.sendSucceeded)
+      } catch {
+        try? updateAuthStatus(.requestFailed)
+        return .success(.sendFailed(error.localizedDescription))
+      }
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .sendSucceeded:
+    state.isSending = false
+    state.failure = nil
+    return .none
+
+  case .sendFailed(let failure):
+    state.isSending = false
+    state.failure = failure
     return .none
 
   case .binding(_):
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index a0a7750f..1dbe26be 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -58,28 +58,128 @@ final class SendRequestFeatureTests: XCTestCase {
   }
 
   func testSendRequest() {
+    var contact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!)
+    contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
+
     var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!)
-    myContact.getFactsFromContact.run = { _ in
-      [
-        Fact(fact: "my-username", type: 0),
-        Fact(fact: "my-email", type: 1),
-        Fact(fact: "my-phone", type: 2),
-      ]
+    let myFacts = [
+      Fact(fact: "my-username", type: 0),
+      Fact(fact: "my-email", type: 1),
+      Fact(fact: "my-phone", type: 2),
+    ]
+    myContact.getFactsFromContact.run = { _ in myFacts }
+
+    let store = TestStore(
+      initialState: SendRequestState(
+        contact: contact,
+        myContact: myContact
+      ),
+      reducer: sendRequestReducer,
+      environment: .unimplemented
+    )
+
+    struct DidBulkUpdateContacts: Equatable {
+      var query: XXModels.Contact.Query
+      var assignments: XXModels.Contact.Assignments
+    }
+    struct DidRequestAuthChannel: Equatable {
+      var partner: XXClient.Contact
+      var myFacts: [XXClient.Fact]
+    }
+
+    var didBulkUpdateContacts: [DidBulkUpdateContacts] = []
+    var didRequestAuthChannel: [DidRequestAuthChannel] = []
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.bulkUpdateContacts.run = { query, assignments in
+        didBulkUpdateContacts.append(.init(query: query, assignments: assignments))
+        return 0
+      }
+      return db
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.requestAuthenticatedChannel.run = { partner, myFacts in
+        didRequestAuthChannel.append(.init(partner: partner, myFacts: myFacts))
+        return 0
+      }
+      return e2e
     }
 
+    store.send(.sendTapped) {
+      $0.isSending = true
+    }
+
+    XCTAssertNoDifference(didBulkUpdateContacts, [
+      .init(
+        query: .init(id: ["contact-id".data(using: .utf8)!]),
+        assignments: .init(authStatus: .requesting)
+      ),
+      .init(
+        query: .init(id: ["contact-id".data(using: .utf8)!]),
+        assignments: .init(authStatus: .requested)
+      )
+    ])
+
+    XCTAssertNoDifference(didRequestAuthChannel, [
+      .init(
+        partner: contact,
+        myFacts: myFacts
+      )
+    ])
+
+    store.receive(.sendSucceeded) {
+      $0.isSending = false
+    }
+  }
+
+  func testSendRequestFailure() {
+    var contact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!)
+    contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
+
+    var myContact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!)
+    let myFacts = [
+      Fact(fact: "my-username", type: 0),
+      Fact(fact: "my-email", type: 1),
+      Fact(fact: "my-phone", type: 2),
+    ]
+    myContact.getFactsFromContact.run = { _ in myFacts }
+
     let store = TestStore(
       initialState: SendRequestState(
-        contact: .unimplemented("contact-data".data(using: .utf8)!),
+        contact: contact,
         myContact: myContact
       ),
       reducer: sendRequestReducer,
       environment: .unimplemented
     )
 
-    store.send(.set(\.$sendPhone, false)) {
-      $0.sendPhone = false
+    struct Failure: Error {}
+    let failure = Failure()
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.bulkUpdateContacts.run = { _, _ in return 0 }
+      return db
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.requestAuthenticatedChannel.run = { _, _ in throw failure }
+      return e2e
     }
 
-    store.send(.sendTapped)
+    store.send(.sendTapped) {
+      $0.isSending = true
+    }
+
+    store.receive(.sendFailed(failure.localizedDescription)) {
+      $0.isSending = false
+      $0.failure = failure.localizedDescription
+    }
   }
 }
-- 
GitLab


From c7012099e74858107a8287ffcf81960f68674ac1 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 16:07:13 +0200
Subject: [PATCH 22/29] Update ContactView

---
 .../Sources/ContactFeature/ContactView.swift  | 30 +++++++------------
 1 file changed, 11 insertions(+), 19 deletions(-)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index a5f07bc0..d14271f0 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -62,15 +62,6 @@ public struct ContactView: View {
                 Spacer()
                 Image(systemName: "person.fill.questionmark")
               }
-              Button {
-                viewStore.send(.sendRequestTapped)
-              } label: {
-                HStack {
-                  Text("Send request")
-                  Spacer()
-                  Image(systemName: "chevron.forward")
-                }
-              }
 
             case .requesting:
               HStack {
@@ -93,15 +84,6 @@ public struct ContactView: View {
                 Image(systemName: "xmark.diamond.fill")
                   .foregroundColor(.red)
               }
-              Button {
-                viewStore.send(.sendRequestTapped)
-              } label: {
-                HStack {
-                  Text("Resend request")
-                  Spacer()
-                  Image(systemName: "paperplane")
-                }
-              }
 
             case .verificationInProgress:
               HStack {
@@ -154,8 +136,17 @@ public struct ContactView: View {
                 Image(systemName: "eye.slash")
               }
             }
+            Button {
+              viewStore.send(.sendRequestTapped)
+            } label: {
+              HStack {
+                Text("Send request")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
           } header: {
-            Text("Auth status")
+            Text("Auth")
           }
           .animation(.default, value: viewStore.dbContact?.authStatus)
         }
@@ -167,6 +158,7 @@ public struct ContactView: View {
           state: \.sendRequest,
           action: ContactAction.sendRequest
         ),
+        mapState: replayNonNil(),
         onDeactivate: { viewStore.send(.sendRequestDismissed) },
         destination: SendRequestView.init(store:)
       ))
-- 
GitLab


From ab4f5b1ddaffaf9a2c90ba94776a8e4797d8adda Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 16:09:36 +0200
Subject: [PATCH 23/29] Dismiss SendRequest on success

---
 .../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 0bf0e29b..c75faa7b 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -115,6 +115,10 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     state.sendRequest = nil
     return .none
 
+  case .sendRequest(.sendSucceeded):
+    state.sendRequest = nil
+    return .none
+
   case .sendRequest(_):
     return .none
   }
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index e629b3f0..4d637ecf 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -146,4 +146,21 @@ final class ContactFeatureTests: XCTestCase {
       $0.sendRequest = nil
     }
   }
+
+  func testSendRequestSucceeded() {
+    let store = TestStore(
+      initialState: ContactState(
+        id: "contact-id".data(using: .utf8)!,
+        sendRequest: SendRequestState(
+          contact: .unimplemented("contact-id".data(using: .utf8)!)
+        )
+      ),
+      reducer: contactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.sendRequest(.sendSucceeded)) {
+      $0.sendRequest = nil
+    }
+  }
 }
-- 
GitLab


From 5c6998723cb3a2f25f08fbdddfe86ca3367d306a Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 16:41:56 +0200
Subject: [PATCH 24/29] Update getFacts calls

---
 .../Sources/RegisterFeature/RegisterFeature.swift          | 2 +-
 .../UserSearchFeature/UserSearchResultFeature.swift        | 7 +++----
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
index b55cae7d..fcb9c62e 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
@@ -83,7 +83,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
         let db = try env.db()
         try env.messenger.register(username: username)
         var contact = try env.messenger.e2e.tryGet().getContact()
-        var facts: [Fact] = (try? contact.getFacts()) ?? []
+        var facts: [Fact] = try contact.getFacts()
         facts.removeAll(where: { $0.type == 0 })
         facts.append(Fact(fact: username, type: 0))
         try contact.setFacts(facts)
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
index ece68742..a331df70 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -44,10 +44,9 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe
 { state, action, env in
   switch action {
   case .start:
-    let facts = (try? state.xxContact.getFacts()) ?? []
-    state.username = facts.first(where: { $0.type == 0 })?.fact
-    state.email = facts.first(where: { $0.type == 1 })?.fact
-    state.phone = facts.first(where: { $0.type == 2 })?.fact
+    state.username = state.xxContact.username
+    state.email = state.xxContact.email
+    state.phone = state.xxContact.phone
     return .none
 
   case .tapped:
-- 
GitLab


From 83bdd9faa9b91b7aa335f40b922112acf8f8313a Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 16:57:56 +0200
Subject: [PATCH 25/29] Update SendRequestView

---
 .../SendRequestFeature/SendRequestView.swift  | 68 ++++++++++---------
 1 file changed, 37 insertions(+), 31 deletions(-)

diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
index f1bd8ff5..159d10e8 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
@@ -34,41 +34,44 @@ public struct SendRequestView: View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
         Section {
-          HStack {
-            Label(viewStore.myContact?.username ?? "", systemImage: "person")
-            Spacer()
-            Toggle(
-              isOn: viewStore.binding(
-                get: \.sendUsername,
-                send: { SendRequestAction.set(\.$sendUsername, $0) }
-              ),
-              label: EmptyView.init
-            )
+          Button {
+            viewStore.send(.set(\.$sendUsername, !viewStore.sendUsername))
+          } label: {
+            HStack {
+              Label(viewStore.myContact?.username ?? "", systemImage: "person")
+                .tint(Color.primary)
+              Spacer()
+              Image(systemName: viewStore.sendUsername ? "checkmark.circle.fill" : "circle")
+                .foregroundColor(.accentColor)
+            }
           }
+          .animation(.default, value: viewStore.sendUsername)
 
-          HStack {
-            Label(viewStore.myContact?.email ?? "", systemImage: "envelope")
-            Spacer()
-            Toggle(
-              isOn: viewStore.binding(
-                get: \.sendEmail,
-                send: { SendRequestAction.set(\.$sendEmail, $0) }
-              ),
-              label: EmptyView.init
-            )
+          Button {
+            viewStore.send(.set(\.$sendEmail, !viewStore.sendEmail))
+          } label: {
+            HStack {
+              Label(viewStore.myContact?.email ?? "", systemImage: "envelope")
+                .tint(Color.primary)
+              Spacer()
+              Image(systemName: viewStore.sendEmail ? "checkmark.circle.fill" : "circle")
+                .foregroundColor(.accentColor)
+            }
           }
+          .animation(.default, value: viewStore.sendEmail)
 
-          HStack {
-            Label(viewStore.myContact?.phone ?? "", systemImage: "phone")
-            Spacer()
-            Toggle(
-              isOn: viewStore.binding(
-                get: \.sendPhone,
-                send: { SendRequestAction.set(\.$sendPhone, $0) }
-              ),
-              label: EmptyView.init
-            )
+          Button {
+            viewStore.send(.set(\.$sendPhone, !viewStore.sendPhone))
+          } label: {
+            HStack {
+              Label(viewStore.myContact?.phone ?? "", systemImage: "phone")
+                .tint(Color.primary)
+              Spacer()
+              Image(systemName: viewStore.sendPhone ? "checkmark.circle.fill" : "circle")
+                .foregroundColor(.accentColor)
+            }
           }
+          .animation(.default, value: viewStore.sendPhone)
         } header: {
           Text("My facts")
         }
@@ -141,7 +144,10 @@ public struct SendRequestView_Previews: PreviewProvider {
             }
             return contact
           }(),
-          isSending: true,
+          sendUsername: true,
+          sendEmail: false,
+          sendPhone: true,
+          isSending: false,
           failure: "Something went wrong"
         ),
         reducer: .empty,
-- 
GitLab


From 16b258b9ff7fcb74ac619cc1d9cd2879fa1b78e5 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 22:29:58 +0200
Subject: [PATCH 26/29] Import selected facts to contact

---
 .../ContactFeature/ContactFeature.swift       | 32 ++++++++++---
 .../Sources/ContactFeature/ContactView.swift  | 47 +++++++++++++++++--
 .../ContactFeatureTests.swift                 |  4 +-
 3 files changed, 71 insertions(+), 12 deletions(-)

diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index c75faa7b..b8da32a4 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -13,27 +13,37 @@ public struct ContactState: Equatable {
     id: Data,
     dbContact: XXModels.Contact? = nil,
     xxContact: XXClient.Contact? = nil,
+    importUsername: Bool = true,
+    importEmail: Bool = true,
+    importPhone: Bool = true,
     sendRequest: SendRequestState? = nil
   ) {
     self.id = id
     self.dbContact = dbContact
     self.xxContact = xxContact
+    self.importUsername = importUsername
+    self.importEmail = importEmail
+    self.importPhone = importPhone
     self.sendRequest = sendRequest
   }
 
   public var id: Data
   public var dbContact: XXModels.Contact?
   public var xxContact: XXClient.Contact?
+  @BindableState public var importUsername: Bool
+  @BindableState public var importEmail: Bool
+  @BindableState public var importPhone: Bool
   public var sendRequest: SendRequestState?
 }
 
-public enum ContactAction: Equatable {
+public enum ContactAction: Equatable, BindableAction {
   case start
   case dbContactFetched(XXModels.Contact?)
-  case saveFactsTapped
+  case importFactsTapped
   case sendRequestTapped
   case sendRequestDismissed
   case sendRequest(SendRequestAction)
+  case binding(BindingAction<ContactState>)
 }
 
 public struct ContactEnvironment {
@@ -89,14 +99,20 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
     state.dbContact = contact
     return .none
 
-  case .saveFactsTapped:
+  case .importFactsTapped:
     guard let xxContact = state.xxContact else { return .none }
     return .fireAndForget { [state] in
       var dbContact = state.dbContact ?? XXModels.Contact(id: state.id)
       dbContact.marshaled = xxContact.data
-      dbContact.username = xxContact.username
-      dbContact.email = xxContact.email
-      dbContact.phone = xxContact.phone
+      if state.importUsername {
+        dbContact.username = xxContact.username
+      }
+      if state.importEmail {
+        dbContact.email = xxContact.email
+      }
+      if state.importPhone {
+        dbContact.phone = xxContact.phone
+      }
       _ = try! env.db().saveContact(dbContact)
     }
     .subscribe(on: env.bgQueue)
@@ -121,8 +137,12 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
 
   case .sendRequest(_):
     return .none
+
+  case .binding(_):
+    return .none
   }
 }
+.binding()
 .presenting(
   sendRequestReducer,
   state: .keyPath(\.sendRequest),
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index d14271f0..a3b6ee08 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -16,10 +16,16 @@ public struct ContactView: View {
   struct ViewState: Equatable {
     var dbContact: XXModels.Contact?
     var xxContact: XXClient.Contact?
+    var importUsername: Bool
+    var importEmail: Bool
+    var importPhone: Bool
 
     init(state: ContactState) {
       dbContact = state.dbContact
       xxContact = state.xxContact
+      importUsername = state.importUsername
+      importEmail = state.importEmail
+      importPhone = state.importPhone
     }
   }
 
@@ -28,11 +34,44 @@ public struct ContactView: View {
       Form {
         if let xxContact = viewStore.xxContact {
           Section {
-            Label(xxContact.username ?? "", systemImage: "person")
-            Label(xxContact.email ?? "", systemImage: "envelope")
-            Label(xxContact.phone ?? "", systemImage: "phone")
             Button {
-              viewStore.send(.saveFactsTapped)
+              viewStore.send(.set(\.$importUsername, !viewStore.importUsername))
+            } label: {
+              HStack {
+                Label(xxContact.username ?? "", systemImage: "person")
+                  .tint(Color.primary)
+                Spacer()
+                Image(systemName: viewStore.importUsername ? "checkmark.circle.fill" : "circle")
+                  .foregroundColor(.accentColor)
+              }
+            }
+
+            Button {
+              viewStore.send(.set(\.$importEmail, !viewStore.importEmail))
+            } label: {
+              HStack {
+                Label(xxContact.email ?? "", systemImage: "envelope")
+                  .tint(Color.primary)
+                Spacer()
+                Image(systemName: viewStore.importEmail ? "checkmark.circle.fill" : "circle")
+                  .foregroundColor(.accentColor)
+              }
+            }
+
+            Button {
+              viewStore.send(.set(\.$importPhone, !viewStore.importPhone))
+            } label: {
+              HStack {
+                Label(xxContact.phone ?? "", systemImage: "phone")
+                  .tint(Color.primary)
+                Spacer()
+                Image(systemName: viewStore.importPhone ? "checkmark.circle.fill" : "circle")
+                  .foregroundColor(.accentColor)
+              }
+            }
+
+            Button {
+              viewStore.send(.importFactsTapped)
             } label: {
               if viewStore.dbContact == nil {
                 Text("Save contact")
diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
index 4d637ecf..244cc900 100644
--- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift
@@ -47,7 +47,7 @@ final class ContactFeatureTests: XCTestCase {
     dbContactsPublisher.send(completion: .finished)
   }
 
-  func testSaveFacts() {
+  func testImportFacts() {
     let dbContact: XXModels.Contact = .init(
       id: "contact-id".data(using: .utf8)!
     )
@@ -84,7 +84,7 @@ final class ContactFeatureTests: XCTestCase {
       return db
     }
 
-    store.send(.saveFactsTapped)
+    store.send(.importFactsTapped)
 
     var expectedSavedContact = dbContact
     expectedSavedContact.marshaled = xxContact.data
-- 
GitLab


From 1c098a7eff357d0179b0b687d63502836967bd46 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 23:24:18 +0200
Subject: [PATCH 27/29] Update presentation id in UserSearchFeature

---
 .../Sources/UserSearchFeature/UserSearchFeature.swift           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index e8cb92c0..1b10c1b3 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -140,7 +140,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
 .presenting(
   contactReducer,
   state: .keyPath(\.contact),
-  id: .notNil(), // TODO: use Contact.ID
+  id: .keyPath(\.?.id),
   action: /UserSearchAction.contact,
   environment: { $0.contact() }
 )
-- 
GitLab


From c727c9ddf76cdee2748b3e963f1e9232a88cea9b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 7 Sep 2022 23:38:04 +0200
Subject: [PATCH 28/29] Refactor fact helpers

---
 .../AppCore/XXClientHelpers/FactHelpers.swift | 55 +++++++++++++++++++
 .../XXClientHelpers/XXContact+Helpers.swift   | 15 -----
 .../ContactFeature/ContactFeature.swift       |  6 +-
 .../Sources/ContactFeature/ContactView.swift  | 18 ++++--
 .../RegisterFeature/RegisterFeature.swift     |  5 +-
 .../SendRequestFeature.swift                  |  6 +-
 .../SendRequestFeature/SendRequestView.swift  | 28 ++++++----
 .../UserSearchResultFeature.swift             |  6 +-
 8 files changed, 95 insertions(+), 44 deletions(-)
 create mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift
 delete mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift

diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift
new file mode 100644
index 00000000..83f04032
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift
@@ -0,0 +1,55 @@
+import XXClient
+
+// TODO: Move to XXClient library
+
+public enum FactType: Equatable {
+  case username
+  case email
+  case phone
+  case other(Int)
+
+  public static let knownTypes: [Self] = [.username, .email, .phone]
+
+  public init(rawValue: Int) {
+    if let known = FactType.knownTypes.first(where: { $0.rawValue == rawValue }) {
+      self = known
+    } else {
+      self = .other(rawValue)
+    }
+  }
+
+  public var rawValue: Int {
+    switch self {
+    case .username: return 0
+    case .email: return 1
+    case .phone: return 2
+    case .other(let rawValue): return rawValue
+    }
+  }
+}
+
+extension Array where Element == Fact {
+  public func get(_ type: FactType) -> Fact? {
+    first(where: { $0.type == type.rawValue })
+  }
+
+  public mutating func set(_ type: FactType, _ value: String?) {
+    removeAll(where: { $0.type == type.rawValue })
+    if let value = value {
+      append(Fact(fact: value, type: type.rawValue))
+      sort(by: { $0.type < $1.type })
+    }
+  }
+}
+
+extension Contact {
+  public func getFact(_ type: FactType) throws -> Fact? {
+    try getFacts().get(type)
+  }
+
+  public mutating func setFact(_ type: FactType, _ value: String?) throws {
+    var facts = try getFacts()
+    facts.set(type, value)
+    try setFacts(facts)
+  }
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift
deleted file mode 100644
index d2c93683..00000000
--- a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/XXContact+Helpers.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import XXClient
-
-extension Contact {
-  public var username: String? {
-    try? getFacts().first(where: { $0.type == 0 })?.fact
-  }
-
-  public var email: String? {
-    try? getFacts().first(where: { $0.type == 1 })?.fact
-  }
-
-  public var phone: String? {
-    try? getFacts().first(where: { $0.type == 2 })?.fact
-  }
-}
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
index b8da32a4..8126b792 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift
@@ -105,13 +105,13 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm
       var dbContact = state.dbContact ?? XXModels.Contact(id: state.id)
       dbContact.marshaled = xxContact.data
       if state.importUsername {
-        dbContact.username = xxContact.username
+        dbContact.username = try? xxContact.getFact(.username)?.fact
       }
       if state.importEmail {
-        dbContact.email = xxContact.email
+        dbContact.email = try? xxContact.getFact(.email)?.fact
       }
       if state.importPhone {
-        dbContact.phone = xxContact.phone
+        dbContact.phone = try? xxContact.getFact(.phone)?.fact
       }
       _ = try! env.db().saveContact(dbContact)
     }
diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
index a3b6ee08..48743b07 100644
--- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
+++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift
@@ -15,14 +15,20 @@ public struct ContactView: View {
 
   struct ViewState: Equatable {
     var dbContact: XXModels.Contact?
-    var xxContact: XXClient.Contact?
+    var xxContactIsSet: Bool
+    var xxContactUsername: String?
+    var xxContactEmail: String?
+    var xxContactPhone: String?
     var importUsername: Bool
     var importEmail: Bool
     var importPhone: Bool
 
     init(state: ContactState) {
       dbContact = state.dbContact
-      xxContact = state.xxContact
+      xxContactIsSet = state.xxContact != nil
+      xxContactUsername = try? state.xxContact?.getFact(.username)?.fact
+      xxContactEmail = try? state.xxContact?.getFact(.email)?.fact
+      xxContactPhone = try? state.xxContact?.getFact(.phone)?.fact
       importUsername = state.importUsername
       importEmail = state.importEmail
       importPhone = state.importPhone
@@ -32,13 +38,13 @@ public struct ContactView: View {
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Form {
-        if let xxContact = viewStore.xxContact {
+        if viewStore.xxContactIsSet {
           Section {
             Button {
               viewStore.send(.set(\.$importUsername, !viewStore.importUsername))
             } label: {
               HStack {
-                Label(xxContact.username ?? "", systemImage: "person")
+                Label(viewStore.xxContactUsername ?? "", systemImage: "person")
                   .tint(Color.primary)
                 Spacer()
                 Image(systemName: viewStore.importUsername ? "checkmark.circle.fill" : "circle")
@@ -50,7 +56,7 @@ public struct ContactView: View {
               viewStore.send(.set(\.$importEmail, !viewStore.importEmail))
             } label: {
               HStack {
-                Label(xxContact.email ?? "", systemImage: "envelope")
+                Label(viewStore.xxContactEmail ?? "", systemImage: "envelope")
                   .tint(Color.primary)
                 Spacer()
                 Image(systemName: viewStore.importEmail ? "checkmark.circle.fill" : "circle")
@@ -62,7 +68,7 @@ public struct ContactView: View {
               viewStore.send(.set(\.$importPhone, !viewStore.importPhone))
             } label: {
               HStack {
-                Label(xxContact.phone ?? "", systemImage: "phone")
+                Label(viewStore.xxContactPhone ?? "", systemImage: "phone")
                   .tint(Color.primary)
                 Spacer()
                 Image(systemName: viewStore.importPhone ? "checkmark.circle.fill" : "circle")
diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
index fcb9c62e..cb43c430 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
@@ -83,10 +83,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
         let db = try env.db()
         try env.messenger.register(username: username)
         var contact = try env.messenger.e2e.tryGet().getContact()
-        var facts: [Fact] = try contact.getFacts()
-        facts.removeAll(where: { $0.type == 0 })
-        facts.append(Fact(fact: username, type: 0))
-        try contact.setFacts(facts)
+        try contact.setFact(.username, username)
         try db.saveContact(Contact(
           id: try contact.getId(),
           marshaled: contact.data,
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
index 684e344a..f2625b91 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift
@@ -110,13 +110,13 @@ public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, Sen
         try updateAuthStatus(.requesting)
         let myFacts = try state.myContact?.getFacts() ?? []
         var includedFacts: [Fact] = []
-        if state.sendUsername, let fact = myFacts.first(where: { $0.type == 0 }) {
+        if state.sendUsername, let fact = myFacts.get(.username) {
           includedFacts.append(fact)
         }
-        if state.sendEmail, let fact = myFacts.first(where: { $0.type == 1 }) {
+        if state.sendEmail, let fact = myFacts.get(.email) {
           includedFacts.append(fact)
         }
-        if state.sendPhone, let fact = myFacts.first(where: { $0.type == 2 }) {
+        if state.sendPhone, let fact = myFacts.get(.phone) {
           includedFacts.append(fact)
         }
         _ = try env.messenger.e2e.tryGet().requestAuthenticatedChannel(
diff --git a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
index 159d10e8..5f1cd7d5 100644
--- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
+++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestView.swift
@@ -11,8 +11,12 @@ public struct SendRequestView: View {
   let store: Store<SendRequestState, SendRequestAction>
 
   struct ViewState: Equatable {
-    var contact: XXClient.Contact
-    var myContact: XXClient.Contact?
+    var contactUsername: String?
+    var contactEmail: String?
+    var contactPhone: String?
+    var myUsername: String?
+    var myEmail: String?
+    var myPhone: String?
     var sendUsername: Bool
     var sendEmail: Bool
     var sendPhone: Bool
@@ -20,8 +24,12 @@ public struct SendRequestView: View {
     var failure: String?
 
     init(state: SendRequestState) {
-      contact = state.contact
-      myContact = state.myContact
+      contactUsername = try? state.contact.getFact(.username)?.fact
+      contactEmail = try? state.contact.getFact(.email)?.fact
+      contactPhone = try? state.contact.getFact(.phone)?.fact
+      myUsername = try? state.myContact?.getFact(.username)?.fact
+      myEmail = try? state.myContact?.getFact(.email)?.fact
+      myPhone = try? state.myContact?.getFact(.phone)?.fact
       sendUsername = state.sendUsername
       sendEmail = state.sendEmail
       sendPhone = state.sendPhone
@@ -38,7 +46,7 @@ public struct SendRequestView: View {
             viewStore.send(.set(\.$sendUsername, !viewStore.sendUsername))
           } label: {
             HStack {
-              Label(viewStore.myContact?.username ?? "", systemImage: "person")
+              Label(viewStore.myUsername ?? "", systemImage: "person")
                 .tint(Color.primary)
               Spacer()
               Image(systemName: viewStore.sendUsername ? "checkmark.circle.fill" : "circle")
@@ -51,7 +59,7 @@ public struct SendRequestView: View {
             viewStore.send(.set(\.$sendEmail, !viewStore.sendEmail))
           } label: {
             HStack {
-              Label(viewStore.myContact?.email ?? "", systemImage: "envelope")
+              Label(viewStore.myEmail ?? "", systemImage: "envelope")
                 .tint(Color.primary)
               Spacer()
               Image(systemName: viewStore.sendEmail ? "checkmark.circle.fill" : "circle")
@@ -64,7 +72,7 @@ public struct SendRequestView: View {
             viewStore.send(.set(\.$sendPhone, !viewStore.sendPhone))
           } label: {
             HStack {
-              Label(viewStore.myContact?.phone ?? "", systemImage: "phone")
+              Label(viewStore.myPhone ?? "", systemImage: "phone")
                 .tint(Color.primary)
               Spacer()
               Image(systemName: viewStore.sendPhone ? "checkmark.circle.fill" : "circle")
@@ -78,9 +86,9 @@ public struct SendRequestView: View {
         .disabled(viewStore.isSending)
 
         Section {
-          Label(viewStore.contact.username ?? "", systemImage: "person")
-          Label(viewStore.contact.email ?? "", systemImage: "envelope")
-          Label(viewStore.contact.phone ?? "", systemImage: "phone")
+          Label(viewStore.contactUsername ?? "", systemImage: "person")
+          Label(viewStore.contactEmail ?? "", systemImage: "envelope")
+          Label(viewStore.contactPhone ?? "", systemImage: "phone")
         } header: {
           Text("Contact")
         }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
index a331df70..839e6886 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchResultFeature.swift
@@ -44,9 +44,9 @@ public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchRe
 { state, action, env in
   switch action {
   case .start:
-    state.username = state.xxContact.username
-    state.email = state.xxContact.email
-    state.phone = state.xxContact.phone
+    state.username = try? state.xxContact.getFact(.username)?.fact
+    state.email = try? state.xxContact.getFact(.email)?.fact
+    state.phone = try? state.xxContact.getFact(.phone)?.fact
     return .none
 
   case .tapped:
-- 
GitLab


From 7ddd7cf6febde59368f3f652e460632a4c53ffd7 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 8 Sep 2022 00:51:06 +0200
Subject: [PATCH 29/29] Remove FactHelpers from AppCore library

---
 .../AppCore/XXClientHelpers/FactHelpers.swift | 55 -------------------
 1 file changed, 55 deletions(-)
 delete mode 100644 Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift

diff --git a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift b/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift
deleted file mode 100644
index 83f04032..00000000
--- a/Examples/xx-messenger/Sources/AppCore/XXClientHelpers/FactHelpers.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-import XXClient
-
-// TODO: Move to XXClient library
-
-public enum FactType: Equatable {
-  case username
-  case email
-  case phone
-  case other(Int)
-
-  public static let knownTypes: [Self] = [.username, .email, .phone]
-
-  public init(rawValue: Int) {
-    if let known = FactType.knownTypes.first(where: { $0.rawValue == rawValue }) {
-      self = known
-    } else {
-      self = .other(rawValue)
-    }
-  }
-
-  public var rawValue: Int {
-    switch self {
-    case .username: return 0
-    case .email: return 1
-    case .phone: return 2
-    case .other(let rawValue): return rawValue
-    }
-  }
-}
-
-extension Array where Element == Fact {
-  public func get(_ type: FactType) -> Fact? {
-    first(where: { $0.type == type.rawValue })
-  }
-
-  public mutating func set(_ type: FactType, _ value: String?) {
-    removeAll(where: { $0.type == type.rawValue })
-    if let value = value {
-      append(Fact(fact: value, type: type.rawValue))
-      sort(by: { $0.type < $1.type })
-    }
-  }
-}
-
-extension Contact {
-  public func getFact(_ type: FactType) throws -> Fact? {
-    try getFacts().get(type)
-  }
-
-  public mutating func setFact(_ type: FactType, _ value: String?) throws {
-    var facts = try getFacts()
-    facts.set(type, value)
-    try setFacts(facts)
-  }
-}
-- 
GitLab