diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..69208709b4458ff0057e77ddada42bc9d39e7f52
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.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 = "CollectionView"
+               BuildableName = "CollectionView"
+               BlueprintName = "CollectionView"
+               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 = "CollectionViewTests"
+               BuildableName = "CollectionViewTests"
+               BlueprintName = "CollectionViewTests"
+               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 = "CollectionView"
+            BuildableName = "CollectionView"
+            BlueprintName = "CollectionView"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Package.swift b/Package.swift
index 1da9ca4b239ac9755df880b5fd3f60131785fbe5..bcec957c2be3321cb1b9f1bcc1dd176814764b54 100644
--- a/Package.swift
+++ b/Package.swift
@@ -27,6 +27,7 @@ let package = Package(
         .library(name: "Integration", targets: ["Integration"]),
         .library(name: "ChatFeature", targets: ["ChatFeature"]),
         .library(name: "PushFeature", targets: ["PushFeature"]),
+        .library(name: "SFTPFeature", targets: ["SFTPFeature"]),
         .library(name: "CrashService", targets: ["CrashService"]),
         .library(name: "Presentation", targets: ["Presentation"]),
         .library(name: "BackupFeature", targets: ["BackupFeature"]),
@@ -34,6 +35,7 @@ let package = Package(
         .library(name: "iCloudFeature", targets: ["iCloudFeature"]),
         .library(name: "SearchFeature", targets: ["SearchFeature"]),
         .library(name: "DrawerFeature", targets: ["DrawerFeature"]),
+        .library(name: "CollectionView", targets: ["CollectionView"]),
         .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
         .library(name: "CrashReporting", targets: ["CrashReporting"]),
         .library(name: "ProfileFeature", targets: ["ProfileFeature"]),
@@ -66,9 +68,12 @@ let package = Package(
         .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.0"),
         .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.1"),
         .package(url: "https://github.com/google/google-api-objectivec-client-for-rest", from: "1.6.0"),
-        .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.6")),
+        .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.8")),
         .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "8.10.0")),
-        .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0"))
+        .package(url: "https://github.com/darrarski/Shout.git", revision: "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0"),
+        .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0")),
+        .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", .upToNextMajor(from: "0.5.0")),
+        .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", .upToNextMajor(from: "0.3.3")),
     ],
     targets: [
         .target(
@@ -81,6 +86,7 @@ let package = Package(
                 "ChatFeature",
                 "MenuFeature",
                 "PushFeature",
+                "SFTPFeature",
                 "ToastFeature",
                 "CrashService",
                 "BackupFeature",
@@ -208,6 +214,25 @@ let package = Package(
                 ]
             ),
 
+        // MARK: - SFTPFeature
+
+            .target(
+                name: "SFTPFeature",
+                dependencies: [
+                    "HUD",
+                    "Models",
+                    "Shared",
+                    "Keychain",
+                    "InputField",
+                    "Presentation",
+                    "DependencyInjection",
+                    .product(
+                        name: "Shout",
+                        package: "Shout"
+                    )
+                ]
+            ),
+
         // MARK: - GoogleDriveFeature
 
             .target(
@@ -398,6 +423,7 @@ let package = Package(
                 dependencies: [
                     "HUD",
                     "Shared",
+                    "SFTPFeature",
                     "Integration",
                     "Presentation",
                     "iCloudFeature",
@@ -612,6 +638,7 @@ let package = Package(
                     "Shared",
                     "Models",
                     "InputField",
+                    "SFTPFeature",
                     "Presentation",
                     "iCloudFeature",
                     "DrawerFeature",
@@ -835,6 +862,23 @@ let package = Package(
                     .product(name: "Quick", package: "Quick"),
                     .product(name: "Nimble", package: "Nimble")
                 ]
-            )
+            ),
+
+        // MARK: - CollectionView
+
+            .target(
+                name: "CollectionView",
+                dependencies: [
+                    .product(name: "ChatLayout", package: "ChatLayout"),
+                    .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
+                ]
+            ),
+            .testTarget(
+                name: "CollectionViewTests",
+                dependencies: [
+                    .target(name: "CollectionView"),
+                    .product(name: "CustomDump", package: "swift-custom-dump"),
+                ]
+            ),
     ]
 )
diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift
index 3675b45050f041af457b16d9bc6105633dabab91..72a7f682477967599a823601b8315c46ed429ee6 100644
--- a/Sources/App/AppDelegate.swift
+++ b/Sources/App/AppDelegate.swift
@@ -1,8 +1,8 @@
 import UIKit
 import BackgroundTasks
 
-import XXModels
 import Theme
+import XXModels
 import XXLogger
 import Defaults
 import Integration
diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift
index 9102274af7c0c37ce986b8aba6b2607a33cd66ca..14a7b7bdb482974cff2b5b2be573d3f5fb1a33fc 100644
--- a/Sources/App/DependencyRegistrator.swift
+++ b/Sources/App/DependencyRegistrator.swift
@@ -18,6 +18,7 @@ import Voxophone
 import Integration
 import Permissions
 import PushFeature
+import SFTPFeature
 import CrashService
 import ToastFeature
 import iCloudFeature
@@ -63,6 +64,7 @@ struct DependencyRegistrator {
 
         /// Restore / Backup
 
+        container.register(SFTPService.mock)
         container.register(iCloudServiceMock() as iCloudInterface)
         container.register(DropboxServiceMock() as DropboxInterface)
         container.register(GoogleDriveServiceMock() as GoogleDriveInterface)
@@ -86,6 +88,7 @@ struct DependencyRegistrator {
 
         /// Restore / Backup
 
+        container.register(SFTPService.live)
         container.register(iCloudService() as iCloudInterface)
         container.register(DropboxService() as DropboxInterface)
         container.register(GoogleDriveService() as GoogleDriveInterface)
@@ -101,7 +104,7 @@ struct DependencyRegistrator {
 
         // MARK: Isolated
 
-        container.register(HUD() as HUDType)
+        container.register(HUD())
         container.register(ThemeController() as ThemeControlling)
         container.register(ToastController())
         container.register(StatusBarController() as StatusBarStyleControlling)
@@ -134,6 +137,8 @@ struct DependencyRegistrator {
 
         container.register(
             SearchCoordinator(
+                contactsFactory: ContactListController.init,
+                requestsFactory: RequestsContainerController.init,
                 contactFactory: ContactController.init(_:),
                 countriesFactory: CountryListController.init(_:)
             ) as SearchCoordinating)
@@ -185,7 +190,7 @@ struct DependencyRegistrator {
 
         container.register(
             RequestsCoordinator(
-                searchFactory: SearchController.init,
+                searchFactory: SearchContainerController.init,
                 contactFactory: ContactController.init(_:),
                 singleChatFactory: SingleChatController.init(_:),
                 groupChatFactory: GroupChatController.init(_:),
@@ -197,7 +202,7 @@ struct DependencyRegistrator {
             OnboardingCoordinator(
                 emailFactory: OnboardingEmailController.init,
                 phoneFactory: OnboardingPhoneController.init,
-                searchFactory: SearchController.init,
+                searchFactory: SearchContainerController.init,
                 welcomeFactory: OnboardingWelcomeController.init,
                 chatListFactory: ChatListController.init,
                 usernameFactory: OnboardingUsernameController.init(_:),
@@ -211,7 +216,7 @@ struct DependencyRegistrator {
         container.register(
             ContactListCoordinator(
                 scanFactory: ScanContainerController.init,
-                searchFactory: SearchController.init,
+                searchFactory: SearchContainerController.init,
                 newGroupFactory: CreateGroupController.init,
                 requestsFactory: RequestsContainerController.init,
                 contactFactory: ContactController.init(_:),
@@ -235,7 +240,7 @@ struct DependencyRegistrator {
         container.register(
             ChatListCoordinator(
                 scanFactory: ScanContainerController.init,
-                searchFactory: SearchController.init,
+                searchFactory: SearchContainerController.init,
                 newGroupFactory: CreateGroupController.init,
                 contactsFactory: ContactListController.init,
                 contactFactory: ContactController.init(_:),
diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift
index c61bd74d6c971c544087d7020db72ce16d11e763..a901e6b9668fc89df2cd1ae1184ca655fa8bdb1b 100644
--- a/Sources/BackupFeature/Controllers/BackupConfigController.swift
+++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift
@@ -105,6 +105,11 @@ final class BackupConfigController: UIViewController {
             .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) }
             .store(in: &cancellables)
 
+        screenView.sftpButton.switcherView
+            .publisher(for: .valueChanged)
+            .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) }
+            .store(in: &cancellables)
+
         screenView.iCloudButton.switcherView
             .publisher(for: .valueChanged)
             .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) }
@@ -115,6 +120,11 @@ final class BackupConfigController: UIViewController {
             .sink { [unowned self] in viewModel.didTapService(.dropbox, self) }
             .store(in: &cancellables)
 
+        screenView.sftpButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in viewModel.didTapService(.sftp, self) }
+            .store(in: &cancellables)
+
         screenView.iCloudButton
             .publisher(for: .touchUpInside)
             .sink { [unowned self] in viewModel.didTapService(.icloud, self) }
@@ -128,16 +138,17 @@ final class BackupConfigController: UIViewController {
         case .none:
             break
         case .icloud:
-            serviceName = "iCloud"
+            serviceName = Localized.Backup.iCloud
             button = screenView.iCloudButton
-
         case .dropbox:
-            serviceName = "Dropbox"
+            serviceName = Localized.Backup.dropbox
             button = screenView.dropboxButton
-
         case .drive:
-            serviceName = "Google Drive"
+            serviceName = Localized.Backup.googleDrive
             button = screenView.googleDriveButton
+        case .sftp:
+            serviceName = Localized.Backup.sftp
+            button = screenView.sftpButton
         }
 
         screenView.enabledSubtitleLabel.text
@@ -146,10 +157,12 @@ final class BackupConfigController: UIViewController {
         = Localized.Backup.Config.frequency(serviceName).uppercased()
 
         guard let button = button else {
+            screenView.sftpButton.isHidden = false
             screenView.iCloudButton.isHidden = false
             screenView.dropboxButton.isHidden = false
             screenView.googleDriveButton.isHidden = false
 
+            screenView.sftpButton.switcherView.isOn = false
             screenView.iCloudButton.switcherView.isOn = false
             screenView.dropboxButton.switcherView.isOn = false
             screenView.googleDriveButton.switcherView.isOn = false
@@ -166,11 +179,13 @@ final class BackupConfigController: UIViewController {
         screenView.latestBackupDetailView.isHidden = false
         screenView.infrastructureDetailView.isHidden = false
 
-        [screenView.iCloudButton, screenView.dropboxButton, screenView.googleDriveButton]
-            .forEach {
-                $0.isHidden = $0 != button
-                $0.switcherView.isOn = $0 == button
-            }
+        [screenView.iCloudButton,
+         screenView.dropboxButton,
+         screenView.googleDriveButton,
+         screenView.sftpButton].forEach {
+            $0.isHidden = $0 != button
+            $0.switcherView.isOn = $0 == button
+        }
     }
 
     private func decorate(connectedServices: Set<CloudService>) {
@@ -191,6 +206,12 @@ final class BackupConfigController: UIViewController {
         } else {
             screenView.googleDriveButton.showChevron()
         }
+
+        if connectedServices.contains(.sftp) {
+            screenView.sftpButton.showSwitcher(enabled: false)
+        } else {
+            screenView.sftpButton.showChevron()
+        }
     }
 
     private func presentInfrastructureDrawer(wifiOnly: Bool) {
diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift
index a01ff0caa91be5ffdfa55e6fe9fe8ea7ebab41bf..a9942d8c03466fe8b620d156c9eb1cc02f2ef70a 100644
--- a/Sources/BackupFeature/Controllers/BackupController.swift
+++ b/Sources/BackupFeature/Controllers/BackupController.swift
@@ -6,7 +6,7 @@ import Combine
 import DependencyInjection
 
 public final class BackupController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency var hud: HUD
 
     private let viewModel = BackupViewModel.live()
     private var cancellables = Set<AnyCancellable>()
@@ -14,7 +14,7 @@ public final class BackupController: UIViewController {
     public override func viewDidLoad() {
         super.viewDidLoad()
         view.backgroundColor = Asset.neutralWhite.color
-        hud.update(with: .on(nil))
+        hud.update(with: .on)
 
         setupNavigationBar()
         setupBindings()
diff --git a/Sources/BackupFeature/Controllers/BackupSetupController.swift b/Sources/BackupFeature/Controllers/BackupSetupController.swift
index 49e05a2bd74ce9141e19320cfebbc33f989414f7..e22de517680d7ab5fc348b56cbb396ca80c4e091 100644
--- a/Sources/BackupFeature/Controllers/BackupSetupController.swift
+++ b/Sources/BackupFeature/Controllers/BackupSetupController.swift
@@ -37,5 +37,10 @@ final class BackupSetupController: UIViewController {
             .publisher(for: .touchUpInside)
             .sink { [unowned self] in viewModel.didTapService(.icloud, self) }
             .store(in: &cancellables)
+
+        screenView.sftpButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in viewModel.didTapService(.sftp, self) }
+            .store(in: &cancellables)
     }
 }
diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift
index 9dbd110e659f066dd25d5bda897ebccc14047748..a49b51bd28fe3a670efeba3c2c9cff8e34e31af6 100644
--- a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift
+++ b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift
@@ -18,16 +18,10 @@ public protocol BackupCoordinating {
 public struct BackupCoordinator: BackupCoordinating {
     var bottomPresenter: Presenting = BottomPresenter()
 
-    var passphraseFactory: (
-        @escaping EmptyClosure,
-        @escaping StringClosure
-    ) -> UIViewController
+    var passphraseFactory: (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
 
     public init(
-        passphraseFactory: @escaping (
-            @escaping EmptyClosure,
-            @escaping StringClosure
-        ) -> UIViewController
+        passphraseFactory: @escaping (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
     ) {
         self.passphraseFactory = passphraseFactory
     }
diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift
index 8b82a8f27c40af85acd836182144d05c945008e1..626adba515d28c1a9969b067da742d07e94c8da5 100644
--- a/Sources/BackupFeature/Service/BackupService.swift
+++ b/Sources/BackupFeature/Service/BackupService.swift
@@ -2,6 +2,8 @@ import UIKit
 import Models
 import Combine
 import Defaults
+import Keychain
+import SFTPFeature
 import iCloudFeature
 import DropboxFeature
 import NetworkMonitor
@@ -9,10 +11,12 @@ import GoogleDriveFeature
 import DependencyInjection
 
 public final class BackupService {
+    @Dependency private var sftpService: SFTPService
     @Dependency private var icloudService: iCloudInterface
     @Dependency private var dropboxService: DropboxInterface
-    @Dependency private var driveService: GoogleDriveInterface
     @Dependency private var networkManager: NetworkMonitoring
+    @Dependency private var keychainHandler: KeychainHandling
+    @Dependency private var driveService: GoogleDriveInterface
 
     @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data
 
@@ -149,6 +153,15 @@ extension BackupService {
                         self.refreshBackups()
                     }.store(in: &cancellables)
             }
+        case .sftp:
+            if !sftpService.isAuthorized() {
+                sftpService.authorizeFlow((screen, { [weak self] in
+                    guard let self = self else { return }
+                    screen.navigationController?.popViewController(animated: true)
+                    self.refreshConnections()
+                    self.refreshBackups()
+                }))
+            }
         }
     }
 }
@@ -167,6 +180,12 @@ extension BackupService {
             settings.value.connectedServices.remove(.dropbox)
         }
 
+        if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) {
+            settings.value.connectedServices.insert(.sftp)
+        } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) {
+            settings.value.connectedServices.remove(.sftp)
+        }
+
         driveService.isAuthorized { [weak settings] isAuthorized in
             guard let settings = settings else { return }
 
@@ -196,6 +215,23 @@ extension BackupService {
             }
         }
 
+        if sftpService.isAuthorized() {
+            sftpService.fetchMetadata { [weak settings] in
+                guard let settings = settings else { return }
+
+                guard let metadata = try? $0.get()?.backup else {
+                    settings.value.backups[.sftp] = nil
+                    return
+                }
+
+                settings.value.backups[.sftp] = Backup(
+                    id: metadata.id,
+                    date: metadata.date,
+                    size: metadata.size
+                )
+            }
+        }
+
         if dropboxService.isAuthorized() {
             dropboxService.downloadMetadata { [weak settings] in
                 guard let settings = settings else { return }
@@ -241,7 +277,7 @@ extension BackupService {
             .appendingPathComponent(UUID().uuidString)
 
         do {
-            try data.write(to: url)
+            try data.write(to: url, options: .atomic)
         } catch {
             print("Couldn't write to temp: \(error.localizedDescription)")
             return
@@ -260,8 +296,6 @@ extension BackupService {
                 case .failure(let error):
                     print(error.localizedDescription)
                 }
-
-                // try? FileManager.default.removeItem(at: url)
             }
         case .icloud:
             icloudService.uploadBackup(url) {
@@ -275,8 +309,6 @@ extension BackupService {
                 case .failure(let error):
                     print(error.localizedDescription)
                 }
-
-                // try? FileManager.default.removeItem(at: url)
             }
         case .dropbox:
             dropboxService.uploadBackup(url) {
@@ -290,8 +322,15 @@ extension BackupService {
                 case .failure(let error):
                     print(error.localizedDescription)
                 }
-
-                // try? FileManager.default.removeItem(at: url)
+            }
+        case .sftp:
+            sftpService.uploadBackup(url: url) {
+                switch $0 {
+                case .success(let backup):
+                    self.settings.value.backups[.sftp] = backup
+                case .failure(let error):
+                    print(error.localizedDescription)
+                }
             }
         }
     }
diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift
index f136ca8c15c83578636ed3ef00643817d73b8de9..0731bc4a94f5be17d6e224d5f67573ea68ea608f 100644
--- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift
+++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift
@@ -30,7 +30,7 @@ struct BackupConfigViewModel {
 extension BackupConfigViewModel {
     static func live() -> Self {
         class Context {
-            @Dependency var hud: HUDType
+            @Dependency var hud: HUD
             @Dependency var service: BackupService
             @Dependency var coordinator: BackupCoordinating
         }
@@ -40,7 +40,7 @@ extension BackupConfigViewModel {
         return .init(
             didTapBackupNow: {
                 context.service.performBackup()
-                context.hud.update(with: .on(nil))
+                context.hud.update(with: .on)
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                     context.hud.update(with: .none)
                 }
@@ -57,7 +57,7 @@ extension BackupConfigViewModel {
                     context.service.toggle(service: service, enabling: false)
                 }, passphraseClosure: { passphrase in
                     context.service.passphrase = passphrase
-                    context.hud.update(with: .on("Initializing and securing your backup file will take few seconds, please keep the app open."))
+                    context.hud.update(with: .onTitle("Initializing and securing your backup file will take few seconds, please keep the app open."))
                     DispatchQueue.global().async {
                         context.service.toggle(service: service, enabling: enabling)
 
diff --git a/Sources/BackupFeature/Views/BackupConfigView.swift b/Sources/BackupFeature/Views/BackupConfigView.swift
index 8c400b3ff9162f380548d0478f2356454910833e..1c65f58f7222f1c069284c717518904dc2e596ca 100644
--- a/Sources/BackupFeature/Views/BackupConfigView.swift
+++ b/Sources/BackupFeature/Views/BackupConfigView.swift
@@ -7,6 +7,7 @@ final class BackupConfigView: UIView {
     let actionView = BackupActionView()
 
     let stackView = UIStackView()
+    let sftpButton = BackupSwitcherButton()
     let iCloudButton = BackupSwitcherButton()
     let dropboxButton = BackupSwitcherButton()
     let googleDriveButton = BackupSwitcherButton()
@@ -44,6 +45,9 @@ final class BackupConfigView: UIView {
         subtitleLabel.numberOfLines = 0
         subtitleLabel.attributedText = attString
 
+        sftpButton.titleLabel.text = Localized.Backup.sftp
+        sftpButton.logoImageView.image = Asset.restoreSFTP.image
+
         iCloudButton.titleLabel.text = Localized.Backup.iCloud
         iCloudButton.logoImageView.image = Asset.restoreIcloud.image
 
@@ -65,6 +69,7 @@ final class BackupConfigView: UIView {
         stackView.addArrangedSubview(googleDriveButton)
         stackView.addArrangedSubview(iCloudButton)
         stackView.addArrangedSubview(dropboxButton)
+        stackView.addArrangedSubview(sftpButton)
         stackView.addArrangedSubview(enabledSubtitleView)
         stackView.addArrangedSubview(latestBackupDetailView)
         stackView.addArrangedSubview(frequencyDetailView)
@@ -75,36 +80,36 @@ final class BackupConfigView: UIView {
         addSubview(actionView)
         addSubview(stackView)
 
-        titleLabel.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(15)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        titleLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(15)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        enabledSubtitleLabel.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(-10)
-            make.left.equalToSuperview().offset(92)
-            make.right.equalToSuperview().offset(-48)
-            make.bottom.equalToSuperview()
+        enabledSubtitleLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(-10)
+            $0.left.equalToSuperview().offset(92)
+            $0.right.equalToSuperview().offset(-48)
+            $0.bottom.equalToSuperview()
         }
 
-        subtitleLabel.snp.makeConstraints { make in
-            make.top.equalTo(titleLabel.snp.bottom).offset(8)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        subtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(8)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        actionView.snp.makeConstraints { make in
-            make.top.equalTo(subtitleLabel.snp.bottom).offset(15)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-38)
+        actionView.snp.makeConstraints {
+            $0.top.equalTo(subtitleLabel.snp.bottom).offset(15)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-38)
         }
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalTo(actionView.snp.bottom).offset(28)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.lessThanOrEqualToSuperview()
+        stackView.snp.makeConstraints {
+            $0.top.equalTo(actionView.snp.bottom).offset(28)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.lessThanOrEqualToSuperview()
         }
     }
 
diff --git a/Sources/BackupFeature/Views/BackupSetupView.swift b/Sources/BackupFeature/Views/BackupSetupView.swift
index eeaad48195da765ac9833293ccd7f7f9a7673ca0..3e19d50034f4d03ba62d3e4d038d82d24196af89 100644
--- a/Sources/BackupFeature/Views/BackupSetupView.swift
+++ b/Sources/BackupFeature/Views/BackupSetupView.swift
@@ -6,6 +6,7 @@ final class BackupSetupView: UIView {
     let subtitleLabel = UILabel()
 
     let stackView = UIStackView()
+    let sftpButton = BackupSwitcherButton()
     let iCloudButton = BackupSwitcherButton()
     let dropboxButton = BackupSwitcherButton()
     let googleDriveButton = BackupSwitcherButton()
@@ -60,32 +61,37 @@ final class BackupSetupView: UIView {
         googleDriveButton.logoImageView.image = Asset.restoreDrive.image
         googleDriveButton.showChevron()
 
+        sftpButton.titleLabel.text = Localized.Backup.sftp
+        sftpButton.logoImageView.image = Asset.restoreSFTP.image
+        sftpButton.showChevron()
+
         stackView.axis = .vertical
         stackView.addArrangedSubview(googleDriveButton)
         stackView.addArrangedSubview(iCloudButton)
         stackView.addArrangedSubview(dropboxButton)
+        stackView.addArrangedSubview(sftpButton)
 
         addSubview(titleLabel)
         addSubview(subtitleLabel)
         addSubview(stackView)
 
-        titleLabel.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(15)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        titleLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(15)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        subtitleLabel.snp.makeConstraints { make in
-            make.top.equalTo(titleLabel.snp.bottom).offset(8)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        subtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(8)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalTo(subtitleLabel.snp.bottom).offset(28)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.lessThanOrEqualToSuperview()
+        stackView.snp.makeConstraints {
+            $0.top.equalTo(subtitleLabel.snp.bottom).offset(28)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.lessThanOrEqualToSuperview()
         }
     }
 
diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift
index b35d314725ea97149e84edb8b9525e1fc010d4f6..f34be438f7df3afd84d5ba7c6db04612bc891cc6 100644
--- a/Sources/ChatFeature/Controllers/SingleChatController.swift
+++ b/Sources/ChatFeature/Controllers/SingleChatController.swift
@@ -24,7 +24,7 @@ extension Message: Differentiable {
 }
 
 public final class SingleChatController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var logger: XXLogger
     @Dependency private var voxophone: Voxophone
     @Dependency private var coordinator: ChatCoordinating
diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
index cd78765910d84d7242ee96ff145c3cd612c2492d..f51a893610402c317f938f0b29445317de9bb6d4 100644
--- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
+++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
@@ -107,7 +107,7 @@ final class SingleChatViewModel {
 
     func didSend(image: UIImage) {
         guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return }
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         session.send(imageData: imageData, to: contact) { [weak self] in
             switch $0 {
diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
index 98febff09e352ff1e0ff495c032f1e7d1ec1613a..f481b96fb7e3e14b74096919090bf1df8c96c840 100644
--- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
+++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
@@ -147,7 +147,7 @@ final class ChatListViewModel {
     }
 
     func leave(_ group: Group) {
-        hudSubject.send(.on(nil))
+        hudSubject.send(.on)
 
         do {
             try session.leave(group: group)
diff --git a/Sources/ChatListFeature/Views/ChatListView.swift b/Sources/ChatListFeature/Views/ChatListView.swift
index c7303c48d04db46983e2985d40745ce236d72c43..03a407980700123d9843d9239e4822b9247dac63 100644
--- a/Sources/ChatListFeature/Views/ChatListView.swift
+++ b/Sources/ChatListFeature/Views/ChatListView.swift
@@ -14,7 +14,7 @@ final class ChatListView: UIView {
         backgroundColor = Asset.neutralWhite.color
         listContainerView.backgroundColor = Asset.neutralWhite.color
         searchListContainerView.backgroundColor = Asset.neutralWhite.color
-        searchView.update(placeholder: "Search chats")
+        searchView.update(placeholder: Localized.ChatList.Search.title)
 
         addSubview(snackBar)
         addSubview(searchView)
@@ -22,6 +22,34 @@ final class ChatListView: UIView {
         containerView.addSubview(searchListContainerView)
         containerView.addSubview(listContainerView)
 
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func showConnectingBanner(_ show: Bool) {
+        if show == true {
+            snackBar.alpha = 0.0
+            snackBar.snp.updateConstraints {
+                $0.bottom
+                    .equalTo(snp.top)
+                    .offset(snackBar.bounds.height)
+            }
+        } else {
+            snackBar.alpha = 1.0
+            snackBar.snp.updateConstraints {
+                $0.bottom.equalTo(snp.top)
+            }
+        }
+
+        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
+            self.setNeedsLayout()
+            self.layoutIfNeeded()
+            self.snackBar.alpha = show ? 1.0 : 0.0
+        }
+    }
+
+    private func setupConstraints() {
         snackBar.snp.makeConstraints {
             $0.left.equalToSuperview()
             $0.right.equalToSuperview()
@@ -49,28 +77,4 @@ final class ChatListView: UIView {
             $0.edges.equalToSuperview()
         }
     }
-
-    required init?(coder: NSCoder) { nil }
-
-    func showConnectingBanner(_ show: Bool) {
-        if show == true {
-            snackBar.alpha = 0.0
-            snackBar.snp.updateConstraints {
-                $0.bottom
-                    .equalTo(snp.top)
-                    .offset(snackBar.bounds.height)
-            }
-        } else {
-            snackBar.alpha = 1.0
-            snackBar.snp.updateConstraints {
-                $0.bottom.equalTo(snp.top)
-            }
-        }
-
-        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
-            self.setNeedsLayout()
-            self.layoutIfNeeded()
-            self.snackBar.alpha = show ? 1.0 : 0.0
-        }
-    }
 }
diff --git a/Sources/CollectionView/CellFactory.swift b/Sources/CollectionView/CellFactory.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bc7bc9bec131565d07dc5805865bdcc13e0959dc
--- /dev/null
+++ b/Sources/CollectionView/CellFactory.swift
@@ -0,0 +1,76 @@
+import UIKit
+import XCTestDynamicOverlay
+
+public struct CellFactory<Model> {
+  public struct Registrar {
+    public init(register: @escaping (UICollectionView) -> Void) {
+      self.register = register
+    }
+
+    public var register: (UICollectionView) -> Void
+
+    public func callAsFunction(in view: UICollectionView) {
+      register(view)
+    }
+  }
+
+  public struct Builder {
+    public init(build: @escaping (Model, UICollectionView, IndexPath) -> UICollectionViewCell?) {
+      self.build = build
+    }
+
+    public var build: (Model, UICollectionView, IndexPath) -> UICollectionViewCell?
+
+    public func callAsFunction(
+      for model: Model,
+      in view: UICollectionView,
+      at indexPath: IndexPath
+    ) -> UICollectionViewCell? {
+      build(model, view, indexPath)
+    }
+  }
+
+  public init(
+    register: Registrar,
+    build: Builder
+  ) {
+    self.register = register
+    self.build = build
+  }
+
+  public var register: Registrar
+  public var build: Builder
+}
+
+extension CellFactory {
+  public static func combined(_ factories: CellFactory...) -> CellFactory {
+    combined(factories)
+  }
+
+  public static func combined(_ factories: [CellFactory]) -> CellFactory {
+    CellFactory(
+      register: .init { collectionView in
+        factories.forEach { $0.register(in: collectionView) }
+      },
+      build: .init { model, collectionView, indexPath in
+        for factory in factories {
+          if let cell = factory.build(for: model, in: collectionView, at: indexPath) {
+            return cell
+          }
+        }
+        return nil
+      }
+    )
+  }
+}
+
+#if DEBUG
+extension CellFactory {
+  public static func unimplemented() -> CellFactory {
+    CellFactory(
+      register: .init(register: XCTUnimplemented("\(Self.self).Registrar")),
+      build: .init(build: XCTUnimplemented("\(Self.self).Builder"))
+    )
+  }
+}
+#endif
diff --git a/Sources/CollectionView/ViewConfigurator.swift b/Sources/CollectionView/ViewConfigurator.swift
new file mode 100644
index 0000000000000000000000000000000000000000..631f9e8d216b0a4ea6da2ce40db4fe48d7f103ef
--- /dev/null
+++ b/Sources/CollectionView/ViewConfigurator.swift
@@ -0,0 +1,22 @@
+import UIKit
+import XCTestDynamicOverlay
+
+public struct ViewConfigurator<View: UIView, Model> {
+  public init(configure: @escaping (View, Model) -> Void) {
+    self.configure = configure
+  }
+
+  public var configure: (View, Model) -> Void
+
+  public func callAsFunction(_ view: View, with model: Model) {
+    configure(view, model)
+  }
+}
+
+#if DEBUG
+extension ViewConfigurator {
+  public static func unimplemented() -> ViewConfigurator {
+    ViewConfigurator(configure: XCTUnimplemented("\(Self.self)"))
+  }
+}
+#endif
diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift
index cf64268edbdd1cad2986105c38d72a74b2273499..02859bab60bf861a5615795a016352a4f4509900 100644
--- a/Sources/ContactFeature/Controllers/ContactController.swift
+++ b/Sources/ContactFeature/Controllers/ContactController.swift
@@ -10,7 +10,7 @@ import DependencyInjection
 import ScrollViewController
 
 public final class ContactController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: ContactCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift
index 67005d4b9d01cbc493ead72d8f6b89e42e64535e..5e6e06698cbfeb359a79ce231dde08f37c03ee6a 100644
--- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift
+++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift
@@ -62,7 +62,7 @@ final class ContactViewModel {
     }
 
     func didTapDelete() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         do {
             try session.deleteContact(contact)
@@ -91,7 +91,7 @@ final class ContactViewModel {
     }
 
     func didTapResend() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
@@ -107,7 +107,7 @@ final class ContactViewModel {
     }
 
     func didTapRequest(with nickname: String) {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
         contact.nickname = nickname
 
         backgroundScheduler.schedule { [weak self] in
@@ -124,7 +124,7 @@ final class ContactViewModel {
     }
 
     func didTapAccept(_ nickname: String) {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
         contact.nickname = nickname
 
         backgroundScheduler.schedule { [weak self] in
diff --git a/Sources/ContactListFeature/Controllers/ContactListTableController.swift b/Sources/ContactListFeature/Controllers/ContactListTableController.swift
index f940366ae08cfe77f9cd54e5d1008a55b7149bc6..ac7a06628045f199393f1839e2a6f700a7cb194e 100644
--- a/Sources/ContactListFeature/Controllers/ContactListTableController.swift
+++ b/Sources/ContactListFeature/Controllers/ContactListTableController.swift
@@ -30,7 +30,7 @@ final class ContactListTableController: UITableViewController {
 
     private func setupTableView() {
         tableView.separatorStyle = .none
-        tableView.register(SmallAvatarAndTitleCell.self)
+        tableView.register(AvatarCell.self)
         tableView.backgroundColor = Asset.neutralWhite.color
         tableView.sectionIndexColor = Asset.neutralDark.color
         tableView.contentInset = UIEdgeInsets(top: -20, left: 0, bottom: 0, right: 0)
@@ -45,11 +45,11 @@ final class ContactListTableController: UITableViewController {
     }
 
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
+        let cell: AvatarCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
         let contact = sections[indexPath.section][indexPath.row]
         let name = (contact.nickname ?? contact.username) ?? "Fetching username..."
-        cell.titleLabel.text = name
-        cell.avatarView.setupProfile(title: name, image: contact.photo, size: .medium)
+
+        cell.setup(title: name, image: contact.photo)
         return cell
     }
 
diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift
index 9e9d039dd5e5fe2dd4adfa179fc736ab51fa6c4b..a9559f5c3c38ccdf7a90543a9963748155e6fa6b 100644
--- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift
+++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift
@@ -7,7 +7,7 @@ import XXModels
 import DependencyInjection
 
 public final class CreateGroupController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: ContactListCoordinating
 
     lazy private var titleLabel = UILabel()
@@ -66,7 +66,7 @@ public final class CreateGroupController: UIViewController {
 
     private func setupTableAndCollection() {
         screenView.tableView.rowHeight = 64.0
-        screenView.tableView.register(SmallAvatarAndTitleCell.self)
+        screenView.tableView.register(AvatarCell.self)
         screenView.collectionView.register(CreateGroupCollectionCell.self)
 
         collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>(
@@ -84,10 +84,10 @@ public final class CreateGroupController: UIViewController {
         tableDataSource = DiffEditableDataSource<SectionId, Contact>(
             tableView: screenView.tableView
         ) { [weak self] tableView, indexPath, contact in
-            let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self)
+            let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self)
             let title = (contact.nickname ?? contact.username) ?? ""
-            cell.titleLabel.text = title
-            cell.avatarView.setupProfile(title: title, image: contact.photo, size: .medium)
+
+            cell.setup(title: title, image: contact.photo)
 
             if let selectedElements = self?.selectedElements, selectedElements.contains(contact) {
                 tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift
index 67e70645ed6c0d00f02862bdb02f199d3d735d9d..a8f94de8042f3cd7f7d0da14a9de703f217f573b 100644
--- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift
+++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift
@@ -76,7 +76,7 @@ final class CreateGroupViewModel {
     }
 
     func create(name: String, welcome: String?, members: [Contact]) {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         session.createGroup(name: name, welcome: welcome, members: members) { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/HUD/HUD.swift b/Sources/HUD/HUD.swift
index 9f45b18078d75a2d11654f99a9bd2d1b2746f4ee..207f3473c9173a3a64c8636a8373d734d207807b 100644
--- a/Sources/HUD/HUD.swift
+++ b/Sources/HUD/HUD.swift
@@ -10,15 +10,17 @@ private enum Constants {
 }
 
 public enum HUDStatus: Equatable {
-    case on(String?)
     case none
+    case on
+    case onTitle(String)
+    case onAction(String)
     case error(HUDError)
 
     var isPresented: Bool {
         switch self {
         case .none:
             return false
-        case .on, .error:
+        case .on, .error, .onTitle, .onAction:
             return true
         }
     }
@@ -50,15 +52,12 @@ public struct HUDError: Equatable {
     }
 }
 
-public protocol HUDType {
-    func update(with status: HUDStatus)
-}
-
-public final class HUD: HUDType {
+public final class HUD {
     private(set) var window: UIWindow?
     private(set) var errorView: ErrorView?
     private(set) var titleLabel: UILabel?
     private(set) var animation: DotAnimation?
+    public var actionButton: CapsuleButton?
     private var cancellables = Set<AnyCancellable>()
 
     private var status: HUDStatus = .none {
@@ -67,16 +66,23 @@ public final class HUD: HUDType {
                 self.errorView = nil
                 self.animation = nil
                 self.window = nil
+                self.actionButton = nil
                 self.titleLabel = nil
 
                 switch status {
-                case .on(let text):
+                case .on:
+                    animation = DotAnimation()
+
+                case .onTitle(let text):
                     animation = DotAnimation()
+                    titleLabel = UILabel()
+                    titleLabel!.text = text
+
+                case .onAction(let title):
+                    animation = DotAnimation()
+                    actionButton = CapsuleButton()
+                    actionButton!.set(style: .seeThroughWhite, title: title)
 
-                    if let text = text {
-                        titleLabel = UILabel()
-                        titleLabel!.text = text
-                    }
                 case .error(let error):
                     errorView = ErrorView(with: error)
                 case .none:
@@ -88,13 +94,19 @@ public final class HUD: HUDType {
 
             if oldValue.isPresented == false && status.isPresented == true {
                 switch status {
-                case .on(let text):
+                case .on:
                     animation = DotAnimation()
 
-                    if let text = text {
-                        titleLabel = UILabel()
-                        titleLabel!.text = text
-                    }
+                case .onTitle(let text):
+                    animation = DotAnimation()
+                    titleLabel = UILabel()
+                    titleLabel!.text = text
+
+                case .onAction(let title):
+                    animation = DotAnimation()
+                    actionButton = CapsuleButton()
+                    actionButton!.set(style: .seeThroughWhite, title: title)
+
                 case .error(let error):
                     errorView = ErrorView(with: error)
                 case .none:
@@ -118,7 +130,7 @@ public final class HUD: HUDType {
 
     private func showWindow() {
         window = Window()
-        window?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
+        window?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
         window?.rootViewController = StatusBarViewController(nil)
 
         if let animation = animation {
@@ -138,6 +150,15 @@ public final class HUD: HUDType {
             }
         }
 
+        if let actionButton = actionButton {
+            window?.addSubview(actionButton)
+            actionButton.snp.makeConstraints {
+                $0.left.equalToSuperview().offset(18)
+                $0.right.equalToSuperview().offset(-18)
+                $0.bottom.equalToSuperview().offset(-50)
+            }
+        }
+
         if let errorView = errorView {
             window?.addSubview(errorView)
             errorView.snp.makeConstraints { make in
@@ -166,6 +187,7 @@ public final class HUD: HUDType {
             self.cancellables.removeAll()
             self.errorView = nil
             self.animation = nil
+            self.actionButton = nil
             self.titleLabel = nil
             self.window = nil
         }
diff --git a/Sources/InputField/OutlinedInputField.swift b/Sources/InputField/OutlinedInputField.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3bb8ad3005167b9e9e99511243da912f7082d434
--- /dev/null
+++ b/Sources/InputField/OutlinedInputField.swift
@@ -0,0 +1,85 @@
+import UIKit
+import Shared
+import Combine
+
+public final class OutlinedInputField: UIView {
+    private let stackView = UIStackView()
+    private let textField = UITextField()
+    private let placeholderLabel = UILabel()
+    private let inputContainerView = UIView()
+
+    private let secureInputButton = SecureInputButton()
+
+    public var textPublisher: AnyPublisher<String, Never> {
+        textField.textPublisher
+    }
+
+    public init() {
+        super.init(frame: .zero)
+
+        layer.borderWidth = 1.0
+        layer.cornerRadius = 4.0
+        layer.masksToBounds = true
+        layer.borderColor = Asset.neutralWeak.color.cgColor
+
+        textField.delegate = self
+        textField.backgroundColor = .clear
+        textField.textColor = Asset.neutralDark.color
+        placeholderLabel.textColor = Asset.neutralWeak.color
+        placeholderLabel.font = Fonts.Mulish.regular.font(size: 16.0)
+
+        secureInputButton.button.addTarget(self, action: #selector(didTapRight), for: .touchUpInside)
+
+        inputContainerView.addSubview(placeholderLabel)
+        inputContainerView.addSubview(textField)
+
+        stackView.addArrangedSubview(inputContainerView)
+        stackView.addArrangedSubview(secureInputButton)
+
+        addSubview(stackView)
+
+        placeholderLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(15)
+            $0.left.equalToSuperview().offset(15)
+            $0.right.lessThanOrEqualToSuperview().offset(-15)
+            $0.bottom.equalToSuperview().offset(-18)
+        }
+
+        textField.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(15)
+            $0.left.equalToSuperview().offset(15)
+            $0.right.equalToSuperview().offset(-15)
+            $0.bottom.equalToSuperview().offset(-18)
+        }
+
+        stackView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    public func setup(title: String, sensitive: Bool = false) {
+        placeholderLabel.text = title
+        textField.isSecureTextEntry = sensitive
+        secureInputButton.isHidden = !sensitive
+    }
+
+    @objc private func didTapRight() {
+        textField.isSecureTextEntry.toggle()
+        secureInputButton.setSecure(textField.isSecureTextEntry)
+    }
+}
+
+extension OutlinedInputField: UITextFieldDelegate {
+    public func textField(
+        _ textField: UITextField,
+        shouldChangeCharactersIn range: NSRange,
+        replacementString string: String
+    ) -> Bool {
+        placeholderLabel.alpha = (textField.text! as NSString)
+            .replacingCharacters(in: range, with: string)
+            .count > 0 ? 0.0 : 1.0
+        return true
+    }
+}
diff --git a/Sources/InputField/SecureInputButton.swift b/Sources/InputField/SecureInputButton.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1f2e6b20751370755ec23b966c153ca5440c9f32
--- /dev/null
+++ b/Sources/InputField/SecureInputButton.swift
@@ -0,0 +1,31 @@
+import UIKit
+import Shared
+
+final class SecureInputButton: UIView {
+    private(set) var button = UIButton()
+    private let color = Asset.neutralSecondaryAlternative.color
+    private lazy var openedImage = Asset.eyeOpen.image.withTintColor(color)
+    private lazy var closedImage = Asset.eyeClosed.image.withTintColor(color)
+
+    init() {
+        super.init(frame: .zero)
+
+        button.setContentCompressionResistancePriority(.required, for: .horizontal)
+        button.setImage(Asset.eyeClosed.image.withTintColor(color), for: .normal)
+
+        addSubview(button)
+
+        button.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview().offset(10)
+            $0.right.equalToSuperview().offset(-10)
+            $0.bottom.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func setSecure(_ bool: Bool) {
+        button.setImage(bool ? closedImage : openedImage, for: .normal)
+    }
+}
diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift
index 7d53fb7efaf8ebe3b4f6485bc38dd101ce8960e5..450c674ebc5647268114eb3b251e6b56c12efca4 100644
--- a/Sources/Integration/Client.swift
+++ b/Sources/Integration/Client.swift
@@ -89,7 +89,12 @@ public class Client {
     //    }
 
     public func addJson(_ string: String) {
-        guard let backupManager = backupManager else { return }
+        guard let backupManager = backupManager else {
+            fatalError("Trying to add json parameters to backup but no backup manager created yet")
+        }
+
+        print("^^^ Set params: \(string) to backup")
+
         backupManager.addJson(string)
     }
 
diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift
index 7c6feb9f10688bc0d34726aec2479c8174438484..a05387dda2653644a26a3bc28b40e011c6eb5e03 100644
--- a/Sources/Integration/Implementations/Bindings.swift
+++ b/Sources/Integration/Implementations/Bindings.swift
@@ -154,17 +154,15 @@ extension BindingsClient: BindingsInterface {
         for env: NetworkEnvironment,
         _ completion: @escaping (Result<Data?, Error>) -> Void
     ) {
-        log(type: .crumbs)
-
         var error: NSError?
         let ndf = BindingsDownloadAndVerifySignedNdfWithUrl(env.url, env.cert, &error)
 
-        if let error = error {
-            log(string: error.localizedDescription, type: .error)
-            completion(.failure(error))
-        } else {
-            completion(.success(ndf))
+        guard error == nil else {
+            Self.updateNDF(for: env, completion)
+            return
         }
+
+        completion(.success(ndf))
     }
 
     /// Fetches a JSON with up-to-date error descriptions
diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift
index fa27fe2ece740c14217d809d8bab6fd48fae670a..7bbe5254ab00b17b8053323586347ef819f10ff1 100644
--- a/Sources/Integration/Session/Session+Contacts.swift
+++ b/Sources/Integration/Session/Session+Contacts.swift
@@ -94,7 +94,7 @@ extension Session {
     }
 
     public func retryRequest(_ contact: Contact) throws {
-        log(string: "Retrying to request a contact", type: .info)
+        let name = (contact.nickname ?? contact.username) ?? ""
 
         client.bindings.add(contact.marshaled!, from: myQR) { [weak self, contact] in
             var contact = contact
@@ -103,11 +103,21 @@ extension Session {
             do {
                 switch $0 {
                 case .success:
-                    log(string: "Retrying to request a contact -- Success", type: .info)
                     contact.authStatus = .requested
-                case .failure(let error):
-                    log(string: "Retrying to request a contact -- Failed: \(error.localizedDescription)", type: .error)
+
+                    self.toastController.enqueueToast(model: .init(
+                        title: Localized.Requests.Sent.Toast.resent(name),
+                        leftImage: Asset.sharedSuccess.image
+                    ))
+
+                case .failure:
                     contact.createdAt = Date()
+
+                    self.toastController.enqueueToast(model: .init(
+                        title: Localized.Requests.Sent.Toast.resentFailed(name),
+                        color: Asset.accentDanger.color,
+                        leftImage: Asset.requestFailedToaster.image
+                    ))
                 }
 
                 _ = try self.dbManager.saveContact(contact)
@@ -118,29 +128,41 @@ extension Session {
     }
 
     public func add(_ contact: Contact) throws {
+        /// Make sure we are not adding ourselves
+        ///
         guard contact.username != username else {
             throw NSError.create("You can't add yourself")
         }
 
-        var contactToOperate: Contact!
+        var contact = contact
 
-        if [.requestFailed, .confirmationFailed, .stranger].contains(contact.authStatus) {
-            contactToOperate = contact
+        /// Check if this contact is actually
+        /// being requested/confirmed after failing
+        ///
+        if [.requestFailed, .confirmationFailed].contains(contact.authStatus) {
+            /// If it is being re-requested or
+            /// re-confirmed, no need to save again
+            ///
+            contact.createdAt = Date()
+
+            if contact.authStatus == .confirmationFailed {
+                try confirm(contact)
+                return
+            }
         } else {
+            /// If its not failed, lets make sure that
+            /// this is an actual new contact
+            ///
             if let _ = try? dbManager.fetchContacts(.init(id: [contact.id])).first {
+                /// Found a user w/ that id already stored
+                ///
                 throw NSError.create("This user has already been requested")
             }
 
-            contactToOperate = try dbManager.saveContact(contact)
-        }
-
-        guard contactToOperate.authStatus != .confirmationFailed else {
-            contactToOperate.createdAt = Date()
-            try confirm(contact)
-            return
+            contact.authStatus = .requesting
         }
 
-        contactToOperate.authStatus = .requesting
+        contact = try dbManager.saveContact(contact)
 
         let myself = client.bindings.meMarshalled(
             username!,
@@ -148,22 +170,30 @@ extension Session {
             phone: isSharingPhone ? phone : nil
         )
 
-        client.bindings.add(contactToOperate.marshaled!, from: myself) { [weak self, contactToOperate] in
-            guard let self = self, var contactToOperate = contactToOperate else { return }
+        client.bindings.add(contact.marshaled!, from: myself) { [weak self, contact] in
+            guard let self = self else { return }
+            var contact = contact
 
             do {
                 switch $0 {
                 case .success(let success):
-                    contactToOperate.authStatus = success ? .requested : .requestFailed
-                    contactToOperate = try self.dbManager.saveContact(contactToOperate)
+                    contact.authStatus = success ? .requested : .requestFailed
+                    contact = try self.dbManager.saveContact(contact)
+
+                    let name = contact.nickname ?? contact.username
+
+                    self.toastController.enqueueToast(model: .init(
+                        title: Localized.Requests.Sent.Toast.sent(name ?? ""),
+                        leftImage: Asset.sharedSuccess.image
+                    ))
 
                 case .failure:
-                    contactToOperate.authStatus = .requestFailed
-                    contactToOperate.createdAt = Date()
-                    contactToOperate = try self.dbManager.saveContact(contactToOperate)
+                    contact.createdAt = Date()
+                    contact.authStatus = .requestFailed
+                    contact = try self.dbManager.saveContact(contact)
 
                     self.toastController.enqueueToast(model: .init(
-                        title: Localized.Requests.Failed.toast(contactToOperate.nickname ?? contact.username!),
+                        title: Localized.Requests.Failed.toast(contact.nickname ?? contact.username!),
                         color: Asset.accentDanger.color,
                         leftImage: Asset.requestFailedToaster.image
                     ))
diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift
index 6dd7472fedb8b3c9fd798134d8ede35a29ce3804..1fe3e2e09b2f7f0ae327b039602320bbc884eb2b 100644
--- a/Sources/Integration/Session/Session+UD.swift
+++ b/Sources/Integration/Session/Session+UD.swift
@@ -1,8 +1,35 @@
 import Models
 import XXModels
 import Foundation
+import Combine
 
 extension Session {
+    public func search(fact: String) -> AnyPublisher<Contact, Error> {
+        Deferred {
+            Future { promise in
+                guard let ud = self.client.userDiscovery else {
+                    let error = NSError(domain: "", code: 0)
+                    promise(.failure(error))
+                    return
+                }
+
+                do {
+                    try self.client.bindings.nodeRegistrationStatus()
+                    try ud.search(fact: fact) {
+                        switch $0 {
+                        case .success(let contact):
+                            promise(.success(contact))
+                        case .failure(let error):
+                            promise(.failure(error))
+                        }
+                    }
+                } catch {
+                    promise(.failure(error))
+                }
+            }
+        }.eraseToAnyPublisher()
+    }
+
     public func search(fact: String, _ completion: @escaping (Result<Contact, Error>) -> Void) throws {
         guard let ud = client.userDiscovery else { return }
         try client.bindings.nodeRegistrationStatus()
@@ -41,20 +68,13 @@ extension Session {
 
                 switch $0 {
                 case .success(_):
-                    _ = try? self.dbManager.saveContact(.init(
-                        id: self.client.bindings.myId,
-                        marshaled: self.client.bindings.meMarshalled,
-                        username: value,
-                        email: nil,
-                        phone: nil,
-                        nickname: nil,
-                        photo: nil,
-                        authStatus: .friend,
-                        isRecent: false,
-                        createdAt: Date()
-                    ))
-
                     self.username = value
+
+                    if var me = try? self.myContact() {
+                        me.username = value
+                        _ = try? self.dbManager.saveContact(me)
+                    }
+
                     completion(.success(nil))
                 case .failure(let error):
                     completion(.failure(error))
@@ -76,6 +96,8 @@ extension Session {
             phone = confirmation.content
         }
 
-        updateFactsOnBackup()
+        if let _ = client.backupManager {
+            updateFactsOnBackup()
+        }
     }
 }
diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift
index ae01b853ac2ac24d7e8427f3a868dcb18b34dcde..f85a6c6f0e3350e20c2bb234e6a92935e589b01f 100644
--- a/Sources/Integration/Session/Session.swift
+++ b/Sources/Integration/Session/Session.swift
@@ -130,8 +130,20 @@ public final class Session: SessionType {
             let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8))
 
             username = params.username
-            phone = params.phone
-            email = params.email
+
+            if let paramsPhone = params.phone, !paramsPhone.isEmpty {
+                phone = paramsPhone
+            }
+
+            if let paramsEmail = params.email, !paramsEmail.isEmpty {
+                email = paramsEmail
+            }
+        }
+
+        print("^^^ \(report.parameters)")
+
+        guard username!.isEmpty == false else {
+            fatalError("Trying to restore an account that has no username")
         }
 
         try continueInitialization()
@@ -183,6 +195,15 @@ public final class Session: SessionType {
     }
 
     private func continueInitialization() throws {
+        var myContact = try self.myContact()
+        myContact.marshaled = client.bindings.meMarshalled
+        myContact.username = username
+        myContact.email = email
+        myContact.phone = phone
+        myContact.authStatus = .friend
+        myContact.isRecent = false
+        _ = try dbManager.saveContact(myContact)
+
         setupBindings()
         networkMonitor.start()
 
@@ -356,6 +377,11 @@ public final class Session: SessionType {
         ).jsonFormat
 
         client.addJson(params)
+
+        guard username!.isEmpty == false else {
+            fatalError("Tried to build a backup with my username but an empty string was set to it")
+        }
+
         backupService.performBackupIfAutomaticIsEnabled()
     }
 
@@ -381,7 +407,6 @@ public final class Session: SessionType {
             .store(in: &cancellables)
 
         client.backup
-            .throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true)
             .sink { [unowned self] in backupService.updateBackup(data: $0) }
             .store(in: &cancellables)
 
@@ -390,9 +415,10 @@ public final class Session: SessionType {
                 /// This will get called when my contact restore its contact.
                 /// TODO: Hold a record on the chat that this contact restored.
                 ///
-                var contact = $0
-                contact.authStatus = .friend
-                _ = try? dbManager.saveContact(contact)
+                if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
+                    contact.authStatus = .friend
+                    _ = try? dbManager.saveContact(contact)
+                }
             }.store(in: &cancellables)
 
         backupService.settingsPublisher
@@ -402,7 +428,6 @@ public final class Session: SessionType {
                 if $0 == true {
                     guard let passphrase = backupService.passphrase else {
                         client.resumeBackup()
-                        updateFactsOnBackup()
                         return
                     }
 
@@ -416,10 +441,6 @@ public final class Session: SessionType {
             }
             .store(in: &cancellables)
 
-        networkMonitor.statusPublisher
-            .sink { print($0) }
-            .store(in: &cancellables)
-
         client.messages
             .sink { [unowned self] in
                 if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first {
@@ -468,4 +489,12 @@ public final class Session: SessionType {
             }
             .store(in: &cancellables)
     }
+
+    func myContact() throws -> Contact {
+        if let contact = try dbManager.fetchContacts(.init(id: [client.bindings.myId])).first {
+            return contact
+        } else {
+            return try dbManager.saveContact(.init(id: client.bindings.myId))
+        }
+    }
 }
diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift
index e99c6f37fb45599aae26836c7a8f248ae20f02f1..b871332b216e5c7dbdc0adfebc4bac3f306913b7 100644
--- a/Sources/Integration/Session/SessionType.swift
+++ b/Sources/Integration/Session/SessionType.swift
@@ -66,4 +66,6 @@ public protocol SessionType {
         members: [Contact],
         _ completion: @escaping (Result<GroupInfo, Error>) -> Void
     )
+
+    func search(fact: String) -> AnyPublisher<Contact, Error>
 }
diff --git a/Sources/Keychain/KeychainHandler.swift b/Sources/Keychain/KeychainHandler.swift
index 23f248879a7f2275b0faddd9f4c08962d9ed6246..6ac0d645d6def33360ee5ed0d0ab1e973a0487fc 100644
--- a/Sources/Keychain/KeychainHandler.swift
+++ b/Sources/Keychain/KeychainHandler.swift
@@ -1,15 +1,24 @@
 import Foundation
 import KeychainAccess
 
+public enum KeychainSFTP: String {
+    case pwd
+    case host
+    case username
+}
+
 public protocol KeychainHandling {
     func clear() throws
     func getPassword() throws -> Data?
     func store(password pwd: Data) throws
+
+    func get(key: KeychainSFTP) throws -> String?
+    func store(key: KeychainSFTP, value: String) throws
 }
 
 public struct KeychainHandler: KeychainHandling {
-    private let password = "password"
     private let keychain: Keychain
+    private let password = "password"
 
     public init() {
         self.keychain = Keychain(service: "XXM")
@@ -26,4 +35,12 @@ public struct KeychainHandler: KeychainHandling {
     public func getPassword() throws -> Data? {
         try keychain.getData(password)
     }
+
+    public func get(key: KeychainSFTP) throws -> String? {
+        try keychain.get(key.rawValue)
+    }
+
+    public func store(key: KeychainSFTP, value: String) throws {
+        try keychain.set(value, key: key.rawValue)
+    }
 }
diff --git a/Sources/Keychain/MockKeychainHandler.swift b/Sources/Keychain/MockKeychainHandler.swift
index c1ff10dd802d5cd230cffbe25f20b744aa06126b..39d4a33ddb1e0e6d8a75d8b5a1ea980d8fb189bf 100644
--- a/Sources/Keychain/MockKeychainHandler.swift
+++ b/Sources/Keychain/MockKeychainHandler.swift
@@ -6,4 +6,6 @@ public struct MockKeychainHandler: KeychainHandling {
     public func clear() throws {}
     public func store(password pwd: Data) throws {}
     public func getPassword() throws -> Data? { Data() }
+    public func get(key: KeychainSFTP) throws -> String? { nil }
+    public func store(key: KeychainSFTP, value: String) throws {}
 }
diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift
index 33c2f8dad55d29d02a7e74b3d70437fbaea4aa6f..2eb373f55784bed3e9a11293acc7cca9aee2fc08 100644
--- a/Sources/LaunchFeature/LaunchController.swift
+++ b/Sources/LaunchFeature/LaunchController.swift
@@ -7,7 +7,7 @@ import DependencyInjection
 
 
 public final class LaunchController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: LaunchCoordinating
 
     lazy private var screenView = LaunchView()
diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift
index 054432b4403e7177bf129c21680a58ac4abb6b12..fefbe7cf6035b19d804febd7e544e22166d11bf8 100644
--- a/Sources/LaunchFeature/LaunchViewModel.swift
+++ b/Sources/LaunchFeature/LaunchViewModel.swift
@@ -5,6 +5,7 @@ import Models
 import Combine
 import Defaults
 import XXModels
+import Keychain
 import Foundation
 import Integration
 import Permissions
@@ -31,6 +32,7 @@ final class LaunchViewModel {
     @Dependency private var network: XXNetworking
     @Dependency private var versionChecker: VersionChecker
     @Dependency private var dropboxService: DropboxInterface
+    @Dependency private var keychainHandler: KeychainHandling
     @Dependency private var permissionHandler: PermissionHandling
 
     @KeyObject(.username, defaultValue: nil) var username: String?
@@ -56,7 +58,7 @@ final class LaunchViewModel {
 
     func viewDidAppear() {
         DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
-            self?.hudSubject.send(.on(nil))
+            self?.hudSubject.send(.on)
             self?.checkVersion()
         }
     }
@@ -90,6 +92,7 @@ final class LaunchViewModel {
                     self.hudSubject.send(.none)
                     self.routeSubject.send(.onboarding(ndf))
                     self.dropboxService.unlink()
+                    try? self.keychainHandler.clear()
                     return
                 }
 
@@ -98,6 +101,7 @@ final class LaunchViewModel {
                     self.hudSubject.send(.none)
                     self.routeSubject.send(.onboarding(ndf))
                     self.dropboxService.unlink()
+                    try? self.keychainHandler.clear()
                     return
                 }
 
diff --git a/Sources/MenuFeature/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift
index a960075a03c8d2a1f337d4d16581a650317420eb..682e23d450bde134916a2db8a00d13c2279c9321 100644
--- a/Sources/MenuFeature/Controllers/MenuController.swift
+++ b/Sources/MenuFeature/Controllers/MenuController.swift
@@ -51,6 +51,7 @@ public final class MenuController: UIViewController {
             image: viewModel.avatar
         )
 
+        screenView.select(item: previousItem)
         screenView.xxdkVersionLabel.text = "XXDK \(viewModel.xxdk)"
         screenView.buildLabel.text = Localized.Menu.build(viewModel.build)
         screenView.versionLabel.text = Localized.Menu.version(viewModel.version)
diff --git a/Sources/MenuFeature/Views/MenuSectionButton.swift b/Sources/MenuFeature/Views/MenuSectionButton.swift
index b24c5dc44efb74c226dee1316f5642f53a1a874d..c5f6ea371a10f0ff3fc087b31f6dc744e11fd4a9 100644
--- a/Sources/MenuFeature/Views/MenuSectionButton.swift
+++ b/Sources/MenuFeature/Views/MenuSectionButton.swift
@@ -40,9 +40,17 @@ final class MenuSectionButton: UIControl {
         notificationLabel.text = "  \(count)  "
     }
 
-    func set(title: String, image: UIImage, color: UIColor = Asset.neutralWeak.color) {
-        titleLabel.text = title
+    func set(color: UIColor) {
         titleLabel.textColor = color
-        imageView.image = image.withTintColor(color)
+
+        if let image = imageView.image {
+            imageView.image = image.withTintColor(color)
+        }
+    }
+
+    func set(title: String, image: UIImage) {
+        titleLabel.text = title
+        titleLabel.textColor = Asset.neutralWeak.color
+        imageView.image = image.withTintColor(Asset.neutralWeak.color)
     }
 }
diff --git a/Sources/MenuFeature/Views/MenuView.swift b/Sources/MenuFeature/Views/MenuView.swift
index 48bee02edc7f7e3e22edfc75281395e968c0c973..b19ec9d1d1367f5d910dfe97d40ee9198b7e3ec0 100644
--- a/Sources/MenuFeature/Views/MenuView.swift
+++ b/Sources/MenuFeature/Views/MenuView.swift
@@ -18,20 +18,9 @@ final class MenuView: UIView {
 
     init() {
         super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    private func setup() {
         backgroundColor = Asset.neutralDark.color
 
-        chatsButton.set(
-            title: Localized.Menu.chats,
-            image: Asset.menuChats.image,
-            color: Asset.brandPrimary.color
-        )
-
+        chatsButton.set(title: Localized.Menu.chats, image: Asset.menuChats.image)
         scanButton.set(title: Localized.Menu.scan, image: Asset.menuScan.image)
         requestsButton.set(title: Localized.Menu.requests, image: Asset.menuRequests.image)
         contactsButton.set(title: Localized.Menu.contacts, image: Asset.menuContacts.image)
@@ -64,33 +53,42 @@ final class MenuView: UIView {
         addSubview(infoStackView)
 
         setupConstraints()
-        setupAccessibility()
     }
 
-    private func setupConstraints() {
-        headerView.snp.makeConstraints { make in
-            make.top.equalTo(safeAreaLayoutGuide).offset(20)
-            make.left.equalToSuperview().offset(30)
-            make.right.equalToSuperview().offset(-24)
+    required init?(coder: NSCoder) { nil }
+
+    func select(item: MenuItem) {
+        switch item {
+        case .chats:
+            chatsButton.set(color: Asset.brandPrimary.color)
+        case .contacts:
+            contactsButton.set(color: Asset.brandPrimary.color)
+        case .requests:
+            requestsButton.set(color: Asset.brandPrimary.color)
+        case .scan:
+            scanButton.set(color: Asset.brandPrimary.color)
+        case .settings:
+            settingsButton.set(color: Asset.brandPrimary.color)
+        case .profile, .dashboard, .join:
+            break
         }
+    }
 
-        stackView.snp.makeConstraints { make in
-            make.left.equalToSuperview().offset(26)
-            make.top.equalTo(headerView.snp.bottom).offset(75)
+    private func setupConstraints() {
+        headerView.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(20)
+            $0.left.equalToSuperview().offset(30)
+            $0.right.equalToSuperview().offset(-24)
         }
 
-        infoStackView.snp.makeConstraints { make in
-            make.bottom.equalTo(safeAreaLayoutGuide).offset(-20)
-            make.left.equalToSuperview().offset(20)
+        stackView.snp.makeConstraints {
+            $0.left.equalToSuperview().offset(26)
+            $0.top.equalTo(headerView.snp.bottom).offset(75)
         }
-    }
 
-    private func setupAccessibility() {
-        scanButton.accessibilityIdentifier = Localized.Accessibility.Menu.scan
-        chatsButton.accessibilityIdentifier = Localized.Accessibility.Menu.chats
-        headerView.accessibilityIdentifier = Localized.Accessibility.Menu.header
-        contactsButton.accessibilityIdentifier = Localized.Accessibility.Menu.contacts
-        requestsButton.accessibilityIdentifier = Localized.Accessibility.Menu.requests
-        settingsButton.accessibilityIdentifier = Localized.Accessibility.Menu.settings
+        infoStackView.snp.makeConstraints {
+            $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20)
+            $0.left.equalToSuperview().offset(20)
+        }
     }
 }
diff --git a/Sources/Models/CloudService.swift b/Sources/Models/CloudService.swift
index 5daba7238d74388dac591b63220dbbc8b1c9f911..d217dca853e6ecf22f119520d9805567f7ead3a5 100644
--- a/Sources/Models/CloudService.swift
+++ b/Sources/Models/CloudService.swift
@@ -2,4 +2,5 @@ public enum CloudService: Equatable, Codable {
     case drive
     case icloud
     case dropbox
+    case sftp
 }
diff --git a/Sources/NetworkMonitor/MockNetworkMonitor.swift b/Sources/NetworkMonitor/MockNetworkMonitor.swift
index 473eb593ece68fd213a4896abe9a74eb3c23d286..30bf846df9e36359ed80a7572a966e56091dce4d 100644
--- a/Sources/NetworkMonitor/MockNetworkMonitor.swift
+++ b/Sources/NetworkMonitor/MockNetworkMonitor.swift
@@ -32,11 +32,11 @@ public struct MockNetworkMonitor: NetworkMonitoring {
         statusRelay.send(status)
 
         if status == .available {
-            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
                 simulateOscilation(.internetNotAvailable)
             }
         } else if status == .internetNotAvailable {
-            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                 simulateOscilation(.available)
             }
         }
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift
index 75b7d0ffb081bec57a2c8cf06d0fa6915fd07092..90c210569b58b904473f5c2e2020740e2945a09d 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailConfirmationController.swift
@@ -9,7 +9,7 @@ import ScrollViewController
 import Models
 
 public final class OnboardingEmailConfirmationController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift
index 68b10159a32ab4e5167d10ba98d993f453fdf24d..54fb8cf4efcf4cd29489729598ed22144b918f65 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift
@@ -8,7 +8,7 @@ import DependencyInjection
 import ScrollViewController
 
 public final class OnboardingEmailController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift
index dbef5be43b8314ffcc71a2481bb4bcc0d67a5e6d..0017798bc5fc2a4703a56cf6b4fc2a45fbcffa55 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneConfirmationController.swift
@@ -9,7 +9,7 @@ import ScrollViewController
 import Models
 
 public final class OnboardingPhoneConfirmationController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift
index b935ae07434e90d94818a2ea400202924323ea32..8793508fe6ec8d94e3ee00ff5f3052031c70ee53 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift
@@ -8,7 +8,7 @@ import DependencyInjection
 import ScrollViewController
 
 public final class OnboardingPhoneController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift
index 2ed639b116b86ab36b2cdf8924b63626bd653d0c..d95169bbd9c5ce078687a77c96768d91e9d2336d 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift
@@ -6,7 +6,7 @@ import Combine
 import DependencyInjection
 
 public final class OnboardingStartController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
 
     lazy private var screenView = OnboardingStartView()
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift
index 19fc945b63b2495f5823af11fb21de205f538d85..3bc4493ebd99899a4b9ea646964816ff0140009e 100644
--- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift
+++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift
@@ -1,14 +1,14 @@
 import HUD
-import DrawerFeature
 import Theme
 import UIKit
 import Shared
 import Combine
+import DrawerFeature
 import DependencyInjection
 import ScrollViewController
 
 public final class OnboardingUsernameController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: OnboardingCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift
index 1263fc26efb0f01d50f00a25e8b4b27e3b821b81..b3871d6234517f1d493ab61da1466bc754446fd7 100644
--- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift
+++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift
@@ -58,7 +58,7 @@ final class OnboardingEmailConfirmationViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift
index 618a903459267835ea47210d17ab0e5f763ac68e..c3cbbb897840964e15f908b9fa83d5d6537f7b23 100644
--- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift
+++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift
@@ -38,7 +38,7 @@ final class OnboardingEmailViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift
index 0af84d605d2c56a938e474d27fd79ad324943ee3..2bd5a7ae35fbca7bf871479f998abeef9e4ce625 100644
--- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift
+++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift
@@ -58,7 +58,7 @@ final class OnboardingPhoneConfirmationViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift
index 4698673d45cbe3b05f73fb3b1943f598f1c9655e..0aff02f402420b959d03f166ba4eb456d1098bea 100644
--- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift
+++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift
@@ -47,7 +47,7 @@ final class OnboardingPhoneViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift
index 8766aac8e7629e4bdb9079cd8a3774aa2d1989a2..ecb64035e73e259bf147ce4ee7f427723d40b3f3 100644
--- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift
+++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift
@@ -42,7 +42,7 @@ final class OnboardingUsernameViewModel {
     }
 
     func didTapRegister() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift
index d909662aa961fda594217d71d4dd940bd0cf80af..c005b0bf9c10daa6db1f8521799da86bbfad26a4 100644
--- a/Sources/ProfileFeature/Controllers/ProfileCodeController.swift
+++ b/Sources/ProfileFeature/Controllers/ProfileCodeController.swift
@@ -10,7 +10,7 @@ import ScrollViewController
 public typealias ControllerClosure = (UIViewController, AttributeConfirmation) -> Void
 
 public final class ProfileCodeController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
 
     lazy private var screenView = ProfileCodeView()
     lazy private var scrollViewController = ScrollViewController()
diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift
index 62a2c90ee9bfb56674f8601bb1007d62c5f3323d..e3138f275dd967d73d58548649e4156409178e57 100644
--- a/Sources/ProfileFeature/Controllers/ProfileController.swift
+++ b/Sources/ProfileFeature/Controllers/ProfileController.swift
@@ -7,7 +7,7 @@ import Combine
 import DependencyInjection
 
 public final class ProfileController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: ProfileCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift
index fb85221449bea9ecd89b1a9507db4182b7365622..97ced65c65a10b7d4ac8033a5b2c0d80cf959997 100644
--- a/Sources/ProfileFeature/Controllers/ProfileEmailController.swift
+++ b/Sources/ProfileFeature/Controllers/ProfileEmailController.swift
@@ -7,7 +7,7 @@ import DependencyInjection
 import ScrollViewController
 
 public final class ProfileEmailController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: ProfileCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift
index b6ddaffb5574bdb44f70994fe2ce2c7b0e98b3c0..eb77fd14d36bce3960f30a73aa4ac7c117e453e7 100644
--- a/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift
+++ b/Sources/ProfileFeature/Controllers/ProfilePhoneController.swift
@@ -9,7 +9,7 @@ import ScrollViewController
 #warning("TODO: Merge ProfilePhoneController/ProfileEmailController")
 
 public final class ProfilePhoneController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: ProfileCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift
index 0cc8f60cfaea6921d99a90ccbba74a0cdfc322c2..d9763e989e23e37c9736582a41fcf1b63aef03b2 100644
--- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift
+++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift
@@ -57,7 +57,7 @@ final class ProfileCodeViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift
index df2bb28cdaa889462d1db1eb506cb14129567312..6b57bc81fe4040faaccaeefff057a275a90deaac 100644
--- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift
+++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift
@@ -40,7 +40,7 @@ final class ProfileEmailViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift
index 9bf5da6e11788124e2bba6b04ba05f2f64b7f033..1013725b419a118c4e437d3e8959c50748447d72 100644
--- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift
+++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift
@@ -47,7 +47,7 @@ final class ProfilePhoneViewModel {
     }
 
     func didTapNext() {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift
index 8f03379f02adde54606a70e4bd7c76f3e1fea87d..a7066eccfaada6e74960beb7a5b3b8b7e72548cb 100644
--- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift
+++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift
@@ -83,7 +83,7 @@ final class ProfileViewModel {
     }
 
     func didTapDelete(isEmail: Bool) {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
diff --git a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift
index c166da6b2c97b01f38e273b2a1a91ccbed475055..c0af65d5f83a4bc1528065eef1551e597a52f6c9 100644
--- a/Sources/RequestsFeature/Controllers/RequestsFailedController.swift
+++ b/Sources/RequestsFeature/Controllers/RequestsFailedController.swift
@@ -5,7 +5,7 @@ import Combine
 import DependencyInjection
 
 final class RequestsFailedController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
 
     lazy private var screenView = RequestsFailedView()
     private var cancellables = Set<AnyCancellable>()
diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift
index 5edb77726fa6d43ba7d4e2a816192fcadd39303b..0a30d05fb32b5ec4d3eb54b1fdeb9ced674a17ac 100644
--- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift
+++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift
@@ -10,7 +10,7 @@ import DrawerFeature
 import DependencyInjection
 
 final class RequestsReceivedController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var toaster: ToastController
     @Dependency private var coordinator: RequestsCoordinating
 
diff --git a/Sources/RequestsFeature/Controllers/RequestsSentController.swift b/Sources/RequestsFeature/Controllers/RequestsSentController.swift
index 36c245f4476fdd8795a6c0c38612f86c1ec6953e..ceed2cc28989b51d72d0191e63e81ade2f930bdd 100644
--- a/Sources/RequestsFeature/Controllers/RequestsSentController.swift
+++ b/Sources/RequestsFeature/Controllers/RequestsSentController.swift
@@ -5,7 +5,7 @@ import Combine
 import DependencyInjection
 
 final class RequestsSentController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
 
     var connectionsPublisher: AnyPublisher<Void, Never> {
         connectionSubject.eraseToAnyPublisher()
diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift
index 9214c9ef3f19d337ae66f0f9a6c8368741ff1096..b7d81db3a65b791e001a897506418295b56f8b3a 100644
--- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift
+++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift
@@ -38,7 +38,7 @@ final class RequestsFailedViewModel {
     func didTapStateButtonFor(request: Request) {
         guard case let .contact(contact) = request, request.status == .failedToRequest else { return }
 
-        hudSubject.send(.on(nil))
+        hudSubject.send(.on)
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
 
diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift
index 7dd9e8c1d07c9b15d9829803be59c55c0955fd12..ded18f77a4da0d956d2bdb0b1779495c818d41f5 100644
--- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift
+++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift
@@ -150,7 +150,7 @@ final class RequestsReceivedViewModel {
     }
 
     func didRequestAccept(group: Group) {
-        hudSubject.send(.on(nil))
+        hudSubject.send(.on)
 
         backgroundScheduler.schedule { [weak self] in
             do {
@@ -208,7 +208,7 @@ final class RequestsReceivedViewModel {
     }
 
     func didRequestAccept(contact: Contact, nickname: String? = nil) {
-        hudSubject.send(.on(nil))
+        hudSubject.send(.on)
 
         var contact = contact
         contact.nickname = nickname ?? contact.username
diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift
index 3789428ffa0a9539e35b3344fdd820ee896d03a2..f94ed8f5164de5c5a419a9b9271499ae7daa4952 100644
--- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift
+++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift
@@ -3,6 +3,7 @@ import UIKit
 import Models
 import Shared
 import Combine
+import XXModels
 import Integration
 import ToastFeature
 import CombineSchedulers
@@ -32,7 +33,12 @@ final class RequestsSentViewModel {
     var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()
 
     init() {
-        session.dbManager.fetchContactsPublisher(.init(authStatus: [.requested]))
+        let query = Contact.Query(authStatus: [
+            .requested,
+            .requesting
+        ])
+
+        session.dbManager.fetchContactsPublisher(query)
             .assertNoFailure()
             .removeDuplicates()
             .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in
@@ -47,7 +53,7 @@ final class RequestsSentViewModel {
     func didTapStateButtonFor(request item: RequestSent) {
         guard case let .contact(contact) = item.request, item.request.status == .requested else { return }
 
-        hudSubject.send(.on(nil))
+        hudSubject.send(.on)
         backgroundScheduler.schedule { [weak self] in
             guard let self = self else { return }
 
@@ -65,8 +71,10 @@ final class RequestsSentViewModel {
                 item.isResent = true
                 allRequests.append(item)
 
+                let name = (contact.nickname ?? contact.username) ?? ""
+
                 self.toastController.enqueueToast(model: .init(
-                    title: Localized.Requests.Sent.Toast.resent(contact.nickname ?? contact.username),
+                    title: Localized.Requests.Sent.Toast.resent(name),
                     leftImage: Asset.requestSentToaster.image
                 ))
 
diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift
index 041717b6dde907568db4872827957d487010d006..b396d1069df7008772290e5c9e2f275adb5ccf4b 100644
--- a/Sources/RestoreFeature/Controllers/RestoreListController.swift
+++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift
@@ -1,12 +1,12 @@
 import HUD
-import DrawerFeature
-import Shared
 import UIKit
+import Shared
 import Combine
+import DrawerFeature
 import DependencyInjection
 
 public final class RestoreListController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: RestoreCoordinating
 
     lazy private var screenView = RestoreListView()
@@ -51,15 +51,16 @@ public final class RestoreListController: UIViewController {
     }
 
     private func setupBindings() {
-        viewModel.hud
+        viewModel.hudPublisher
             .receive(on: DispatchQueue.main)
             .sink { [hud] in hud.update(with: $0) }
             .store(in: &cancellables)
 
-        viewModel.didFetchBackup
+        viewModel.backupPublisher
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toRestore(using: ndf, with: $0, from: self) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                coordinator.toRestore(using: ndf, with: $0, from: self)
+            }.store(in: &cancellables)
 
         screenView.cancelButton
             .publisher(for: .touchUpInside)
@@ -68,18 +69,27 @@ public final class RestoreListController: UIViewController {
 
         screenView.driveButton
             .publisher(for: .touchUpInside)
-            .sink { [unowned self] in viewModel.didTapCloud(.drive, from: self) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                viewModel.didTapCloud(.drive, from: self)
+            }.store(in: &cancellables)
 
         screenView.icloudButton
             .publisher(for: .touchUpInside)
-            .sink { [unowned self] in viewModel.didTapCloud(.icloud, from: self) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                viewModel.didTapCloud(.icloud, from: self)
+            }.store(in: &cancellables)
 
         screenView.dropboxButton
             .publisher(for: .touchUpInside)
-            .sink { [unowned self] in viewModel.didTapCloud(.dropbox, from: self) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                viewModel.didTapCloud(.dropbox, from: self)
+            }.store(in: &cancellables)
+
+        screenView.sftpButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in
+                viewModel.didTapCloud(.sftp, from: self)
+            }.store(in: &cancellables)
     }
 
     @objc private func didTapBack() {
diff --git a/Sources/RestoreFeature/Service/MockRestoreService.swift b/Sources/RestoreFeature/Service/MockRestoreService.swift
deleted file mode 100644
index 1a013434211580df1c99ac1fa3842ab0fc797d59..0000000000000000000000000000000000000000
--- a/Sources/RestoreFeature/Service/MockRestoreService.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-import UIKit
-import Models
-import Combine
-import Foundation
-import GoogleDriveFeature
-import DependencyInjection
-
-public struct RestoreServiceMock: RestoreServiceType {
-    public var inProgress: AnyPublisher<Void, Never> {
-        fatalError()
-    }
-
-    public var settings: AnyPublisher<RestoreSettings, Never> {
-        fatalError()
-    }
-
-    public init() {}
-
-    public func didSelectBackup(at url: URL) {}
-
-    public func authorize(service: CloudService, from: UIViewController) {}
-
-    public func download(
-        from settings: RestoreSettings,
-        progress: @escaping RestoreProgress,
-        whenFinished: @escaping RestoreDownloadFinished
-    ) {
-        fatalError()
-    }
-}
diff --git a/Sources/RestoreFeature/Service/RestoreServiceType.swift b/Sources/RestoreFeature/Service/RestoreServiceType.swift
deleted file mode 100644
index 78a32e9f6d9e199d78fa9c17a4a06b7ca1de51f3..0000000000000000000000000000000000000000
--- a/Sources/RestoreFeature/Service/RestoreServiceType.swift
+++ /dev/null
@@ -1,20 +0,0 @@
-import UIKit
-import Models
-import Combine
-
-public typealias RestoreProgress = (Float) -> Void
-public typealias RestoreDownloadFinished = (Result<Data, Error>) -> Void
-
-public protocol RestoreServiceType {
-    var inProgress: AnyPublisher<Void, Never> { get }
-
-    var settings: AnyPublisher<RestoreSettings, Never> { get }
-
-    func authorize(service: CloudService, from: UIViewController)
-
-    func download(
-        from settings: RestoreSettings,
-        progress: @escaping RestoreProgress,
-        whenFinished: @escaping RestoreDownloadFinished
-    )
-}
diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift
index 3e91448aed07d65e2cdf650033be9d15be08fb4d..438207bb1cc5df10104d3be19029ef01cc7d3c19 100644
--- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift
+++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift
@@ -6,20 +6,26 @@ import Combine
 import BackupFeature
 import DependencyInjection
 
+import SFTPFeature
 import iCloudFeature
 import DropboxFeature
 import GoogleDriveFeature
 
 final class RestoreListViewModel {
-    @Dependency private var icloud: iCloudInterface
-    @Dependency private var dropbox: DropboxInterface
-    @Dependency private var drive: GoogleDriveInterface
+    @Dependency private var sftpService: SFTPService
+    @Dependency private var icloudService: iCloudInterface
+    @Dependency private var dropboxService: DropboxInterface
+    @Dependency private var googleDriveService: GoogleDriveInterface
 
-    var hud: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() }
-    var didFetchBackup: AnyPublisher<RestoreSettings, Never> { backupSubject.eraseToAnyPublisher() }
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
 
-    private var dropboxAuthCancellable: AnyCancellable?
+    var backupPublisher: AnyPublisher<RestoreSettings, Never> {
+        backupSubject.eraseToAnyPublisher()
+    }
 
+    private var dropboxAuthCancellable: AnyCancellable?
     private let hudSubject = PassthroughSubject<HUDStatus, Never>()
     private let backupSubject = PassthroughSubject<RestoreSettings, Never>()
 
@@ -31,15 +37,43 @@ final class RestoreListViewModel {
             didRequestICloudAuthorization()
         case .dropbox:
             didRequestDropboxAuthorization(from: parent)
+        case .sftp:
+            didRequestSFTPAuthorization(from: parent)
         }
     }
 
+    private func didRequestSFTPAuthorization(from controller: UIViewController) {
+        let params = SFTPAuthorizationParams(controller, { [weak self] in
+            guard let self = self else { return }
+            controller.navigationController?.popViewController(animated: true)
+
+            self.hudSubject.send(.on)
+
+            self.sftpService.fetchMetadata{ result in
+                switch result {
+                case .success(let settings):
+                    self.hudSubject.send(.none)
+
+                    if let settings = settings {
+                        self.backupSubject.send(settings)
+                    } else {
+                        self.backupSubject.send(.init(cloudService: .sftp))
+                    }
+                case .failure(let error):
+                    self.hudSubject.send(.error(.init(with: error)))
+                }
+            }
+        })
+
+        sftpService.authorizeFlow(params)
+    }
+
     private func didRequestDriveAuthorization(from controller: UIViewController) {
-        drive.authorize(presenting: controller) { authResult in
+        googleDriveService.authorize(presenting: controller) { authResult in
             switch authResult {
             case .success:
-                self.hudSubject.send(.on(nil))
-                self.drive.downloadMetadata { downloadResult in
+                self.hudSubject.send(.on)
+                self.googleDriveService.downloadMetadata { downloadResult in
                     switch downloadResult {
                     case .success(let metadata):
                         var backup: Backup?
@@ -62,10 +96,10 @@ final class RestoreListViewModel {
     }
 
     private func didRequestICloudAuthorization() {
-        if icloud.isAuthorized() {
-            self.hudSubject.send(.on(nil))
+        if icloudService.isAuthorized() {
+            self.hudSubject.send(.on)
 
-            icloud.downloadMetadata { result in
+            icloudService.downloadMetadata { result in
                 switch result {
                 case .success(let metadata):
                     var backup: Backup?
@@ -83,20 +117,20 @@ final class RestoreListViewModel {
         } else {
             /// This could be an alert controller asking if user wants to enable/deeplink
             ///
-            icloud.openSettings()
+            icloudService.openSettings()
         }
     }
 
     private func didRequestDropboxAuthorization(from controller: UIViewController) {
-        dropboxAuthCancellable = dropbox.authorize(presenting: controller)
+        dropboxAuthCancellable = dropboxService.authorize(presenting: controller)
             .receive(on: DispatchQueue.main)
             .sink { [unowned self] authResult in
                 switch authResult {
                 case .success(let bool):
                     guard bool == true else { return }
 
-                    self.hudSubject.send(.on(nil))
-                    dropbox.downloadMetadata { metadataResult in
+                    self.hudSubject.send(.on)
+                    dropboxService.downloadMetadata { metadataResult in
                         switch metadataResult {
                         case .success(let metadata):
                             var backup: Backup?
diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift
index 37a5bbe469665e83df59f9a5c3215c4223e426d5..bea37e5113ad33a547e5680e478a0ed7717db741 100644
--- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift
+++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift
@@ -8,6 +8,7 @@ import Integration
 import BackupFeature
 import DependencyInjection
 
+import SFTPFeature
 import iCloudFeature
 import DropboxFeature
 import GoogleDriveFeature
@@ -38,13 +39,16 @@ extension RestorationStep: Equatable {
 }
 
 final class RestoreViewModel {
+    @Dependency private var sftpService: SFTPService
     @Dependency private var iCloudService: iCloudInterface
     @Dependency private var dropboxService: DropboxInterface
     @Dependency private var googleService: GoogleDriveInterface
 
     @KeyObject(.username, defaultValue: nil) var username: String?
 
-    var step: AnyPublisher<RestorationStep, Never> { stepRelay.eraseToAnyPublisher() }
+    var step: AnyPublisher<RestorationStep, Never> {
+        stepRelay.eraseToAnyPublisher()
+    }
 
     // TO REFACTOR:
     //
@@ -80,6 +84,22 @@ final class RestoreViewModel {
             downloadBackupForDropbox(backup)
         case .icloud:
             downloadBackupForiCloud(backup)
+        case .sftp:
+            downloadBackupForSFTP(backup)
+        }
+    }
+
+    private func downloadBackupForSFTP(_ backup: Backup) {
+        sftpService.downloadBackup(path: backup.id) { [weak self] in
+            guard let self = self else { return }
+            self.stepRelay.send(.downloading(backup.size, backup.size))
+
+            switch $0 {
+            case .success(let data):
+                self.continueRestoring(data: data)
+            case .failure(let error):
+                self.stepRelay.send(.failDownload(error))
+            }
         }
     }
 
diff --git a/Sources/RestoreFeature/Views/RestoreListView.swift b/Sources/RestoreFeature/Views/RestoreListView.swift
index 2a688760bc68cd509775fa97d28a37538dc44d3a..a955173a742b7c0b94f95601f90634353ada327f 100644
--- a/Sources/RestoreFeature/Views/RestoreListView.swift
+++ b/Sources/RestoreFeature/Views/RestoreListView.swift
@@ -6,6 +6,7 @@ final class RestoreListView: UIView {
     let stackView = UIStackView()
     let firstSubtitleLabel = UILabel()
     let secondSubtitleLabel = UILabel()
+    let sftpButton = RowButton()
     let driveButton = RowButton()
     let icloudButton = RowButton()
     let dropboxButton = RowButton()
@@ -34,6 +35,7 @@ final class RestoreListView: UIView {
         secondSubtitleLabel.numberOfLines = 0
         secondSubtitleLabel.attributedText = attrString
 
+        sftpButton.setup(title: Localized.Backup.sftp, icon: Asset.restoreSFTP.image)
         icloudButton.setup(title: Localized.Backup.iCloud, icon: Asset.restoreIcloud.image)
         dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image)
         driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image)
@@ -41,9 +43,11 @@ final class RestoreListView: UIView {
         cancelButton.set(style: .seeThrough, title: Localized.AccountRestore.List.cancel)
 
         stackView.axis = .vertical
+        stackView.distribution = .fillEqually
         stackView.addArrangedSubview(driveButton)
         stackView.addArrangedSubview(icloudButton)
         stackView.addArrangedSubview(dropboxButton)
+        stackView.addArrangedSubview(sftpButton)
 
         addSubview(titleLabel)
         addSubview(firstSubtitleLabel)
@@ -51,35 +55,35 @@ final class RestoreListView: UIView {
         addSubview(stackView)
         addSubview(cancelButton)
 
-        titleLabel.snp.makeConstraints { make in
-            make.top.equalTo(safeAreaLayoutGuide).offset(15)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(15)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        firstSubtitleLabel.snp.makeConstraints { make in
-            make.top.equalTo(titleLabel.snp.bottom).offset(8)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        firstSubtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(8)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        secondSubtitleLabel.snp.makeConstraints { make in
-            make.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-41)
+        secondSubtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
         }
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28)
-            make.left.equalToSuperview().offset(24)
-            make.right.equalToSuperview().offset(-24)
+        stackView.snp.makeConstraints {
+            $0.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28)
+            $0.left.equalToSuperview().offset(24)
+            $0.right.equalToSuperview().offset(-24)
         }
 
-        cancelButton.snp.makeConstraints { make in
-            make.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20)
-            make.left.equalToSuperview().offset(40)
-            make.right.equalToSuperview().offset(-40)
-            make.bottom.equalTo(safeAreaLayoutGuide).offset(-50)
+        cancelButton.snp.makeConstraints {
+            $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20)
+            $0.left.equalToSuperview().offset(40)
+            $0.right.equalToSuperview().offset(-40)
+            $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50)
         }
     }
 
diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift
index e36e5d5b1dd97cb735ac84f9d1c5c56fdb19b5c3..ba2e643cafc4dcca652ac7daac8ad14d22f52d15 100644
--- a/Sources/RestoreFeature/Views/RestoreView.swift
+++ b/Sources/RestoreFeature/Views/RestoreView.swift
@@ -39,36 +39,36 @@ final class RestoreView: UIView {
         bottomStackView.addArrangedSubview(cancelButton)
         bottomStackView.addArrangedSubview(backButton)
 
-        titleLabel.snp.makeConstraints { make in
-            make.top.equalTo(safeAreaLayoutGuide).offset(20)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-38)
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(20)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-38)
         }
 
-        subtitleLabel.snp.makeConstraints { make in
-            make.top.equalTo(titleLabel.snp.bottom).offset(20)
-            make.left.equalToSuperview().offset(38)
-            make.right.equalToSuperview().offset(-38)
+        subtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(20)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-38)
         }
 
-        detailsView.snp.makeConstraints { make in
-            make.top.equalTo(subtitleLabel.snp.bottom).offset(40)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
+        detailsView.snp.makeConstraints {
+            $0.top.equalTo(subtitleLabel.snp.bottom).offset(40)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
         }
 
-        progressView.snp.makeConstraints { make in
-            make.top.greaterThanOrEqualTo(detailsView.snp.bottom)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.lessThanOrEqualTo(bottomStackView.snp.top)
+        progressView.snp.makeConstraints {
+            $0.top.greaterThanOrEqualTo(detailsView.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.lessThanOrEqualTo(bottomStackView.snp.top)
         }
 
-        bottomStackView.snp.makeConstraints { make in
-            make.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10)
-            make.left.equalToSuperview().offset(40)
-            make.right.equalToSuperview().offset(-40)
-            make.bottom.equalTo(safeAreaLayoutGuide).offset(-20)
+        bottomStackView.snp.makeConstraints {
+            $0.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10)
+            $0.left.equalToSuperview().offset(40)
+            $0.right.equalToSuperview().offset(-40)
+            $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20)
         }
     }
 
@@ -151,6 +151,8 @@ private extension CloudService {
             return Localized.Backup.iCloud
         case .dropbox:
             return Localized.Backup.dropbox
+        case .sftp:
+            return Localized.Backup.sftp
         }
     }
 
@@ -162,6 +164,8 @@ private extension CloudService {
             return Asset.restoreIcloud.image
         case .dropbox:
             return Asset.restoreDropbox.image
+        case .sftp:
+            return Asset.restoreSFTP.image
         }
     }
 }
diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift b/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift
new file mode 100644
index 0000000000000000000000000000000000000000..389cdd46fc0c0136247e09f3dbd900ec265a7858
--- /dev/null
+++ b/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift
@@ -0,0 +1,54 @@
+import Shout
+import Socket
+import Keychain
+import Foundation
+import DependencyInjection
+
+public struct SFTPAuthenticator {
+    public var authenticate: (String, String, String) throws -> Void
+
+    public func callAsFunction(host: String, username: String, password: String) throws {
+        try authenticate(host, username, password)
+    }
+}
+
+extension SFTPAuthenticator {
+    static let mock = SFTPAuthenticator { host, username, password in
+        print("^^^ Requested authentication on sftp service.")
+        print("^^^ Host: \(host)")
+        print("^^^ Username: \(username)")
+        print("^^^ Password: \(password)")
+    }
+
+    static let live = SFTPAuthenticator { host, username, password in
+        do {
+            try SSH.connect(
+                host: host,
+                port: 22,
+                username: username,
+                authMethod: SSHPassword(password)) { ssh in
+                    _ = try ssh.openSftp()
+
+                    let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling
+                    try keychain.store(key: .host, value: host)
+                    try keychain.store(key: .pwd, value: password)
+                    try keychain.store(key: .username, value: username)
+                }
+        } catch {
+            if let error = error as? SSHError {
+                print(error.kind)
+                print(error.message)
+                print(error.description)
+            } else if let error = error as? Socket.Error {
+                print(error.errorCode)
+                print(error.description)
+                print(error.errorReason)
+                print(error.localizedDescription)
+            } else {
+                print(error.localizedDescription)
+            }
+
+            throw error
+        }
+    }
+}
diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6a435df0051a2755f2fb73522f7be92a24c4dd77
--- /dev/null
+++ b/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift
@@ -0,0 +1,56 @@
+import Shout
+import Socket
+import Keychain
+import Foundation
+import DependencyInjection
+
+public typealias SFTPDownloadResult = (Result<Data, Error>) -> Void
+
+public struct SFTPDownloader {
+    public var download: (String, @escaping SFTPDownloadResult) -> Void
+
+    public func callAsFunction(path: String, completion: @escaping SFTPDownloadResult) {
+        download(path, completion)
+    }
+}
+
+extension SFTPDownloader {
+    static let mock = SFTPDownloader { path, _ in
+        print("^^^ Requested backup download on sftp service.")
+        print("^^^ Path: \(path)")
+    }
+
+    static let live = SFTPDownloader { path, completion in
+        DispatchQueue.global().async {
+            do {
+                let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling
+                let host = try keychain.get(key: .host)
+                let password = try keychain.get(key: .pwd)
+                let username = try keychain.get(key: .username)
+
+                let ssh = try SSH(host: host!, port: 22)
+                try ssh.authenticate(username: username!, password: password!)
+                let sftp = try ssh.openSftp()
+
+                let localURL = FileManager.default
+                    .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
+                    .appendingPathComponent("sftp")
+
+                try sftp.download(remotePath: path, localURL: localURL)
+
+                let data = try Data(contentsOf: localURL)
+                completion(.success(data))
+            } catch {
+                completion(.failure(error))
+
+                if var error = error as? SSHError {
+                    print(error.kind)
+                    print(error.message)
+                    print(error.description)
+                } else {
+                    print(error.localizedDescription)
+                }
+            }
+        }
+    }
+}
diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift b/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a27df80ffe8e9be6f41f09185e9962351c44cff9
--- /dev/null
+++ b/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift
@@ -0,0 +1,68 @@
+import Shout
+import Socket
+import Models
+import Keychain
+import Foundation
+import DependencyInjection
+
+public typealias SFTPFetchResult = (Result<RestoreSettings?, Error>) -> Void
+
+public struct SFTPFetcher {
+    public var fetch: (@escaping SFTPFetchResult) -> Void
+
+    public func callAsFunction(completion: @escaping SFTPFetchResult) {
+        fetch(completion)
+    }
+}
+
+extension SFTPFetcher {
+    static let mock = SFTPFetcher { _ in
+        print("^^^ Requested backup metadata on sftp service.")
+    }
+
+    static let live = SFTPFetcher { completion in
+        DispatchQueue.global().async {
+            do {
+                let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling
+                let host = try keychain.get(key: .host)
+                let password = try keychain.get(key: .pwd)
+                let username = try keychain.get(key: .username)
+
+                let ssh = try SSH(host: host!, port: 22)
+                try ssh.authenticate(username: username!, password: password!)
+                let sftp = try ssh.openSftp()
+
+                if let files = try? sftp.listFiles(in: "backup"),
+                   let backup = files.filter({ file in file.0 == "backup.xxm" }).first {
+                    completion(.success(.init(
+                        backup: .init(
+                            id: "backup/backup.xxm",
+                            date: backup.value.lastModified,
+                            size: Float(backup.value.size)
+                        ),
+                        cloudService: .sftp
+                    )))
+
+                    return
+                }
+
+                completion(.success(nil))
+            } catch {
+                if let error = error as? SSHError {
+                    print(error.kind)
+                    print(error.message)
+                    print(error.description)
+                } else if let error = error as? Socket.Error {
+                    print(error.errorCode)
+                    print(error.description)
+                    print(error.errorReason)
+                    print(error.localizedDescription)
+                } else {
+                    print(error.localizedDescription)
+                }
+
+                completion(.failure(error))
+            }
+        }
+    }
+}
diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fee691d1b7e1669226fecfad6ecc37c97e64f9c5
--- /dev/null
+++ b/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift
@@ -0,0 +1,69 @@
+import Shout
+import Socket
+import Models
+import Keychain
+import Foundation
+import DependencyInjection
+
+public typealias SFTPUploadResult = (Result<Backup, Error>) -> Void
+
+public struct SFTPUploader {
+    public var upload: (URL, @escaping SFTPUploadResult) -> Void
+
+    public func callAsFunction(url: URL, completion: @escaping SFTPUploadResult) {
+        upload(url, completion)
+    }
+}
+
+extension SFTPUploader {
+    static let mock = SFTPUploader(
+        upload: { url, _ in
+            print("^^^ Requested upload on sftp service")
+            print("^^^ URL path: \(url.path)")
+        }
+    )
+
+    static let live = SFTPUploader { url, completion in
+        DispatchQueue.global().async {
+            do {
+                let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling
+                let host = try keychain.get(key: .host)
+                let password = try keychain.get(key: .pwd)
+                let username = try keychain.get(key: .username)
+
+                let ssh = try SSH(host: host!, port: 22)
+                try ssh.authenticate(username: username!, password: password!)
+                let sftp = try ssh.openSftp()
+
+                let data = try Data(contentsOf: url)
+
+                if (try? sftp.listFiles(in: "backup")) == nil {
+                    try sftp.createDirectory("backup")
+                }
+
+                try sftp.upload(data: data, remotePath: "backup/backup.xxm")
+
+                completion(.success(.init(
+                    id: "backup/backup.xxm",
+                    date: Date(),
+                    size: Float(data.count)
+                )))
+            } catch {
+                if let error = error as? SSHError {
+                    print(error.kind)
+                    print(error.message)
+                    print(error.description)
+                } else if let error = error as? Socket.Error {
+                    print(error.errorCode)
+                    print(error.description)
+                    print(error.errorReason)
+                    print(error.localizedDescription)
+                } else {
+                    print(error.localizedDescription)
+                }
+
+                completion(.failure(error))
+            }
+        }
+    }
+}
diff --git a/Sources/SFTPFeature/SFTPController.swift b/Sources/SFTPFeature/SFTPController.swift
new file mode 100644
index 0000000000000000000000000000000000000000..f80908b9cdf6106bacef3c2fa9f6c4eac46e8145
--- /dev/null
+++ b/Sources/SFTPFeature/SFTPController.swift
@@ -0,0 +1,96 @@
+import HUD
+import UIKit
+import Combine
+import DependencyInjection
+import ScrollViewController
+
+public final class SFTPController: UIViewController {
+    @Dependency private var hud: HUD
+
+    lazy private var screenView = SFTPView()
+    lazy private var scrollViewController = ScrollViewController()
+
+    private let completion: () -> Void
+    private let viewModel = SFTPViewModel()
+    private var cancellables = Set<AnyCancellable>()
+
+    public init(_ completion: @escaping () -> Void) {
+        self.completion = completion
+        super.init(nibName: nil, bundle: nil)
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+        setupScrollView()
+        setupNavigationBar()
+        setupBindings()
+    }
+
+    private func setupScrollView() {
+        scrollViewController.scrollView.backgroundColor = .white
+
+        addChild(scrollViewController)
+        view.addSubview(scrollViewController.view)
+        scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() }
+        scrollViewController.didMove(toParent: self)
+        scrollViewController.contentView = screenView
+    }
+
+    private func setupNavigationBar() {
+        navigationItem.backButtonTitle = ""
+
+        let back = UIButton.back()
+        back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside)
+
+        navigationItem.leftBarButtonItem = UIBarButtonItem(
+            customView: UIStackView(arrangedSubviews: [back])
+        )
+    }
+
+    private func setupBindings() {
+        viewModel.hudPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [hud] in hud.update(with: $0) }
+            .store(in: &cancellables)
+
+        viewModel.authPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in completion() }
+            .store(in: &cancellables)
+
+        screenView.hostField
+            .textPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in viewModel.didEnterHost($0) }
+            .store(in: &cancellables)
+
+        screenView.usernameField
+            .textPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in viewModel.didEnterUsername($0) }
+            .store(in: &cancellables)
+
+        screenView.passwordField
+            .textPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in viewModel.didEnterPassword($0) }
+            .store(in: &cancellables)
+
+        viewModel.statePublisher
+            .receive(on: DispatchQueue.main)
+            .map(\.isButtonEnabled)
+            .sink { [unowned self] in screenView.loginButton.isEnabled = $0 }
+            .store(in: &cancellables)
+
+        screenView.loginButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in viewModel.didTapLogin() }
+            .store(in: &cancellables)
+    }
+
+    @objc private func didTapBack() {
+        navigationController?.popViewController(animated: true)
+    }
+}
diff --git a/Sources/SFTPFeature/SFTPService.swift b/Sources/SFTPFeature/SFTPService.swift
new file mode 100644
index 0000000000000000000000000000000000000000..f1a908564df810ed2aeaa1b4af358b367253f84b
--- /dev/null
+++ b/Sources/SFTPFeature/SFTPService.swift
@@ -0,0 +1,47 @@
+import UIKit
+import Keychain
+import Presentation
+import DependencyInjection
+
+public typealias SFTPAuthorizationParams = (UIViewController, () -> Void)
+
+public struct SFTPService {
+    public var isAuthorized: () -> Bool
+    public var fetchMetadata: SFTPFetcher
+    public var uploadBackup: SFTPUploader
+    public var authorizeFlow: (SFTPAuthorizationParams) -> Void
+    public var authenticate: SFTPAuthenticator
+    public var downloadBackup: SFTPDownloader
+}
+
+public extension SFTPService {
+    static var mock = SFTPService(
+        isAuthorized: { true },
+        fetchMetadata: .mock,
+        uploadBackup: .mock,
+        authorizeFlow: { (_, completion) in completion() },
+        authenticate: .mock,
+        downloadBackup: .mock
+    )
+
+    static var live = SFTPService(
+        isAuthorized: {
+            if let keychain = try? DependencyInjection.Container.shared.resolve() as KeychainHandling,
+               let pwd = try? keychain.get(key: .pwd),
+               let host = try? keychain.get(key: .host),
+               let username = try? keychain.get(key: .username) {
+                return true
+            }
+
+            return false
+        },
+        fetchMetadata: .live,
+        uploadBackup: .live ,
+        authorizeFlow: { controller, completion in
+            var pushPresenter: Presenting = PushPresenter()
+            pushPresenter.present(SFTPController(completion), from: controller)
+        },
+        authenticate: .live,
+        downloadBackup: .live
+    )
+}
diff --git a/Sources/SFTPFeature/SFTPView.swift b/Sources/SFTPFeature/SFTPView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3e62e4caeb0eca188fd1dad4db740545f0b3f045
--- /dev/null
+++ b/Sources/SFTPFeature/SFTPView.swift
@@ -0,0 +1,76 @@
+import UIKit
+import Shared
+import InputField
+
+final class SFTPView: UIView {
+    let titleLabel = UILabel()
+    let subtitleLabel = UILabel()
+    let hostField = OutlinedInputField()
+    let usernameField = OutlinedInputField()
+    let passwordField = OutlinedInputField()
+    let loginButton = CapsuleButton()
+    let stackView = UIStackView()
+
+    init() {
+        super.init(frame: .zero)
+        backgroundColor = Asset.neutralWhite.color
+
+        titleLabel.textColor = Asset.neutralDark.color
+        titleLabel.text = Localized.AccountRestore.Sftp.title
+        titleLabel.font = Fonts.Mulish.bold.font(size: 24.0)
+
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.alignment = .left
+        paragraph.lineHeightMultiple = 1.15
+
+        let attString = NSAttributedString(
+            string: Localized.AccountRestore.Sftp.subtitle,
+            attributes: [
+                .foregroundColor: Asset.neutralBody.color,
+                .font: Fonts.Mulish.regular.font(size: 16.0) as Any,
+                .paragraphStyle: paragraph
+            ])
+
+        subtitleLabel.numberOfLines = 0
+        subtitleLabel.attributedText = attString
+
+        hostField.setup(title: Localized.AccountRestore.Sftp.host)
+        usernameField.setup(title: Localized.AccountRestore.Sftp.username)
+        passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true)
+
+        loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login)
+
+        stackView.spacing = 30
+        stackView.axis = .vertical
+        stackView.distribution = .fillEqually
+        stackView.addArrangedSubview(hostField)
+        stackView.addArrangedSubview(usernameField)
+        stackView.addArrangedSubview(passwordField)
+        stackView.addArrangedSubview(loginButton)
+
+        addSubview(titleLabel)
+        addSubview(subtitleLabel)
+        addSubview(stackView)
+
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(15)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
+        }
+
+        subtitleLabel.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(8)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-41)
+        }
+
+        stackView.snp.makeConstraints {
+            $0.top.equalTo(subtitleLabel.snp.bottom).offset(28)
+            $0.left.equalToSuperview().offset(38)
+            $0.right.equalToSuperview().offset(-38)
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/SFTPFeature/SFTPViewModel.swift b/Sources/SFTPFeature/SFTPViewModel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e64536bfd96277642d25ddb13f5c81570836a22b
--- /dev/null
+++ b/Sources/SFTPFeature/SFTPViewModel.swift
@@ -0,0 +1,77 @@
+import HUD
+import Combine
+import Foundation
+import DependencyInjection
+
+struct SFTPViewState {
+    var host: String = ""
+    var username: String = ""
+    var password: String = ""
+    var isButtonEnabled: Bool = false
+}
+
+final class SFTPViewModel {
+    @Dependency private var service: SFTPService
+
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
+
+    var statePublisher: AnyPublisher<SFTPViewState, Never> {
+        stateSubject.eraseToAnyPublisher()
+    }
+
+    var authPublisher: AnyPublisher<Void, Never> {
+        authSubject.eraseToAnyPublisher()
+    }
+
+    private let authSubject = PassthroughSubject<Void, Never>()
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
+    private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init())
+
+    func didEnterHost(_ string: String) {
+        stateSubject.value.host = string
+        validate()
+    }
+
+    func didEnterUsername(_ string: String) {
+        stateSubject.value.username = string
+        validate()
+    }
+
+    func didEnterPassword(_ string: String) {
+        stateSubject.value.password = string
+        validate()
+    }
+
+    func didTapLogin() {
+        hudSubject.send(.on)
+
+        let host = stateSubject.value.host
+        let username = stateSubject.value.username
+        let password = stateSubject.value.password
+
+        DispatchQueue.global().async { [weak self] in
+            guard let self = self else { return }
+            do {
+                try self.service.authenticate(
+                    host: host,
+                    username: username,
+                    password: password
+                )
+
+                self.hudSubject.send(.none)
+                self.authSubject.send(())
+            } catch {
+                self.hudSubject.send(.error(.init(with: error)))
+            }
+        }
+    }
+
+    private func validate() {
+        stateSubject.value.isButtonEnabled =
+        !stateSubject.value.host.isEmpty &&
+        !stateSubject.value.username.isEmpty &&
+        !stateSubject.value.password.isEmpty
+    }
+}
diff --git a/Sources/ScanFeature/Views/ScanContainerView.swift b/Sources/ScanFeature/Views/ScanContainerView.swift
index b9b35cef12d02a3a8ea88ead84692938e31ed773..7e5eeee37a1a32765a33735ec8e1c65a05473dd5 100644
--- a/Sources/ScanFeature/Views/ScanContainerView.swift
+++ b/Sources/ScanFeature/Views/ScanContainerView.swift
@@ -3,7 +3,7 @@ import Shared
 
 final class ScanContainerView: UIView {
     let scrollView = UIScrollView()
-    let segmentedControl = SegmentedControl()
+    let segmentedControl = ScanSegmentedControl()
 
     init() {
         super.init(frame: .zero)
diff --git a/Sources/ScanFeature/Views/SegmentedControl.swift b/Sources/ScanFeature/Views/ScanSegmentedControl.swift
similarity index 93%
rename from Sources/ScanFeature/Views/SegmentedControl.swift
rename to Sources/ScanFeature/Views/ScanSegmentedControl.swift
index 11faf5a55448b39530fc66a71610a8b49852dfd0..f3fd72acf3326b05b8057e5a9fe0948aa113688c 100644
--- a/Sources/ScanFeature/Views/SegmentedControl.swift
+++ b/Sources/ScanFeature/Views/ScanSegmentedControl.swift
@@ -2,15 +2,15 @@ import UIKit
 import Shared
 import SnapKit
 
-final class SegmentedControl: UIView {
+final class ScanSegmentedControl: UIView {
     private let trackHeight = 2.0
     private let numberOfTabs = 2.0
     private let trackView = UIView()
     private let stackView = UIStackView()
     private var leftConstraint: Constraint?
     private let trackIndicatorView = UIView()
-    private(set) var leftButton = SegmentedControlButton()
-    private(set) var rightButton = SegmentedControlButton()
+    private(set) var leftButton = ScanSegmentedControlButton()
+    private(set) var rightButton = ScanSegmentedControlButton()
 
     init() {
         super.init(frame: .zero)
diff --git a/Sources/ScanFeature/Views/SegmentedControlButton.swift b/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift
similarity index 95%
rename from Sources/ScanFeature/Views/SegmentedControlButton.swift
rename to Sources/ScanFeature/Views/ScanSegmentedControlButton.swift
index c23f16c1539338457198024f84216ffffdca1590..b9e711c614b239dfb55158c530adbe121f0ca691 100644
--- a/Sources/ScanFeature/Views/SegmentedControlButton.swift
+++ b/Sources/ScanFeature/Views/ScanSegmentedControlButton.swift
@@ -1,7 +1,7 @@
 import UIKit
 import Shared
 
-final class SegmentedControlButton: UIControl {
+final class ScanSegmentedControlButton: UIControl {
     private let titleLabel = UILabel()
     private let imageView = UIImageView()
 
diff --git a/Sources/SearchFeature/Controllers/CameraController.swift b/Sources/SearchFeature/Controllers/CameraController.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7473920844e51bf6b77634818ee56282cb00b5d2
--- /dev/null
+++ b/Sources/SearchFeature/Controllers/CameraController.swift
@@ -0,0 +1,61 @@
+import Combine
+import AVFoundation
+
+final class CameraController: NSObject {
+    var dataPublisher: AnyPublisher<Data, Never> {
+        dataSubject
+            .receive(on: DispatchQueue.main)
+            .eraseToAnyPublisher()
+    }
+
+    lazy var previewLayer: CALayer = {
+        let layer = AVCaptureVideoPreviewLayer(session: session)
+        layer.videoGravity = .resizeAspectFill
+        return layer
+    }()
+
+    private let session = AVCaptureSession()
+    private let metadataOutput = AVCaptureMetadataOutput()
+    private let dataSubject = PassthroughSubject<Data, Never>()
+
+    override init() {
+        super.init()
+        setupCameraDevice()
+    }
+
+    func start() {
+        guard session.isRunning == false else { return }
+        session.startRunning()
+    }
+
+    func stop() {
+        guard session.isRunning == true else { return }
+        session.stopRunning()
+    }
+
+    private func setupCameraDevice() {
+        if let captureDevice = AVCaptureDevice.default(for: .video),
+           let input = try? AVCaptureDeviceInput(device: captureDevice) {
+
+            if session.canAddInput(input) && session.canAddOutput(metadataOutput) {
+                session.addInput(input)
+                session.addOutput(metadataOutput)
+            }
+
+            metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
+            metadataOutput.metadataObjectTypes = [.qr]
+        }
+    }
+
+    func metadataOutput(
+        _ output: AVCaptureMetadataOutput,
+        didOutput metadataObjects: [AVMetadataObject],
+        from connection: AVCaptureConnection
+    ) {
+        guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
+              let data = object.stringValue?.data(using: .nonLossyASCII), object.type == .qr else { return }
+        dataSubject.send(data)
+    }
+}
+
+extension CameraController: AVCaptureMetadataOutputObjectsDelegate {}
diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ad85b25640a1f1a5e418bf9974b2a0656021d202
--- /dev/null
+++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift
@@ -0,0 +1,185 @@
+import UIKit
+import Theme
+import Shared
+import Combine
+import XXModels
+import DrawerFeature
+import DependencyInjection
+
+public final class SearchContainerController: UIViewController {
+    @Dependency var coordinator: SearchCoordinating
+    @Dependency var statusBarController: StatusBarStyleControlling
+
+    lazy private var screenView = SearchContainerView()
+
+    private var contentOffset: CGPoint?
+    private var cancellables = Set<AnyCancellable>()
+    private let viewModel = SearchContainerViewModel()
+    private let leftController = SearchLeftController()
+    private let rightController = SearchRightController()
+    private var drawerCancellables = Set<AnyCancellable>()
+
+    public override func loadView() {
+        view = screenView
+        embedControllers()
+    }
+
+    public override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        statusBarController.style.send(.darkContent)
+        navigationController?.navigationBar.customize(
+            backgroundColor: Asset.neutralWhite.color
+        )
+
+        if let contentOffset = self.contentOffset {
+            screenView.scrollView.setContentOffset(contentOffset, animated: true)
+        }
+    }
+
+    public override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        contentOffset = screenView.scrollView.contentOffset
+    }
+
+    public override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        viewModel.didAppear()
+        rightController.viewModel.viewWillAppear()
+    }
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+        setupNavigationBar()
+        setupBindings()
+    }
+
+    private func setupNavigationBar() {
+        navigationItem.backButtonTitle = " "
+
+        let titleLabel = UILabel()
+        titleLabel.text = Localized.Ud.title
+        titleLabel.textColor = Asset.neutralActive.color
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
+
+        let backButton = UIButton.back()
+        backButton.addTarget(self, action: #selector(didTapBack), for: .touchUpInside)
+
+        navigationItem.leftBarButtonItem = UIBarButtonItem(
+            customView: UIStackView(arrangedSubviews: [backButton, titleLabel])
+        )
+    }
+
+    private func setupBindings() {
+        screenView.segmentedControl
+            .actionPublisher
+            .removeDuplicates()
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                if $0 == .qr {
+                    let point = CGPoint(x: screenView.frame.width, y: 0.0)
+                    screenView.scrollView.setContentOffset(point, animated: true)
+                    leftController.endEditing()
+                } else {
+                    screenView.scrollView.setContentOffset(.zero, animated: true)
+                    leftController.viewModel.didSelectItem($0)
+                }
+            }.store(in: &cancellables)
+
+        viewModel.coverTrafficPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in presentCoverTrafficDrawer() }
+            .store(in: &cancellables)
+    }
+
+    @objc private func didTapBack() {
+        navigationController?.popViewController(animated: true)
+    }
+
+    private func embedControllers() {
+        addChild(leftController)
+        addChild(rightController)
+
+        screenView.scrollView.addSubview(leftController.view)
+        screenView.scrollView.addSubview(rightController.view)
+
+        leftController.view.snp.makeConstraints {
+            $0.top.equalTo(screenView.segmentedControl.snp.bottom)
+            $0.width.equalTo(screenView)
+            $0.bottom.equalTo(screenView)
+            $0.left.equalToSuperview()
+            $0.right.equalTo(rightController.view.snp.left)
+        }
+
+        rightController.view.snp.makeConstraints {
+            $0.top.equalTo(screenView.segmentedControl.snp.bottom)
+            $0.width.equalTo(screenView)
+            $0.bottom.equalTo(screenView)
+        }
+
+        leftController.didMove(toParent: self)
+        rightController.didMove(toParent: self)
+    }
+}
+
+extension SearchContainerController {
+    private func presentCoverTrafficDrawer() {
+        let enableButton = CapsuleButton()
+        enableButton.set(
+            style: .brandColored,
+            title: Localized.ChatList.Traffic.positive
+        )
+
+        let dismissButton = CapsuleButton()
+        dismissButton.set(
+            style: .seeThrough,
+            title: Localized.ChatList.Traffic.negative
+        )
+
+        let drawer = DrawerController(with: [
+            DrawerText(
+                font: Fonts.Mulish.bold.font(size: 26.0),
+                text: Localized.ChatList.Traffic.title,
+                color: Asset.neutralActive.color,
+                alignment: .left,
+                spacingAfter: 19
+            ),
+            DrawerText(
+                font: Fonts.Mulish.regular.font(size: 16.0),
+                text: Localized.ChatList.Traffic.subtitle,
+                color: Asset.neutralBody.color,
+                alignment: .left,
+                lineHeightMultiple: 1.1,
+                spacingAfter: 39
+            ),
+            DrawerStack(
+                axis: .horizontal,
+                spacing: 20,
+                distribution: .fillEqually,
+                views: [enableButton, dismissButton]
+            )
+        ])
+
+        enableButton
+            .publisher(for: .touchUpInside)
+            .receive(on: DispatchQueue.main)
+            .sink {
+                drawer.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
+                    self.viewModel.didEnableCoverTraffic()
+                }
+            }.store(in: &drawerCancellables)
+
+        dismissButton
+            .publisher(for: .touchUpInside)
+            .receive(on: DispatchQueue.main)
+            .sink {
+                drawer.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
+                }
+            }.store(in: &drawerCancellables)
+
+        coordinator.toDrawer(drawer, from: self)
+    }
+}
diff --git a/Sources/SearchFeature/Controllers/SearchController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift
similarity index 54%
rename from Sources/SearchFeature/Controllers/SearchController.swift
rename to Sources/SearchFeature/Controllers/SearchLeftController.swift
index 795c60009cb827d2aacca5d3e27eb89e7114b26b..bbfabd2836990bb00c86e01a09527bdbac8b1145 100644
--- a/Sources/SearchFeature/Controllers/SearchController.swift
+++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift
@@ -1,259 +1,312 @@
 import HUD
-import Theme
 import UIKit
 import Shared
-import Models
 import Combine
-import Defaults
 import XXModels
+import Defaults
 import Countries
 import DrawerFeature
 import DependencyInjection
-import ScrollViewController
 
-public final class SearchController: UIViewController {
+final class SearchLeftController: UIViewController {
+    @Dependency private var hud: HUD
+    @Dependency private var coordinator: SearchCoordinating
+
     @KeyObject(.email, defaultValue: nil) var email: String?
     @KeyObject(.phone, defaultValue: nil) var phone: String?
     @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool
     @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool
 
-    @Dependency private var hud: HUDType
-    @Dependency private var coordinator: SearchCoordinating
-    @Dependency private var statusBarController: StatusBarStyleControlling
-
-    lazy private var tableController = SearchTableController(viewModel)
-    lazy private var screenView = SearchView {
-        let actionButton = CapsuleButton()
-        actionButton.set(
-            style: .seeThrough,
-            title: Localized.ContactSearch.Placeholder.Drawer.action
-        )
-
-        let drawer = DrawerController(with: [
-            DrawerText(
-                font: Fonts.Mulish.bold.font(size: 26.0),
-                text: Localized.ContactSearch.Placeholder.Drawer.title,
-                color: Asset.neutralActive.color,
-                alignment: .left,
-                spacingAfter: 19
-            ),
-            DrawerLinkText(
-                text: Localized.ContactSearch.Placeholder.Drawer.subtitle,
-                urlString: "https://links.xx.network/adrp",
-                spacingAfter: 37
-            ),
-            DrawerStack(views: [
-                actionButton,
-                FlexibleSpace()
-            ])
-        ])
-
-        actionButton.publisher(for: .touchUpInside)
-            .receive(on: DispatchQueue.main)
-            .sink {
-                drawer.dismiss(animated: true) { [weak self] in
-                    guard let self = self else { return }
-                    self.drawerCancellables.removeAll()
-                }
-            }.store(in: &self.drawerCancellables)
+    lazy private var screenView = SearchLeftView()
 
-        self.coordinator.toDrawer(drawer, from: self)
-    }
+    private var dataSource: SearchDiffableDataSource!
+    private(set) var viewModel = SearchLeftViewModel()
+    private var drawerCancellables = Set<AnyCancellable>()
+    private let adrpURLString = "https://links.xx.network/adrp"
 
-    private let viewModel = SearchViewModel()
     private var cancellables = Set<AnyCancellable>()
-    private var drawerCancellables = Set<AnyCancellable>()
+    private var hudCancellables = Set<AnyCancellable>()
 
-    public override func loadView() {
+    override func loadView() {
         view = screenView
     }
 
-    public override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-        statusBarController.style.send(.darkContent)
-
-        navigationController?.navigationBar.customize(
-            backgroundColor: Asset.neutralWhite.color,
-            shadowColor: Asset.neutralDisabled.color
-        )
-    }
-
-    public override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
-        viewModel.didAppear()
-    }
-
-    public override func viewDidLoad() {
+    override func viewDidLoad() {
         super.viewDidLoad()
-        setupNavigationBar()
         setupTableView()
         setupBindings()
-        setupFilterBindings()
     }
 
-    private func setupTableView() {
-        addChild(tableController)
-        screenView.addSubview(tableController.view)
+    func endEditing() {
+        screenView.inputField.endEditing(true)
+    }
 
-        tableController.view.snp.makeConstraints {
-            $0.top.equalTo(screenView.stack.snp.bottom).offset(20)
-            $0.left.bottom.right.equalToSuperview()
-        }
+    private func setupTableView() {
+        screenView.tableView.separatorStyle = .none
+        screenView.tableView.tableFooterView = UIView()
+        screenView.tableView.register(AvatarCell.self)
+        screenView.tableView.dataSource = dataSource
+        screenView.tableView.delegate = self
+
+        dataSource = SearchDiffableDataSource(
+            tableView: screenView.tableView
+        ) { tableView, indexPath, item in
+            let contact: Contact
+            let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: AvatarCell.self)
+
+            let h1Text: String
+            var h2Text: String?
+
+            switch item {
+            case .stranger(let stranger):
+                contact = stranger
+                h1Text = stranger.username ?? ""
+
+                if stranger.authStatus == .requested {
+                    h2Text = "Request pending"
+                } else if stranger.authStatus == .requestFailed {
+                    h2Text = "Request failed"
+                }
 
-        tableController.didMove(toParent: self)
-        tableController.tableView.delegate = self
-        screenView.bringSubviewToFront(screenView.empty)
-        screenView.bringSubviewToFront(screenView.placeholder)
-    }
+            case .connection(let connection):
+                contact = connection
+                h1Text = (connection.nickname ?? contact.username) ?? ""
 
-    private func setupNavigationBar() {
-        navigationItem.backButtonTitle = " "
+                if connection.nickname != nil {
+                    h2Text = contact.username ?? ""
+                }
+            }
 
-        let titleLabel = UILabel()
-        titleLabel.text = Localized.ContactSearch.title
-        titleLabel.textColor = Asset.neutralActive.color
-        titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
+            cell.setup(
+                title: h1Text,
+                image: contact.photo,
+                firstSubtitle: h2Text,
+                secondSubtitle: contact.email,
+                thirdSubtitle: contact.phone,
+                showSeparator: false,
+                sent: contact.authStatus == .requested
+            )
 
-        let backButton = UIButton.back()
-        backButton.addTarget(self, action: #selector(didTapBack), for: .touchUpInside)
+            cell.didTapStateButton = { [weak self] in
+                guard let self = self else { return }
+                self.viewModel.didTapResend(contact: contact)
+                cell.updateToResent()
+            }
 
-        navigationItem.leftBarButtonItem = UIBarButtonItem(
-            customView: UIStackView(arrangedSubviews: [backButton, titleLabel])
-        )
+            return cell
+        }
     }
 
     private func setupBindings() {
-        viewModel.successPublisher
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in presentSucessDrawerFor(contact: $0) }
-            .store(in: &cancellables)
-
         viewModel.hudPublisher
+            .removeDuplicates()
             .receive(on: DispatchQueue.main)
-            .sink { [hud] in hud.update(with: $0) }
+            .sink { [unowned self] in
+                hud.update(with: $0)
+
+                if case .onAction = $0, let hudBtn = hud.actionButton {
+                    hudBtn.publisher(for: .touchUpInside)
+                        .receive(on: DispatchQueue.main)
+                        .sink { [unowned self] in viewModel.didTapCancelSearch() }
+                        .store(in: &self.hudCancellables)
+                } else {
+                    hudCancellables.forEach { $0.cancel() }
+                    hudCancellables.removeAll()
+                }
+            }
             .store(in: &cancellables)
 
-        viewModel.coverTrafficPublisher
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in presentCoverTrafficDrawer() }
-            .store(in: &cancellables)
 
-        viewModel
-            .itemsRelay
+        viewModel.statePublisher
+            .map(\.item)
             .removeDuplicates()
-            .map(\.count)
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in screenView.empty.isHidden = $0 > 0 }
+            .sink { [unowned self] in screenView.updateUIForItem(item: $0) }
             .store(in: &cancellables)
 
-        viewModel.placeholderPublisher
+        viewModel.statePublisher
+            .map(\.country)
+            .removeDuplicates()
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in screenView.placeholder.isHidden = !$0 }
+            .sink { [unowned self] in screenView.countryButton.setFlag($0.flag, prefix: $0.prefix) }
             .store(in: &cancellables)
 
         viewModel.statePublisher
-            .map(\.country)
-            .removeDuplicates()
+            .compactMap(\.snapshot)
             .receive(on: DispatchQueue.main)
             .sink { [unowned self] in
-                screenView.phoneInput.set(prefix: $0.prefixWithFlag)
-                screenView.phoneInput.update(placeholder: $0.example)
-            }
+                screenView.placeholderView.isHidden = true
+                screenView.emptyView.isHidden = $0.numberOfItems != 0
+
+                dataSource.apply($0, animatingDifferences: false)
+            }.store(in: &cancellables)
+
+        screenView.placeholderView
+            .infoPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in presentSearchDisclaimer() }
             .store(in: &cancellables)
 
-        screenView.input
+        screenView.countryButton
+            .publisher(for: .touchUpInside)
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                coordinator.toCountries(from: self) { [weak self] country in
+                    guard let self = self else { return }
+                    self.viewModel.didPick(country: country)
+                }
+            }.store(in: &cancellables)
+
+        screenView.inputField
             .textPublisher
-            .removeDuplicates()
-            .compactMap { $0 }
-            .sink { [unowned self] in viewModel.didInput($0) }
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in viewModel.didEnterInput($0) }
             .store(in: &cancellables)
 
-        screenView.input
+        screenView.inputField
             .returnPublisher
-            .sink { [unowned self] in viewModel.didTapSearch() }
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] _ in viewModel.didStartSearching() }
             .store(in: &cancellables)
 
-        screenView.phoneInput
-            .returnPublisher
-            .sink { [unowned self] in viewModel.didTapSearch() }
-            .store(in: &cancellables)
+        screenView.inputField
+            .isEditingPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] isEditing in
+                UIView.animate(withDuration: 0.25) {
+                    self.screenView.placeholderView.titleLabel.alpha = isEditing ? 0.1 : 1.0
+                    self.screenView.placeholderView.subtitleWithInfo.alpha = isEditing ? 0.1 : 1.0
+                }
+            }.store(in: &cancellables)
 
-        screenView
-            .phoneInput
-            .textPublisher
-            .removeDuplicates()
+        viewModel.successPublisher
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in viewModel.didInputPhone($0) }
+            .sink { [unowned self] in presentSucessDrawerFor(contact: $0) }
             .store(in: &cancellables)
+    }
+
+    private func presentSearchDisclaimer() {
+        let actionButton = CapsuleButton()
+        actionButton.set(
+            style: .seeThrough,
+            title: Localized.Ud.Placeholder.Drawer.action
+        )
+
+        let drawer = DrawerController(with: [
+            DrawerText(
+                font: Fonts.Mulish.bold.font(size: 26.0),
+                text: Localized.Ud.Placeholder.Drawer.title,
+                color: Asset.neutralActive.color,
+                alignment: .left,
+                spacingAfter: 19
+            ),
+            DrawerLinkText(
+                text: Localized.Ud.Placeholder.Drawer.subtitle,
+                urlString: adrpURLString,
+                spacingAfter: 37
+            ),
+            DrawerStack(views: [
+                actionButton,
+                FlexibleSpace()
+            ])
+        ])
 
-        screenView
-            .phoneInput
-            .codePublisher
+        actionButton.publisher(for: .touchUpInside)
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in
-                coordinator.toCountries(from: self) {
-                    self.viewModel.didChooseCountry($0)
+            .sink {
+                drawer.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
                 }
-            }.store(in: &cancellables)
+            }.store(in: &self.drawerCancellables)
+
+        coordinator.toDrawer(drawer, from: self)
     }
 
-    private func setupFilterBindings() {
-        screenView.username
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] _ in viewModel.didSelect(filter: .username) }
-            .store(in: &cancellables)
+    private func presentSucessDrawerFor(contact: Contact) {
+        var items: [DrawerItem] = []
 
-        screenView.phone
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] _ in viewModel.didSelect(filter: .phone) }
-            .store(in: &cancellables)
+        let drawerTitle = DrawerText(
+            font: Fonts.Mulish.extraBold.font(size: 26.0),
+            text: Localized.Ud.NicknameDrawer.title,
+            color: Asset.neutralDark.color,
+            spacingAfter: 20
+        )
 
-        screenView.email
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] _ in viewModel.didSelect(filter: .email) }
-            .store(in: &cancellables)
+        let drawerSubtitle = DrawerText(
+            font: Fonts.Mulish.regular.font(size: 16.0),
+            text: Localized.Ud.NicknameDrawer.subtitle,
+            color: Asset.neutralDark.color,
+            spacingAfter: 20
+        )
 
-        viewModel.statePublisher
-            .map(\.selectedFilter)
-            .removeDuplicates()
-            .sink { [unowned self] in screenView.alternateFieldsOver(filter: $0) }
-            .store(in: &cancellables)
+        items.append(contentsOf: [
+            drawerTitle,
+            drawerSubtitle
+        ])
 
-        viewModel.statePublisher
-            .map(\.selectedFilter)
-            .removeDuplicates()
-            .dropFirst()
-            .sink { [unowned self] in screenView.select(filter: $0) }
-            .store(in: &cancellables)
-    }
+        let drawerNicknameInput = DrawerInput(
+            placeholder: contact.username!,
+            validator: .init(
+                wrongIcon: .image(Asset.sharedError.image),
+                correctIcon: .image(Asset.sharedSuccess.image),
+                shouldAcceptPlaceholder: true
+            ),
+            spacingAfter: 29
+        )
 
-    @objc private func didTapBack() {
-        navigationController?.popViewController(animated: true)
-    }
+        items.append(drawerNicknameInput)
 
-    public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
-        let contact = viewModel.itemsRelay.value[indexPath.row]
+        let drawerSaveButton = DrawerCapsuleButton(
+            model: .init(
+                title: Localized.Ud.NicknameDrawer.save,
+                style: .brandColored
+            ), spacingAfter: 5
+        )
 
-        guard contact.authStatus == .stranger else {
-            coordinator.toContact(contact, from: self)
-            return
-        }
+        items.append(drawerSaveButton)
 
-        presentRequestDrawer(forContact: contact)
-    }
-}
+        let drawer = DrawerController(with: items)
+        var nickname: String?
+        var allowsSave = true
+
+        drawerNicknameInput.validationPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { allowsSave = $0 }
+            .store(in: &drawerCancellables)
 
-extension SearchController: UITableViewDelegate {}
+        drawerNicknameInput.inputPublisher
+            .receive(on: DispatchQueue.main)
+            .sink {
+                guard !$0.isEmpty else {
+                    nickname = contact.username
+                    return
+                }
 
-// MARK: - Contact Request Drawer
+                nickname = $0
+            }
+            .store(in: &drawerCancellables)
+
+        drawerSaveButton.action
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                guard allowsSave else { return }
+
+                drawer.dismiss(animated: true) {
+                    self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact)
+                }
+            }
+            .store(in: &drawerCancellables)
+
+        coordinator.toNicknameDrawer(drawer, from: self)
+    }
 
-extension SearchController {
     private func presentRequestDrawer(forContact contact: Contact) {
         var items: [DrawerItem] = []
 
         let drawerTitle = DrawerText(
             font: Fonts.Mulish.extraBold.font(size: 26.0),
-            text: Localized.ContactSearch.RequestDrawer.title,
+            text: Localized.Ud.RequestDrawer.title,
             color: Asset.neutralDark.color,
             spacingAfter: 20
         )
@@ -288,7 +341,7 @@ extension SearchController {
 
         if let email = email {
             let drawerEmail = DrawerSwitch(
-                title: Localized.ContactSearch.RequestDrawer.email,
+                title: Localized.Ud.RequestDrawer.email,
                 content: email,
                 spacingAfter: phone != nil ? 23 : 31,
                 isInitiallyOn: isSharingEmail
@@ -304,7 +357,7 @@ extension SearchController {
 
         if let phone = phone {
             let drawerPhone = DrawerSwitch(
-                title: Localized.ContactSearch.RequestDrawer.phone,
+                title: Localized.Ud.RequestDrawer.phone,
                 content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))",
                 spacingAfter: 31,
                 isInitiallyOn: isSharingPhone
@@ -320,14 +373,14 @@ extension SearchController {
 
         let drawerSendButton = DrawerCapsuleButton(
             model: .init(
-                title: Localized.ContactSearch.RequestDrawer.send,
+                title: Localized.Ud.RequestDrawer.send,
                 style: .brandColored
             ), spacingAfter: 5
         )
 
         let drawerCancelButton = DrawerCapsuleButton(
             model: .init(
-                title: Localized.ContactSearch.RequestDrawer.cancel,
+                title: Localized.Ud.RequestDrawer.cancel,
                 style: .simplestColoredBrand
             ), spacingAfter: 5
         )
@@ -350,149 +403,31 @@ extension SearchController {
 
         coordinator.toDrawer(drawer, from: self)
     }
-}
-
-// MARK: - Cover Traffic Drawer
 
-extension SearchController {
-    private func presentCoverTrafficDrawer() {
-        let enableButton = CapsuleButton()
-        enableButton.set(
-            style: .brandColored,
-            title: Localized.ChatList.Traffic.positive
-        )
-
-        let dismissButton = CapsuleButton()
-        dismissButton.set(
-            style: .seeThrough,
-            title: Localized.ChatList.Traffic.negative
-        )
-
-        let drawer = DrawerController(with: [
-            DrawerText(
-                font: Fonts.Mulish.bold.font(size: 26.0),
-                text: Localized.ChatList.Traffic.title,
-                color: Asset.neutralActive.color,
-                alignment: .left,
-                spacingAfter: 19
-            ),
-            DrawerText(
-                font: Fonts.Mulish.regular.font(size: 16.0),
-                text: Localized.ChatList.Traffic.subtitle,
-                color: Asset.neutralBody.color,
-                alignment: .left,
-                lineHeightMultiple: 1.1,
-                spacingAfter: 39
-            ),
-            DrawerStack(
-                axis: .horizontal,
-                spacing: 20,
-                distribution: .fillEqually,
-                views: [enableButton, dismissButton]
-            )
-        ])
-
-        enableButton
-            .publisher(for: .touchUpInside)
-            .receive(on: DispatchQueue.main)
-            .sink {
-                drawer.dismiss(animated: true) { [weak self] in
-                    guard let self = self else { return }
-                    self.drawerCancellables.removeAll()
-                    self.viewModel.didEnableCoverTraffic()
-                }
-            }.store(in: &drawerCancellables)
-
-        dismissButton
-            .publisher(for: .touchUpInside)
-            .receive(on: DispatchQueue.main)
-            .sink {
-                drawer.dismiss(animated: true) { [weak self] in
-                    guard let self = self else { return }
-                    self.drawerCancellables.removeAll()
-                }
-            }.store(in: &drawerCancellables)
-
-        coordinator.toDrawer(drawer, from: self)
-    }
 }
 
-extension SearchController {
-    private func presentSucessDrawerFor(contact: Contact) {
-        var items: [DrawerItem] = []
-
-        let drawerTitle = DrawerText(
-            font: Fonts.Mulish.extraBold.font(size: 26.0),
-            text: Localized.ContactSearch.NicknameDrawer.title,
-            color: Asset.neutralDark.color,
-            spacingAfter: 20
-        )
-
-        let drawerSubtitle = DrawerText(
-            font: Fonts.Mulish.regular.font(size: 16.0),
-            text: Localized.ContactSearch.NicknameDrawer.subtitle,
-            color: Asset.neutralDark.color,
-            spacingAfter: 20
-        )
-
-        items.append(contentsOf: [
-            drawerTitle,
-            drawerSubtitle
-        ])
-
-        let drawerNicknameInput = DrawerInput(
-            placeholder: contact.username!,
-            validator: .init(
-                wrongIcon: .image(Asset.sharedError.image),
-                correctIcon: .image(Asset.sharedSuccess.image),
-                shouldAcceptPlaceholder: true
-            ),
-            spacingAfter: 29
-        )
-
-        items.append(drawerNicknameInput)
-
-        let drawerSaveButton = DrawerCapsuleButton(
-            model: .init(
-                title: Localized.ContactSearch.NicknameDrawer.save,
-                style: .brandColored
-            ), spacingAfter: 5
-        )
-
-        items.append(drawerSaveButton)
-
-        let drawer = DrawerController(with: items)
-        var nickname: String?
-        var allowsSave = true
-
-        drawerNicknameInput.validationPublisher
-            .receive(on: DispatchQueue.main)
-            .sink { allowsSave = $0 }
-            .store(in: &drawerCancellables)
-
-        drawerNicknameInput.inputPublisher
-            .receive(on: DispatchQueue.main)
-            .sink {
-                guard !$0.isEmpty else {
-                    nickname = contact.username
-                    return
-                }
-
-                nickname = $0
+extension SearchLeftController: UITableViewDelegate {
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        if let item = dataSource.itemIdentifier(for: indexPath) {
+            switch item {
+            case .stranger(let contact):
+                didTap(contact: contact)
+            case .connection(let contact):
+                didTap(contact: contact)
             }
-            .store(in: &drawerCancellables)
+        }
+    }
 
-        drawerSaveButton.action
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in
-                guard allowsSave else { return }
+    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
+        (view as! UITableViewHeaderFooterView).textLabel?.textColor = Asset.neutralWeak.color
+    }
 
-                drawer.dismiss(animated: true) {
-                    self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact)
-                }
-            }
-            .store(in: &drawerCancellables)
+    private func didTap(contact: Contact) {
+        guard contact.authStatus == .stranger else {
+            coordinator.toContact(contact, from: self)
+            return
+        }
 
-        coordinator.toNicknameDrawer(drawer, from: self)
+        presentRequestDrawer(forContact: contact)
     }
 }
diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift
new file mode 100644
index 0000000000000000000000000000000000000000..35240054497992ff2b5a277618321dceffeb652a
--- /dev/null
+++ b/Sources/SearchFeature/Controllers/SearchRightController.swift
@@ -0,0 +1,81 @@
+import UIKit
+import Combine
+import DependencyInjection
+
+final class SearchRightController: UIViewController {
+    @Dependency var coordinator: SearchCoordinating
+
+    lazy private var screenView = SearchRightView()
+
+    private var cancellables = Set<AnyCancellable>()
+    private let cameraController = CameraController()
+    private(set) var viewModel = SearchRightViewModel()
+
+    override func loadView() {
+        view = screenView
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        screenView.layer.insertSublayer(cameraController.previewLayer, at: 0)
+        setupBindings()
+    }
+
+    override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+        cameraController.previewLayer.frame = screenView.bounds
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        viewModel.viewWillDisappear()
+    }
+
+    private func setupBindings() {
+        cameraController
+            .dataPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in viewModel.didScan(data: $0) }
+            .store(in: &cancellables)
+
+        viewModel.cameraSemaphorePublisher
+            .removeDuplicates()
+            .receive(on: DispatchQueue.global())
+            .sink { [unowned self] setOn in
+                if setOn {
+                    cameraController.start()
+                } else {
+                    cameraController.stop()
+                }
+            }.store(in: &cancellables)
+
+        viewModel.foundPublisher
+            .receive(on: DispatchQueue.main)
+            .delay(for: 1, scheduler: DispatchQueue.main)
+            .sink { [unowned self] in coordinator.toContact($0, from: self) }
+            .store(in: &cancellables)
+
+        viewModel.statusPublisher
+            .receive(on: DispatchQueue.main)
+            .removeDuplicates()
+            .sink { [unowned self] in screenView.update(status: $0) }
+            .store(in: &cancellables)
+
+        screenView.actionButton
+            .publisher(for: .touchUpInside)
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                switch viewModel.statusSubject.value {
+                case .failed(.cameraPermission):
+                    guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
+                    UIApplication.shared.open(url, options: [:])
+                case .failed(.requestOpened):
+                    coordinator.toRequests(from: self)
+                case .failed(.alreadyFriends):
+                    coordinator.toContacts(from: self)
+                default:
+                    break
+                }
+            }.store(in: &cancellables)
+    }
+}
diff --git a/Sources/SearchFeature/Controllers/SearchTableController.swift b/Sources/SearchFeature/Controllers/SearchTableController.swift
deleted file mode 100644
index a18b5856fe98c454c1a54b3ee4ca3c70d25d49dd..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/Controllers/SearchTableController.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-import UIKit
-import Models
-import Combine
-import XXModels
-
-final class SearchTableController: UITableViewController {
-    // MARK: Properties
-
-    private let viewModel: SearchViewModel
-    private var cancellables = Set<AnyCancellable>()
-    private(set) var dataSource = [Contact]()
-
-    // MARK: Lifecycle
-
-    init(_ viewModel: SearchViewModel) {
-        self.viewModel = viewModel
-        super.init(style: .grouped)
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        setupTableView()
-        setupBindings()
-    }
-
-    // MARK: Private
-
-    private func setupTableView() {
-        tableView.backgroundColor = .clear
-        tableView.separatorStyle = .none
-        tableView.register(SearchCell.self)
-    }
-
-    private func setupBindings() {
-        viewModel
-            .itemsRelay
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in
-                dataSource = $0
-                tableView.reloadData()
-            }.store(in: &cancellables)
-    }
-
-    // MARK: UITableViewDataSource
-
-    override func tableView(_ tableView: UITableView,
-                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self)
-        cell.title.text = dataSource[indexPath.row].username
-        cell.subtitle.text = dataSource[indexPath.row].username
-        cell.avatar.setupProfile(title: dataSource[indexPath.row].username!, image: nil, size: .large)
-        return cell
-    }
-
-    override func tableView(_ tableView: UITableView,
-                            numberOfRowsInSection section: Int) -> Int { dataSource.count }
-}
-
diff --git a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift
index 2f66a6228fd96bb156266a80c16a9ac5ba53c5d9..21a9d7b3d5eb9b1cef283a410135157fb7571ef1 100644
--- a/Sources/SearchFeature/Coordinator/SearchCoordinator.swift
+++ b/Sources/SearchFeature/Coordinator/SearchCoordinator.swift
@@ -6,6 +6,8 @@ import Presentation
 import ScrollViewController
 
 public protocol SearchCoordinating {
+    func toRequests(from: UIViewController)
+    func toContacts(from: UIViewController)
     func toContact(_: Contact, from: UIViewController)
     func toDrawer(_: UIViewController, from: UIViewController)
     func toNicknameDrawer(_: UIViewController, from: UIViewController)
@@ -15,21 +17,38 @@ public protocol SearchCoordinating {
 public struct SearchCoordinator {
     var pushPresenter: Presenting = PushPresenter()
     var bottomPresenter: Presenting = BottomPresenter()
+    var replacePresenter: Presenting = ReplacePresenter()
     var fullscreenPresenter: Presenting = FullscreenPresenter()
 
+    var contactsFactory: () -> UIViewController
+    var requestsFactory: () -> UIViewController
     var contactFactory: (Contact) -> UIViewController
     var countriesFactory: (@escaping (Country) -> Void) -> UIViewController
 
     public init(
+        contactsFactory: @escaping () -> UIViewController,
+        requestsFactory: @escaping () -> UIViewController,
         contactFactory: @escaping (Contact) -> UIViewController,
         countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController
     ) {
         self.contactFactory = contactFactory
+        self.contactsFactory = contactsFactory
+        self.requestsFactory = requestsFactory
         self.countriesFactory = countriesFactory
     }
 }
 
 extension SearchCoordinator: SearchCoordinating {
+    public func toRequests(from parent: UIViewController) {
+        let screen = requestsFactory()
+        replacePresenter.present(screen, from: parent)
+    }
+
+    public func toContacts(from parent: UIViewController) {
+        let screen = contactsFactory()
+        replacePresenter.present(screen, from: parent)
+    }
+
     public func toContact(_ contact: Contact, from parent: UIViewController) {
         let screen = contactFactory(contact)
         pushPresenter.present(screen, from: parent)
diff --git a/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift
new file mode 100644
index 0000000000000000000000000000000000000000..10d7bccc8c0f100b32cd34bf7a91312796ae9f15
--- /dev/null
+++ b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift
@@ -0,0 +1,23 @@
+import UIKit
+import XXModels
+
+enum SearchSection {
+    case stranger
+    case connections
+}
+
+enum SearchItem: Equatable, Hashable {
+    case stranger(Contact)
+    case connection(Contact)
+}
+
+class SearchDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> {
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        switch snapshot().sectionIdentifiers[section] {
+        case .stranger:
+            return ""
+        case .connections:
+            return "LOCAL RESULTS"
+        }
+    }
+}
diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e308f6a5b21134224ec17155b9585a9718cf1204
--- /dev/null
+++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift
@@ -0,0 +1,58 @@
+import UIKit
+import Combine
+import Defaults
+import Integration
+import PushFeature
+import DependencyInjection
+
+final class SearchContainerViewModel {
+    @Dependency var session: SessionType
+    @Dependency var pushHandler: PushHandling
+
+    @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled
+    @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications
+    @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic
+
+    var coverTrafficPublisher: AnyPublisher<Void, Never> {
+        coverTrafficSubject.eraseToAnyPublisher()
+    }
+
+    private let coverTrafficSubject = PassthroughSubject<Void, Never>()
+
+    func didAppear() {
+        verifyCoverTraffic()
+        verifyNotifications()
+    }
+
+    func didEnableCoverTraffic() {
+        isCoverTrafficEnabled = true
+        session.setDummyTraffic(status: true)
+    }
+
+    private func verifyCoverTraffic() {
+        guard offeredCoverTraffic == false else { return }
+        offeredCoverTraffic = true
+        coverTrafficSubject.send()
+    }
+
+    private func verifyNotifications() {
+        guard pushNotifications == false else { return }
+
+        pushHandler.requestAuthorization { [weak self] result in
+            guard let self = self else { return }
+
+            switch result {
+            case .success(let granted):
+                if granted {
+                    DispatchQueue.main.async {
+                        UIApplication.shared.registerForRemoteNotifications()
+                    }
+                }
+
+                self.pushNotifications = granted
+            case .failure:
+                self.pushNotifications = false
+            }
+        }
+    }
+}
diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..30f77b191cbd9115f856d6133a2df3b2fa01fb0b
--- /dev/null
+++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift
@@ -0,0 +1,137 @@
+import HUD
+import UIKit
+import Shared
+import Combine
+import XXModels
+import Countries
+import Integration
+import DependencyInjection
+
+typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
+
+struct SearchLeftViewState {
+    var input = ""
+    var snapshot: SearchSnapshot?
+    var country: Country = .fromMyPhone()
+    var item: SearchSegmentedControl.Item = .username
+}
+
+final class SearchLeftViewModel {
+    @Dependency var session: SessionType
+
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
+
+    var successPublisher: AnyPublisher<Contact, Never> {
+        successSubject.eraseToAnyPublisher()
+    }
+
+    var statePublisher: AnyPublisher<SearchLeftViewState, Never> {
+        stateSubject.eraseToAnyPublisher()
+    }
+
+    private var searchCancellables = Set<AnyCancellable>()
+    private let successSubject = PassthroughSubject<Contact, Never>()
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
+    private let stateSubject = CurrentValueSubject<SearchLeftViewState, Never>(.init())
+
+    func didEnterInput(_ string: String) {
+        stateSubject.value.input = string
+    }
+
+    func didPick(country: Country) {
+        stateSubject.value.country = country
+    }
+
+    func didSelectItem(_ item: SearchSegmentedControl.Item) {
+        stateSubject.value.item = item
+    }
+
+    func didTapCancelSearch() {
+        searchCancellables.forEach { $0.cancel() }
+        searchCancellables.removeAll()
+        hudSubject.send(.none)
+    }
+
+    func didStartSearching() {
+        guard stateSubject.value.input.isEmpty == false else { return }
+
+        hudSubject.send(.onAction(Localized.Ud.Search.cancel))
+
+        var content = stateSubject.value.input
+        let prefix = stateSubject.value.item.written.first!.uppercased()
+
+        if stateSubject.value.item == .phone {
+            content += stateSubject.value.country.code
+        }
+
+        session.search(fact: "\(prefix)\(content)")
+            .sink { [unowned self] in
+                if case .failure(let error) = $0 {
+                    self.appendToLocalSearch(nil)
+                    self.hudSubject.send(.error(.init(with: error)))
+                }
+            } receiveValue: { contact in
+                self.hudSubject.send(.none)
+                self.appendToLocalSearch(contact)
+            }.store(in: &searchCancellables)
+    }
+
+    func didTapResend(contact: Contact) {
+        hudSubject.send(.on)
+
+        do {
+            try self.session.retryRequest(contact)
+            hudSubject.send(.none)
+        } catch {
+            hudSubject.send(.error(.init(with: error)))
+        }
+    }
+
+    func didTapRequest(contact: Contact) {
+        hudSubject.send(.on)
+        var contact = contact
+        contact.nickname = contact.username
+
+        do {
+            try self.session.add(contact)
+            hudSubject.send(.none)
+            successSubject.send(contact)
+        } catch {
+            hudSubject.send(.error(.init(with: error)))
+        }
+    }
+
+    func didSet(nickname: String, for contact: Contact) {
+        if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first {
+            contact.nickname = nickname
+            _ = try? session.dbManager.saveContact(contact)
+        }
+    }
+
+    private func appendToLocalSearch(_ user: Contact?) {
+        var snapshot = SearchSnapshot()
+
+        if var user = user {
+            if let contact = try? session.dbManager.fetchContacts(.init(id: [user.id])).first {
+                user.authStatus = contact.authStatus
+            }
+
+            if user.authStatus != .friend {
+                snapshot.appendSections([.stranger])
+                snapshot.appendItems([.stranger(user)], toSection: .stranger)
+            }
+        }
+
+        let localsQuery = Contact.Query(text: stateSubject.value.input, authStatus: [.friend])
+
+        if let locals = try? session.dbManager.fetchContacts(localsQuery), locals.count > 0 {
+            let localsWithoutMe = locals.filter { $0.id != session.myId }
+            snapshot.appendSections([.connections])
+            snapshot.appendItems(localsWithoutMe.map(SearchItem.connection), toSection: .connections)
+        }
+
+        stateSubject.value.snapshot = snapshot
+    }
+}
diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5cfe38db68c498a15522f745174e41415281ba17
--- /dev/null
+++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift
@@ -0,0 +1,111 @@
+import Shared
+import Combine
+import XXModels
+import Foundation
+import Permissions
+import Integration
+import DependencyInjection
+
+enum ScanningStatus: Equatable {
+    case reading
+    case processing
+    case success
+    case failed(ScanningError)
+}
+
+enum ScanningError: Equatable {
+    case requestOpened
+    case unknown(String)
+    case cameraPermission
+    case alreadyFriends(String)
+}
+
+final class SearchRightViewModel {
+    @Dependency var session: SessionType
+    @Dependency var permissions: PermissionHandling
+
+    var foundPublisher: AnyPublisher<Contact, Never> {
+        foundSubject.eraseToAnyPublisher()
+    }
+
+    var cameraSemaphorePublisher: AnyPublisher<Bool, Never> {
+        cameraSemaphoreSubject.eraseToAnyPublisher()
+    }
+
+    var statusPublisher: AnyPublisher<ScanningStatus, Never> {
+        statusSubject.eraseToAnyPublisher()
+    }
+
+    private let foundSubject = PassthroughSubject<Contact, Never>()
+    private let cameraSemaphoreSubject = PassthroughSubject<Bool, Never>()
+    private(set) var statusSubject = CurrentValueSubject<ScanningStatus, Never>(.reading)
+
+    func viewWillAppear() {
+        permissions.requestCamera { [weak self] granted in
+            guard let self = self else { return }
+
+            if granted {
+                self.statusSubject.value = .reading
+                self.cameraSemaphoreSubject.send(true)
+            } else {
+                self.statusSubject.send(.failed(.cameraPermission))
+            }
+        }
+    }
+
+    func viewWillDisappear() {
+        cameraSemaphoreSubject.send(false)
+    }
+
+    func didScan(data: Data) {
+        /// We need to be accepting new readings in order
+        /// to process what just got scanned.
+        ///
+        guard statusSubject.value == .reading else { return }
+        statusSubject.send(.processing)
+
+        /// Whatever got scanned, needs to have id and username
+        /// otherwise is just noise or an unknown qr code
+        ///
+        guard let userId = session.getId(from: data),
+              let username = try? session.extract(fact: .username, from: data) else {
+            let errorTitle = Localized.Scan.Error.invalid
+            statusSubject.send(.failed(.unknown(errorTitle)))
+            return
+        }
+
+        /// Make sure we are not processing a contact
+        /// that we already have
+        ///
+        if let alreadyContact = try? session.dbManager.fetchContacts(.init(id: [userId])).first {
+            /// Show error accordingly to the auth status
+            ///
+            if alreadyContact.authStatus == .friend {
+                statusSubject.send(.failed(.alreadyFriends(username)))
+            } else if [.requested, .verified].contains(alreadyContact.authStatus) {
+                statusSubject.send(.failed(.requestOpened))
+            } else {
+                let generalErrorTitle = Localized.Scan.Error.general
+                statusSubject.send(.failed(.unknown(generalErrorTitle)))
+            }
+
+            return
+        }
+
+        statusSubject.send(.success)
+        cameraSemaphoreSubject.send(false)
+
+        foundSubject.send(.init(
+            id: userId,
+            marshaled: data,
+            username: username,
+            email: try? session.extract(fact: .email, from: data),
+            phone: try? session.extract(fact: .phone, from: data),
+            nickname: nil,
+            photo: nil,
+            authStatus: .stranger,
+            isRecent: false,
+            createdAt: Date()
+        ))
+    }
+}
diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift
deleted file mode 100644
index 7b52fb9762f47cfab9d17f2fd02383560be228bd..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift
+++ /dev/null
@@ -1,193 +0,0 @@
-import HUD
-import UIKit
-import Models
-import Combine
-import Defaults
-import XXModels
-import Countries
-import Foundation
-import Integration
-import PushFeature
-import CombineSchedulers
-import DependencyInjection
-
-enum SelectedFilter {
-    case username
-    case email
-    case phone
-
-    var prefix: String {
-        switch self {
-        case .username:
-            return "U"
-        case .phone:
-            return "P"
-        case .email:
-            return "E"
-        }
-    }
-}
-
-struct SearchViewState: Equatable {
-    var input: String = ""
-    var phoneInput: String = ""
-    var selectedFilter: SelectedFilter = .username
-    var country: Country = .fromMyPhone()
-}
-
-final class SearchViewModel {
-    @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled: Bool
-    @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications
-    @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic: Bool
-
-    @Dependency private var session: SessionType
-    @Dependency private var pushHandler: PushHandling
-
-    var hudPublisher: AnyPublisher<HUDStatus, Never> {
-        hudSubject.eraseToAnyPublisher()
-    }
-
-    var placeholderPublisher: AnyPublisher<Bool, Never> {
-        placeholderSubject.eraseToAnyPublisher()
-    }
-
-    var coverTrafficPublisher: AnyPublisher<Void, Never> {
-        coverTrafficSubject.eraseToAnyPublisher()
-    }
-
-    var statePublisher: AnyPublisher<SearchViewState, Never> {
-        stateSubject.eraseToAnyPublisher()
-    }
-
-    var successPublisher: AnyPublisher<Contact, Never> {
-        successSubject.eraseToAnyPublisher()
-    }
-
-    var backgroundScheduler: AnySchedulerOf<DispatchQueue>
-    = DispatchQueue.global().eraseToAnyScheduler()
-
-    let itemsRelay = CurrentValueSubject<[Contact], Never>([])
-    private let successSubject = PassthroughSubject<Contact, Never>()
-    private let coverTrafficSubject = PassthroughSubject<Void, Never>()
-    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
-    private let placeholderSubject = CurrentValueSubject<Bool, Never>(true)
-    private let stateSubject = CurrentValueSubject<SearchViewState, Never>(.init())
-
-    func didAppear() {
-        verifyCoverTraffic()
-        verifyNotifications()
-    }
-
-    func didSelect(filter: SelectedFilter) {
-        stateSubject.value.selectedFilter = filter
-    }
-
-    func didInput(_ string: String) {
-        stateSubject.value.input = string.trimmingCharacters(in: .whitespacesAndNewlines)
-    }
-
-    func didInputPhone(_ string: String) {
-        stateSubject.value.phoneInput = string.trimmingCharacters(in: .whitespacesAndNewlines)
-    }
-
-    func didChooseCountry(_ country: Country) {
-        stateSubject.value.country = country
-    }
-
-    func didEnableCoverTraffic() {
-        isCoverTrafficEnabled = true
-        session.setDummyTraffic(status: true)
-    }
-
-    func didTapSearch() {
-        hudSubject.send(.on(nil))
-
-        backgroundScheduler.schedule { [weak self] in
-            guard let self = self else { return }
-
-            do {
-                var content = self.stateSubject.value.selectedFilter.prefix
-
-                if self.stateSubject.value.selectedFilter == .phone {
-                    content += self.stateSubject.value.phoneInput + self.stateSubject.value.country.code
-                } else {
-                    content += self.stateSubject.value.input
-                }
-
-                try self.session.search(fact: content) { result in
-                    self.placeholderSubject.send(false)
-
-                    switch result {
-                    case .success(let searched):
-                        self.hudSubject.send(.none)
-                        self.itemsRelay.send([searched])
-                    case .failure(let error):
-                        self.hudSubject.send(.error(.init(with: error)))
-                        self.itemsRelay.send([])
-                    }
-                }
-            } catch {
-                self.hudSubject.send(.error(.init(with: error)))
-            }
-        }
-    }
-
-    private func verifyCoverTraffic() {
-        guard offeredCoverTraffic == false else {
-            return
-        }
-
-        offeredCoverTraffic = true
-        coverTrafficSubject.send()
-    }
-
-    private func verifyNotifications() {
-        guard pushNotifications == false else { return }
-
-        pushHandler.requestAuthorization { [weak self] result in
-            guard let self = self else { return }
-
-            switch result {
-            case .success(let granted):
-                if granted {
-                    DispatchQueue.main.async {
-                        UIApplication.shared.registerForRemoteNotifications()
-                    }
-                }
-
-                self.pushNotifications = granted
-            case .failure:
-                self.pushNotifications = false
-            }
-        }
-    }
-
-    func didSet(nickname: String, for contact: Contact) {
-        var contact = contact
-        contact.nickname = nickname
-
-        backgroundScheduler.schedule { [weak self] in
-            guard let self = self else { return }
-            _ = try? self.session.dbManager.saveContact(contact)
-        }
-    }
-
-    func didTapRequest(contact: Contact) {
-        hudSubject.send(.on(nil))
-        var contact = contact
-        contact.nickname = contact.username
-
-        backgroundScheduler.schedule { [weak self] in
-            guard let self = self else { return }
-
-            do {
-                try self.session.add(contact)
-                self.hudSubject.send(.none)
-                self.successSubject.send(contact)
-            } catch {
-                self.hudSubject.send(.error(.init(with: error)))
-            }
-        }
-
-    }
-}
diff --git a/Sources/SearchFeature/Views/FilterItemView.swift b/Sources/SearchFeature/Views/FilterItemView.swift
deleted file mode 100644
index 2bf4d2a81a2e998b5bad69b6d71c5cfa688127ee..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/Views/FilterItemView.swift
+++ /dev/null
@@ -1,68 +0,0 @@
-import UIKit
-import Shared
-
-final class FilterItemView: UIControl {
-    enum Style {
-        case selected
-        case unselected
-    }
-
-    private let title = UILabel()
-    private let image = UIImageView()
-
-    private var icon: UIImage?
-
-    var style: Style = .unselected {
-        didSet {
-            image.image = icon?.withRenderingMode(.alwaysTemplate)
-
-            switch style {
-            case .selected:
-                backgroundColor = Asset.brandDefault.color
-                image.tintColor = Asset.neutralWhite.color
-                title.textColor = Asset.neutralWhite.color
-                title.font = Fonts.Mulish.bold.font(size: 14.0)
-                layer.borderColor = Asset.brandDefault.color.cgColor
-
-            case .unselected:
-                image.tintColor = Asset.neutralActive.color
-                title.textColor = Asset.neutralActive.color
-                backgroundColor = Asset.neutralSecondary.color
-                title.font = Fonts.Mulish.regular.font(size: 14.0)
-                layer.borderColor = Asset.neutralLine.color.cgColor
-            }
-        }
-    }
-
-    init() {
-        super.init(frame: .zero)
-
-        layer.borderWidth = 1
-        layer.cornerRadius = 4
-        image.contentMode = .center
-
-        let stack = UIStackView()
-        stack.isUserInteractionEnabled = false
-
-        stack.spacing = 8
-        stack.addArrangedSubview(image)
-        stack.addArrangedSubview(title)
-
-        addSubview(stack)
-
-        stack.snp.makeConstraints { $0.center.equalToSuperview() }
-        snp.makeConstraints { $0.height.equalTo(40) }
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    func set(
-        title: String,
-        icon: UIImage?,
-        style: Style = .unselected
-    ) {
-        self.icon = icon
-        self.style = style
-        self.title.text = title
-    }
-}
diff --git a/Sources/SearchFeature/Views/OverlayView.swift b/Sources/SearchFeature/Views/OverlayView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8242857716936fe49cf21a59bad280195c726165
--- /dev/null
+++ b/Sources/SearchFeature/Views/OverlayView.swift
@@ -0,0 +1,181 @@
+import UIKit
+import Shared
+
+final class OverlayView: UIView {
+    private let cropView = UIView()
+    private let scanViewLength = 266.0
+    private let maskLayer = CAShapeLayer()
+    private let topLeftLayer = CAShapeLayer()
+    private let topRightLayer = CAShapeLayer()
+    private let bottomLeftLayer = CAShapeLayer()
+    private let bottomRightLayer = CAShapeLayer()
+
+    init() {
+        super.init(frame: .zero)
+        backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5)
+
+        addSubview(cropView)
+
+        cropView.snp.makeConstraints {
+            $0.width.equalTo(scanViewLength)
+            $0.centerY.equalToSuperview().offset(-50)
+            $0.centerX.equalToSuperview()
+            $0.height.equalTo(scanViewLength)
+        }
+
+        maskLayer.fillRule = .evenOdd
+        layer.mask = maskLayer
+        layer.masksToBounds = true
+
+        [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach {
+            $0.strokeColor = Asset.brandPrimary.color.cgColor
+            $0.fillColor = UIColor.clear.cgColor
+            $0.lineWidth = 3.0
+            $0.lineCap = .round
+            layer.addSublayer($0)
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+
+        maskLayer.frame = bounds
+        let path = UIBezierPath(rect: bounds)
+        path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0))
+        maskLayer.path = path.cgPath
+
+        topLeftLayer.frame = bounds
+        topRightLayer.frame = bounds
+        bottomRightLayer.frame = bounds
+        bottomLeftLayer.frame = bounds
+
+        topLeftLayer.path = topLeftPath()
+        topRightLayer.path = topRightPath()
+        bottomRightLayer.path = bottomRightPath()
+        bottomLeftLayer.path = bottomLeftPath()
+    }
+
+    func updateCornerColor(_ color: UIColor) {
+        [topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach {
+            $0.strokeColor = color.cgColor
+        }
+    }
+
+    func topLeftPath() -> CGPath {
+        let path = UIBezierPath()
+
+        let vert0X = cropView.frame.minX - 15
+        let vert0Y = cropView.frame.minY + 45
+        let vert0 = CGPoint(x: vert0X, y: vert0Y)
+        path.move(to: vert0)
+
+        let vertNX = cropView.frame.minX - 15
+        let vertNY = cropView.frame.minY + 15
+        let vertN = CGPoint(x: vertNX, y: vertNY)
+        path.addLine(to: vertN)
+
+        let arcCenterX = cropView.frame.minX + 15
+        let arcCenterY = cropView.frame.minY + 15
+        let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: .pi)
+
+        let horizX = cropView.frame.minX + 45
+        let horizY = cropView.frame.minY - 15
+        let horiz = CGPoint(x: horizX, y: horizY)
+        path.addLine(to: horiz)
+
+        return path.cgPath
+    }
+
+    func topRightPath() -> CGPath {
+        let path = UIBezierPath()
+
+        let horiz0X = cropView.frame.maxX - 45
+        let horiz0Y = cropView.frame.minY - 15
+        let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
+        path.move(to: horiz0)
+
+        let horizNX = cropView.frame.maxX - 15
+        let horizNY = cropView.frame.minY - 15
+        let horizN = CGPoint(x: horizNX, y: horizNY)
+        path.addLine(to: horizN)
+
+        let arcCenterX = cropView.frame.maxX - 15
+        let arcCenterY = cropView.frame.minY + 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: 3 * .pi/2)
+
+        let vertX = cropView.frame.maxX + 15
+        let vertY = cropView.frame.minY + 45
+        let vert = CGPoint(x: vertX, y: vertY)
+        path.addLine(to: vert)
+
+        return path.cgPath
+    }
+
+    func bottomRightPath() -> CGPath {
+        let path = UIBezierPath()
+
+        let vert0X = cropView.frame.maxX + 15
+        let vert0Y = cropView.frame.maxY - 45
+        let vert0 = CGPoint(x: vert0X, y: vert0Y)
+        path.move(to: vert0)
+
+        let vertNX = cropView.frame.maxX + 15
+        let vertNY = cropView.frame.maxY - 15
+        let vertN = CGPoint(x: vertNX, y: vertNY)
+        path.addLine(to: vertN)
+
+        let arcCenterX = cropView.frame.maxX - 15
+        let arcCenterY = cropView.frame.maxY - 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: 0)
+
+        let horizX = cropView.frame.maxX - 45
+        let horizY = cropView.frame.maxY + 15
+        let horiz = CGPoint(x: horizX, y: horizY)
+        path.addLine(to: horiz)
+
+        return path.cgPath
+    }
+
+    func bottomLeftPath() -> CGPath {
+        let path = UIBezierPath()
+
+        let horiz0X = cropView.frame.minX + 45
+        let horiz0Y = cropView.frame.maxY + 15
+        let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
+        path.move(to: horiz0)
+
+        let horizNX = cropView.frame.minX + 15
+        let horizNY = cropView.frame.maxY + 15
+        let horizN = CGPoint(x: horizNX, y: horizNY)
+        path.addLine(to: horizN)
+
+        let arcCenterX = cropView.frame.minX + 15
+        let arcCenterY = cropView.frame.maxY - 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: .pi/2)
+
+        let vertX = cropView.frame.minX - 15
+        let vertY = cropView.frame.maxY - 45
+        let vert = CGPoint(x: vertX, y: vertY)
+        path.addLine(to: vert)
+
+        return path.cgPath
+    }
+}
+
+private extension UIBezierPath {
+    func addArc(center: CGPoint, startAngle: CGFloat) {
+        addArc(
+            withCenter: center,
+            radius: 30,
+            startAngle: startAngle,
+            endAngle: startAngle + .pi/2,
+            clockwise: true
+        )
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchCell.swift b/Sources/SearchFeature/Views/SearchCell.swift
deleted file mode 100644
index 8819425791ed9dbde382626ac7d03fd868b7b31e..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/Views/SearchCell.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-import UIKit
-import Shared
-
-final class SearchCell: UITableViewCell {
-    // MARK: UI
-
-    let title = UILabel()
-    let subtitle = UILabel()
-    let separator = UIView()
-    let avatar = AvatarView()
-
-    // MARK: Lifecycle
-
-    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
-        super.init(style: style, reuseIdentifier: reuseIdentifier)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    override func prepareForReuse() {
-        super.prepareForReuse()
-        title.text = nil
-    }
-
-    // MARK: Private
-
-    private func setup() {
-        selectionStyle = .none
-        backgroundColor = Asset.neutralWhite.color
-
-        title.textColor = Asset.neutralActive.color
-        subtitle.textColor = Asset.neutralDisabled.color
-        separator.backgroundColor = Asset.neutralLine.color
-
-        title.font = Fonts.Mulish.semiBold.font(size: 14.0)
-        subtitle.font = Fonts.Mulish.regular.font(size: 12.0)
-
-        contentView.addSubview(title)
-        contentView.addSubview(avatar)
-        contentView.addSubview(subtitle)
-        contentView.addSubview(separator)
-
-        setupConstraints()
-    }
-
-    private func setupConstraints() {
-        title.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(10)
-            make.left.equalTo(avatar.snp.right).offset(16)
-            make.right.lessThanOrEqualToSuperview().offset(-20)
-        }
-
-        subtitle.snp.makeConstraints { make in
-            make.top.equalTo(title.snp.bottom).offset(3)
-            make.left.equalTo(title)
-            make.bottom.equalToSuperview().offset(-22)
-        }
-
-        avatar.snp.makeConstraints { make in
-            make.left.equalToSuperview().offset(28)
-            make.width.height.equalTo(48)
-            make.bottom.equalToSuperview().offset(-16)
-        }
-
-        separator.snp.makeConstraints { make in
-            make.height.equalTo(1)
-            make.left.equalToSuperview().offset(24)
-            make.right.equalToSuperview().offset(-24)
-            make.bottom.equalToSuperview()
-        }
-    }
-}
diff --git a/Sources/SearchFeature/Views/SearchContainerView.swift b/Sources/SearchFeature/Views/SearchContainerView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3a270bc5a52891fc915951bf68f738f2e5cdcd6f
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchContainerView.swift
@@ -0,0 +1,35 @@
+import UIKit
+import Shared
+
+final class SearchContainerView: UIView {
+    let scrollView = UIScrollView()
+    let segmentedControl = SearchSegmentedControl()
+
+    init() {
+        super.init(frame: .zero)
+
+        backgroundColor = Asset.neutralWhite.color
+        addSubview(segmentedControl)
+        addSubview(scrollView)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    private func setupConstraints() {
+        segmentedControl.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(10)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.height.equalTo(60)
+        }
+
+        scrollView.snp.makeConstraints {
+            $0.top.equalTo(segmentedControl.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..84c64c87a6096a16b2bbf793a9eadc1c95495324
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift
@@ -0,0 +1,26 @@
+import UIKit
+import Shared
+
+final class SearchLeftEmptyView: UIView {
+    let titleLabel = UILabel()
+
+    init() {
+        super.init(frame: .zero)
+        backgroundColor = Asset.neutralWhite.color
+
+        titleLabel.numberOfLines = 0
+        titleLabel.textAlignment = .center
+        titleLabel.font = Fonts.Mulish.regular.font(size: 15.0)
+        titleLabel.textColor = Asset.neutralSecondaryAlternative.color
+
+        addSubview(titleLabel)
+
+        titleLabel.snp.makeConstraints {
+            $0.center.equalToSuperview()
+            $0.left.equalToSuperview().offset(20)
+            $0.right.equalToSuperview().offset(-20)
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7742ff1d64c56151a88ae9b5ae47e82ebb59dc1b
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift
@@ -0,0 +1,74 @@
+import UIKit
+import Shared
+import Combine
+
+final class SearchLeftPlaceholderView: UIView {
+    let titleLabel = UILabel()
+    let subtitleWithInfo = TextWithInfoView()
+
+    var infoPublisher: AnyPublisher<Void, Never> {
+        infoSubject.eraseToAnyPublisher()
+    }
+
+    private let infoSubject = PassthroughSubject<Void, Never>()
+
+    init() {
+        super.init(frame: .zero)
+        backgroundColor = Asset.neutralWhite.color
+
+        let attrString = NSMutableAttributedString(
+            string: Localized.Ud.Search.Placeholder.title,
+            attributes: [
+                .foregroundColor: Asset.neutralDark.color,
+                .font: Fonts.Mulish.bold.font(size: 32.0)
+            ]
+        )
+
+        attrString.addAttribute(
+            name: .foregroundColor,
+            value: Asset.brandPrimary.color,
+            betweenCharacters: "#"
+        )
+
+        titleLabel.numberOfLines = 0
+        titleLabel.attributedText = attrString
+
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.lineHeightMultiple = 1.3
+
+        subtitleWithInfo.setup(
+            text: Localized.Ud.Search.Placeholder.subtitle,
+            attributes: [
+                .paragraphStyle: paragraph,
+                .foregroundColor: Asset.neutralBody.color,
+                .font: Fonts.Mulish.regular.font(size: 16.0)
+            ],
+            didTapInfo: { [weak self] in
+                guard let self = self else { return }
+                self.infoSubject.send(())
+            }
+        )
+
+        addSubview(titleLabel)
+        addSubview(subtitleWithInfo)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    private func setupConstraints() {
+        titleLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(50)
+            $0.left.equalToSuperview().offset(32.5)
+            $0.right.equalToSuperview().offset(-32.5)
+        }
+
+        subtitleWithInfo.snp.makeConstraints {
+            $0.top.equalTo(titleLabel.snp.bottom).offset(30)
+            $0.left.equalToSuperview().offset(32.5)
+            $0.right.equalToSuperview().offset(-32.5)
+            $0.bottom.equalToSuperview()
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchLeftView.swift b/Sources/SearchFeature/Views/SearchLeftView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bbdda3f926818439e4c28d5beb020239a623048a
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchLeftView.swift
@@ -0,0 +1,71 @@
+import UIKit
+import Shared
+
+final class SearchLeftView: UIView {
+    let tableView = UITableView()
+    let inputStackView = UIStackView()
+    let inputField = SearchComponent()
+    let emptyView = SearchLeftEmptyView()
+    let countryButton = SearchCountryComponent()
+    let placeholderView = SearchLeftPlaceholderView()
+
+    init() {
+        super.init(frame: .zero)
+
+        emptyView.isHidden = true
+        backgroundColor = Asset.neutralWhite.color
+        tableView.backgroundColor = Asset.neutralWhite.color
+
+        inputStackView.spacing = 5
+        inputStackView.addArrangedSubview(countryButton)
+        inputStackView.addArrangedSubview(inputField)
+
+        addSubview(inputStackView)
+        addSubview(tableView)
+        addSubview(emptyView)
+        addSubview(placeholderView)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func updateUIForItem(item: SearchSegmentedControl.Item) {
+        countryButton.isHidden = item != .phone
+
+        let emptyTitle = Localized.Ud.Search.empty(item.written)
+        emptyView.titleLabel.text = emptyTitle
+
+        let inputFieldTitle = Localized.Ud.Search.input(item.written)
+        inputField.set(placeholder: inputFieldTitle, imageAtRight: nil)
+    }
+
+    private func setupConstraints() {
+        inputStackView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(20)
+            $0.left.equalToSuperview().offset(20)
+            $0.right.equalToSuperview().offset(-20)
+        }
+
+        tableView.snp.makeConstraints {
+            $0.top.equalTo(inputField.snp.bottom).offset(20)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
+        emptyView.snp.makeConstraints {
+            $0.top.equalTo(inputField.snp.bottom).offset(20)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
+        placeholderView.snp.makeConstraints {
+            $0.top.equalTo(inputField.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchPlaceholderView.swift b/Sources/SearchFeature/Views/SearchPlaceholderView.swift
deleted file mode 100644
index e6697688f13ab4b13c023f9e8579f081ff00be44..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/Views/SearchPlaceholderView.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-import UIKit
-import Shared
-
-final class SearchPlaceholderView: UIView {
-    let titleView = TextWithInfoView()
-    let didTapInfo: () -> Void
-
-    init(didTapInfo: @escaping () -> Void) {
-        self.didTapInfo = didTapInfo
-
-        super.init(frame: .zero)
-
-        let paragraph = NSMutableParagraphStyle()
-        paragraph.lineSpacing = 5
-        paragraph.lineHeightMultiple = 1.0
-        paragraph.alignment = .center
-
-        titleView.setup(
-            text: "Your searches are anonymous.\nSearch information is never linked to your account or personally identifiable.",
-            attributes: [
-                .foregroundColor: Asset.neutralBody.color,
-                .font: Fonts.Mulish.regular.font(size: 16.0) as Any,
-                .paragraphStyle: paragraph
-            ],
-            didTapInfo: { didTapInfo() }
-        )
-
-        addSubview(titleView)
-
-        titleView.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(60)
-            make.left.equalToSuperview().offset(60)
-            make.right.equalToSuperview().offset(-60)
-        }
-    }
-
-    required init?(coder: NSCoder) { nil }
-}
-
-final class SearchEmptyView: UIView {
-    private let title = UILabel()
-
-    init() {
-        super.init(frame: .zero)
-
-        backgroundColor = Asset.neutralWhite.color
-
-        title.textColor = Asset.neutralBody.color
-        title.font = Fonts.Mulish.regular.font(size: 12.0)
-
-        addSubview(title)
-
-        title.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(20)
-            make.left.equalToSuperview().offset(30)
-            make.right.equalToSuperview().offset(-30)
-        }
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    func set(filter: String) {
-        title.text = Localized.ContactSearch.noneFound(filter)
-    }
-}
diff --git a/Sources/SearchFeature/Views/SearchRightView.swift b/Sources/SearchFeature/Views/SearchRightView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..363808754852969a75c3c811836faa9b40455034
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchRightView.swift
@@ -0,0 +1,116 @@
+import UIKit
+import Shared
+
+final class SearchRightView: UIView {
+    let statusLabel = UILabel()
+    let imageView = UIImageView()
+    let stackView = UIStackView()
+    let overlayView = OverlayView()
+    let animationView = DotAnimation()
+    let actionButton = CapsuleButton()
+
+    init() {
+        super.init(frame: .zero)
+        imageView.contentMode = .center
+        actionButton.setStyle(.brandColored)
+
+        statusLabel.numberOfLines = 0
+        statusLabel.textAlignment = .center
+        statusLabel.textColor = Asset.neutralWhite.color
+        statusLabel.font = Fonts.Mulish.regular.font(size: 14.0)
+
+        stackView.spacing = 15
+        stackView.axis = .vertical
+        stackView.addArrangedSubview(animationView)
+        stackView.addArrangedSubview(imageView)
+        stackView.addArrangedSubview(statusLabel)
+        stackView.addArrangedSubview(actionButton)
+
+        imageView.isHidden = true
+        actionButton.isHidden = true
+        animationView.isHidden = false
+
+        addSubview(overlayView)
+        addSubview(stackView)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func update(status: ScanningStatus) {
+        var text: String
+
+        switch status {
+        case .reading, .processing:
+            imageView.isHidden = true
+            actionButton.isHidden = true
+            text = Localized.Scan.Status.reading
+            overlayView.updateCornerColor(Asset.brandPrimary.color)
+
+        case .success:
+            animationView.isHidden = true
+            actionButton.isHidden = true
+            imageView.isHidden = false
+            imageView.image = Asset.sharedSuccess.image
+            text = Localized.Scan.Status.success
+            overlayView.updateCornerColor(Asset.accentSuccess.color)
+
+        case .failed(let error):
+            animationView.isHidden = true
+            imageView.image = Asset.scanError.image
+            imageView.isHidden = false
+            overlayView.updateCornerColor(Asset.accentDanger.color)
+
+            switch error {
+            case .requestOpened:
+                text = Localized.Scan.Error.requested
+                actionButton.setTitle(Localized.Scan.requests, for: .normal)
+                actionButton.isHidden = false
+
+            case .alreadyFriends(let name):
+                text = Localized.Scan.Error.friends(name)
+                actionButton.setTitle(Localized.Scan.contact, for: .normal)
+                actionButton.isHidden = false
+
+            case .cameraPermission:
+                text = Localized.Scan.Error.denied
+                actionButton.setTitle(Localized.Scan.settings, for: .normal)
+                actionButton.isHidden = false
+
+            case .unknown(let content):
+                text = content
+            }
+        }
+
+        let attString = NSMutableAttributedString(string: text)
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.alignment = .center
+        paragraph.lineHeightMultiple = 1.35
+
+        attString.addAttribute(.paragraphStyle, value: paragraph)
+        attString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color)
+        attString.addAttribute(.font, value: Fonts.Mulish.regular.font(size: 14.0) as Any)
+
+        if text.contains("#") {
+            attString.addAttribute(name: .foregroundColor, value: Asset.brandPrimary.color, betweenCharacters: "#")
+        }
+
+        statusLabel.attributedText = attString
+    }
+
+    private func setupConstraints() {
+        overlayView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
+        stackView.snp.makeConstraints {
+            $0.left.equalToSuperview().offset(57)
+            $0.right.equalToSuperview().offset(-57)
+            $0.bottom.equalTo(safeAreaLayoutGuide).offset(-100)
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchSegmentedButton.swift b/Sources/SearchFeature/Views/SearchSegmentedButton.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3b8e65fb1b778703a748626155d6f2b0b18ec94b
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchSegmentedButton.swift
@@ -0,0 +1,49 @@
+import UIKit
+import Shared
+
+final class SearchSegmentedButton: UIControl {
+    private let titleLabel = UILabel()
+    private let imageView = UIImageView()
+    private let highlightColor = Asset.brandPrimary.color
+    private let discreteColor = Asset.neutralDisabled.color
+
+    init() {
+        super.init(frame: .zero)
+
+        imageView.contentMode = .center
+        titleLabel.textAlignment = .center
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
+
+        addSubview(titleLabel)
+        addSubview(imageView)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func setup(title: String, icon: UIImage) {
+        imageView.image = icon
+        titleLabel.text = title
+        imageView.tintColor = discreteColor
+        titleLabel.textColor = discreteColor
+    }
+
+    func setSelected(_ bool: Bool) {
+        imageView.tintColor = bool ? highlightColor : discreteColor
+        titleLabel.textColor = bool ? highlightColor : discreteColor
+    }
+
+    private func setupConstraints() {
+        imageView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(7.5)
+            $0.centerX.equalToSuperview()
+        }
+
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(imageView.snp.bottom).offset(2)
+            $0.centerX.equalToSuperview()
+            $0.bottom.equalToSuperview().offset(-7.5)
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6141360378cd659e89dbdf67c4180f4a0e9b4ce6
--- /dev/null
+++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift
@@ -0,0 +1,120 @@
+import UIKit
+import Shared
+import SnapKit
+import Combine
+
+final class SearchSegmentedControl: UIView {
+    enum Item: Int {
+        case username = 0
+        case email
+        case phone
+        case qr
+
+        var written: String {
+            switch self {
+            case .qr: return "qr"
+            case .email: return "email"
+            case .phone: return "phone number"
+            case .username: return "username"
+            }
+        }
+    }
+
+    private let trackView = UIView()
+    private let stackView = UIStackView()
+    private var leftConstraint: Constraint?
+    private let trackIndicatorView = UIView()
+    private let emailButton = SearchSegmentedButton()
+    private let phoneButton = SearchSegmentedButton()
+    private let qrCodeButton = SearchSegmentedButton()
+    private let usernameButton = SearchSegmentedButton()
+
+    var actionPublisher: AnyPublisher<Item, Never> {
+        actionSubject.eraseToAnyPublisher()
+    }
+
+    private var cancellables = Set<AnyCancellable>()
+    private let actionSubject = CurrentValueSubject<Item, Never>(.username)
+
+    init() {
+        super.init(frame: .zero)
+        trackView.backgroundColor = Asset.neutralLine.color
+        trackIndicatorView.backgroundColor = Asset.brandPrimary.color
+
+        qrCodeButton.setup(title: Localized.Ud.Tab.qr, icon: Asset.searchTabQr.image)
+        emailButton.setup(title: Localized.Ud.Tab.email, icon: Asset.searchTabEmail.image)
+        phoneButton.setup(title: Localized.Ud.Tab.phone, icon: Asset.searchTabPhone.image)
+        usernameButton.setup(title: Localized.Ud.Tab.username, icon: Asset.searchTabUsername.image)
+
+        stackView.distribution = .fillEqually
+        stackView.addArrangedSubview(usernameButton)
+        stackView.addArrangedSubview(emailButton)
+        stackView.addArrangedSubview(phoneButton)
+        stackView.addArrangedSubview(qrCodeButton)
+        stackView.backgroundColor = Asset.neutralWhite.color
+
+        addSubview(stackView)
+        addSubview(trackView)
+        trackView.addSubview(trackIndicatorView)
+
+        setupBindings()
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    private func setupBindings() {
+        usernameButton.publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.username) }
+            .store(in: &cancellables)
+
+        emailButton.publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.email) }
+            .store(in: &cancellables)
+
+        phoneButton.publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.phone) }
+            .store(in: &cancellables)
+
+        qrCodeButton.publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.qr) }
+            .store(in: &cancellables)
+
+        actionSubject
+            .removeDuplicates()
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                let tabWidth = bounds.width / 4
+                if let leftConstraint = leftConstraint {
+                    leftConstraint.update(offset: tabWidth * CGFloat($0.rawValue))
+                    setNeedsLayout()
+                    UIView.animate(withDuration: 0.25) { self.layoutIfNeeded() }
+                }
+
+                qrCodeButton.setSelected($0 == .qr)
+                emailButton.setSelected($0 == .email)
+                phoneButton.setSelected($0 == .phone)
+                usernameButton.setSelected($0 == .username)
+            }.store(in: &cancellables)
+    }
+
+    private func setupConstraints() {
+        stackView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+
+        trackView.snp.makeConstraints {
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+            $0.height.equalTo(2)
+        }
+
+        trackIndicatorView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            leftConstraint = $0.left.equalToSuperview().constraint
+            $0.width.equalToSuperview().dividedBy(4)
+            $0.bottom.equalToSuperview()
+        }
+    }
+}
diff --git a/Sources/SearchFeature/Views/SearchView.swift b/Sources/SearchFeature/Views/SearchView.swift
deleted file mode 100644
index 86f929b1000aca396cd1fd8c63de372ff399472f..0000000000000000000000000000000000000000
--- a/Sources/SearchFeature/Views/SearchView.swift
+++ /dev/null
@@ -1,129 +0,0 @@
-import UIKit
-import Shared
-import InputField
-
-final class SearchView: UIView {
-    private enum Constants {
-        static let phone = Localized.ContactSearch.Filter.phone
-        static let email = Localized.ContactSearch.Filter.email
-        static let username = Localized.ContactSearch.Filter.username
-    }
-
-    let input = InputField()
-    let stack = UIStackView()
-    let filters = UIStackView()
-    let email = FilterItemView()
-    let phone = FilterItemView()
-    let empty = SearchEmptyView()
-    let phoneInput = InputField()
-    let username = FilterItemView()
-    lazy var placeholder = SearchPlaceholderView { self.didTapInfo() }
-
-    let didTapInfo: () -> Void
-
-    init(didTapInfo: @escaping () -> Void) {
-        self.didTapInfo = didTapInfo
-
-        super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    func alternateFieldsOver(filter: SelectedFilter) {
-        switch filter {
-        case .username, .email:
-            input.isHidden = false
-            phoneInput.isHidden = true
-        case .phone:
-            input.isHidden = true
-            phoneInput.isHidden = false
-        }
-    }
-
-    func select(filter: SelectedFilter) {
-        [username, email, phone].forEach { $0.style = .unselected }
-
-        switch filter {
-        case .username:
-            username.style = .selected
-            empty.set(filter: Constants.username.lowercased())
-            input.makeFirstResponder()
-        case .email:
-            email.style = .selected
-            empty.set(filter: Constants.email.lowercased())
-            input.makeFirstResponder()
-        case .phone:
-            phone.style = .selected
-            empty.set(filter: Constants.phone.lowercased())
-            phoneInput.makeFirstResponder()
-        }
-    }
-
-    // MARK: Private
-
-    private func setup() {
-        backgroundColor = Asset.neutralWhite.color
-
-        input.setup(
-            placeholder: Localized.ContactSearch.title,
-            leftView: .image(Asset.lens.image.withTintColor(Asset.neutralDisabled.color)),
-            accessibility: Localized.Accessibility.Search.input,
-            allowsEmptySpace: false,
-            autocapitalization: .none,
-            returnKeyType: .search,
-            clearable: true
-        )
-
-        phoneInput.setup(
-            style: .phone,
-            placeholder: "1509192596",
-            rightView: .image(Asset.searchLens.image),
-            accessibility: Localized.Accessibility.Search.phoneInput,
-            keyboardType: .numberPad,
-            contentType: .telephoneNumber,
-            returnKeyType: .search,
-            toolbarButtonTitle: Localized.Shared.Search.placeholder,
-            codeAccessibility: Localized.Accessibility.Search.countryCode
-        )
-
-        email.set(title: Constants.email, icon: Asset.searchEmail.image)
-        phone.set(title: Constants.phone, icon: Asset.searchPhone.image)
-        username.set(title: Constants.username, icon: Asset.searchUsername.image, style: .selected)
-
-        email.accessibilityIdentifier = Localized.Accessibility.Search.email
-        phone.accessibilityIdentifier = Localized.Accessibility.Search.phone
-        username.accessibilityIdentifier = Localized.Accessibility.Search.username
-
-        filters.addArrangedSubview(username)
-        filters.addArrangedSubview(email)
-        filters.addArrangedSubview(phone)
-        filters.distribution = .fillEqually
-        filters.spacing = 20
-
-        stack.axis = .vertical
-        stack.addArrangedSubview(filters)
-        stack.addArrangedSubview(input)
-        stack.addArrangedSubview(phoneInput)
-
-        addSubview(stack)
-        addSubview(empty)
-        addSubview(placeholder)
-
-        stack.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(14)
-            make.left.equalToSuperview().offset(17)
-            make.right.equalToSuperview().offset(-17)
-        }
-
-        placeholder.snp.makeConstraints { make in
-            make.top.equalTo(stack.snp.bottom)
-            make.left.bottom.right.equalToSuperview()
-        }
-
-        empty.snp.makeConstraints { make in
-            make.top.equalTo(stack.snp.bottom)
-            make.left.bottom.right.equalToSuperview()
-        }
-    }
-}
diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
index 7eae36405a3bf673064d10fd743b648d99d0fc28..1dc36e2ef958cc0c7fba6177e621915d96cfc4cc 100644
--- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
+++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift
@@ -10,7 +10,7 @@ import DependencyInjection
 public final class AccountDeleteController: UIViewController {
     @KeyObject(.username, defaultValue: "") var username: String
 
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: SettingsCoordinating
 
     lazy private var screenView = AccountDeleteView()
diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift
index e12674906cde540f35bbff5741f3ec5e816b4507..52e78e17a42c3933f671917aca89b186362f349a 100644
--- a/Sources/SettingsFeature/Controllers/SettingsController.swift
+++ b/Sources/SettingsFeature/Controllers/SettingsController.swift
@@ -8,7 +8,7 @@ import DependencyInjection
 import ScrollViewController
 
 public final class SettingsController: UIViewController {
-    @Dependency private var hud: HUDType
+    @Dependency private var hud: HUD
     @Dependency private var coordinator: SettingsCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
index 0547bbbce7fd175563e525276af29f27c2a2df6c..db7e64016a317b8fe74fa496102437c771f500db 100644
--- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift
@@ -17,7 +17,7 @@ final class AccountDeleteViewModel {
         deleting = true
 
         DispatchQueue.main.async { [weak self] in
-            self?.hudRelay.send(.on(nil))
+            self?.hudRelay.send(.on)
         }
 
         do {
diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
index 97c146a6aa7645ef12298e8bcc2dae2edf2072e2..9b985922329599ac17010bdd2d16c43dec81a454 100644
--- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
@@ -110,7 +110,7 @@ final class SettingsViewModel {
     }
 
     private func pushNotifications(enable: Bool) {
-        hudRelay.send(.on(nil))
+        hudRelay.send(.on)
 
         if enable == true {
             pushHandler.requestAuthorization { [weak self] result in
diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/Shared/AutoGenerated/Assets.swift
index 2b2248fb83ad430ec95d9378a5d5762e8d74c046..6755de526a369e8245365d38a47984ea96a02e42 100644
--- a/Sources/Shared/AutoGenerated/Assets.swift
+++ b/Sources/Shared/AutoGenerated/Assets.swift
@@ -97,6 +97,7 @@ public enum Asset {
   public static let requestsTabReceived = ImageAsset(name: "requests_tab_received")
   public static let requestsTabSent = ImageAsset(name: "requests_tab_sent")
   public static let requestsVerificationFailed = ImageAsset(name: "requests_verification_failed")
+  public static let restoreSFTP = ImageAsset(name: "restore_SFTP")
   public static let restoreDrive = ImageAsset(name: "restore_drive")
   public static let restoreDropbox = ImageAsset(name: "restore_dropbox")
   public static let restoreIcloud = ImageAsset(name: "restore_icloud")
@@ -113,6 +114,10 @@ public enum Asset {
   public static let searchLens = ImageAsset(name: "search_lens")
   public static let searchPhone = ImageAsset(name: "search_phone")
   public static let searchPlaceholderImage = ImageAsset(name: "search_placeholder_image")
+  public static let searchTabEmail = ImageAsset(name: "search_tab_email")
+  public static let searchTabPhone = ImageAsset(name: "search_tab_phone")
+  public static let searchTabQr = ImageAsset(name: "search_tab_qr")
+  public static let searchTabUsername = ImageAsset(name: "search_tab_username")
   public static let searchUsername = ImageAsset(name: "search_username")
   public static let icon32 = ImageAsset(name: "Icon-32")
   public static let settingsAdvanced = ImageAsset(name: "settings_advanced")
diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift
index 07f2cee03ff185b2bd7f9fd768d00758302c74a2..ed861800297b7c53e92a0e83f19aa322de123ec4 100644
--- a/Sources/Shared/AutoGenerated/Strings.swift
+++ b/Sources/Shared/AutoGenerated/Strings.swift
@@ -211,6 +211,20 @@ public enum Localized {
       /// Backup not found
       public static let title = Localized.tr("Localizable", "accountRestore.notFound.title")
     }
+    public enum Sftp {
+      /// Host
+      public static let host = Localized.tr("Localizable", "accountRestore.sftp.host")
+      /// Login
+      public static let login = Localized.tr("Localizable", "accountRestore.sftp.login")
+      /// Password
+      public static let password = Localized.tr("Localizable", "accountRestore.sftp.password")
+      /// Login to your server. Your credentials will be automatically and securely saved locally on your device.
+      public static let subtitle = Localized.tr("Localizable", "accountRestore.sftp.subtitle")
+      /// Login to your SFTP
+      public static let title = Localized.tr("Localizable", "accountRestore.sftp.title")
+      /// Username
+      public static let username = Localized.tr("Localizable", "accountRestore.sftp.username")
+    }
     public enum Success {
       /// You now have access to all your contacts.
       public static let subtitle = Localized.tr("Localizable", "accountRestore.success.subtitle")
@@ -236,6 +250,8 @@ public enum Localized {
     public static let header = Localized.tr("Localizable", "backup.header")
     /// iCloud
     public static let iCloud = Localized.tr("Localizable", "backup.iCloud")
+    /// SFTP
+    public static let sftp = Localized.tr("Localizable", "backup.SFTP")
     /// Back up your account to a cloud storage service, you can restore it along with only your contacts when you reinstall xx Messenger on another device.
     public static let subtitle = Localized.tr("Localizable", "backup.subtitle")
     public enum Config {
@@ -414,6 +430,10 @@ public enum Localized {
       /// Cancel
       public static let cancel = Localized.tr("Localizable", "chatList.navigationBar.cancel")
     }
+    public enum Search {
+      /// Search chats
+      public static let title = Localized.tr("Localizable", "chatList.search.title")
+    }
     public enum Traffic {
       /// Not now
       public static let negative = Localized.tr("Localizable", "chatList.traffic.negative")
@@ -534,57 +554,6 @@ public enum Localized {
     }
   }
 
-  public enum ContactSearch {
-    /// There are no users with that %@.
-    public static func noneFound(_ p1: Any) -> String {
-      return Localized.tr("Localizable", "contactSearch.noneFound", String(describing: p1))
-    }
-    /// User
-    public static let sectionTitle = Localized.tr("Localizable", "contactSearch.sectionTitle")
-    /// Search
-    public static let title = Localized.tr("Localizable", "contactSearch.title")
-    public enum Filter {
-      /// Email
-      public static let email = Localized.tr("Localizable", "contactSearch.filter.email")
-      /// Phone
-      public static let phone = Localized.tr("Localizable", "contactSearch.filter.phone")
-      /// Username
-      public static let username = Localized.tr("Localizable", "contactSearch.filter.username")
-    }
-    public enum NicknameDrawer {
-      /// Save
-      public static let save = Localized.tr("Localizable", "contactSearch.nicknameDrawer.save")
-      /// Edit your new contact’s nickname so you know who they are.
-      public static let subtitle = Localized.tr("Localizable", "contactSearch.nicknameDrawer.subtitle")
-      /// Add a nickname
-      public static let title = Localized.tr("Localizable", "contactSearch.nicknameDrawer.title")
-    }
-    public enum Placeholder {
-      /// Searching is private by nature. The network cannot identify who a search request came from.
-      public static let title = Localized.tr("Localizable", "contactSearch.placeholder.title")
-      public enum Drawer {
-        /// Got it
-        public static let action = Localized.tr("Localizable", "contactSearch.placeholder.drawer.action")
-        /// You can search for users by their username, email, or phone number using the xx network’s #Anonymous Data Retrieval protocol# which keeps a user’s identity anonymous while requesting data. All sent requests contain salted hashes of what you are searching for. Raw data on emails, usernames, and phone numbers do not leave your phone.
-        public static let subtitle = Localized.tr("Localizable", "contactSearch.placeholder.drawer.subtitle")
-        /// Search
-        public static let title = Localized.tr("Localizable", "contactSearch.placeholder.drawer.title")
-      }
-    }
-    public enum RequestDrawer {
-      /// Cancel
-      public static let cancel = Localized.tr("Localizable", "contactSearch.requestDrawer.cancel")
-      /// EMAIL ADDRESS
-      public static let email = Localized.tr("Localizable", "contactSearch.requestDrawer.email")
-      /// PHONE NUMBER
-      public static let phone = Localized.tr("Localizable", "contactSearch.requestDrawer.phone")
-      /// Send Contact Request
-      public static let send = Localized.tr("Localizable", "contactSearch.requestDrawer.send")
-      /// Request Contact
-      public static let title = Localized.tr("Localizable", "contactSearch.requestDrawer.title")
-    }
-  }
-
   public enum Countries {
     /// Country Code
     public static let title = Localized.tr("Localizable", "countries.title")
@@ -996,6 +965,14 @@ public enum Localized {
         public static func resent(_ p1: Any) -> String {
           return Localized.tr("Localizable", "requests.sent.toast.resent", String(describing: p1))
         }
+        /// Request couldn't be resent to %@
+        public static func resentFailed(_ p1: Any) -> String {
+          return Localized.tr("Localizable", "requests.sent.toast.resentFailed", String(describing: p1))
+        }
+        /// Request successfully sent to %@
+        public static func sent(_ p1: Any) -> String {
+          return Localized.tr("Localizable", "requests.sent.toast.sent", String(describing: p1))
+        }
       }
     }
   }
@@ -1231,6 +1208,73 @@ public enum Localized {
     }
   }
 
+  public enum Ud {
+    /// There are no users with that %@.
+    public static func noneFound(_ p1: Any) -> String {
+      return Localized.tr("Localizable", "ud.noneFound", String(describing: p1))
+    }
+    /// Search
+    public static let title = Localized.tr("Localizable", "ud.title")
+    public enum NicknameDrawer {
+      /// Save
+      public static let save = Localized.tr("Localizable", "ud.nicknameDrawer.save")
+      /// Edit your new contact’s nickname so you know who they are.
+      public static let subtitle = Localized.tr("Localizable", "ud.nicknameDrawer.subtitle")
+      /// Add a nickname
+      public static let title = Localized.tr("Localizable", "ud.nicknameDrawer.title")
+    }
+    public enum Placeholder {
+      public enum Drawer {
+        /// Got it
+        public static let action = Localized.tr("Localizable", "ud.placeholder.drawer.action")
+        /// You can search for users by their username, email, or phone number using the xx network’s #Anonymous Data Retrieval protocol# which keeps a user’s identity anonymous while requesting data. All sent requests contain salted hashes of what you are searching for. Raw data on emails, usernames, and phone numbers do not leave your phone.
+        public static let subtitle = Localized.tr("Localizable", "ud.placeholder.drawer.subtitle")
+        /// Search
+        public static let title = Localized.tr("Localizable", "ud.placeholder.drawer.title")
+      }
+    }
+    public enum RequestDrawer {
+      /// Cancel
+      public static let cancel = Localized.tr("Localizable", "ud.requestDrawer.cancel")
+      /// EMAIL ADDRESS
+      public static let email = Localized.tr("Localizable", "ud.requestDrawer.email")
+      /// PHONE NUMBER
+      public static let phone = Localized.tr("Localizable", "ud.requestDrawer.phone")
+      /// Send Contact Request
+      public static let send = Localized.tr("Localizable", "ud.requestDrawer.send")
+      /// Request Contact
+      public static let title = Localized.tr("Localizable", "ud.requestDrawer.title")
+    }
+    public enum Search {
+      /// Cancel search
+      public static let cancel = Localized.tr("Localizable", "ud.search.cancel")
+      /// There are no users with that %@.
+      public static func empty(_ p1: Any) -> String {
+        return Localized.tr("Localizable", "ud.search.empty", String(describing: p1))
+      }
+      /// Search by %@
+      public static func input(_ p1: Any) -> String {
+        return Localized.tr("Localizable", "ud.search.input", String(describing: p1))
+      }
+      public enum Placeholder {
+        /// Your searches are anonymous. Search information is never linked to your account or personally identifiable.
+        public static let subtitle = Localized.tr("Localizable", "ud.search.placeholder.subtitle")
+        /// Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel.
+        public static let title = Localized.tr("Localizable", "ud.search.placeholder.title")
+      }
+    }
+    public enum Tab {
+      /// Email
+      public static let email = Localized.tr("Localizable", "ud.tab.email")
+      /// Phone
+      public static let phone = Localized.tr("Localizable", "ud.tab.phone")
+      /// QR Code
+      public static let qr = Localized.tr("Localizable", "ud.tab.qr")
+      /// Username
+      public static let username = Localized.tr("Localizable", "ud.tab.username")
+    }
+  }
+
   public enum Validator {
     public enum Code {
       /// Code length should be at least 4 chars
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..08fcf11943d754dfc4e7427d290def530dc7dbcb
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf
@@ -0,0 +1,447 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 15.426392 33.381592 cm
+0.693500 0.711750 0.730000 scn
+23.717567 7.403564 m
+1.429677 7.403564 l
+0.639939 7.403564 0.000000 6.763626 0.000000 5.973887 c
+0.000000 0.000000 l
+25.147245 0.000000 l
+25.147245 5.973887 l
+25.147245 6.763626 24.507305 7.403564 23.717567 7.403564 c
+h
+3.436188 1.708138 m
+2.466323 1.708138 l
+2.293242 1.708138 2.152940 1.848440 2.152940 2.021521 c
+2.152940 2.194602 2.293242 2.334904 2.466323 2.334904 c
+3.436188 2.334904 l
+3.609268 2.334904 3.749571 2.194602 3.749571 2.021521 c
+3.749265 1.848440 3.608962 1.708138 3.436188 1.708138 c
+h
+3.436188 3.388399 m
+2.466323 3.388399 l
+2.293242 3.388399 2.152940 3.528702 2.152940 3.701782 c
+2.152940 3.874863 2.293242 4.015165 2.466323 4.015165 c
+3.436188 4.015165 l
+3.609268 4.015165 3.749571 3.874863 3.749571 3.701782 c
+3.749265 3.528702 3.608962 3.388399 3.436188 3.388399 c
+h
+3.436188 5.069273 m
+2.466323 5.069273 l
+2.293242 5.069273 2.152940 5.209575 2.152940 5.382656 c
+2.152940 5.555737 2.293242 5.696039 2.466323 5.696039 c
+3.436188 5.696039 l
+3.609268 5.696039 3.749571 5.555737 3.749571 5.382656 c
+3.749571 5.209575 3.608962 5.069273 3.436188 5.069273 c
+h
+6.515186 1.708138 m
+5.545321 1.708138 l
+5.372241 1.708138 5.231938 1.848440 5.231938 2.021521 c
+5.231938 2.194602 5.372241 2.334904 5.545321 2.334904 c
+6.515186 2.334904 l
+6.688267 2.334904 6.828569 2.194602 6.828569 2.021521 c
+6.828263 1.848440 6.687960 1.708138 6.515186 1.708138 c
+h
+6.515186 3.388399 m
+5.545321 3.388399 l
+5.372241 3.388399 5.231938 3.528702 5.231938 3.701782 c
+5.231938 3.874863 5.372241 4.015165 5.545321 4.015165 c
+6.515186 4.015165 l
+6.688267 4.015165 6.828569 3.874863 6.828569 3.701782 c
+6.828263 3.528702 6.687960 3.388399 6.515186 3.388399 c
+h
+6.515186 5.069273 m
+5.545321 5.069273 l
+5.372241 5.069273 5.231938 5.209575 5.231938 5.382656 c
+5.231938 5.555737 5.372241 5.696039 5.545321 5.696039 c
+6.515186 5.696039 l
+6.688267 5.696039 6.828569 5.555737 6.828569 5.382656 c
+6.828569 5.209575 6.687960 5.069273 6.515186 5.069273 c
+h
+9.594184 1.708138 m
+8.624320 1.708138 l
+8.451240 1.708138 8.310936 1.848440 8.310936 2.021521 c
+8.310936 2.194602 8.451240 2.334904 8.624320 2.334904 c
+9.594184 2.334904 l
+9.767264 2.334904 9.907569 2.194602 9.907569 2.021521 c
+9.907262 1.848440 9.766958 1.708138 9.594184 1.708138 c
+h
+9.594184 3.388399 m
+8.624320 3.388399 l
+8.451240 3.388399 8.310936 3.528702 8.310936 3.701782 c
+8.310936 3.874863 8.451240 4.015165 8.624320 4.015165 c
+9.594184 4.015165 l
+9.767264 4.015165 9.907569 3.874863 9.907569 3.701782 c
+9.907262 3.528702 9.766958 3.388399 9.594184 3.388399 c
+h
+9.594184 5.069273 m
+8.624320 5.069273 l
+8.451240 5.069273 8.310936 5.209575 8.310936 5.382656 c
+8.310936 5.555737 8.451240 5.696039 8.624320 5.696039 c
+9.594184 5.696039 l
+9.767264 5.696039 9.907569 5.555737 9.907569 5.382656 c
+9.907569 5.209575 9.766958 5.069273 9.594184 5.069273 c
+h
+12.673183 1.708138 m
+11.703625 1.708138 l
+11.530544 1.708138 11.390241 1.848440 11.390241 2.021521 c
+11.390241 2.194602 11.530544 2.334904 11.703625 2.334904 c
+12.673183 2.334904 l
+12.846264 2.334904 12.986566 2.194602 12.986566 2.021521 c
+12.986259 1.848440 12.845958 1.708138 12.673183 1.708138 c
+h
+12.673183 3.388399 m
+11.703625 3.388399 l
+11.530544 3.388399 11.390241 3.528702 11.390241 3.701782 c
+11.390241 3.874863 11.530544 4.015165 11.703625 4.015165 c
+12.673183 4.015165 l
+12.846264 4.015165 12.986566 3.874863 12.986566 3.701782 c
+12.986259 3.528702 12.845958 3.388399 12.673183 3.388399 c
+h
+12.673183 5.069273 m
+11.703625 5.069273 l
+11.530544 5.069273 11.390241 5.209575 11.390241 5.382656 c
+11.390241 5.555737 11.530544 5.696039 11.703625 5.696039 c
+12.673183 5.696039 l
+12.846264 5.696039 12.986566 5.555737 12.986566 5.382656 c
+12.986566 5.209575 12.845958 5.069273 12.673183 5.069273 c
+h
+16.798935 2.525446 m
+16.149193 2.525446 15.622601 3.052040 15.622601 3.701782 c
+15.622601 4.351524 16.149193 4.878118 16.798935 4.878118 c
+17.448677 4.878118 17.975273 4.351524 17.975273 3.701782 c
+17.975273 3.052040 17.448677 2.525446 16.798935 2.525446 c
+h
+21.504585 2.525446 m
+20.854843 2.525446 20.328251 3.052040 20.328251 3.701782 c
+20.328251 4.351524 20.854843 4.878118 21.504585 4.878118 c
+22.154327 4.878118 22.680923 4.351524 22.680923 3.701782 c
+22.680923 3.052040 22.154327 2.525446 21.504585 2.525446 c
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 15.426392 23.298096 cm
+0.693500 0.711750 0.730000 scn
+0.000000 7.403564 m
+0.000000 0.000000 l
+25.147245 0.000000 l
+25.147245 7.403564 l
+0.000000 7.403564 l
+h
+3.436188 1.707832 m
+2.466323 1.707832 l
+2.293242 1.707832 2.152940 1.847827 2.152940 2.021214 c
+2.152940 2.194602 2.293242 2.334598 2.466323 2.334598 c
+3.436188 2.334598 l
+3.609268 2.334598 3.749571 2.194602 3.749571 2.021214 c
+3.749571 1.847827 3.608962 1.707832 3.436188 1.707832 c
+h
+3.436188 3.388399 m
+2.466323 3.388399 l
+2.293242 3.388399 2.152940 3.528702 2.152940 3.701782 c
+2.152940 3.874863 2.293242 4.015165 2.466323 4.015165 c
+3.436188 4.015165 l
+3.609268 4.015165 3.749571 3.874863 3.749571 3.701782 c
+3.749265 3.528702 3.608962 3.388399 3.436188 3.388399 c
+h
+3.436188 5.068967 m
+2.466323 5.068967 l
+2.293242 5.068967 2.152940 5.209269 2.152940 5.382350 c
+2.152940 5.555430 2.293242 5.695733 2.466323 5.695733 c
+3.436188 5.695733 l
+3.609268 5.695733 3.749571 5.555430 3.749571 5.382350 c
+3.749265 5.209269 3.608962 5.068967 3.436188 5.068967 c
+h
+6.515186 1.707832 m
+5.545321 1.707832 l
+5.372241 1.707832 5.231938 1.847827 5.231938 2.021214 c
+5.231938 2.194602 5.372241 2.334598 5.545321 2.334598 c
+6.515186 2.334598 l
+6.688267 2.334598 6.828569 2.194602 6.828569 2.021214 c
+6.828569 1.847827 6.687960 1.707832 6.515186 1.707832 c
+h
+6.515186 3.388399 m
+5.545321 3.388399 l
+5.372241 3.388399 5.231938 3.528702 5.231938 3.701782 c
+5.231938 3.874863 5.372241 4.015165 5.545321 4.015165 c
+6.515186 4.015165 l
+6.688267 4.015165 6.828569 3.874863 6.828569 3.701782 c
+6.828263 3.528702 6.687960 3.388399 6.515186 3.388399 c
+h
+6.515186 5.068967 m
+5.545321 5.068967 l
+5.372241 5.068967 5.231938 5.209269 5.231938 5.382350 c
+5.231938 5.555430 5.372241 5.695733 5.545321 5.695733 c
+6.515186 5.695733 l
+6.688267 5.695733 6.828569 5.555430 6.828569 5.382350 c
+6.828263 5.209269 6.687960 5.068967 6.515186 5.068967 c
+h
+9.594184 1.707832 m
+8.624320 1.707832 l
+8.451240 1.707832 8.310936 1.847827 8.310936 2.021214 c
+8.310936 2.194602 8.451240 2.334598 8.624320 2.334598 c
+9.594184 2.334598 l
+9.767264 2.334598 9.907569 2.194602 9.907569 2.021214 c
+9.907569 1.847827 9.766958 1.707832 9.594184 1.707832 c
+h
+9.594184 3.388399 m
+8.624320 3.388399 l
+8.451240 3.388399 8.310936 3.528702 8.310936 3.701782 c
+8.310936 3.874863 8.451240 4.015165 8.624320 4.015165 c
+9.594184 4.015165 l
+9.767264 4.015165 9.907569 3.874863 9.907569 3.701782 c
+9.907262 3.528702 9.766958 3.388399 9.594184 3.388399 c
+h
+9.594184 5.068967 m
+8.624320 5.068967 l
+8.451240 5.068967 8.310936 5.209269 8.310936 5.382350 c
+8.310936 5.555430 8.451240 5.695733 8.624320 5.695733 c
+9.594184 5.695733 l
+9.767264 5.695733 9.907569 5.555430 9.907569 5.382350 c
+9.907262 5.209269 9.766958 5.068967 9.594184 5.068967 c
+h
+12.673183 1.707832 m
+11.703625 1.707832 l
+11.530544 1.707832 11.390241 1.847827 11.390241 2.021214 c
+11.390241 2.194602 11.530544 2.334598 11.703625 2.334598 c
+12.673183 2.334598 l
+12.846264 2.334598 12.986566 2.194602 12.986566 2.021214 c
+12.986566 1.847827 12.845958 1.707832 12.673183 1.707832 c
+h
+12.673183 3.388399 m
+11.703625 3.388399 l
+11.530544 3.388399 11.390241 3.528702 11.390241 3.701782 c
+11.390241 3.874863 11.530544 4.015165 11.703625 4.015165 c
+12.673183 4.015165 l
+12.846264 4.015165 12.986566 3.874863 12.986566 3.701782 c
+12.986259 3.528702 12.845958 3.388399 12.673183 3.388399 c
+h
+12.673183 5.068967 m
+11.703625 5.068967 l
+11.530544 5.068967 11.390241 5.209269 11.390241 5.382350 c
+11.390241 5.555430 11.530544 5.695733 11.703625 5.695733 c
+12.673183 5.695733 l
+12.846264 5.695733 12.986566 5.555430 12.986566 5.382350 c
+12.986259 5.209269 12.845958 5.068967 12.673183 5.068967 c
+h
+16.798935 2.525446 m
+16.149193 2.525446 15.622601 3.052040 15.622601 3.701782 c
+15.622601 4.351524 16.149193 4.878119 16.798935 4.878119 c
+17.448677 4.878119 17.975273 4.351524 17.975273 3.701782 c
+17.975273 3.052040 17.448677 2.525446 16.798935 2.525446 c
+h
+21.504585 2.525446 m
+20.854843 2.525446 20.328251 3.052040 20.328251 3.701782 c
+20.328251 4.351524 20.854843 4.878119 21.504585 4.878119 c
+22.154327 4.878119 22.680923 4.351524 22.680923 3.701782 c
+22.680923 3.052040 22.154327 2.525446 21.504585 2.525446 c
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 15.426392 13.214844 cm
+0.693500 0.711750 0.730000 scn
+0.000000 7.403564 m
+0.000000 1.429677 l
+0.000000 0.639939 0.639939 0.000000 1.429677 0.000000 c
+23.717876 0.000000 l
+24.507307 0.000000 25.147552 0.639939 25.147552 1.429677 c
+25.147552 7.403564 l
+0.000000 7.403564 l
+h
+3.436188 1.707830 m
+2.466323 1.707830 l
+2.293243 1.707830 2.152940 1.847827 2.152940 2.021214 c
+2.152940 2.194295 2.293243 2.334599 2.466323 2.334599 c
+3.436188 2.334599 l
+3.609269 2.334599 3.749571 2.194602 3.749571 2.021214 c
+3.749265 1.848134 3.608963 1.707830 3.436188 1.707830 c
+h
+3.436188 3.388398 m
+2.466323 3.388398 l
+2.293243 3.388398 2.152940 3.528395 2.152940 3.701782 c
+2.152940 3.875169 2.293243 4.015166 2.466323 4.015166 c
+3.436188 4.015166 l
+3.609269 4.015166 3.749571 3.875169 3.749571 3.701782 c
+3.749571 3.528395 3.608963 3.388398 3.436188 3.388398 c
+h
+3.436188 5.068966 m
+2.466323 5.068966 l
+2.293243 5.068966 2.152940 5.208962 2.152940 5.382350 c
+2.152940 5.555430 2.293243 5.695734 2.466323 5.695734 c
+3.436188 5.695734 l
+3.609269 5.695734 3.749571 5.555737 3.749571 5.382350 c
+3.749265 5.209269 3.608963 5.068966 3.436188 5.068966 c
+h
+6.515187 1.707830 m
+5.545322 1.707830 l
+5.372241 1.707830 5.231939 1.847827 5.231939 2.021214 c
+5.231939 2.194295 5.372241 2.334599 5.545322 2.334599 c
+6.515187 2.334599 l
+6.688267 2.334599 6.828570 2.194602 6.828570 2.021214 c
+6.828264 1.848134 6.687961 1.707830 6.515187 1.707830 c
+h
+6.515187 3.388398 m
+5.545322 3.388398 l
+5.372241 3.388398 5.231939 3.528395 5.231939 3.701782 c
+5.231939 3.875169 5.372241 4.015166 5.545322 4.015166 c
+6.515187 4.015166 l
+6.688267 4.015166 6.828570 3.875169 6.828570 3.701782 c
+6.828570 3.528395 6.687961 3.388398 6.515187 3.388398 c
+h
+6.515187 5.068966 m
+5.545322 5.068966 l
+5.372241 5.068966 5.231939 5.208962 5.231939 5.382350 c
+5.231939 5.555430 5.372241 5.695734 5.545322 5.695734 c
+6.515187 5.695734 l
+6.688267 5.695734 6.828570 5.555737 6.828570 5.382350 c
+6.828264 5.209269 6.687961 5.068966 6.515187 5.068966 c
+h
+9.594185 1.707830 m
+8.624321 1.707830 l
+8.451241 1.707830 8.310937 1.847827 8.310937 2.021214 c
+8.310937 2.194295 8.451241 2.334599 8.624321 2.334599 c
+9.594185 2.334599 l
+9.767265 2.334599 9.907570 2.194602 9.907570 2.021214 c
+9.907263 1.848134 9.766959 1.707830 9.594185 1.707830 c
+h
+9.594185 3.388398 m
+8.624321 3.388398 l
+8.451241 3.388398 8.310937 3.528395 8.310937 3.701782 c
+8.310937 3.875169 8.451241 4.015166 8.624321 4.015166 c
+9.594185 4.015166 l
+9.767265 4.015166 9.907570 3.875169 9.907570 3.701782 c
+9.907570 3.528395 9.766959 3.388398 9.594185 3.388398 c
+h
+9.594185 5.068966 m
+8.624321 5.068966 l
+8.451241 5.068966 8.310937 5.208962 8.310937 5.382350 c
+8.310937 5.555430 8.451241 5.695734 8.624321 5.695734 c
+9.594185 5.695734 l
+9.767265 5.695734 9.907570 5.555737 9.907570 5.382350 c
+9.907263 5.209269 9.766959 5.068966 9.594185 5.068966 c
+h
+12.673184 1.707830 m
+11.703626 1.707830 l
+11.530545 1.707830 11.390242 1.847827 11.390242 2.021214 c
+11.390242 2.194295 11.530545 2.334599 11.703626 2.334599 c
+12.673184 2.334599 l
+12.846266 2.334599 12.986567 2.194602 12.986567 2.021214 c
+12.986260 1.848134 12.845959 1.707830 12.673184 1.707830 c
+h
+12.673184 3.388398 m
+11.703626 3.388398 l
+11.530545 3.388398 11.390242 3.528395 11.390242 3.701782 c
+11.390242 3.875169 11.530545 4.015166 11.703626 4.015166 c
+12.673184 4.015166 l
+12.846266 4.015166 12.986567 3.875169 12.986567 3.701782 c
+12.986567 3.528395 12.845959 3.388398 12.673184 3.388398 c
+h
+12.673184 5.068966 m
+11.703626 5.068966 l
+11.530545 5.068966 11.390242 5.208962 11.390242 5.382350 c
+11.390242 5.555430 11.530545 5.695734 11.703626 5.695734 c
+12.673184 5.695734 l
+12.846266 5.695734 12.986567 5.555737 12.986567 5.382350 c
+12.986260 5.209269 12.845959 5.068966 12.673184 5.068966 c
+h
+16.798937 2.525447 m
+16.149195 2.525447 15.622602 3.052040 15.622602 3.701782 c
+15.622602 4.351524 16.149195 4.878119 16.798937 4.878119 c
+17.448679 4.878119 17.975275 4.351524 17.975275 3.701782 c
+17.975275 3.052040 17.448679 2.525447 16.798937 2.525447 c
+h
+21.504587 2.525447 m
+20.854845 2.525447 20.328253 3.052040 20.328253 3.701782 c
+20.328253 4.351524 20.854845 4.878119 21.504587 4.878119 c
+22.154329 4.878119 22.680925 4.351524 22.680925 3.701782 c
+22.680925 3.052040 22.154329 2.525447 21.504587 2.525447 c
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 17.400757 21.498291 cm
+0.693500 0.711750 0.730000 scn
+21.201620 0.918945 m
+0.000000 0.918945 l
+0.000000 -0.000067 l
+21.201620 -0.000067 l
+21.201620 0.918945 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 17.400757 31.582764 cm
+0.693500 0.711750 0.730000 scn
+21.201620 0.918945 m
+0.000000 0.918945 l
+0.000000 -0.000067 l
+21.201620 -0.000067 l
+21.201620 0.918945 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  13265
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 56.000000 56.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000013355 00000 n
+0000013379 00000 n
+0000013552 00000 n
+0000013626 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+13685
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9a97df899595ebc48148baa9cbedb9be8f763fa
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..488ddd5591377db8b2e8c7e3199c76dfe643394f
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_email.imageset/Icon.pdf
@@ -0,0 +1,87 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 3.000000 4.599121 cm
+0.809948 0.827749 0.850000 scn
+16.199999 0.000879 m
+1.800000 0.000879 l
+0.805887 0.000879 0.000000 0.806765 0.000000 1.800879 c
+0.000000 12.679177 l
+0.041948 13.642586 0.835680 14.401790 1.800000 14.400878 c
+16.199999 14.400878 l
+17.194113 14.400878 18.000000 13.594990 18.000000 12.600878 c
+18.000000 1.800879 l
+18.000000 0.806765 17.194113 0.000879 16.199999 0.000879 c
+h
+1.800000 10.919678 m
+1.800000 1.800878 l
+16.199999 1.800878 l
+16.199999 10.919678 l
+9.000000 6.120877 l
+1.800000 10.919678 l
+h
+2.520000 12.600878 m
+9.000000 8.280877 l
+15.480000 12.600878 l
+2.520000 12.600878 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  681
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000000771 00000 n
+0000000793 00000 n
+0000000966 00000 n
+0000001040 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+1099
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9a97df899595ebc48148baa9cbedb9be8f763fa
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..e53e9789a38f9a7ffb48759a3c5c0e8c23dbd900
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_phone.imageset/Icon.pdf
@@ -0,0 +1,202 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 3.000000 0.831543 cm
+0.809948 0.827749 0.850000 scn
+5.310195 13.179303 m
+4.370402 13.521046 l
+4.207593 13.073322 l
+4.452703 12.664807 l
+5.310195 13.179303 l
+h
+5.934925 14.897307 m
+5.006448 15.268698 l
+5.000554 15.253963 l
+4.995131 15.239050 l
+5.934925 14.897307 l
+h
+6.052061 16.107719 m
+7.032641 16.303835 l
+7.027975 16.327169 l
+7.022203 16.350256 l
+6.052061 16.107719 l
+h
+5.036876 20.168457 m
+6.007019 20.410994 l
+5.787323 21.289776 l
+4.891161 21.157784 l
+5.036876 20.168457 l
+h
+0.000000 19.426592 m
+-0.145715 20.415918 l
+-1.053928 20.282150 l
+-0.998153 19.365835 l
+0.000000 19.426592 l
+h
+5.310195 7.517696 m
+4.598751 6.814928 l
+4.603089 6.810590 l
+5.310195 7.517696 l
+h
+17.414316 2.168457 m
+17.363102 1.169769 l
+18.311787 1.121119 l
+18.409060 2.066057 l
+17.414316 2.168457 l
+h
+17.960955 7.478652 m
+18.955698 7.376251 l
+19.046246 8.255862 l
+18.184332 8.453384 l
+17.960955 7.478652 l
+h
+14.212583 8.337654 m
+14.435959 9.312387 l
+14.413817 9.317461 l
+14.391468 9.321525 l
+14.212583 8.337654 l
+h
+12.963124 8.181472 m
+13.363943 7.265314 l
+13.387419 7.275585 l
+13.410338 7.287045 l
+12.963124 8.181472 l
+h
+11.088938 7.361515 m
+10.574442 6.504023 l
+11.016961 6.238511 l
+11.489756 6.445358 l
+11.088938 7.361515 l
+h
+6.249989 12.837560 m
+6.874718 14.555564 l
+4.995131 15.239050 l
+4.370402 13.521046 l
+6.249989 12.837560 l
+h
+6.863401 14.525917 m
+7.089864 15.092072 7.149697 15.718555 7.032641 16.303835 c
+5.071480 15.911604 l
+5.110607 15.715972 5.092350 15.483454 5.006448 15.268698 c
+6.863401 14.525917 l
+h
+7.022203 16.350256 m
+6.007019 20.410994 l
+4.066734 19.925920 l
+5.081918 15.865184 l
+7.022203 16.350256 l
+h
+4.891161 21.157784 m
+-0.145715 20.415918 l
+0.145715 18.437265 l
+5.182591 19.179131 l
+4.891161 21.157784 l
+h
+-0.998153 19.365835 m
+-0.710203 14.635235 1.265032 10.189831 4.598764 6.814941 c
+6.021627 8.220452 l
+3.029979 11.249034 1.256841 15.237471 0.998153 19.487349 c
+-0.998153 19.365835 l
+h
+4.603089 6.810590 m
+8.022314 3.391365 12.552481 1.416468 17.363102 1.169769 c
+17.465532 3.167145 l
+13.139493 3.388992 9.079639 5.162467 6.017303 8.224804 c
+4.603089 6.810590 l
+h
+18.409060 2.066057 m
+18.955698 7.376251 l
+16.966211 7.581052 l
+16.419573 2.270857 l
+18.409060 2.066057 l
+h
+18.184332 8.453384 m
+14.435959 9.312387 l
+13.989206 7.362922 l
+17.737579 6.503920 l
+18.184332 8.453384 l
+h
+14.391468 9.321525 m
+13.786641 9.431493 13.131458 9.383673 12.515911 9.075899 c
+13.410338 7.287045 l
+13.575702 7.369726 13.779521 7.399999 14.033697 7.353785 c
+14.391468 9.321525 l
+h
+12.562305 9.097629 m
+10.688119 8.277673 l
+11.489756 6.445358 l
+13.363943 7.265314 l
+12.562305 9.097629 l
+h
+11.603433 8.219008 m
+10.504310 8.878483 9.475373 9.686473 8.555264 10.606584 c
+7.141050 9.192369 l
+8.173217 8.160202 9.330832 7.250189 10.574442 6.504023 c
+11.603433 8.219008 l
+h
+8.555264 10.606584 m
+7.605314 11.556533 6.834208 12.582933 6.167688 13.693798 c
+4.452703 12.664807 l
+5.191823 11.432940 6.060631 10.272789 7.141050 9.192369 c
+8.555264 10.606584 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  3012
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000003102 00000 n
+0000003125 00000 n
+0000003298 00000 n
+0000003372 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+3431
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9a97df899595ebc48148baa9cbedb9be8f763fa
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..8ff4fc0bf67f1a731cc8516ce77a2de344bc843c
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_qr.imageset/Icon.pdf
@@ -0,0 +1,189 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 5.000000 12.871094 cm
+0.809948 0.827749 0.850000 scn
+5.685554 6.128906 m
+0.435554 6.128906 l
+0.378356 6.128906 0.321724 6.117636 0.268880 6.095747 c
+0.216036 6.073858 0.168026 6.041771 0.127580 6.001326 c
+0.087135 5.960881 0.055048 5.912870 0.033159 5.860026 c
+0.011270 5.807182 0.000000 5.750551 0.000000 5.693353 c
+0.000000 0.443353 l
+-0.000018 0.327108 0.045616 0.215499 0.127082 0.132578 c
+0.208548 0.049657 0.319328 0.002053 0.435554 0.000013 c
+5.685554 0.000013 l
+5.802508 0.002013 5.914114 0.049368 5.996826 0.132080 c
+6.079538 0.214792 6.126893 0.326398 6.128893 0.443353 c
+6.128893 5.693353 l
+6.126853 5.809579 6.079249 5.920358 5.996328 6.001824 c
+5.913407 6.083291 5.801798 6.128924 5.685554 6.128906 c
+h
+4.371108 1.757799 m
+1.750000 1.757799 l
+1.750000 4.378906 l
+4.371108 4.378906 l
+4.371108 1.757799 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 5.000000 5.000000 cm
+0.809948 0.827749 0.850000 scn
+5.685554 6.128906 m
+0.435554 6.128906 l
+0.319328 6.126867 0.208548 6.079262 0.127082 5.996341 c
+0.045616 5.913420 -0.000018 5.801811 0.000000 5.685567 c
+0.000000 0.435567 l
+0.000000 0.378369 0.011270 0.321738 0.033159 0.268894 c
+0.055048 0.216050 0.087135 0.168039 0.127580 0.127594 c
+0.168026 0.087149 0.216036 0.055061 0.268880 0.033173 c
+0.321724 0.011284 0.378356 0.000013 0.435554 0.000013 c
+5.685554 0.000013 l
+5.801798 -0.000004 5.913407 0.045629 5.996328 0.127095 c
+6.079249 0.208562 6.126853 0.319341 6.128893 0.435567 c
+6.128893 5.685567 l
+6.126893 5.802522 6.079538 5.914128 5.996826 5.996840 c
+5.914114 6.079551 5.802508 6.126906 5.685554 6.128906 c
+h
+4.371108 1.750013 m
+1.750000 1.750013 l
+1.750000 4.371121 l
+4.371108 4.371121 l
+4.371108 1.750013 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 12.871094 12.871094 cm
+0.809948 0.827749 0.850000 scn
+5.693339 6.128906 m
+0.443339 6.128906 l
+0.327095 6.128924 0.215486 6.083291 0.132565 6.001824 c
+0.049644 5.920358 0.002039 5.809579 0.000000 5.693353 c
+0.000000 0.443353 l
+0.002000 0.326398 0.049355 0.214792 0.132067 0.132080 c
+0.214778 0.049368 0.326384 0.002013 0.443339 0.000013 c
+5.693339 0.000013 l
+5.809565 0.002053 5.920344 0.049657 6.001811 0.132578 c
+6.083277 0.215499 6.128911 0.327108 6.128893 0.443353 c
+6.128893 5.693353 l
+6.128893 5.808869 6.082995 5.919643 6.001312 6.001326 c
+5.919630 6.083008 5.808856 6.128906 5.693339 6.128906 c
+h
+4.378893 1.757799 m
+1.757785 1.757799 l
+1.757785 4.378906 l
+4.378893 4.378906 l
+4.378893 1.757799 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 12.871094 8.500000 cm
+0.809948 0.827749 0.850000 scn
+0.443339 0.000013 m
+0.327095 -0.000005 0.215486 0.045629 0.132565 0.127095 c
+0.049644 0.208562 0.002039 0.319341 0.000000 0.435567 c
+0.000000 2.185567 l
+0.002000 2.302522 0.049355 2.414128 0.132067 2.496840 c
+0.214778 2.579551 0.326384 2.626907 0.443339 2.628906 c
+2.193339 2.628906 l
+2.309565 2.626867 2.420345 2.579262 2.501811 2.496341 c
+2.583277 2.413420 2.628911 2.301811 2.628893 2.185567 c
+2.628893 0.435567 l
+2.628893 0.320050 2.582995 0.209276 2.501312 0.127594 c
+2.419630 0.045911 2.308856 0.000013 2.193339 0.000013 c
+0.443339 0.000013 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 12.871094 5.000000 cm
+0.809948 0.827749 0.850000 scn
+5.693339 6.128906 m
+4.814446 6.128906 l
+4.698220 6.126867 4.587441 6.079262 4.505975 5.996341 c
+4.424509 5.913420 4.378875 5.801811 4.378893 5.685567 c
+4.378893 3.500014 l
+3.064446 3.500014 l
+2.948930 3.500014 2.838156 3.454116 2.756473 3.372433 c
+2.674791 3.290751 2.628893 3.179976 2.628893 3.064460 c
+2.628893 1.750013 l
+0.443339 1.750013 l
+0.327095 1.750031 0.215486 1.704398 0.132565 1.622931 c
+0.049644 1.541465 0.002039 1.430686 0.000000 1.314460 c
+0.000000 0.435567 l
+0.002039 0.319341 0.049644 0.208562 0.132565 0.127095 c
+0.215486 0.045629 0.327095 -0.000004 0.443339 0.000013 c
+5.693339 0.000013 l
+5.808856 0.000013 5.919630 0.045911 6.001312 0.127594 c
+6.082995 0.209276 6.128893 0.320051 6.128893 0.435567 c
+6.128893 5.685567 l
+6.128911 5.801811 6.083277 5.913420 6.001811 5.996341 c
+5.920344 6.079262 5.809565 6.126867 5.693339 6.128906 c
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  4107
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000004197 00000 n
+0000004220 00000 n
+0000004393 00000 n
+0000004467 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+4526
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9a97df899595ebc48148baa9cbedb9be8f763fa
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..5f75d04ba8fcb3c602e0eb08226b68a3e6cc3d1c
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsSearch/search_tab_username.imageset/Icon.pdf
@@ -0,0 +1,95 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 4.000000 11.000000 cm
+0.809948 0.827749 0.850000 scn
+3.000000 5.000000 m
+3.000000 7.761424 5.238576 10.000000 8.000000 10.000000 c
+10.761423 10.000000 13.000000 7.761424 13.000000 5.000000 c
+13.000000 2.238576 10.761423 0.000000 8.000000 0.000000 c
+5.238576 0.000000 3.000000 2.238576 3.000000 5.000000 c
+h
+8.000000 2.000000 m
+9.656855 2.000000 11.000000 3.343146 11.000000 5.000000 c
+11.000000 6.656854 9.656855 8.000000 8.000000 8.000000 c
+6.343146 8.000000 5.000000 6.656854 5.000000 5.000000 c
+5.000000 3.343146 6.343146 2.000000 8.000000 2.000000 c
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 4.000000 13.000000 cm
+0.809948 0.827749 0.850000 scn
+2.343146 -5.343145 m
+0.842855 -6.843436 0.000000 -8.878267 0.000000 -11.000000 c
+2.000000 -11.000000 l
+2.000000 -9.408701 2.632141 -7.882577 3.757360 -6.757359 c
+4.882578 -5.632140 6.408701 -5.000000 8.000000 -5.000000 c
+9.591299 -5.000000 11.117423 -5.632140 12.242641 -6.757359 c
+13.367860 -7.882577 14.000000 -9.408701 14.000000 -11.000000 c
+16.000000 -11.000000 l
+16.000000 -8.878267 15.157146 -6.843436 13.656855 -5.343145 c
+12.156564 -3.842854 10.121732 -3.000000 8.000000 -3.000000 c
+5.878268 -3.000000 3.843437 -3.842854 2.343146 -5.343145 c
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  1279
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001369 00000 n
+0000001392 00000 n
+0000001565 00000 n
+0000001639 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+1698
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings
index aebff7b7bcdb9b2c872577060b8f9244300baa90..dd95d4002c125c9caa98007bf7074c172bf5aaf8 100644
--- a/Sources/Shared/Resources/en.lproj/Localizable.strings
+++ b/Sources/Shared/Resources/en.lproj/Localizable.strings
@@ -25,6 +25,8 @@
 
 // ChatListFeature
 
+"chatList.search.title"
+= "Search chats";
 "chatList.navigationBar.cancel"
 = "Cancel";
 "chatList.title"
@@ -351,8 +353,12 @@
 = "Search for connections";
 "requests.sent.empty"
 = "You haven't sent any requests";
+"requests.sent.toast.sent"
+= "Request successfully sent to %@";
 "requests.sent.toast.resent"
 = "Request successfully resent to %@";
+"requests.sent.toast.resentFailed"
+= "Request couldn't be resent to %@";
 
 // RequestsFeature - Failed
 
@@ -629,6 +635,8 @@
 = "Dropbox";
 "backup.googleDrive"
 = "Google Drive";
+"backup.SFTP"
+= "SFTP";
 
 // Settings - Delete Account
 
@@ -838,6 +846,19 @@
 "accountRestore.list.cancel"
 = "Cancel";
 
+"accountRestore.sftp.title"
+= "Login to your SFTP";
+"accountRestore.sftp.subtitle"
+= "Login to your server. Your credentials will be automatically and securely saved locally on your device.";
+"accountRestore.sftp.host"
+= "Host";
+"accountRestore.sftp.username"
+= "Username";
+"accountRestore.sftp.password"
+= "Password";
+"accountRestore.sftp.login"
+= "Login";
+
 "accountRestore.header"
 = "Account restore";
 "accountRestore.found.title"
@@ -931,43 +952,53 @@
 
 // SearchFeature
 
-"contactSearch.title"
+"ud.title"
 = "Search";
-"contactSearch.placeholder.title"
-= "Searching is private by nature. The network cannot identify who a search request came from.";
-"contactSearch.placeholder.drawer.title"
+"ud.tab.username"
+= "Username";
+"ud.tab.email"
+= "Email";
+"ud.tab.phone"
+= "Phone";
+"ud.tab.qr"
+= "QR Code";
+"ud.placeholder.drawer.title"
 = "Search";
-"contactSearch.placeholder.drawer.subtitle"
+"ud.placeholder.drawer.subtitle"
 = "You can search for users by their username, email, or phone number using the xx network’s #Anonymous Data Retrieval protocol# which keeps a user’s identity anonymous while requesting data. All sent requests contain salted hashes of what you are searching for. Raw data on emails, usernames, and phone numbers do not leave your phone.";
-"contactSearch.placeholder.drawer.action"
+"ud.placeholder.drawer.action"
 = "Got it";
-"contactSearch.filter.phone"
-= "Phone";
-"contactSearch.filter.email"
-= "Email";
-"contactSearch.filter.username"
-= "Username";
-"contactSearch.sectionTitle"
-= "User";
-"contactSearch.noneFound"
+"ud.noneFound"
 = "There are no users with that %@.";
-"contactSearch.requestDrawer.title"
+"ud.requestDrawer.title"
 = "Request Contact";
-"contactSearch.requestDrawer.email"
+"ud.requestDrawer.email"
 = "EMAIL ADDRESS";
-"contactSearch.requestDrawer.phone"
+"ud.requestDrawer.phone"
 = "PHONE NUMBER";
-"contactSearch.requestDrawer.send"
+"ud.requestDrawer.send"
 = "Send Contact Request";
-"contactSearch.requestDrawer.cancel"
+"ud.requestDrawer.cancel"
 = "Cancel";
-"contactSearch.nicknameDrawer.title"
+"ud.nicknameDrawer.title"
 = "Add a nickname";
-"contactSearch.nicknameDrawer.subtitle"
+"ud.nicknameDrawer.subtitle"
 = "Edit your new contact’s nickname so you know who they are.";
-"contactSearch.nicknameDrawer.save"
+"ud.nicknameDrawer.save"
 = "Save";
 
+"ud.search.input"
+= "Search by %@";
+"ud.search.empty"
+= "There are no users with that %@.";
+"ud.search.cancel"
+= "Cancel search";
+
+"ud.search.placeholder.title"
+= "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel.";
+"ud.search.placeholder.subtitle"
+= "Your searches are anonymous. Search information is never linked to your account or personally identifiable.";
+
 // LaunchFeature
 
 "launch.version.failed"
diff --git a/Sources/Shared/Views/AvatarCell.swift b/Sources/Shared/Views/AvatarCell.swift
new file mode 100644
index 0000000000000000000000000000000000000000..43f0e3fd6686464b6d66c31770627134e9c075c0
--- /dev/null
+++ b/Sources/Shared/Views/AvatarCell.swift
@@ -0,0 +1,187 @@
+import UIKit
+import Combine
+
+final class AvatarCellButton: UIControl {
+    let titleLabel = UILabel()
+    let imageView = UIImageView()
+
+    init() {
+        super.init(frame: .zero)
+        titleLabel.numberOfLines = 0
+        titleLabel.textAlignment = .right
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
+
+        addSubview(imageView)
+        addSubview(titleLabel)
+
+        imageView.snp.makeConstraints {
+            $0.top.greaterThanOrEqualToSuperview()
+            $0.left.equalToSuperview()
+            $0.centerY.equalToSuperview()
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+
+        titleLabel.snp.makeConstraints {
+            $0.top.greaterThanOrEqualToSuperview()
+            $0.left.equalTo(imageView.snp.right).offset(5)
+            $0.centerY.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.width.equalTo(60)
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+}
+
+public final class AvatarCell: UITableViewCell {
+    let h1Label = UILabel()
+    let h2Label = UILabel()
+    let h3Label = UILabel()
+    let h4Label = UILabel()
+    let separatorView = UIView()
+    let avatarView = AvatarView()
+    let stackView = UIStackView()
+    let stateButton = AvatarCellButton()
+
+    var cancellables = Set<AnyCancellable>()
+    public var didTapStateButton: (() -> Void)!
+
+    public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+        selectedBackgroundView = UIView()
+        multipleSelectionBackgroundView = UIView()
+        backgroundColor = Asset.neutralWhite.color
+
+        h1Label.textColor = Asset.neutralActive.color
+        h2Label.textColor = Asset.neutralSecondaryAlternative.color
+        h3Label.textColor = Asset.neutralSecondaryAlternative.color
+        h4Label.textColor = Asset.neutralSecondaryAlternative.color
+
+        h1Label.font = Fonts.Mulish.semiBold.font(size: 14.0)
+        h2Label.font = Fonts.Mulish.regular.font(size: 14.0)
+        h3Label.font = Fonts.Mulish.regular.font(size: 14.0)
+        h4Label.font = Fonts.Mulish.regular.font(size: 14.0)
+
+        stackView.spacing = 4
+        stackView.axis = .vertical
+        
+        stackView.addArrangedSubview(h1Label)
+        stackView.addArrangedSubview(h2Label)
+        stackView.addArrangedSubview(h3Label)
+        stackView.addArrangedSubview(h4Label)
+
+        separatorView.backgroundColor = Asset.neutralLine.color
+
+        contentView.addSubview(stackView)
+        contentView.addSubview(avatarView)
+        contentView.addSubview(stateButton)
+        contentView.addSubview(separatorView)
+
+        setupConstraints()
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    public override func prepareForReuse() {
+        super.prepareForReuse()
+        h1Label.text = nil
+        h2Label.text = nil
+        h3Label.text = nil
+        h4Label.text = nil
+
+        stateButton.imageView.image = nil
+        stateButton.titleLabel.text = nil
+
+        avatarView.prepareForReuse()
+        cancellables.removeAll()
+    }
+
+    public func setup(
+        title: String,
+        image: Data?,
+        firstSubtitle: String? = nil,
+        secondSubtitle: String? = nil,
+        thirdSubtitle: String? = nil,
+        showSeparator: Bool = true,
+        sent: Bool = false
+    ) {
+        h1Label.text = title
+
+        if let firstSubtitle = firstSubtitle {
+            h2Label.isHidden = false
+            h2Label.text = firstSubtitle
+        } else {
+            h2Label.isHidden = true
+        }
+
+        if let secondSubtitle = secondSubtitle {
+            h3Label.isHidden = false
+            h3Label.text = secondSubtitle
+        } else {
+            h3Label.isHidden = true
+        }
+
+        if let thirdSubtitle = thirdSubtitle {
+            h4Label.isHidden = false
+            h4Label.text = thirdSubtitle
+        } else {
+            h4Label.isHidden = true
+        }
+
+        avatarView.setupProfile(title: title, image: image, size: .medium)
+        separatorView.alpha = showSeparator ? 1.0 : 0.0
+
+        cancellables.removeAll()
+
+        if sent {
+            stateButton.imageView.image = Asset.requestsResend.image
+            stateButton.titleLabel.text = Localized.Requests.Cell.requested
+            stateButton.titleLabel.textColor = Asset.brandPrimary.color
+
+            stateButton
+                .publisher(for: .touchUpInside)
+                .sink { [unowned self] in didTapStateButton() }
+                .store(in: &cancellables)
+        }
+    }
+
+    public func updateToResent() {
+        stateButton.imageView.image = Asset.requestsResent.image
+        stateButton.titleLabel.text = Localized.Requests.Cell.resent
+        stateButton.titleLabel.textColor = Asset.neutralWeak.color
+
+        cancellables.forEach { $0.cancel() }
+        cancellables.removeAll()
+    }
+
+    private func setupConstraints() {
+        avatarView.snp.makeConstraints {
+            $0.width.height.equalTo(36)
+            $0.left.equalToSuperview().offset(27)
+            $0.centerY.equalToSuperview()
+        }
+
+        stackView.snp.makeConstraints {
+            $0.top.equalTo(avatarView)
+            $0.left.equalTo(avatarView.snp.right).offset(14)
+            $0.right.lessThanOrEqualToSuperview().offset(-10)
+            $0.bottom.greaterThanOrEqualTo(avatarView)
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+
+        separatorView.snp.makeConstraints {
+            $0.height.equalTo(1)
+            $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(10)
+            $0.left.equalToSuperview().offset(25)
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
+        stateButton.snp.makeConstraints {
+            $0.centerY.equalTo(stackView)
+            $0.right.equalToSuperview().offset(-24)
+        }
+    }
+}
diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift
index 2e87cfbf6dceddcb50009a78b41fd2c0aa598ea5..9608aad71aaa56a5da852516405d069837a703c5 100644
--- a/Sources/Shared/Views/SearchComponent.swift
+++ b/Sources/Shared/Views/SearchComponent.swift
@@ -15,6 +15,10 @@ public final class SearchComponent: UIView {
         textSubject.eraseToAnyPublisher()
     }
 
+    public var returnPublisher: AnyPublisher<Void, Never> {
+        returnSubject.eraseToAnyPublisher()
+    }
+
     private var rightImage = Asset.sharedScan.image {
         didSet {
             rightButton.setImage(rightImage, for: .normal)
@@ -26,13 +30,59 @@ public final class SearchComponent: UIView {
     }
 
     private var cancellables = Set<AnyCancellable>()
-    private var rightSubject = PassthroughSubject<Void, Never>()
-    private var textSubject = PassthroughSubject<String, Never>()
-    private var isEditingSubject = CurrentValueSubject<Bool, Never>(false)
+    private let rightSubject = PassthroughSubject<Void, Never>()
+    private let textSubject = PassthroughSubject<String, Never>()
+    private let returnSubject = PassthroughSubject<Void, Never>()
+    private let isEditingSubject = CurrentValueSubject<Bool, Never>(false)
 
     public init() {
         super.init(frame: .zero)
-        setup()
+
+        containerView.layer.cornerRadius = 25
+        containerView.backgroundColor = Asset.neutralSecondary.color
+
+        leftImageView.image = Asset.lens.image
+        leftImageView.contentMode = .center
+        leftImageView.tintColor = Asset.neutralDisabled.color
+
+        rightButton.tintColor = Asset.neutralBody.color
+        rightButton.setImage(rightImage, for: .normal)
+        rightButton.setContentHuggingPriority(.required, for: .horizontal)
+        rightButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        inputField.delegate = self
+        inputField.textColor = Asset.neutralActive.color
+        inputField.font = Fonts.Mulish.regular.font(size: 16.0)
+
+        let attrPlaceholder
+        = NSAttributedString(
+            string: Localized.Shared.Search.placeholder,
+            attributes: [
+                .font: Fonts.Mulish.regular.font(size: 14.0) as Any,
+                .foregroundColor: Asset.neutralWeak.color
+            ])
+
+        inputField.attributedPlaceholder = attrPlaceholder
+
+        inputField.textPublisher
+            .sink { [weak textSubject] in textSubject?.send($0) }
+            .store(in: &cancellables)
+
+        rightButton.publisher(for: .touchUpInside)
+            .sink { [weak rightSubject, self] in
+                if isEditingSubject.value == true {
+                    abortEditing()
+                } else {
+                    rightSubject?.send()
+                }
+            }.store(in: &cancellables)
+
+        addSubview(containerView)
+        containerView.addSubview(inputField)
+        containerView.addSubview(leftImageView)
+        containerView.addSubview(rightButton)
+
+        setupConstraints()
     }
 
     required init?(coder: NSCoder) { nil }
@@ -81,55 +131,6 @@ public final class SearchComponent: UIView {
         isEditingSubject.send(false)
     }
 
-    private func setup() {
-        containerView.layer.cornerRadius = 25
-        containerView.backgroundColor = Asset.neutralSecondary.color
-
-        leftImageView.image = Asset.lens.image
-        leftImageView.contentMode = .center
-        leftImageView.tintColor = Asset.neutralDisabled.color
-
-        rightButton.tintColor = Asset.neutralBody.color
-        rightButton.setImage(rightImage, for: .normal)
-        rightButton.setContentHuggingPriority(.required, for: .horizontal)
-        rightButton.setContentCompressionResistancePriority(.required, for: .horizontal)
-
-        inputField.delegate = self
-        inputField.textColor = Asset.neutralActive.color
-        inputField.font = Fonts.Mulish.regular.font(size: 16.0)
-
-        let attrPlaceholder
-            = NSAttributedString(
-                string: Localized.Shared.Search.placeholder,
-                attributes: [
-                    .font: Fonts.Mulish.regular.font(size: 14.0) as Any,
-                    .foregroundColor: Asset.neutralWeak.color
-                ])
-
-        inputField.attributedPlaceholder = attrPlaceholder
-
-        inputField.textPublisher
-            .sink { [weak textSubject] in textSubject?.send($0) }
-            .store(in: &cancellables)
-
-        rightButton.publisher(for: .touchUpInside)
-            .sink { [weak rightSubject, self] in
-                if isEditingSubject.value == true {
-                    abortEditing()
-                } else {
-                    rightSubject?.send()
-                }
-            }.store(in: &cancellables)
-
-        addSubview(containerView)
-        containerView.addSubview(inputField)
-        containerView.addSubview(leftImageView)
-        containerView.addSubview(rightButton)
-
-        setupConstraints()
-        setupAccessibility()
-    }
-
     private func setupConstraints() {
         containerView.snp.makeConstraints {
             $0.top.equalToSuperview()
@@ -159,16 +160,17 @@ public final class SearchComponent: UIView {
         }
     }
 
-    private func setupAccessibility() {
-        inputField.accessibilityIdentifier = Localized.Accessibility.Shared.Search.textField
-        rightButton.accessibilityIdentifier = Localized.Accessibility.Shared.Search.rightButton
-    }
-
     public func textFieldDidBeginEditing(_ textField: UITextField) {
         rightButton.setImage(Asset.sharedCross.image, for: .normal)
         isEditingSubject.send(true)
     }
 
+    public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        inputField.resignFirstResponder()
+        returnSubject.send(())
+        return true
+    }
+
     public func textFieldDidEndEditing(_ textField: UITextField) {
         rightButton.setImage(rightImage, for: .normal)
         isEditingSubject.send(false)
diff --git a/Sources/Shared/Views/SearchCountryComponent.swift b/Sources/Shared/Views/SearchCountryComponent.swift
new file mode 100644
index 0000000000000000000000000000000000000000..186c58b95981ce45a799c93e27c8a53530b16cf4
--- /dev/null
+++ b/Sources/Shared/Views/SearchCountryComponent.swift
@@ -0,0 +1,57 @@
+import UIKit
+
+public final class SearchCountryComponent: UIControl {
+    let flagLabel = UILabel()
+    let prefixLabel = UILabel()
+    let containerView = UIView()
+
+    public init() {
+        super.init(frame: .zero)
+
+        containerView.layer.cornerRadius = 25
+        containerView.backgroundColor = Asset.neutralSecondary.color
+
+        flagLabel.text = "🇺🇸"
+        prefixLabel.text = "+1"
+        prefixLabel.textColor = Asset.neutralDisabled.color
+        prefixLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
+
+        addSubview(containerView)
+        containerView.addSubview(flagLabel)
+        containerView.addSubview(prefixLabel)
+
+        containerView.isUserInteractionEnabled = false
+
+        setupConstraints()
+        flagLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+        prefixLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    public func setFlag(_ flag: String, prefix: String) {
+        flagLabel.text = flag
+        prefixLabel.text = prefix
+    }
+
+    private func setupConstraints() {
+        containerView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+            $0.height.equalTo(50)
+        }
+
+        flagLabel.snp.makeConstraints {
+            $0.left.equalToSuperview().offset(13)
+            $0.centerY.equalToSuperview()
+        }
+
+        prefixLabel.snp.makeConstraints {
+            $0.left.equalTo(flagLabel.snp.right).offset(10)
+            $0.right.equalToSuperview().offset(-13)
+            $0.centerY.equalToSuperview()
+        }
+    }
+}
diff --git a/Sources/Shared/Views/SmallAvatarAndTitleCell.swift b/Sources/Shared/Views/SmallAvatarAndTitleCell.swift
deleted file mode 100644
index 4c37a96467729da2f4234ad646287a6127455f63..0000000000000000000000000000000000000000
--- a/Sources/Shared/Views/SmallAvatarAndTitleCell.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-import UIKit
-
-public final class SmallAvatarAndTitleCell: UITableViewCell {
-    let separatorView = UIView()
-    public let titleLabel = UILabel()
-    public let avatarView = AvatarView()
-
-    public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
-        super.init(style: style, reuseIdentifier: reuseIdentifier)
-
-        selectedBackgroundView = UIView()
-        multipleSelectionBackgroundView = UIView()
-        backgroundColor = Asset.neutralWhite.color
-
-        titleLabel.textColor = Asset.neutralActive.color
-        titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
-        separatorView.backgroundColor = Asset.neutralLine.color
-
-        contentView.addSubview(titleLabel)
-        contentView.addSubview(avatarView)
-        contentView.addSubview(separatorView)
-
-        avatarView.snp.makeConstraints {
-            $0.width.height.equalTo(36)
-            $0.left.equalToSuperview().offset(27)
-            $0.centerY.equalToSuperview()
-        }
-
-        titleLabel.snp.makeConstraints {
-            $0.centerY.equalTo(avatarView)
-            $0.left.equalTo(avatarView.snp.right).offset(14)
-            $0.right.lessThanOrEqualToSuperview().offset(-10)
-        }
-
-        separatorView.snp.makeConstraints {
-            $0.height.equalTo(1)
-            $0.left.equalToSuperview().offset(25)
-            $0.right.equalToSuperview()
-            $0.bottom.equalToSuperview()
-        }
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    public override func prepareForReuse() {
-        super.prepareForReuse()
-        titleLabel.text = nil
-        avatarView.prepareForReuse()
-    }
-}
diff --git a/Tests/CollectionViewTests/CellFactoryTests.swift b/Tests/CollectionViewTests/CellFactoryTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..0fc5063570ee1d2f615d24e5cc2d38292e0ab003
--- /dev/null
+++ b/Tests/CollectionViewTests/CellFactoryTests.swift
@@ -0,0 +1,105 @@
+import CustomDump
+import XCTest
+@testable import CollectionView
+
+final class CellFactoryTests: XCTestCase {
+  func testCombined() {
+    struct Cell: Equatable {
+      var model: Int
+      var collectionView: UICollectionView
+      var indexPath: IndexPath
+    }
+
+    var didRegisterFirst = [UICollectionView]()
+    var didRegisterSecond = [UICollectionView]()
+    var didRegisterThird = [UICollectionView]()
+
+    var didBuildFirst = [Cell]()
+    var didBuildSecond = [Cell]()
+    var didBuildThird = [Cell]()
+
+    let factory = CellFactory<Int>.combined(
+      .init(
+        register: .init { didRegisterFirst.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 1 else { return nil }
+          didBuildFirst.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      ),
+      .init(
+        register: .init { didRegisterSecond.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 2 else { return nil }
+          didBuildSecond.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      ),
+      .init(
+        register: .init { didRegisterThird.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 3 else { return nil }
+          didBuildThird.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      )
+    )
+
+    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())
+
+    factory.register(in: collectionView)
+
+    XCTAssertEqual(didRegisterFirst, [collectionView])
+    XCTAssertEqual(didRegisterSecond, [collectionView])
+    XCTAssertEqual(didRegisterThird, [collectionView])
+
+    let firstCell = factory.build(for: 1, in: collectionView, at: IndexPath(item: 0, section: 1))
+
+    XCTAssertNotNil(firstCell)
+    XCTAssertNoDifference(didBuildFirst, [Cell(
+      model: 1,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 0, section: 1)
+    )])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let secondCell = factory.build(for: 2, in: collectionView, at: IndexPath(item: 2, section: 3))
+
+    XCTAssertNotNil(secondCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [Cell(
+      model: 2,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 2, section: 3)
+    )])
+    XCTAssertNoDifference(didBuildThird, [])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let thirdCell = factory.build(for: 3, in: collectionView, at: IndexPath(item: 4, section: 5))
+
+    XCTAssertNotNil(thirdCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [Cell(
+      model: 3,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 4, section: 5)
+    )])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let otherCell = factory.build(for: 4, in: collectionView, at: IndexPath(item: 0, section: 0))
+
+    XCTAssertNil(otherCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [])
+  }
+}
diff --git a/Tests/CollectionViewTests/ViewConfiguratorTests.swift b/Tests/CollectionViewTests/ViewConfiguratorTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..df2e993b0f70bceefc124cca9fda9c3be5723db6
--- /dev/null
+++ b/Tests/CollectionViewTests/ViewConfiguratorTests.swift
@@ -0,0 +1,33 @@
+import CustomDump
+import XCTest
+@testable import CollectionView
+
+// MARK: - Example view configurator:
+
+private class ProfileView: UIView {
+  let username = UILabel()
+}
+
+private struct User {
+  var name: String
+}
+
+private extension ViewConfigurator where View == ProfileView, Model == User {
+  static let profileViewUserConfigurator = ViewConfigurator { view, model in
+    view.username.text = model.name
+  }
+}
+
+// MARK: - Tests:
+
+final class ViewConfiguratorTests: XCTestCase {
+  func testExampleConfigurator() {
+    let profileView = ProfileView()
+    let user = User(name: "John")
+
+    let configure = ViewConfigurator.profileViewUserConfigurator
+    configure(profileView, with: user)
+
+    XCTAssertNoDifference(profileView.username.text, user.name)
+  }
+}
diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
index d12bf1ea4dafe0fcef393271bb98e5e8709031ae..2932261c8cc3c80fb2730138af5c8ff904e9334f 100644
--- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -27,6 +27,15 @@
         "version" : "1.4.0"
       }
     },
+    {
+      "identity" : "bluesocket",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/IBM-Swift/BlueSocket.git",
+      "state" : {
+        "revision" : "c9894fd117457f1d006575fbfb2fdfd6f79eac03",
+        "version" : "1.0.200"
+      }
+    },
     {
       "identity" : "boringssl-swiftpm",
       "kind" : "remoteSourceControl",
@@ -50,8 +59,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://git.xx.network/elixxir/client-ios-db.git",
       "state" : {
-        "revision" : "f52a83a8095c36f4e84afa51b52c929117c8779a",
-        "version" : "1.0.6"
+        "revision" : "785e1f653ee5eaaaf58a82c8abbcda2174fbc27a",
+        "version" : "1.0.8"
       }
     },
     {
@@ -207,6 +216,15 @@
         "version" : "1.22.2"
       }
     },
+    {
+      "identity" : "libssh2prebuild",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/DimaRU/Libssh2Prebuild.git",
+      "state" : {
+        "branch" : "1.10.0+OpenSSL_1_1_1o",
+        "revision" : "a91bcf205a6cbc84144f840c44145656abbd266a"
+      }
+    },
     {
       "identity" : "nanopb",
       "kind" : "remoteSourceControl",
@@ -261,6 +279,14 @@
         "version" : "1.2.0"
       }
     },
+    {
+      "identity" : "shout",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/darrarski/Shout.git",
+      "state" : {
+        "revision" : "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0"
+      }
+    },
     {
       "identity" : "snapkit",
       "kind" : "remoteSourceControl",
@@ -302,8 +328,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-custom-dump",
       "state" : {
-        "revision" : "51698ece74ecf31959d3fa81733f0a5363ef1b4e",
-        "version" : "0.3.0"
+        "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
+        "version" : "0.5.0"
       }
     },
     {
@@ -347,8 +373,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
       "state" : {
-        "revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
-        "version" : "0.2.1"
+        "revision" : "ef8e14e7ce1c0c304c644c6ba365d06c468ded6b",
+        "version" : "0.3.3"
       }
     }
   ],