diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/UserSearchFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/UserSearchFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..2f80716697639781318b3bf917803385b18422ae
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/UserSearchFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "UserSearchFeature"
+               BuildableName = "UserSearchFeature"
+               BlueprintName = "UserSearchFeature"
+               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 = "UserSearchFeatureTests"
+               BuildableName = "UserSearchFeatureTests"
+               BlueprintName = "UserSearchFeatureTests"
+               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 = "UserSearchFeature"
+            BuildableName = "UserSearchFeature"
+            BlueprintName = "UserSearchFeature"
+            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 8f1e26f79d8f80884a0e60b4a81a249fb00f76d8..f9ceb75fdf29afbda5362c20139003d77ebb131f 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -23,6 +23,7 @@ let package = Package(
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
+    .library(name: "UserSearchFeature", targets: ["UserSearchFeature"]),
     .library(name: "WelcomeFeature", targets: ["WelcomeFeature"]),
   ],
   dependencies: [
@@ -70,6 +71,7 @@ let package = Package(
         .target(name: "HomeFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
+        .target(name: "UserSearchFeature"),
         .target(name: "WelcomeFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
@@ -90,6 +92,7 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .target(name: "RegisterFeature"),
+        .target(name: "UserSearchFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
@@ -134,6 +137,22 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "UserSearchFeature",
+      dependencies: [
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "UserSearchFeatureTests",
+      dependencies: [
+        .target(name: "UserSearchFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "WelcomeFeature",
       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 669debfc2ca857cae6de176b19f3caeb579e56e6..041cf3f70c88c188af478b1c3ec2bb88d5a7e4f1 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -79,6 +79,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "UserSearchFeatureTests"
+               BuildableName = "UserSearchFeatureTests"
+               BlueprintName = "UserSearchFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 19f1b7f5a750f278ca41ff031acf08409cccaa02..5d7777802ed09dcfb2e36b14ba2a5b55f1fe906e 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -3,6 +3,7 @@ import Foundation
 import HomeFeature
 import RegisterFeature
 import RestoreFeature
+import UserSearchFeature
 import WelcomeFeature
 import XXMessengerClient
 import XXModels
@@ -44,6 +45,13 @@ extension AppEnvironment {
               mainQueue: mainQueue,
               bgQueue: bgQueue
             )
+          },
+          userSearch: {
+            UserSearchEnvironment(
+              messenger: messenger,
+              mainQueue: mainQueue,
+              bgQueue: bgQueue
+            )
           }
         )
       }
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
index 0a019179b2d45f23a41e54ecb084ac7ec835fe2d..51083ea0ccea39c7bf13abdd0ff0566dac623529 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
@@ -4,6 +4,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import Foundation
 import RegisterFeature
+import UserSearchFeature
 import XXClient
 import XXMessengerClient
 
@@ -14,13 +15,15 @@ public struct HomeState: Equatable {
     networkNodesReport: NodeRegistrationReport? = nil,
     isDeletingAccount: Bool = false,
     alert: AlertState<HomeAction>? = nil,
-    register: RegisterState? = nil
+    register: RegisterState? = nil,
+    userSearch: UserSearchState? = nil
   ) {
     self.failure = failure
     self.isNetworkHealthy = isNetworkHealthy
     self.isDeletingAccount = isDeletingAccount
     self.alert = alert
     self.register = register
+    self.userSearch = userSearch
   }
 
   public var failure: String?
@@ -29,6 +32,7 @@ public struct HomeState: Equatable {
   public var isDeletingAccount: Bool
   public var alert: AlertState<HomeAction>?
   public var register: RegisterState?
+  public var userSearch: UserSearchState?
 }
 
 public enum HomeAction: Equatable {
@@ -58,7 +62,10 @@ public enum HomeAction: Equatable {
   case deleteAccount(DeleteAccount)
   case didDismissAlert
   case didDismissRegister
+  case userSearchButtonTapped
+  case didDismissUserSearch
   case register(RegisterAction)
+  case userSearch(UserSearchAction)
 }
 
 public struct HomeEnvironment {
@@ -67,13 +74,15 @@ public struct HomeEnvironment {
     db: DBManagerGetDB,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>,
-    register: @escaping () -> RegisterEnvironment
+    register: @escaping () -> RegisterEnvironment,
+    userSearch: @escaping () -> UserSearchEnvironment
   ) {
     self.messenger = messenger
     self.db = db
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
     self.register = register
+    self.userSearch = userSearch
   }
 
   public var messenger: Messenger
@@ -81,6 +90,7 @@ public struct HomeEnvironment {
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var register: () -> RegisterEnvironment
+  public var userSearch: () -> UserSearchEnvironment
 }
 
 extension HomeEnvironment {
@@ -89,7 +99,8 @@ extension HomeEnvironment {
     db: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented,
-    register: { .unimplemented }
+    register: { .unimplemented },
+    userSearch: { .unimplemented }
   )
 }
 
@@ -219,11 +230,19 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
     state.register = nil
     return .none
 
+  case .userSearchButtonTapped:
+    state.userSearch = UserSearchState()
+    return .none
+
+  case .didDismissUserSearch:
+    state.userSearch = nil
+    return .none
+
   case .register(.finished):
     state.register = nil
     return Effect(value: .messenger(.start))
 
-  case .register(_):
+  case .register(_), .userSearch(_):
     return .none
   }
 }
@@ -234,3 +253,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
   action: /HomeAction.register,
   environment: { $0.register() }
 )
+.presenting(
+  userSearchReducer,
+  state: .keyPath(\.userSearch),
+  id: .notNil(),
+  action: /HomeAction.userSearch,
+  environment: { $0.userSearch() }
+)
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
index ce8aac42ab93d9578770f7cb606b7090a6fb6082..e52e8d1b7fb94004fc29b4cb82041400467dbe8c 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
@@ -2,6 +2,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import RegisterFeature
 import SwiftUI
+import UserSearchFeature
 import XXClient
 
 public struct HomeView: View {
@@ -86,6 +87,20 @@ public struct HomeView: View {
             Text("Network")
           }
 
+          Section {
+            Button {
+              viewStore.send(.userSearchButtonTapped)
+            } label: {
+              HStack {
+                Text("Search users")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
+          } header: {
+            Text("Contacts")
+          }
+
           Section {
             Button(role: .destructive) {
               viewStore.send(.deleteAccount(.buttonTapped))
@@ -108,6 +123,16 @@ public struct HomeView: View {
           store.scope(state: \.alert),
           dismiss: HomeAction.didDismissAlert
         )
+        .background(NavigationLinkWithStore(
+          store.scope(
+            state: \.userSearch,
+            action: HomeAction.userSearch
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissUserSearch)
+          },
+          destination: UserSearchView.init(store:)
+        ))
       }
       .navigationViewStyle(.stack)
       .task { viewStore.send(.messenger(.start)) }
diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
index a8d3280c5e321cb78ade668570dcc40159f0b7b7..8e1f35411fdd0ec6d952f556420f9c72073c1e1f 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift
@@ -1,6 +1,6 @@
 import AppCore
 import ComposableArchitecture
-import SwiftUI
+import Foundation
 import XCTestDynamicOverlay
 import XXMessengerClient
 import XXModels
@@ -13,11 +13,13 @@ public struct RegisterState: Equatable {
   public init(
     focusedField: Field? = nil,
     username: String = "",
-    isRegistering: Bool = false
+    isRegistering: Bool = false,
+    failure: String? = nil
   ) {
     self.focusedField = focusedField
     self.username = username
     self.isRegistering = isRegistering
+    self.failure = failure
   }
 
   @BindableState public var focusedField: Field?
diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift
index 195b655b4806b3156a4fe4353fb4cd2ec708c51b..c3601630ea07f3acdd2e6a5ba497705b06d12417 100644
--- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift
+++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift
@@ -37,6 +37,8 @@ public struct RegisterView: View {
               label: { Text("Username") }
             )
             .focused($focusedField, equals: .username)
+            .textInputAutocapitalization(.never)
+            .disableAutocorrection(true)
           } header: {
             Text("Username")
           }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
new file mode 100644
index 0000000000000000000000000000000000000000..86a13c0ec97966619bea881f5ac0b8c9de962fed
--- /dev/null
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -0,0 +1,135 @@
+import ComposableArchitecture
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+
+public struct UserSearchState: Equatable {
+  public enum Field: String, Hashable {
+    case username
+    case email
+    case phone
+  }
+
+  public struct Result: Equatable, Identifiable {
+    public init(
+      id: Data,
+      contact: Contact,
+      username: String? = nil,
+      email: String? = nil,
+      phone: String? = nil
+    ) {
+      self.id = id
+      self.contact = contact
+      self.username = username
+      self.email = email
+      self.phone = phone
+    }
+
+    public var id: Data
+    public var contact: XXClient.Contact
+    public var username: String?
+    public var email: String?
+    public var phone: String?
+  }
+
+  public init(
+    focusedField: Field? = nil,
+    query: MessengerSearchUsers.Query = .init(),
+    isSearching: Bool = false,
+    failure: String? = nil,
+    results: IdentifiedArrayOf<Result> = []
+  ) {
+    self.focusedField = focusedField
+    self.query = query
+    self.isSearching = isSearching
+    self.failure = failure
+    self.results = results
+  }
+
+  @BindableState public var focusedField: Field?
+  @BindableState public var query: MessengerSearchUsers.Query
+  public var isSearching: Bool
+  public var failure: String?
+  public var results: IdentifiedArrayOf<Result>
+}
+
+public enum UserSearchAction: Equatable, BindableAction {
+  case searchTapped
+  case didFail(String)
+  case didSucceed([Contact])
+  case binding(BindingAction<UserSearchState>)
+}
+
+public struct UserSearchEnvironment {
+  public init(
+    messenger: Messenger,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
+}
+
+#if DEBUG
+extension UserSearchEnvironment {
+  public static let unimplemented = UserSearchEnvironment(
+    messenger: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
+}
+#endif
+
+public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSearchEnvironment>
+{ state, action, env in
+  switch action {
+  case .searchTapped:
+    state.focusedField = nil
+    state.isSearching = true
+    state.results = []
+    state.failure = nil
+    return .result { [query = state.query] in
+      do {
+        return .success(.didSucceed(try env.messenger.searchUsers(query: query)))
+      } catch {
+        return .success(.didFail(error.localizedDescription))
+      }
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .didSucceed(let contacts):
+    state.isSearching = false
+    state.failure = nil
+    state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in
+      guard let id = try? contact.getId() else { return nil }
+      let facts = (try? contact.getFacts()) ?? []
+      return UserSearchState.Result(
+        id: id,
+        contact: contact,
+        username: facts.first(where: { $0.type == 0 })?.fact,
+        email: facts.first(where: { $0.type == 1 })?.fact,
+        phone: facts.first(where: { $0.type == 2 })?.fact
+      )
+    })
+    return .none
+
+  case .didFail(let failure):
+    state.isSearching = false
+    state.failure = failure
+    state.results = []
+    return .none
+
+  case .binding(_):
+    return .none
+  }
+}
+.binding()
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..266938a29b096d05f695c2f39f55f8c0fb993442
--- /dev/null
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
@@ -0,0 +1,125 @@
+import ComposableArchitecture
+import SwiftUI
+import XXMessengerClient
+
+public struct UserSearchView: View {
+  public init(store: Store<UserSearchState, UserSearchAction>) {
+    self.store = store
+  }
+
+  let store: Store<UserSearchState, UserSearchAction>
+  @FocusState var focusedField: UserSearchState.Field?
+
+  struct ViewState: Equatable {
+    var focusedField: UserSearchState.Field?
+    var query: MessengerSearchUsers.Query
+    var isSearching: Bool
+    var failure: String?
+    var results: IdentifiedArrayOf<UserSearchState.Result>
+
+    init(state: UserSearchState) {
+      focusedField = state.focusedField
+      query = state.query
+      isSearching = state.isSearching
+      failure = state.failure
+      results = state.results
+    }
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Form {
+        Section {
+          TextField(
+            text: viewStore.binding(
+              get: { $0.query.username ?? "" },
+              send: { UserSearchAction.set(\.$query.username, $0.isEmpty ? nil : $0) }
+            ),
+            prompt: Text("Enter username"),
+            label: { Text("Username") }
+          )
+          .focused($focusedField, equals: .username)
+
+          TextField(
+            text: viewStore.binding(
+              get: { $0.query.email ?? "" },
+              send: { UserSearchAction.set(\.$query.email, $0.isEmpty ? nil : $0) }
+            ),
+            prompt: Text("Enter email"),
+            label: { Text("Email") }
+          )
+          .focused($focusedField, equals: .email)
+
+          TextField(
+            text: viewStore.binding(
+              get: { $0.query.phone ?? "" },
+              send: { UserSearchAction.set(\.$query.phone, $0.isEmpty ? nil : $0) }
+            ),
+            prompt: Text("Enter phone"),
+            label: { Text("Phone") }
+          )
+          .focused($focusedField, equals: .phone)
+
+          Button {
+            viewStore.send(.searchTapped)
+          } label: {
+            HStack {
+              Text("Search")
+              Spacer()
+              if viewStore.isSearching {
+                ProgressView()
+              } else {
+                Image(systemName: "magnifyingglass")
+              }
+            }
+          }
+          .disabled(viewStore.query.isEmpty)
+        }
+        .disabled(viewStore.isSearching)
+        .textInputAutocapitalization(.never)
+        .disableAutocorrection(true)
+
+        if let failure = viewStore.failure {
+          Section {
+            Text(failure)
+          } header: {
+            Text("Error")
+          }
+        }
+
+        ForEach(viewStore.results) { result in
+          Section {
+            if let username = result.username {
+              Text(username)
+            }
+            if let email = result.email {
+              Text(email)
+            }
+            if let phone = result.phone {
+              Text(phone)
+            }
+            if result.username == nil, result.email == nil, result.phone == nil {
+              Image(systemName: "questionmark")
+                .frame(maxWidth: .infinity)
+            }
+          }
+        }
+      }
+      .onChange(of: viewStore.focusedField) { focusedField = $0 }
+      .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
+      .navigationTitle("User Search")
+    }
+  }
+}
+
+#if DEBUG
+public struct UserSearchView_Previews: PreviewProvider {
+  public static var previews: some View {
+    UserSearchView(store: Store(
+      initialState: UserSearchState(),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index c7168d5b4537564a69ac82145b433c6ab22c65ff..aa309016c08c4a75e62c6ecd5cf47e32191832e1 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import RegisterFeature
+import UserSearchFeature
 import XCTest
 import XXClient
 import XXMessengerClient
@@ -437,4 +438,30 @@ final class HomeFeatureTests: XCTestCase {
       $0.register = nil
     }
   }
+
+  func testUserSearchButtonTapped() {
+    let store = TestStore(
+      initialState: HomeState(),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.userSearchButtonTapped) {
+      $0.userSearch = UserSearchState()
+    }
+  }
+
+  func testDidDismissUserSearch() {
+    let store = TestStore(
+      initialState: HomeState(
+        userSearch: UserSearchState()
+      ),
+      reducer: homeReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.didDismissUserSearch) {
+      $0.userSearch = nil
+    }
+  }
 }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8d2f414a0a6c4fa130ea285863ae92ef80f70c21
--- /dev/null
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -0,0 +1,119 @@
+import ComposableArchitecture
+import XCTest
+import XXClient
+import XXMessengerClient
+@testable import UserSearchFeature
+
+final class UserSearchFeatureTests: XCTestCase {
+  func testSearch() {
+    let store = TestStore(
+      initialState: UserSearchState(),
+      reducer: userSearchReducer,
+      environment: .unimplemented
+    )
+
+    var didSearchWithQuery: [MessengerSearchUsers.Query] = []
+
+    struct GetIdFromContactError: Error {}
+    struct GetFactsFromContactError: Error {}
+
+    var contact1 = Contact.unimplemented("contact-1".data(using: .utf8)!)
+    contact1.getIdFromContact.run = { _ in "contact-1-id".data(using: .utf8)! }
+    contact1.getFactsFromContact.run = { _ in
+      [Fact(fact: "contact-1-username", type: 0),
+       Fact(fact: "contact-1-email", type: 1),
+       Fact(fact: "contact-1-phone", type: 2)]
+    }
+    var contact2 = Contact.unimplemented("contact-1".data(using: .utf8)!)
+    contact2.getIdFromContact.run = { _ in "contact-2-id".data(using: .utf8)! }
+    contact2.getFactsFromContact.run = { _ in
+      [Fact(fact: "contact-2-username", type: 0),
+       Fact(fact: "contact-2-email", type: 1),
+       Fact(fact: "contact-2-phone", type: 2)]
+    }
+    var contact3 = Contact.unimplemented("contact-3".data(using: .utf8)!)
+    contact3.getIdFromContact.run = { _ in throw GetIdFromContactError() }
+    var contact4 = Contact.unimplemented("contact-4".data(using: .utf8)!)
+    contact4.getIdFromContact.run = { _ in "contact-4-id".data(using: .utf8)! }
+    contact4.getFactsFromContact.run = { _ in throw GetFactsFromContactError() }
+    let contacts = [contact1, contact2, contact3, contact4]
+
+    store.environment.bgQueue = .immediate
+    store.environment.mainQueue = .immediate
+    store.environment.messenger.searchUsers.run = { query in
+      didSearchWithQuery.append(query)
+      return contacts
+    }
+
+    store.send(.set(\.$focusedField, .username)) {
+      $0.focusedField = .username
+    }
+
+    store.send(.set(\.$query.username, "Username")) {
+      $0.query.username = "Username"
+    }
+
+    store.send(.searchTapped) {
+      $0.focusedField = nil
+      $0.isSearching = true
+      $0.results = []
+      $0.failure = nil
+    }
+
+    store.receive(.didSucceed(contacts)) {
+      $0.isSearching = false
+      $0.failure = nil
+      $0.results = [
+        .init(
+          id: "contact-1-id".data(using: .utf8)!,
+          contact: contact1,
+          username: "contact-1-username",
+          email: "contact-1-email",
+          phone: "contact-1-phone"
+        ),
+        .init(
+          id: "contact-2-id".data(using: .utf8)!,
+          contact: contact2,
+          username: "contact-2-username",
+          email: "contact-2-email",
+          phone: "contact-2-phone"
+        ),
+        .init(
+          id: "contact-4-id".data(using: .utf8)!,
+          contact: contact4,
+          username: nil,
+          email: nil,
+          phone: nil
+        )
+      ]
+    }
+  }
+
+  func testSearchFailure() {
+    let store = TestStore(
+      initialState: UserSearchState(),
+      reducer: userSearchReducer,
+      environment: .unimplemented
+    )
+
+    struct Failure: Error {}
+    let failure = Failure()
+
+    store.environment.bgQueue = .immediate
+    store.environment.mainQueue = .immediate
+    store.environment.messenger.searchUsers.run = { _ in throw failure }
+
+    store.send(.searchTapped) {
+      $0.focusedField = nil
+      $0.isSearching = true
+      $0.results = []
+      $0.failure = nil
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.isSearching = false
+      $0.failure = failure.localizedDescription
+      $0.results = []
+    }
+  }
+}