diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 74bda195db87f33a04093b8c46e28554bf7b2b4d..f9ceb75fdf29afbda5362c20139003d77ebb131f 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -141,6 +141,8 @@ let package = Package(
       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
     ),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index 1fc08a8cd576f898f440d6347f2ab3aaf0472b33..5d7777802ed09dcfb2e36b14ba2a5b55f1fe906e 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -47,7 +47,11 @@ extension AppEnvironment {
             )
           },
           userSearch: {
-            UserSearchEnvironment()
+            UserSearchEnvironment(
+              messenger: messenger,
+              mainQueue: mainQueue,
+              bgQueue: bgQueue
+            )
           }
         )
       }
diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
index 43b82dfd4b83c426c15dec01d7ea560e5afc6e1c..86a13c0ec97966619bea881f5ac0b8c9de962fed 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift
@@ -1,20 +1,135 @@
 import ComposableArchitecture
+import Foundation
 import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
 
 public struct UserSearchState: Equatable {
-  public init() {}
+  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 {}
+public enum UserSearchAction: Equatable, BindableAction {
+  case searchTapped
+  case didFail(String)
+  case didSucceed([Contact])
+  case binding(BindingAction<UserSearchState>)
+}
 
 public struct UserSearchEnvironment {
-  public init() {}
+  public init(
+    messenger: Messenger,
+    mainQueue: AnySchedulerOf<DispatchQueue>,
+    bgQueue: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.messenger = messenger
+    self.mainQueue = mainQueue
+    self.bgQueue = bgQueue
+  }
+
+  public var messenger: Messenger
+  public var mainQueue: AnySchedulerOf<DispatchQueue>
+  public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
 
 #if DEBUG
 extension UserSearchEnvironment {
-  public static let unimplemented = UserSearchEnvironment()
+  public static let unimplemented = UserSearchEnvironment(
+    messenger: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented
+  )
 }
 #endif
 
-public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSearchEnvironment>.empty
+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
index 27f94bfa58b0e84836178db110e15c1223dc8a00..266938a29b096d05f695c2f39f55f8c0fb993442 100644
--- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
+++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift
@@ -1,5 +1,6 @@
 import ComposableArchitecture
 import SwiftUI
+import XXMessengerClient
 
 public struct UserSearchView: View {
   public init(store: Store<UserSearchState, UserSearchAction>) {
@@ -7,14 +8,106 @@ public struct UserSearchView: View {
   }
 
   let store: Store<UserSearchState, UserSearchAction>
+  @FocusState var focusedField: UserSearchState.Field?
 
   struct ViewState: Equatable {
-    init(state: UserSearchState) {}
+    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
-      Text("UserSearchView")
+      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")
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
index 11dfb3171209890b634974342348689e82ce339f..8d2f414a0a6c4fa130ea285863ae92ef80f70c21 100644
--- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift
@@ -1,8 +1,119 @@
+import ComposableArchitecture
 import XCTest
+import XXClient
+import XXMessengerClient
 @testable import UserSearchFeature
 
 final class UserSearchFeatureTests: XCTestCase {
-  func testExample() {
-    XCTAssert(true)
+  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 = []
+    }
   }
 }