From 1117ef010d1aef5086ef730ee1d989dfec71ddd2 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 09:13:47 +0200
Subject: [PATCH 01/22] Add MyContactFeature library

---
 .../xcschemes/MyContactFeature.xcscheme       | 78 +++++++++++++++++++
 Examples/xx-messenger/Package.swift           | 19 +++++
 .../xcschemes/XXMessenger.xcscheme            | 10 +++
 .../MyContactFeature/MyContactFeature.swift   | 28 +++++++
 .../MyContactFeature/MyContactView.swift      | 35 +++++++++
 .../MyContactFeatureTests.swift               | 15 ++++
 6 files changed, 185 insertions(+)
 create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme
 create mode 100644 Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
 create mode 100644 Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
 create mode 100644 Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift

diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme
new file mode 100644
index 00000000..4eb2a433
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1400"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyContactFeature"
+               BuildableName = "MyContactFeature"
+               BlueprintName = "MyContactFeature"
+               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 = "MyContactFeatureTests"
+               BuildableName = "MyContactFeatureTests"
+               BlueprintName = "MyContactFeatureTests"
+               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 = "MyContactFeature"
+            BuildableName = "MyContactFeature"
+            BlueprintName = "MyContactFeature"
+            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 c5bcba6a..be093741 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -26,6 +26,7 @@ let package = Package(
     .library(name: "ContactFeature", targets: ["ContactFeature"]),
     .library(name: "ContactsFeature", targets: ["ContactsFeature"]),
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
+    .library(name: "MyContactFeature", targets: ["MyContactFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
     .library(name: "SendRequestFeature", targets: ["SendRequestFeature"]),
@@ -216,6 +217,24 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "MyContactFeature",
+      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
+    ),
+    .testTarget(
+      name: "MyContactFeatureTests",
+      dependencies: [
+        .target(name: "MyContactFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "RegisterFeature",
       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 65400dff..aa97ea7d 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -109,6 +109,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyContactFeatureTests"
+               BuildableName = "MyContactFeatureTests"
+               BlueprintName = "MyContactFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
new file mode 100644
index 00000000..8d335305
--- /dev/null
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -0,0 +1,28 @@
+import ComposableArchitecture
+import XCTestDynamicOverlay
+
+public struct MyContactState: Equatable {
+  public init() {}
+}
+
+public enum MyContactAction: Equatable {
+  case start
+}
+
+public struct MyContactEnvironment {
+  public init() {}
+}
+
+#if DEBUG
+extension MyContactEnvironment {
+  public static let unimplemented = MyContactEnvironment()
+}
+#endif
+
+public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
+{ state, action, env in
+  switch action {
+  case .start:
+    return .none
+  }
+}
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
new file mode 100644
index 00000000..d5092e4b
--- /dev/null
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -0,0 +1,35 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct MyContactView: View {
+  public init(store: Store<MyContactState, MyContactAction>) {
+    self.store = store
+  }
+
+  let store: Store<MyContactState, MyContactAction>
+
+  struct ViewState: Equatable {
+    init(state: MyContactState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+
+      }
+      .navigationTitle("My Contact")
+    }
+  }
+}
+
+#if DEBUG
+public struct MyContactView_Previews: PreviewProvider {
+  public static var previews: some View {
+    MyContactView(store: Store(
+      initialState: MyContactState(),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
new file mode 100644
index 00000000..6807dcd6
--- /dev/null
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import MyContactFeature
+
+final class MyContactFeatureTests: XCTestCase {
+  func testStart() {
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.start)
+  }
+}
-- 
GitLab


From f893860df269d18c8148af7238bd3c83a0115999 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 09:29:33 +0200
Subject: [PATCH 02/22] Present MyContact from Contacts

---
 Examples/xx-messenger/Package.swift           |  2 ++
 .../AppFeature/AppEnvironment+Live.swift      |  6 +++-
 .../ContactsFeature/ContactsFeature.swift     | 34 ++++++++++++++++---
 .../ContactsFeature/ContactsView.swift        | 29 ++++++++++++----
 .../ContactsFeatureTests.swift                | 27 +++++++++++++++
 5 files changed, 87 insertions(+), 11 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index be093741..c184020f 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -84,6 +84,7 @@ let package = Package(
         .target(name: "ContactFeature"),
         .target(name: "ContactsFeature"),
         .target(name: "HomeFeature"),
+        .target(name: "MyContactFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
         .target(name: "SendRequestFeature"),
@@ -182,6 +183,7 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .target(name: "ContactFeature"),
+        .target(name: "MyContactFeature"),
         .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 611123e0..b93b0359 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -6,6 +6,7 @@ import ContactFeature
 import ContactsFeature
 import Foundation
 import HomeFeature
+import MyContactFeature
 import RegisterFeature
 import RestoreFeature
 import SendRequestFeature
@@ -122,7 +123,10 @@ extension AppEnvironment {
               db: dbManager.getDB,
               mainQueue: mainQueue,
               bgQueue: bgQueue,
-              contact: { contactEnvironment }
+              contact: { contactEnvironment },
+              myContact: {
+                MyContactEnvironment()
+              }
             )
           },
           userSearch: {
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
index 1ded89de..680a231e 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift
@@ -3,6 +3,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import ContactFeature
 import Foundation
+import MyContactFeature
 import XCTestDynamicOverlay
 import XXClient
 import XXMessengerClient
@@ -12,16 +13,19 @@ public struct ContactsState: Equatable {
   public init(
     myId: Data? = nil,
     contacts: IdentifiedArrayOf<XXModels.Contact> = [],
-    contact: ContactState? = nil
+    contact: ContactState? = nil,
+    myContact: MyContactState? = nil
   ) {
     self.myId = myId
     self.contacts = contacts
     self.contact = contact
+    self.myContact = myContact
   }
 
   public var myId: Data?
   public var contacts: IdentifiedArrayOf<XXModels.Contact>
   public var contact: ContactState?
+  public var myContact: MyContactState?
 }
 
 public enum ContactsAction: Equatable {
@@ -30,6 +34,9 @@ public enum ContactsAction: Equatable {
   case contactSelected(XXModels.Contact)
   case contactDismissed
   case contact(ContactAction)
+  case myContactSelected
+  case myContactDismissed
+  case myContact(MyContactAction)
 }
 
 public struct ContactsEnvironment {
@@ -38,13 +45,15 @@ public struct ContactsEnvironment {
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
-    contact: @escaping () -> ContactEnvironment
+    contact: @escaping () -> ContactEnvironment,
+    myContact: @escaping () -> MyContactEnvironment
   ) {
     self.messenger = messenger
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.contact = contact
+    self.myContact = myContact
   }
 
   public var messenger: Messenger
@@ -52,6 +61,7 @@ public struct ContactsEnvironment {
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var contact: () -> ContactEnvironment
+  public var myContact: () -> MyContactEnvironment
 }
 
 #if DEBUG
@@ -61,7 +71,8 @@ extension ContactsEnvironment {
     db: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
-    contact: { .unimplemented }
+    contact: { .unimplemented },
+    myContact: { .unimplemented }
   )
 }
 #endif
@@ -96,7 +107,15 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
     state.contact = nil
     return .none
 
-  case .contact(_):
+  case .myContactSelected:
+    state.myContact = MyContactState()
+    return .none
+
+  case .myContactDismissed:
+    state.myContact = nil
+    return .none
+
+  case .contact(_), .myContact(_):
     return .none
   }
 }
@@ -107,3 +126,10 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
   action: /ContactsAction.contact,
   environment: { $0.contact() }
 )
+.presenting(
+  myContactReducer,
+  state: .keyPath(\.myContact),
+  id: .notNil(),
+  action: /ContactsAction.myContact,
+  environment: { $0.myContact() }
+)
diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
index ce811f9d..e09725d9 100644
--- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
+++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift
@@ -2,6 +2,7 @@ import AppCore
 import ComposableArchitecture
 import ComposablePresentation
 import ContactFeature
+import MyContactFeature
 import SwiftUI
 import XXModels
 
@@ -28,13 +29,21 @@ public struct ContactsView: View {
         ForEach(viewStore.contacts) { contact in
           if contact.id == viewStore.myId {
             Section {
-              VStack(alignment: .leading, spacing: 8) {
-                Label(contact.username ?? "", systemImage: "person")
-                Label(contact.email ?? "", systemImage: "envelope")
-                Label(contact.phone ?? "", systemImage: "phone")
+              Button {
+                viewStore.send(.myContactSelected)
+              } label: {
+                HStack {
+                  VStack(alignment: .leading, spacing: 8) {
+                    Label(contact.username ?? "", systemImage: "person")
+                    Label(contact.email ?? "", systemImage: "envelope")
+                    Label(contact.phone ?? "", systemImage: "phone")
+                  }
+                  .font(.callout)
+                  .tint(Color.primary)
+                  Spacer()
+                  Image(systemName: "chevron.forward")
+                }
               }
-              .font(.callout)
-              .tint(Color.primary)
             } header: {
               Text("My contact")
             }
@@ -70,6 +79,14 @@ public struct ContactsView: View {
         onDeactivate: { viewStore.send(.contactDismissed) },
         destination: ContactView.init(store:)
       ))
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.myContact,
+          action: ContactsAction.myContact
+        ),
+        onDeactivate: { viewStore.send(.myContactDismissed) },
+        destination: MyContactView.init(store:)
+      ))
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
index a0c0291e..ffd3eafe 100644
--- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift
@@ -2,6 +2,7 @@ import Combine
 import ComposableArchitecture
 import ContactFeature
 import CustomDump
+import MyContactFeature
 import XCTest
 import XXClient
 import XXMessengerClient
@@ -94,4 +95,30 @@ final class ContactsFeatureTests: XCTestCase {
       $0.contact = nil
     }
   }
+
+  func testSelectMyContact() {
+    let store = TestStore(
+      initialState: ContactsState(),
+      reducer: contactsReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.myContactSelected) {
+      $0.myContact = MyContactState()
+    }
+  }
+
+  func testDismissMyContact() {
+    let store = TestStore(
+      initialState: ContactsState(
+        myContact: MyContactState()
+      ),
+      reducer: contactsReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.myContactDismissed) {
+      $0.myContact = nil
+    }
+  }
 }
-- 
GitLab


From 6d0f1a18332868b199643ea117a0814842ef2a1f Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 09:30:20 +0200
Subject: [PATCH 03/22] Update example app package dependencies

---
 .../xcshareddata/swiftpm/Package.resolved          | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 45de0ce8..42a8b2ba 100644
--- a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -59,8 +59,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
       "state" : {
-        "revision" : "cbe013b42b3c368957f8f882c960b93845e1589d",
-        "version" : "0.40.1"
+        "revision" : "9ea8c763061287052a68d5e6723fed45e898b7d9",
+        "version" : "0.40.2"
       }
     },
     {
@@ -75,10 +75,10 @@
     {
       "identity" : "swift-custom-dump",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/pointfreeco/swift-custom-dump",
+      "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
       "state" : {
-        "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
-        "version" : "0.5.0"
+        "revision" : "c9b6b940d95c0a925c63f6858943415714d8a981",
+        "version" : "0.5.2"
       }
     },
     {
@@ -95,8 +95,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       "state" : {
-        "revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260",
-        "version" : "0.4.0"
+        "revision" : "30314f1ece684dd60679d598a9b89107557b67d9",
+        "version" : "0.4.1"
       }
     }
   ],
-- 
GitLab


From 97727f6fc811025a13f25fe94378981fe016c835 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 10:22:08 +0200
Subject: [PATCH 04/22] Fetch my contact

---
 .../AppFeature/AppEnvironment+Live.swift      |   7 +-
 .../MyContactFeature/MyContactFeature.swift   |  95 ++++++++++++++-
 .../MyContactFeature/MyContactView.swift      | 111 +++++++++++++++++-
 .../MyContactFeatureTests.swift               | 107 +++++++++++++++++
 4 files changed, 309 insertions(+), 11 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index b93b0359..d824d6d4 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -125,7 +125,12 @@ extension AppEnvironment {
               bgQueue: bgQueue,
               contact: { contactEnvironment },
               myContact: {
-                MyContactEnvironment()
+                MyContactEnvironment(
+                  messenger: messenger,
+                  db: dbManager.getDB,
+                  mainQueue: mainQueue,
+                  bgQueue: bgQueue
+                )
               }
             )
           },
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index 8d335305..c2cfe972 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -1,28 +1,115 @@
+import AppCore
 import ComposableArchitecture
+import Foundation
 import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
 
 public struct MyContactState: Equatable {
-  public init() {}
+  public enum Field: String, Hashable {
+    case email
+    case phone
+  }
+
+  public init(
+    contact: XXModels.Contact? = nil,
+    focusedField: Field? = nil,
+    email: String = "",
+    phone: String = ""
+  ) {
+    self.contact = contact
+    self.focusedField = focusedField
+    self.email = email
+    self.phone = phone
+  }
+
+  public var contact: XXModels.Contact?
+  @BindableState public var focusedField: Field?
+  @BindableState public var email: String
+  @BindableState public var phone: String
 }
 
-public enum MyContactAction: Equatable {
+public enum MyContactAction: Equatable, BindableAction {
   case start
+  case contactFetched(XXModels.Contact?)
+  case registerEmailTapped
+  case unregisterEmailTapped
+  case registerPhoneTapped
+  case unregisterPhoneTapped
+  case loadFactsTapped
+  case binding(BindingAction<MyContactState>)
 }
 
 public struct MyContactEnvironment {
-  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 MyContactEnvironment {
-  public static let unimplemented = MyContactEnvironment()
+  public static let unimplemented = MyContactEnvironment(
+    messenger: .unimplemented,
+    db: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
 public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
 { 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(MyContactAction.contactFetched)
+      .subscribe(on: env.bgQueue)
+      .receive(on: env.mainQueue)
+      .eraseToEffect()
+      .cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
+
+  case .contactFetched(let contact):
+    state.contact = contact
+    return .none
+
+  case .registerEmailTapped:
+    return .none
+
+  case .unregisterEmailTapped:
+    return .none
+
+  case .registerPhoneTapped:
+    return .none
+
+  case .unregisterPhoneTapped:
+    return .none
+
+  case .loadFactsTapped:
+    return .none
+
+  case .binding(_):
     return .none
   }
 }
+.binding()
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index d5092e4b..0887f682 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import SwiftUI
+import XXModels
 
 public struct MyContactView: View {
   public init(store: Store<MyContactState, MyContactAction>) {
@@ -7,17 +8,113 @@ public struct MyContactView: View {
   }
 
   let store: Store<MyContactState, MyContactAction>
+  @FocusState var focusedField: MyContactState.Field?
 
   struct ViewState: Equatable {
-    init(state: MyContactState) {}
+    init(state: MyContactState) {
+      contact = state.contact
+      focusedField = state.focusedField
+      email = state.email
+      phone = state.phone
+    }
+
+    var contact: XXModels.Contact?
+    var focusedField: MyContactState.Field?
+    var email: String
+    var phone: String
   }
 
   public var body: some View {
     WithViewStore(store, observe: ViewState.init) { viewStore in
       Form {
+        Section {
+          Text(viewStore.contact?.username ?? "")
+        } header: {
+          Label("Username", systemImage: "person")
+        }
+
+        Section {
+          if let contact = viewStore.contact {
+            if let email = contact.email {
+              Text(email)
+              Button(role: .destructive) {
+                viewStore.send(.unregisterEmailTapped)
+              } label: {
+                Text("Unregister")
+              }
+            } else {
+              TextField(
+                text: viewStore.binding(
+                  get: \.email,
+                  send: { MyContactAction.set(\.$email, $0) }
+                ),
+                prompt: Text("Enter email"),
+                label: { Text("Email") }
+              )
+              .focused($focusedField, equals: .email)
+              .textInputAutocapitalization(.never)
+              .disableAutocorrection(true)
+              Button {
+                viewStore.send(.registerEmailTapped)
+              } label: {
+                Text("Register")
+              }
+            }
+          } else {
+            Text("")
+          }
+        } header: {
+          Label("Email", systemImage: "envelope")
+        }
 
+        Section {
+          if let contact = viewStore.contact {
+            if let phone = contact.phone {
+              Text(phone)
+              Button(role: .destructive) {
+                viewStore.send(.unregisterPhoneTapped)
+              } label: {
+                Text("Unregister")
+              }
+            } else {
+              TextField(
+                text: viewStore.binding(
+                  get: \.phone,
+                  send: { MyContactAction.set(\.$phone, $0) }
+                ),
+                prompt: Text("Enter phone"),
+                label: { Text("Phone") }
+              )
+              .focused($focusedField, equals: .phone)
+              .textInputAutocapitalization(.never)
+              .disableAutocorrection(true)
+              Button {
+                viewStore.send(.registerPhoneTapped)
+              } label: {
+                Text("Register")
+              }
+            }
+          } else {
+            Text("")
+          }
+        } header: {
+          Label("Phone", systemImage: "phone")
+        }
+
+        Section {
+          Button {
+            viewStore.send(.loadFactsTapped)
+          } label: {
+            Text("Load facts from client")
+          }
+        } header: {
+          Text("Actions")
+        }
       }
       .navigationTitle("My Contact")
+      .task { viewStore.send(.start) }
+      .onChange(of: viewStore.focusedField) { focusedField = $0 }
+      .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
     }
   }
 }
@@ -25,11 +122,13 @@ public struct MyContactView: View {
 #if DEBUG
 public struct MyContactView_Previews: PreviewProvider {
   public static var previews: some View {
-    MyContactView(store: Store(
-      initialState: MyContactState(),
-      reducer: .empty,
-      environment: ()
-    ))
+    NavigationView {
+      MyContactView(store: Store(
+        initialState: MyContactState(),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
   }
 }
 #endif
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 6807dcd6..eba876a9 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -1,15 +1,122 @@
+import Combine
 import ComposableArchitecture
+import CustomDump
 import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
 @testable import MyContactFeature
 
 final class MyContactFeatureTests: XCTestCase {
   func testStart() {
+    let contactId = "contact-id".data(using: .utf8)!
+
     let store = TestStore(
       initialState: MyContactState(),
       reducer: myContactReducer,
       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(Data())
+        contact.getIdFromContact.run = { _ in contactId }
+        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: [contactId])])
+
+    dbContactsPublisher.send([])
+
+    store.receive(.contactFetched(nil))
+
+    let contact = XXModels.Contact(id: contactId)
+    dbContactsPublisher.send([contact])
+
+    store.receive(.contactFetched(contact)) {
+      $0.contact = contact
+    }
+
+    dbContactsPublisher.send(completion: .finished)
+  }
+
+  func testRegisterEmail() {
+    let email = "test@email.com"
+
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.set(\.$email, email)) {
+      $0.email = email
+    }
+
+    store.send(.registerEmailTapped)
+  }
+
+  func testUnregisterEmail() {
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.unregisterEmailTapped)
+  }
+
+  func testRegisterPhone() {
+    let phone = "123456789"
+
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.set(\.$phone, phone)) {
+      $0.phone = phone
+    }
+
+    store.send(.registerPhoneTapped)
+  }
+
+  func testUnregisterPhone() {
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.unregisterPhoneTapped)
+  }
+
+  func testLoadFactsFromClient() {
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.loadFactsTapped)
   }
 }
-- 
GitLab


From 2bd54573482aed14c8bd9623d17baec22ffae406 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 11:36:52 +0200
Subject: [PATCH 05/22] Add error alert to MyContactFeature

---
 .../Sources/MyContactFeature/Alerts.swift      | 11 +++++++++++
 .../MyContactFeature/MyContactFeature.swift    | 15 ++++++++++++++-
 .../MyContactFeature/MyContactView.swift       |  1 +
 .../MyContactFeatureTests.swift                | 18 ++++++++++++++++++
 4 files changed, 44 insertions(+), 1 deletion(-)
 create mode 100644 Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift
new file mode 100644
index 00000000..321139ae
--- /dev/null
+++ b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift
@@ -0,0 +1,11 @@
+import ComposableArchitecture
+
+extension AlertState {
+  public static func error(_ message: String) -> AlertState<MyContactAction> {
+    AlertState<MyContactAction>(
+      title: TextState("Error"),
+      message: TextState(message),
+      buttons: []
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index c2cfe972..9669cac5 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -16,18 +16,21 @@ public struct MyContactState: Equatable {
     contact: XXModels.Contact? = nil,
     focusedField: Field? = nil,
     email: String = "",
-    phone: String = ""
+    phone: String = "",
+    alert: AlertState<MyContactAction>? = nil
   ) {
     self.contact = contact
     self.focusedField = focusedField
     self.email = email
     self.phone = phone
+    self.alert = alert
   }
 
   public var contact: XXModels.Contact?
   @BindableState public var focusedField: Field?
   @BindableState public var email: String
   @BindableState public var phone: String
+  public var alert: AlertState<MyContactAction>?
 }
 
 public enum MyContactAction: Equatable, BindableAction {
@@ -38,6 +41,8 @@ public enum MyContactAction: Equatable, BindableAction {
   case registerPhoneTapped
   case unregisterPhoneTapped
   case loadFactsTapped
+  case didFail(String)
+  case alertDismissed
   case binding(BindingAction<MyContactState>)
 }
 
@@ -108,6 +113,14 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
   case .loadFactsTapped:
     return .none
 
+  case .didFail(let failure):
+    state.alert = .error(failure)
+    return .none
+
+  case .alertDismissed:
+    state.alert = nil
+    return .none
+
   case .binding(_):
     return .none
   }
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index 0887f682..6e2cbc8f 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -115,6 +115,7 @@ public struct MyContactView: View {
       .task { viewStore.send(.start) }
       .onChange(of: viewStore.focusedField) { focusedField = $0 }
       .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
+      .alert(store.scope(state: \.alert), dismiss: .alertDismissed)
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index eba876a9..169450c8 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -119,4 +119,22 @@ final class MyContactFeatureTests: XCTestCase {
 
     store.send(.loadFactsTapped)
   }
+
+  func testErrorAlert() {
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    let failure = "Something went wrong"
+
+    store.send(.didFail(failure)) {
+      $0.alert = .error(failure)
+    }
+
+    store.send(.alertDismissed) {
+      $0.alert = nil
+    }
+  }
 }
-- 
GitLab


From 1f6e477c8fe5e338812a1d5bee7a7cbc6a017919 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 12:26:41 +0200
Subject: [PATCH 06/22] Implement loading my facts from the client

---
 .../MyContactFeature/MyContactFeature.swift   | 20 +++++++-
 .../MyContactFeatureTests.swift               | 48 +++++++++++++++++++
 2 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index 9669cac5..818807c8 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -1,4 +1,5 @@
 import AppCore
+import Combine
 import ComposableArchitecture
 import Foundation
 import XCTestDynamicOverlay
@@ -111,7 +112,24 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     return .none
 
   case .loadFactsTapped:
-    return .none
+    return Effect.run { subscriber in
+      do {
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          let facts = try env.messenger.ud.tryGet().getFacts()
+          dbContact.email = facts.get(.email)?.value
+          dbContact.phone = facts.get(.phone)?.value
+          try env.db().saveContact(dbContact)
+        }
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .didFail(let failure):
     state.alert = .error(failure)
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 169450c8..62e7fcd9 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -111,13 +111,61 @@ final class MyContactFeatureTests: XCTestCase {
   }
 
   func testLoadFactsFromClient() {
+    let contactId = "contact-id".data(using: .utf8)!
+    let dbContact = XXModels.Contact(id: contactId)
+    let email = "test@email.com"
+    let phone = "123456789"
+
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
       initialState: MyContactState(),
       reducer: myContactReducer,
       environment: .unimplemented
     )
 
+    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(Data())
+        contact.getIdFromContact.run = { _ in contactId }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.getFacts.run = {
+        [
+          Fact(type: .email, value: email),
+          Fact(type: .phone, value: phone),
+        ]
+      }
+      return ud
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
     store.send(.loadFactsTapped)
+
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.email = email
+    expectedSavedContact.phone = phone
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
   }
 
   func testErrorAlert() {
-- 
GitLab


From 1d201c28badc516a14ced0297a0eb98c88c07377 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 12:32:00 +0200
Subject: [PATCH 07/22] Add facts loading indicator

---
 .../Sources/MyContactFeature/MyContactFeature.swift   |  5 +++++
 .../Sources/MyContactFeature/MyContactView.swift      | 11 ++++++++++-
 .../MyContactFeatureTests/MyContactFeatureTests.swift |  8 +++++++-
 3 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index 818807c8..c08ed6b7 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -18,12 +18,14 @@ public struct MyContactState: Equatable {
     focusedField: Field? = nil,
     email: String = "",
     phone: String = "",
+    isLoadingFacts: Bool = false,
     alert: AlertState<MyContactAction>? = nil
   ) {
     self.contact = contact
     self.focusedField = focusedField
     self.email = email
     self.phone = phone
+    self.isLoadingFacts = isLoadingFacts
     self.alert = alert
   }
 
@@ -31,6 +33,7 @@ public struct MyContactState: Equatable {
   @BindableState public var focusedField: Field?
   @BindableState public var email: String
   @BindableState public var phone: String
+  @BindableState public var isLoadingFacts: Bool
   public var alert: AlertState<MyContactAction>?
 }
 
@@ -112,6 +115,7 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     return .none
 
   case .loadFactsTapped:
+    state.isLoadingFacts = true
     return Effect.run { subscriber in
       do {
         let contactId = try env.messenger.e2e.tryGet().getContact().getId()
@@ -124,6 +128,7 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
       } catch {
         subscriber.send(.didFail(error.localizedDescription))
       }
+      subscriber.send(.set(\.$isLoadingFacts, false))
       subscriber.send(completion: .finished)
       return AnyCancellable {}
     }
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index 6e2cbc8f..7eb8e7aa 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -16,12 +16,14 @@ public struct MyContactView: View {
       focusedField = state.focusedField
       email = state.email
       phone = state.phone
+      isLoadingFacts = state.isLoadingFacts
     }
 
     var contact: XXModels.Contact?
     var focusedField: MyContactState.Field?
     var email: String
     var phone: String
+    var isLoadingFacts: Bool
   }
 
   public var body: some View {
@@ -105,8 +107,15 @@ public struct MyContactView: View {
           Button {
             viewStore.send(.loadFactsTapped)
           } label: {
-            Text("Load facts from client")
+            HStack {
+              Text("Reload facts")
+              Spacer()
+              if viewStore.isLoadingFacts {
+                ProgressView()
+              }
+            }
           }
+          .disabled(viewStore.isLoadingFacts)
         } header: {
           Text("Actions")
         }
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 62e7fcd9..4193e935 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -159,13 +159,19 @@ final class MyContactFeatureTests: XCTestCase {
       return db
     }
 
-    store.send(.loadFactsTapped)
+    store.send(.loadFactsTapped) {
+      $0.isLoadingFacts = true
+    }
 
     XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])])
     var expectedSavedContact = dbContact
     expectedSavedContact.email = email
     expectedSavedContact.phone = phone
     XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$isLoadingFacts, false)) {
+      $0.isLoadingFacts = false
+    }
   }
 
   func testErrorAlert() {
-- 
GitLab


From 4ad76dc368eb7623fd4a9421e0a204f6df76df1c Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 12:34:30 +0200
Subject: [PATCH 08/22] Update tests

---
 .../MyContactFeatureTests.swift               | 35 +++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 4193e935..9c30f7df 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -174,6 +174,41 @@ final class MyContactFeatureTests: XCTestCase {
     }
   }
 
+  func testLoadFactsFromClientFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    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(Data())
+        contact.getIdFromContact.run = { _ in throw failure }
+        return contact
+      }
+      return e2e
+    }
+
+    store.send(.loadFactsTapped) {
+      $0.isLoadingFacts = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isLoadingFacts, false)) {
+      $0.isLoadingFacts = false
+    }
+  }
+
   func testErrorAlert() {
     let store = TestStore(
       initialState: MyContactState(),
-- 
GitLab


From bfad972facde6bb7fa44514b4a2a3722ceedacaf Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 13:28:25 +0200
Subject: [PATCH 09/22] Implement facts registration and confirmation

---
 .../MyContactFeature/MyContactFeature.swift   | 120 +++++-
 .../MyContactFeature/MyContactView.swift      | 102 ++++-
 .../MyContactFeatureTests.swift               | 370 +++++++++++++++++-
 3 files changed, 579 insertions(+), 13 deletions(-)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index c08ed6b7..99486c8a 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -10,21 +10,39 @@ import XXModels
 public struct MyContactState: Equatable {
   public enum Field: String, Hashable {
     case email
+    case emailCode
     case phone
+    case phoneCode
   }
 
   public init(
     contact: XXModels.Contact? = nil,
     focusedField: Field? = nil,
     email: String = "",
+    emailConfirmationID: String? = nil,
+    emailConfirmationCode: String = "",
+    isRegisteringEmail: Bool = false,
+    isConfirmingEmail: Bool = false,
     phone: String = "",
+    phoneConfirmationID: String? = nil,
+    phoneConfirmationCode: String = "",
+    isRegisteringPhone: Bool = false,
+    isConfirmingPhone: Bool = false,
     isLoadingFacts: Bool = false,
     alert: AlertState<MyContactAction>? = nil
   ) {
     self.contact = contact
     self.focusedField = focusedField
     self.email = email
+    self.emailConfirmationID = emailConfirmationID
+    self.emailConfirmationCode = emailConfirmationCode
+    self.isRegisteringEmail = isRegisteringEmail
+    self.isConfirmingEmail = isConfirmingEmail
     self.phone = phone
+    self.phoneConfirmationID = phoneConfirmationID
+    self.phoneConfirmationCode = phoneConfirmationCode
+    self.isRegisteringPhone = isRegisteringPhone
+    self.isConfirmingPhone = isConfirmingPhone
     self.isLoadingFacts = isLoadingFacts
     self.alert = alert
   }
@@ -32,7 +50,15 @@ public struct MyContactState: Equatable {
   public var contact: XXModels.Contact?
   @BindableState public var focusedField: Field?
   @BindableState public var email: String
+  @BindableState public var emailConfirmationID: String?
+  @BindableState public var emailConfirmationCode: String
+  @BindableState public var isRegisteringEmail: Bool
+  @BindableState public var isConfirmingEmail: Bool
   @BindableState public var phone: String
+  @BindableState public var phoneConfirmationID: String?
+  @BindableState public var phoneConfirmationCode: String
+  @BindableState public var isRegisteringPhone: Bool
+  @BindableState public var isConfirmingPhone: Bool
   @BindableState public var isLoadingFacts: Bool
   public var alert: AlertState<MyContactAction>?
 }
@@ -41,8 +67,10 @@ public enum MyContactAction: Equatable, BindableAction {
   case start
   case contactFetched(XXModels.Contact?)
   case registerEmailTapped
+  case confirmEmailTapped
   case unregisterEmailTapped
   case registerPhoneTapped
+  case confirmPhoneTapped
   case unregisterPhoneTapped
   case loadFactsTapped
   case didFail(String)
@@ -103,13 +131,101 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     return .none
 
   case .registerEmailTapped:
-    return .none
+    state.focusedField = nil
+    state.isRegisteringEmail = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .email, value: state.email)
+        let confirmationID = try ud.sendRegisterFact(fact)
+        subscriber.send(.set(\.$emailConfirmationID, confirmationID))
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isRegisteringEmail, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .confirmEmailTapped:
+    guard let confirmationID = state.emailConfirmationID else { return .none }
+    state.focusedField = nil
+    state.isConfirmingEmail = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud = try env.messenger.ud.tryGet()
+        try ud.confirmFact(confirmationId: confirmationID, code: state.emailConfirmationCode)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.email = state.email
+          try env.db().saveContact(dbContact)
+        }
+        subscriber.send(.set(\.$email, ""))
+        subscriber.send(.set(\.$emailConfirmationID, nil))
+        subscriber.send(.set(\.$emailConfirmationCode, ""))
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isConfirmingEmail, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .unregisterEmailTapped:
     return .none
 
   case .registerPhoneTapped:
-    return .none
+    state.focusedField = nil
+    state.isRegisteringPhone = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .phone, value: state.phone)
+        let confirmationID = try ud.sendRegisterFact(fact)
+        subscriber.send(.set(\.$phoneConfirmationID, confirmationID))
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isRegisteringPhone, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .confirmPhoneTapped:
+    guard let confirmationID = state.phoneConfirmationID else { return .none }
+    state.focusedField = nil
+    state.isConfirmingPhone = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud = try env.messenger.ud.tryGet()
+        try ud.confirmFact(confirmationId: confirmationID, code: state.phoneConfirmationCode)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.phone = state.phone
+          try env.db().saveContact(dbContact)
+        }
+        subscriber.send(.set(\.$phone, ""))
+        subscriber.send(.set(\.$phoneConfirmationID, nil))
+        subscriber.send(.set(\.$phoneConfirmationCode, ""))
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isConfirmingPhone, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .unregisterPhoneTapped:
     return .none
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index 7eb8e7aa..3f65ba80 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -15,14 +15,30 @@ public struct MyContactView: View {
       contact = state.contact
       focusedField = state.focusedField
       email = state.email
+      emailConfirmation = state.emailConfirmationID != nil
+      emailCode = state.emailConfirmationCode
+      isRegisteringEmail = state.isRegisteringEmail
+      isConfirmingEmail = state.isConfirmingEmail
       phone = state.phone
+      phoneConfirmation = state.phoneConfirmationID != nil
+      phoneCode = state.phoneConfirmationCode
+      isRegisteringPhone = state.isRegisteringPhone
+      isConfirmingPhone = state.isConfirmingPhone
       isLoadingFacts = state.isLoadingFacts
     }
 
     var contact: XXModels.Contact?
     var focusedField: MyContactState.Field?
     var email: String
+    var emailConfirmation: Bool
+    var emailCode: String
+    var isRegisteringEmail: Bool
+    var isConfirmingEmail: Bool
     var phone: String
+    var phoneConfirmation: Bool
+    var phoneCode: String
+    var isRegisteringPhone: Bool
+    var isConfirmingPhone: Bool
     var isLoadingFacts: Bool
   }
 
@@ -56,10 +72,45 @@ public struct MyContactView: View {
               .focused($focusedField, equals: .email)
               .textInputAutocapitalization(.never)
               .disableAutocorrection(true)
-              Button {
-                viewStore.send(.registerEmailTapped)
-              } label: {
-                Text("Register")
+              .disabled(viewStore.isRegisteringEmail || viewStore.emailConfirmation)
+              if viewStore.emailConfirmation {
+                TextField(
+                  text: viewStore.binding(
+                    get: \.emailCode,
+                    send: { MyContactAction.set(\.$emailConfirmationCode, $0) }
+                  ),
+                  prompt: Text("Enter confirmation code"),
+                  label: { Text("Confirmation code") }
+                )
+                .focused($focusedField, equals: .emailCode)
+                .textInputAutocapitalization(.never)
+                .disableAutocorrection(true)
+                .disabled(viewStore.isConfirmingEmail)
+                Button {
+                  viewStore.send(.confirmEmailTapped)
+                } label: {
+                  HStack {
+                    Text("Confirm")
+                    Spacer()
+                    if viewStore.isConfirmingEmail {
+                      ProgressView()
+                    }
+                  }
+                }
+                .disabled(viewStore.isConfirmingEmail)
+              } else {
+                Button {
+                  viewStore.send(.registerEmailTapped)
+                } label: {
+                  HStack {
+                    Text("Register")
+                    Spacer()
+                    if viewStore.isRegisteringEmail {
+                      ProgressView()
+                    }
+                  }
+                }
+                .disabled(viewStore.isRegisteringEmail)
               }
             }
           } else {
@@ -90,10 +141,45 @@ public struct MyContactView: View {
               .focused($focusedField, equals: .phone)
               .textInputAutocapitalization(.never)
               .disableAutocorrection(true)
-              Button {
-                viewStore.send(.registerPhoneTapped)
-              } label: {
-                Text("Register")
+              .disabled(viewStore.isRegisteringPhone || viewStore.phoneConfirmation)
+              if viewStore.phoneConfirmation {
+                TextField(
+                  text: viewStore.binding(
+                    get: \.phoneCode,
+                    send: { MyContactAction.set(\.$phoneConfirmationCode, $0) }
+                  ),
+                  prompt: Text("Enter confirmation code"),
+                  label: { Text("Confirmation code") }
+                )
+                .focused($focusedField, equals: .phoneCode)
+                .textInputAutocapitalization(.never)
+                .disableAutocorrection(true)
+                .disabled(viewStore.isConfirmingPhone)
+                Button {
+                  viewStore.send(.confirmPhoneTapped)
+                } label: {
+                  HStack {
+                    Text("Confirm")
+                    Spacer()
+                    if viewStore.isConfirmingPhone {
+                      ProgressView()
+                    }
+                  }
+                }
+                .disabled(viewStore.isConfirmingPhone)
+              } else {
+                Button {
+                  viewStore.send(.registerPhoneTapped)
+                } label: {
+                  HStack {
+                    Text("Register")
+                    Spacer()
+                    if viewStore.isRegisteringPhone {
+                      ProgressView()
+                    }
+                  }
+                }
+                .disabled(viewStore.isRegisteringPhone)
               }
             }
           } else {
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index 9c30f7df..a4f25954 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -60,6 +60,9 @@ final class MyContactFeatureTests: XCTestCase {
 
   func testRegisterEmail() {
     let email = "test@email.com"
+    let confirmationID = "123"
+
+    var didSendRegisterFact: [Fact] = []
 
     let store = TestStore(
       initialState: MyContactState(),
@@ -67,11 +70,190 @@ final class MyContactFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.sendRegisterFact.run = { fact in
+        didSendRegisterFact.append(fact)
+        return confirmationID
+      }
+      return ud
+    }
+
+    store.send(.set(\.$focusedField, .email)) {
+      $0.focusedField = .email
+    }
+
     store.send(.set(\.$email, email)) {
       $0.email = email
     }
 
-    store.send(.registerEmailTapped)
+    store.send(.registerEmailTapped) {
+      $0.focusedField = nil
+      $0.isRegisteringEmail = true
+    }
+
+    XCTAssertNoDifference(didSendRegisterFact, [.init(type: .email, value: email)])
+
+    store.receive(.set(\.$emailConfirmationID, confirmationID)) {
+      $0.emailConfirmationID = confirmationID
+    }
+
+    store.receive(.set(\.$isRegisteringEmail, false)) {
+      $0.isRegisteringEmail = false
+    }
+  }
+
+  func testRegisterEmailFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.sendRegisterFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.registerEmailTapped) {
+      $0.isRegisteringEmail = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isRegisteringEmail, false)) {
+      $0.isRegisteringEmail = false
+    }
+  }
+
+  func testConfirmEmail() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let email = "test@email.com"
+    let confirmationID = "123"
+    let confirmationCode = "321"
+    let dbContact = XXModels.Contact(id: contactID)
+
+    var didConfirmWithID: [String] = []
+    var didConfirmWithCode: [String] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
+    let store = TestStore(
+      initialState: MyContactState(
+        email: email,
+        emailConfirmationID: confirmationID
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.confirmFact.run = { id, code in
+        didConfirmWithID.append(id)
+        didConfirmWithCode.append(code)
+      }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.set(\.$focusedField, .emailCode)) {
+      $0.focusedField = .emailCode
+    }
+
+    store.send(.set(\.$emailConfirmationCode, confirmationCode)) {
+      $0.emailConfirmationCode = confirmationCode
+    }
+
+    store.send(.confirmEmailTapped) {
+      $0.focusedField = nil
+      $0.isConfirmingEmail = true
+    }
+
+    XCTAssertNoDifference(didConfirmWithID, [confirmationID])
+    XCTAssertNoDifference(didConfirmWithCode, [confirmationCode])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.email = email
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$email, "")) {
+      $0.email = ""
+    }
+    store.receive(.set(\.$emailConfirmationID, nil)) {
+      $0.emailConfirmationID = nil
+    }
+    store.receive(.set(\.$emailConfirmationCode, "")) {
+      $0.emailConfirmationCode = ""
+    }
+    store.receive(.set(\.$isConfirmingEmail, false)) {
+      $0.isConfirmingEmail = false
+    }
+  }
+
+  func testConfirmEmailFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        emailConfirmationID: "123"
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.confirmFact.run = { _, _ in throw failure }
+      return ud
+    }
+
+    store.send(.confirmEmailTapped) {
+      $0.isConfirmingEmail = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isConfirmingEmail, false)) {
+      $0.isConfirmingEmail = false
+    }
   }
 
   func testUnregisterEmail() {
@@ -85,7 +267,10 @@ final class MyContactFeatureTests: XCTestCase {
   }
 
   func testRegisterPhone() {
-    let phone = "123456789"
+    let phone = "+123456789"
+    let confirmationID = "123"
+
+    var didSendRegisterFact: [Fact] = []
 
     let store = TestStore(
       initialState: MyContactState(),
@@ -93,13 +278,192 @@ final class MyContactFeatureTests: XCTestCase {
       environment: .unimplemented
     )
 
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.sendRegisterFact.run = { fact in
+        didSendRegisterFact.append(fact)
+        return confirmationID
+      }
+      return ud
+    }
+
+    store.send(.set(\.$focusedField, .phone)) {
+      $0.focusedField = .phone
+    }
+
     store.send(.set(\.$phone, phone)) {
       $0.phone = phone
     }
 
-    store.send(.registerPhoneTapped)
+    store.send(.registerPhoneTapped) {
+      $0.focusedField = nil
+      $0.isRegisteringPhone = true
+    }
+
+    XCTAssertNoDifference(didSendRegisterFact, [.init(type: .phone, value: phone)])
+
+    store.receive(.set(\.$phoneConfirmationID, confirmationID)) {
+      $0.phoneConfirmationID = confirmationID
+    }
+
+    store.receive(.set(\.$isRegisteringPhone, false)) {
+      $0.isRegisteringPhone = false
+    }
+  }
+
+  func testRegisterPhoneFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.sendRegisterFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.registerPhoneTapped) {
+      $0.isRegisteringPhone = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isRegisteringPhone, false)) {
+      $0.isRegisteringPhone = false
+    }
   }
 
+  func testConfirmPhone() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let phone = "+123456789"
+    let confirmationID = "123"
+    let confirmationCode = "321"
+    let dbContact = XXModels.Contact(id: contactID)
+
+    var didConfirmWithID: [String] = []
+    var didConfirmWithCode: [String] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
+    let store = TestStore(
+      initialState: MyContactState(
+        phone: phone,
+        phoneConfirmationID: confirmationID
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.confirmFact.run = { id, code in
+        didConfirmWithID.append(id)
+        didConfirmWithCode.append(code)
+      }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.set(\.$focusedField, .phoneCode)) {
+      $0.focusedField = .phoneCode
+    }
+
+    store.send(.set(\.$phoneConfirmationCode, confirmationCode)) {
+      $0.phoneConfirmationCode = confirmationCode
+    }
+
+    store.send(.confirmPhoneTapped) {
+      $0.focusedField = nil
+      $0.isConfirmingPhone = true
+    }
+
+    XCTAssertNoDifference(didConfirmWithID, [confirmationID])
+    XCTAssertNoDifference(didConfirmWithCode, [confirmationCode])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.phone = phone
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$phone, "")) {
+      $0.phone = ""
+    }
+    store.receive(.set(\.$phoneConfirmationID, nil)) {
+      $0.phoneConfirmationID = nil
+    }
+    store.receive(.set(\.$phoneConfirmationCode, "")) {
+      $0.phoneConfirmationCode = ""
+    }
+    store.receive(.set(\.$isConfirmingPhone, false)) {
+      $0.isConfirmingPhone = false
+    }
+  }
+
+  func testConfirmPhoneFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        phoneConfirmationID: "123"
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.confirmFact.run = { _, _ in throw failure }
+      return ud
+    }
+
+    store.send(.confirmPhoneTapped) {
+      $0.isConfirmingPhone = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isConfirmingPhone, false)) {
+      $0.isConfirmingPhone = false
+    }
+  }
+  
   func testUnregisterPhone() {
     let store = TestStore(
       initialState: MyContactState(),
-- 
GitLab


From 726aab7e32844079f39ccdb0bbb301d30dfa70be Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 13:43:51 +0200
Subject: [PATCH 10/22] Implement facts unregistration

---
 .../MyContactFeature/MyContactFeature.swift   |  52 +++++-
 .../MyContactFeature/MyContactView.swift      |  22 ++-
 .../MyContactFeatureTests.swift               | 176 +++++++++++++++++-
 3 files changed, 242 insertions(+), 8 deletions(-)

diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
index 99486c8a..b25d0212 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift
@@ -23,11 +23,13 @@ public struct MyContactState: Equatable {
     emailConfirmationCode: String = "",
     isRegisteringEmail: Bool = false,
     isConfirmingEmail: Bool = false,
+    isUnregisteringEmail: Bool = false,
     phone: String = "",
     phoneConfirmationID: String? = nil,
     phoneConfirmationCode: String = "",
     isRegisteringPhone: Bool = false,
     isConfirmingPhone: Bool = false,
+    isUnregisteringPhone: Bool = false,
     isLoadingFacts: Bool = false,
     alert: AlertState<MyContactAction>? = nil
   ) {
@@ -38,11 +40,13 @@ public struct MyContactState: Equatable {
     self.emailConfirmationCode = emailConfirmationCode
     self.isRegisteringEmail = isRegisteringEmail
     self.isConfirmingEmail = isConfirmingEmail
+    self.isUnregisteringEmail = isUnregisteringEmail
     self.phone = phone
     self.phoneConfirmationID = phoneConfirmationID
     self.phoneConfirmationCode = phoneConfirmationCode
     self.isRegisteringPhone = isRegisteringPhone
     self.isConfirmingPhone = isConfirmingPhone
+    self.isUnregisteringPhone = isUnregisteringPhone
     self.isLoadingFacts = isLoadingFacts
     self.alert = alert
   }
@@ -54,11 +58,13 @@ public struct MyContactState: Equatable {
   @BindableState public var emailConfirmationCode: String
   @BindableState public var isRegisteringEmail: Bool
   @BindableState public var isConfirmingEmail: Bool
+  @BindableState public var isUnregisteringEmail: Bool
   @BindableState public var phone: String
   @BindableState public var phoneConfirmationID: String?
   @BindableState public var phoneConfirmationCode: String
   @BindableState public var isRegisteringPhone: Bool
   @BindableState public var isConfirmingPhone: Bool
+  @BindableState public var isUnregisteringPhone: Bool
   @BindableState public var isLoadingFacts: Bool
   public var alert: AlertState<MyContactAction>?
 }
@@ -178,7 +184,28 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     .eraseToEffect()
 
   case .unregisterEmailTapped:
-    return .none
+    guard let email = state.contact?.email else { return .none }
+    state.isUnregisteringEmail = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud: UserDiscovery = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .email, value: email)
+        try ud.removeFact(fact)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.email = nil
+          try env.db().saveContact(dbContact)
+        }
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isUnregisteringEmail, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .registerPhoneTapped:
     state.focusedField = nil
@@ -228,7 +255,28 @@ public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContact
     .eraseToEffect()
 
   case .unregisterPhoneTapped:
-    return .none
+    guard let phone = state.contact?.phone else { return .none }
+    state.isUnregisteringPhone = true
+    return Effect.run { [state] subscriber in
+      do {
+        let ud: UserDiscovery = try env.messenger.ud.tryGet()
+        let fact = Fact(type: .phone, value: phone)
+        try ud.removeFact(fact)
+        let contactId = try env.messenger.e2e.tryGet().getContact().getId()
+        if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
+          dbContact.phone = nil
+          try env.db().saveContact(dbContact)
+        }
+      } catch {
+        subscriber.send(.didFail(error.localizedDescription))
+      }
+      subscriber.send(.set(\.$isUnregisteringPhone, false))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
 
   case .loadFactsTapped:
     state.isLoadingFacts = true
diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
index 3f65ba80..f32af242 100644
--- a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
+++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift
@@ -19,11 +19,13 @@ public struct MyContactView: View {
       emailCode = state.emailConfirmationCode
       isRegisteringEmail = state.isRegisteringEmail
       isConfirmingEmail = state.isConfirmingEmail
+      isUnregisteringEmail = state.isUnregisteringEmail
       phone = state.phone
       phoneConfirmation = state.phoneConfirmationID != nil
       phoneCode = state.phoneConfirmationCode
       isRegisteringPhone = state.isRegisteringPhone
       isConfirmingPhone = state.isConfirmingPhone
+      isUnregisteringPhone = state.isUnregisteringPhone
       isLoadingFacts = state.isLoadingFacts
     }
 
@@ -34,11 +36,13 @@ public struct MyContactView: View {
     var emailCode: String
     var isRegisteringEmail: Bool
     var isConfirmingEmail: Bool
+    var isUnregisteringEmail: Bool
     var phone: String
     var phoneConfirmation: Bool
     var phoneCode: String
     var isRegisteringPhone: Bool
     var isConfirmingPhone: Bool
+    var isUnregisteringPhone: Bool
     var isLoadingFacts: Bool
   }
 
@@ -58,8 +62,15 @@ public struct MyContactView: View {
               Button(role: .destructive) {
                 viewStore.send(.unregisterEmailTapped)
               } label: {
-                Text("Unregister")
+                HStack {
+                  Text("Unregister")
+                  Spacer()
+                  if viewStore.isUnregisteringEmail {
+                    ProgressView()
+                  }
+                }
               }
+              .disabled(viewStore.isUnregisteringEmail)
             } else {
               TextField(
                 text: viewStore.binding(
@@ -127,8 +138,15 @@ public struct MyContactView: View {
               Button(role: .destructive) {
                 viewStore.send(.unregisterPhoneTapped)
               } label: {
-                Text("Unregister")
+                HStack {
+                  Text("Unregister")
+                  Spacer()
+                  if viewStore.isUnregisteringPhone {
+                    ProgressView()
+                  }
+                }
               }
+              .disabled(viewStore.isUnregisteringPhone)
             } else {
               TextField(
                 text: viewStore.binding(
diff --git a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index a4f25954..8d5fac94 100644
--- a/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -257,13 +257,97 @@ final class MyContactFeatureTests: XCTestCase {
   }
 
   func testUnregisterEmail() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let email = "test@email.com"
+    let dbContact = XXModels.Contact(id: contactID, email: email)
+
+    var didRemoveFact: [Fact] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
-      initialState: MyContactState(),
+      initialState: MyContactState(
+        contact: dbContact
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { didRemoveFact.append($0) }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.unregisterEmailTapped) {
+      $0.isUnregisteringEmail = true
+    }
+
+    XCTAssertNoDifference(didRemoveFact, [.init(type: .email, value: email)])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.email = nil
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$isUnregisteringEmail, false)) {
+      $0.isUnregisteringEmail = false
+    }
+  }
+
+  func testUnregisterEmailFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        contact: .init(id: Data(), email: "test@email.com")
+      ),
       reducer: myContactReducer,
       environment: .unimplemented
     )
 
-    store.send(.unregisterEmailTapped)
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.unregisterEmailTapped) {
+      $0.isUnregisteringEmail = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isUnregisteringEmail, false)) {
+      $0.isUnregisteringEmail = false
+    }
   }
 
   func testRegisterPhone() {
@@ -465,13 +549,97 @@ final class MyContactFeatureTests: XCTestCase {
   }
   
   func testUnregisterPhone() {
+    let contactID = "contact-id".data(using: .utf8)!
+    let phone = "+123456789"
+    let dbContact = XXModels.Contact(id: contactID, phone: phone)
+
+    var didRemoveFact: [Fact] = []
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
     let store = TestStore(
-      initialState: MyContactState(),
+      initialState: MyContactState(
+        contact: dbContact
+      ),
+      reducer: myContactReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { didRemoveFact.append($0) }
+      return ud
+    }
+    store.environment.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in contactID }
+        return contact
+      }
+      return e2e
+    }
+    store.environment.db.run = {
+      var db: Database = .failing
+      db.fetchContacts.run = { query in
+        didFetchContacts.append(query)
+        return [dbContact]
+      }
+      db.saveContact.run = { contact in
+        didSaveContact.append(contact)
+        return contact
+      }
+      return db
+    }
+
+    store.send(.unregisterPhoneTapped) {
+      $0.isUnregisteringPhone = true
+    }
+
+    XCTAssertNoDifference(didRemoveFact, [.init(type: .phone, value: phone)])
+    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.phone = nil
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+
+    store.receive(.set(\.$isUnregisteringPhone, false)) {
+      $0.isUnregisteringPhone = false
+    }
+  }
+
+  func testUnregisterPhoneFailure() {
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: MyContactState(
+        contact: .init(id: Data(), phone: "+123456789")
+      ),
       reducer: myContactReducer,
       environment: .unimplemented
     )
 
-    store.send(.unregisterPhoneTapped)
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.messenger.ud.get = {
+      var ud: UserDiscovery = .unimplemented
+      ud.removeFact.run = { _ in throw failure }
+      return ud
+    }
+
+    store.send(.unregisterPhoneTapped) {
+      $0.isUnregisteringPhone = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.alert = .error(failure.localizedDescription)
+    }
+
+    store.receive(.set(\.$isUnregisteringPhone, false)) {
+      $0.isUnregisteringPhone = false
+    }
   }
 
   func testLoadFactsFromClient() {
-- 
GitLab


From ab3cef3ed320c3b0ad9ae9279beeb9c5f0812ba6 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 13:56:33 +0200
Subject: [PATCH 11/22] Bump package dependencies versions

---
 Examples/xx-messenger/Package.swift | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index c184020f..8367b5ef 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -40,7 +40,7 @@ let package = Package(
     ),
     .package(
       url: "https://github.com/pointfreeco/swift-composable-architecture.git",
-      .upToNextMajor(from: "0.40.1")
+      .upToNextMajor(from: "0.40.2")
     ),
     .package(
       url: "https://git.xx.network/elixxir/client-ios-db.git",
@@ -52,7 +52,7 @@ let package = Package(
     ),
     .package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
-      .upToNextMajor(from: "0.4.0")
+      .upToNextMajor(from: "0.4.1")
     ),
   ],
   targets: [
-- 
GitLab


From cdae17a17937fe016b0c97f277f748fc9421ed31 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:13:28 +0200
Subject: [PATCH 12/22] Update package dependencies

---
 Package.resolved | 8 ++++----
 Package.swift    | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/Package.resolved b/Package.resolved
index 6741fe9d..225eb0fd 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -14,8 +14,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
       "state" : {
-        "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
-        "version" : "0.5.0"
+        "revision" : "c9b6b940d95c0a925c63f6858943415714d8a981",
+        "version" : "0.5.2"
       }
     },
     {
@@ -23,8 +23,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       "state" : {
-        "revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260",
-        "version" : "0.4.0"
+        "revision" : "30314f1ece684dd60679d598a9b89107557b67d9",
+        "version" : "0.4.1"
       }
     }
   ],
diff --git a/Package.swift b/Package.swift
index 30e5c604..d34f3701 100644
--- a/Package.swift
+++ b/Package.swift
@@ -26,11 +26,11 @@ let package = Package(
   dependencies: [
     .package(
       url: "https://github.com/pointfreeco/swift-custom-dump.git",
-      .upToNextMajor(from: "0.5.0")
+      .upToNextMajor(from: "0.5.2")
     ),
     .package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
-      .upToNextMajor(from: "0.4.0")
+      .upToNextMajor(from: "0.4.1")
     ),
     .package(
       url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
-- 
GitLab


From 2d7f7791f3b90cec21ac6f2391becfd9fc9f127b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:28:42 +0200
Subject: [PATCH 13/22] Add missing imports

---
 .../xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift     | 1 +
 .../xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift   | 1 +
 .../Tests/RegisterFeatureTests/RegisterFeatureTests.swift        | 1 +
 .../Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift  | 1 +
 4 files changed, 4 insertions(+)

diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
index 5a013b28..fc58fe4e 100644
--- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -1,4 +1,5 @@
 import ComposableArchitecture
+import CustomDump
 import HomeFeature
 import RestoreFeature
 import WelcomeFeature
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index fc671b1e..d922d4f9 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -1,6 +1,7 @@
 import AppCore
 import ComposableArchitecture
 import ContactsFeature
+import CustomDump
 import RegisterFeature
 import UserSearchFeature
 import XCTest
diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
index 6c802874..8da1e1f1 100644
--- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift
@@ -1,4 +1,5 @@
 import ComposableArchitecture
+import CustomDump
 import XCTest
 import XXClient
 import XXMessengerClient
diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
index cec3587e..3f35d3df 100644
--- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift
@@ -1,5 +1,6 @@
 import Combine
 import ComposableArchitecture
+import CustomDump
 import XCTest
 import XXClient
 import XXModels
-- 
GitLab


From 450633f2343cf9c3ce1b9a3792f1bf6aec943601 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:29:02 +0200
Subject: [PATCH 14/22] Update UserSearchFeatureTests

---
 .../Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift  | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index 44731c1f..33f1edb9 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import ContactFeature
+import CustomDump
 import XCTest
 import XXClient
 import XXMessengerClient
@@ -64,6 +65,8 @@ final class UserSearchFeatureTests: XCTestCase {
       $0.failure = nil
     }
 
+    XCTAssertNoDifference(didSearchWithQuery, [.init(username: "Username")])
+
     store.receive(.didSucceed(contacts)) {
       $0.isSearching = false
       $0.failure = nil
-- 
GitLab


From e17db8e25301073e84de6d242342a6c9f50490ee Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:29:35 +0200
Subject: [PATCH 15/22] Add explicit CustomDump dependency to test targets

---
 Examples/xx-messenger/Package.swift | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 8367b5ef..c0a4f905 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -54,6 +54,10 @@ let package = Package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       .upToNextMajor(from: "0.4.1")
     ),
+    .package(
+      url: "https://github.com/pointfreeco/swift-custom-dump.git",
+      .upToNextMajor(from: "0.5.2")
+    ),
   ],
   targets: [
     .target(
@@ -70,7 +74,8 @@ let package = Package(
     .testTarget(
       name: "AppCoreTests",
       dependencies: [
-        .target(name: "AppCore")
+        .target(name: "AppCore"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -102,6 +107,7 @@ let package = Package(
       name: "AppFeatureTests",
       dependencies: [
         .target(name: "AppFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -120,6 +126,7 @@ let package = Package(
       name: "ChatFeatureTests",
       dependencies: [
         .target(name: "ChatFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -137,6 +144,7 @@ let package = Package(
       name: "CheckContactAuthFeatureTests",
       dependencies: [
         .target(name: "CheckContactAuthFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ]
     ),
     .target(
@@ -153,6 +161,7 @@ let package = Package(
       name: "ConfirmRequestFeatureTests",
       dependencies: [
         .target(name: "ConfirmRequestFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ]
     ),
     .target(
@@ -175,6 +184,7 @@ let package = Package(
       name: "ContactFeatureTests",
       dependencies: [
         .target(name: "ContactFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -196,6 +206,7 @@ let package = Package(
       name: "ContactsFeatureTests",
       dependencies: [
         .target(name: "ContactsFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -216,6 +227,7 @@ let package = Package(
       name: "HomeFeatureTests",
       dependencies: [
         .target(name: "HomeFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -234,6 +246,7 @@ let package = Package(
       name: "MyContactFeatureTests",
       dependencies: [
         .target(name: "MyContactFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -252,6 +265,7 @@ let package = Package(
       name: "RegisterFeatureTests",
       dependencies: [
         .target(name: "RegisterFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -266,6 +280,7 @@ let package = Package(
       name: "RestoreFeatureTests",
       dependencies: [
         .target(name: "RestoreFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -284,6 +299,7 @@ let package = Package(
       name: "SendRequestFeatureTests",
       dependencies: [
         .target(name: "SendRequestFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -303,6 +319,7 @@ let package = Package(
       name: "UserSearchFeatureTests",
       dependencies: [
         .target(name: "UserSearchFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
@@ -320,6 +337,7 @@ let package = Package(
       name: "VerifyContactFeatureTests",
       dependencies: [
         .target(name: "VerifyContactFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ]
     ),
     .target(
@@ -334,6 +352,7 @@ let package = Package(
       name: "WelcomeFeatureTests",
       dependencies: [
         .target(name: "WelcomeFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
       ],
       swiftSettings: swiftSettings
     ),
-- 
GitLab


From dbf5b0986e5f3def537497f1faba063d99abd8d6 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:47:37 +0200
Subject: [PATCH 16/22] Add xcode-remove-caches script

---
 xcode-remove-caches.sh | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 xcode-remove-caches.sh

diff --git a/xcode-remove-caches.sh b/xcode-remove-caches.sh
new file mode 100644
index 00000000..34e26621
--- /dev/null
+++ b/xcode-remove-caches.sh
@@ -0,0 +1,4 @@
+rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang/ModuleCache"
+rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang.$(whoami)/ModuleCache"
+rm -rf ~/Library/Developer/Xcode/DerivedData/*
+rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
-- 
GitLab


From 116858c76ca5c739e6adcdc19c60508038d77383 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:48:44 +0200
Subject: [PATCH 17/22] Remove caches before running example tests on CI

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 10f45cd3..64dbfb17 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,5 +27,6 @@ test-examples-ios:
   tags: 
     - ios
   script:
+    - ./xcode-remove-caches.sh
     - ./run-tests.sh examples-ios
   retry: 1
-- 
GitLab


From 912c45b5b7f6b88661fd92d072b2d0307c1a49bd Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:50:01 +0200
Subject: [PATCH 18/22] Make xcode-remove-caches executable

---
 xcode-remove-caches.sh | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 mode change 100644 => 100755 xcode-remove-caches.sh

diff --git a/xcode-remove-caches.sh b/xcode-remove-caches.sh
old mode 100644
new mode 100755
-- 
GitLab


From 098730660417b60725ec3cebf88a1d9b37c8d0bf Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:51:51 +0200
Subject: [PATCH 19/22] Update xcode-remove-caches script

---
 xcode-remove-caches.sh | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/xcode-remove-caches.sh b/xcode-remove-caches.sh
index 34e26621..4d24eab4 100755
--- a/xcode-remove-caches.sh
+++ b/xcode-remove-caches.sh
@@ -1,3 +1,5 @@
+pkill -int com.apple.CoreSimulator.CoreSimulatorService
+killall Xcode
 rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang/ModuleCache"
 rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang.$(whoami)/ModuleCache"
 rm -rf ~/Library/Developer/Xcode/DerivedData/*
-- 
GitLab


From bd28d1095b4b0866ded5df5eb6eabed53b16db8f Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:52:22 +0200
Subject: [PATCH 20/22] Revert "Remove caches before running example tests on
 CI"

This reverts commit 116858c76ca5c739e6adcdc19c60508038d77383.
---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 64dbfb17..10f45cd3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,6 +27,5 @@ test-examples-ios:
   tags: 
     - ios
   script:
-    - ./xcode-remove-caches.sh
     - ./run-tests.sh examples-ios
   retry: 1
-- 
GitLab


From 83d355a7f947a2241366f93992959062b6d05484 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 14:53:15 +0200
Subject: [PATCH 21/22] Run xcode-remove-caches on CI

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 10f45cd3..b3307061 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ before_script:
   - for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts
   - for ip in $(dig @8.8.8.8 git.xx.network +short); do ssh-keyscan git.xx.network,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts
   - xcodebuild -version
+  - ./xcode-remove-caches.sh
 
 stages:
   - test
-- 
GitLab


From 4f3ebddf4f92adb29f797a86f8709d3bcce5552b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Tue, 20 Sep 2022 15:01:22 +0200
Subject: [PATCH 22/22] Revert "Run xcode-remove-caches on CI"

This reverts commit 83d355a7f947a2241366f93992959062b6d05484.
---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b3307061..10f45cd3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,6 @@ before_script:
   - for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts
   - for ip in $(dig @8.8.8.8 git.xx.network +short); do ssh-keyscan git.xx.network,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts
   - xcodebuild -version
-  - ./xcode-remove-caches.sh
 
 stages:
   - test
-- 
GitLab