diff --git a/Sources/SearchFeature/Controllers/SearchQRController.swift b/Sources/SearchFeature/Controllers/SearchQRController.swift
index 04fe440f0ab5c3b3753f0758c2e58aa7edaa1432..49718728ad83faa1ba6eda74eb48c2d46e60392f 100644
--- a/Sources/SearchFeature/Controllers/SearchQRController.swift
+++ b/Sources/SearchFeature/Controllers/SearchQRController.swift
@@ -1,9 +1,164 @@
 import UIKit
+import Permissions
+import DependencyInjection
+
+enum SearchQRStatus: Equatable {
+    case reading
+    case processing
+    case success
+    case failed(SearchQRError)
+}
+
+enum SearchQRError: Equatable {
+    case requestOpened
+    case unknown(String)
+    case cameraPermission
+    case alreadyFriends(String)
+}
 
 final class SearchQRController: UIViewController {
+    @Dependency private var permissions: PermissionHandling
+
     lazy private var screenView = SearchQRView()
 
+    private let camera = Camera()
+    private var status: SearchQRStatus?
+
     override func loadView() {
         view = screenView
     }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        ///screenView.layer.insertSublayer(camera.previewLayer, at: 0)
+        ///setupBindings()
+    }
+
+    override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+        ///camera.previewLayer.frame = screenView.bounds
+    }
+
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        ///viewModel.resetScanner()
+        ///startCamera()
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+//        backgroundScheduler.schedule { [weak self] in
+//            guard let self = self else { return }
+//            self.camera.stop()
+//        }
+    }
+
+    private func startCamera() {
+        permissions.requestCamera { [weak self] granted in
+            guard let self = self else { return }
+
+            if granted {
+                DispatchQueue.main.async { [weak self] in
+                    guard let self = self else { return }
+                    self.camera.start()
+                }
+            } else {
+                DispatchQueue.main.async {
+                    self.status = .failed(.cameraPermission)
+                    self.screenView.update(with: .failed(.cameraPermission))
+                }
+            }
+        }
+    }
+}
+
+
+import Combine
+import AVFoundation
+
+protocol CameraType {
+    func start()
+    func stop()
+
+    var previewLayer: CALayer { get }
+    var dataPublisher: AnyPublisher<Data, Never> { get }
+}
+
+final class Camera: NSObject, CameraType {
+    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]
+        }
+    }
+}
+
+extension Camera: AVCaptureMetadataOutputObjectsDelegate {
+    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)
+    }
+}
+
+final class MockCamera: NSObject, CameraType {
+    private let dataSubject = PassthroughSubject<Data, Never>()
+
+    func start() {
+        DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in
+            self?.dataSubject.send("###".data(using: .utf8)!)
+        }
+    }
+
+    func stop() {}
+
+    var previewLayer: CALayer { CALayer() }
+
+    var dataPublisher: AnyPublisher<Data, Never> {
+        dataSubject
+            .receive(on: DispatchQueue.main)
+            .eraseToAnyPublisher()
+    }
 }
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/SearchEmailView.swift b/Sources/SearchFeature/Views/SearchEmailView.swift
index 053c62592714b4822b94e063d3747d1a693b141f..8110ec4bbeeda5d7a06c453675cfc204f95546f9 100644
--- a/Sources/SearchFeature/Views/SearchEmailView.swift
+++ b/Sources/SearchFeature/Views/SearchEmailView.swift
@@ -1,17 +1,15 @@
 import UIKit
 import Shared
-import InputField
 
 final class SearchEmailView: UIView {
-    let inputField = InputField()
+    let inputField = SearchComponent()
 
     init() {
         super.init(frame: .zero)
 
-        inputField.setup(
-            style: .regular,
-            title: "Email",
-            placeholder: "Email"
+        inputField.set(
+            placeholder: Localized.Ud.Search.Email.input,
+            imageAtRight: nil
         )
 
         addSubview(inputField)
diff --git a/Sources/SearchFeature/Views/SearchPhoneView.swift b/Sources/SearchFeature/Views/SearchPhoneView.swift
index 1868cec6960091bcf08f9294740d8121b1a41bca..9b086bcfc8c33761efd54aad852a001204a2c239 100644
--- a/Sources/SearchFeature/Views/SearchPhoneView.swift
+++ b/Sources/SearchFeature/Views/SearchPhoneView.swift
@@ -1,17 +1,15 @@
 import UIKit
 import Shared
-import InputField
 
 final class SearchPhoneView: UIView {
-    let inputField = InputField()
+    let inputField = SearchComponent()
 
     init() {
         super.init(frame: .zero)
 
-        inputField.setup(
-            style: .regular,
-            title: "Phone",
-            placeholder: "Phone"
+        inputField.set(
+            placeholder: Localized.Ud.Search.Phone.input,
+            imageAtRight: nil
         )
 
         addSubview(inputField)
diff --git a/Sources/SearchFeature/Views/SearchQRView.swift b/Sources/SearchFeature/Views/SearchQRView.swift
index 4adae04506dd7f525df2563018aeedb3dba37b10..24278ce7902fc59719da416a719084a50d79f35c 100644
--- a/Sources/SearchFeature/Views/SearchQRView.swift
+++ b/Sources/SearchFeature/Views/SearchQRView.swift
@@ -1,28 +1,56 @@
 import UIKit
 import Shared
-import InputField
 
 final class SearchQRView: UIView {
-    let inputField = InputField()
+    let statusLabel = UILabel()
+    let imageView = UIImageView()
+    let stackView = UIStackView()
+    let animationView = DotAnimation()
+    let overlayView = OverlayView()
+    let actionButton = CapsuleButton()
 
     init() {
         super.init(frame: .zero)
 
-        inputField.setup(
-            style: .regular,
-            title: "QR",
-            placeholder: "QR"
-        )
+        imageView.contentMode = .center
+        actionButton.setStyle(.brandColored)
 
-        addSubview(inputField)
+        statusLabel.numberOfLines = 0
+        statusLabel.textAlignment = .center
+        statusLabel.textColor = Asset.neutralWhite.color
+        statusLabel.font = Fonts.Mulish.regular.font(size: 14.0)
 
-        inputField.snp.makeConstraints {
-            $0.top.equalToSuperview().offset(15)
-            $0.left.equalToSuperview().offset(15)
-            $0.right.equalToSuperview().offset(-15)
-            $0.bottom.lessThanOrEqualToSuperview()
-        }
+        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 }
+
+    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/SearchUsernamePlaceholderView.swift b/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift
index 8b249aac7ed1e4abf255686e63030fcec351f2ab..aa8843c47c3aea291f5e5a3f2fe10ef874c30afb 100644
--- a/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift
+++ b/Sources/SearchFeature/Views/SearchUsernamePlaceholderView.swift
@@ -16,7 +16,7 @@ final class SearchUsernamePlaceholderView: UIView {
         super.init(frame: .zero)
 
         let attrString = NSMutableAttributedString(
-            string: Localized.Ud.Username.Search.Placeholder.title,
+            string: Localized.Ud.Search.Username.Placeholder.title,
             attributes: [
                 .foregroundColor: Asset.neutralDark.color,
                 .font: Fonts.Mulish.bold.font(size: 32.0)
@@ -36,7 +36,7 @@ final class SearchUsernamePlaceholderView: UIView {
         paragraph.lineHeightMultiple = 1.3
 
         subtitleWithInfo.setup(
-            text: Localized.Ud.Username.Search.Placeholder.subtitle,
+            text: Localized.Ud.Search.Username.Placeholder.subtitle,
             attributes: [
                 .paragraphStyle: paragraph,
                 .foregroundColor: Asset.neutralBody.color,
diff --git a/Sources/SearchFeature/Views/SearchUsernameView.swift b/Sources/SearchFeature/Views/SearchUsernameView.swift
index f8c562237e0957b1d0de12dc5093de76735d2736..e6de2ce0856a7592522702e6393f6cc402153c28 100644
--- a/Sources/SearchFeature/Views/SearchUsernameView.swift
+++ b/Sources/SearchFeature/Views/SearchUsernameView.swift
@@ -1,6 +1,5 @@
 import UIKit
 import Shared
-import InputField
 
 final class SearchUsernameView: UIView {
     let inputField = SearchComponent()
@@ -10,7 +9,7 @@ final class SearchUsernameView: UIView {
         super.init(frame: .zero)
 
         inputField.set(
-            placeholder: Localized.Ud.Username.Search.inputPlaceholder,
+            placeholder: Localized.Ud.Search.Username.input,
             imageAtRight: nil
         )
 
diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift
index 2265b055612f17dd882b8790597ae15f94272edd..0117c9b8d9abd5c6a51d004886c649c491e3e918 100644
--- a/Sources/Shared/AutoGenerated/Strings.swift
+++ b/Sources/Shared/AutoGenerated/Strings.swift
@@ -1205,8 +1205,6 @@ public enum Localized {
     public static func noneFound(_ p1: Any) -> String {
       return Localized.tr("Localizable", "ud.noneFound", String(describing: p1))
     }
-    /// User
-    public static let sectionTitle = Localized.tr("Localizable", "ud.sectionTitle")
     /// Search
     public static let title = Localized.tr("Localizable", "ud.title")
     public enum NicknameDrawer {
@@ -1218,8 +1216,6 @@ public enum Localized {
       public static let title = Localized.tr("Localizable", "ud.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", "ud.placeholder.title")
       public enum Drawer {
         /// Got it
         public static let action = Localized.tr("Localizable", "ud.placeholder.drawer.action")
@@ -1241,6 +1237,26 @@ public enum Localized {
       /// Request Contact
       public static let title = Localized.tr("Localizable", "ud.requestDrawer.title")
     }
+    public enum Search {
+      public enum Email {
+        /// Search by email
+        public static let input = Localized.tr("Localizable", "ud.search.email.input")
+      }
+      public enum Phone {
+        /// Search by phone number
+        public static let input = Localized.tr("Localizable", "ud.search.phone.input")
+      }
+      public enum Username {
+        /// Search by username
+        public static let input = Localized.tr("Localizable", "ud.search.username.input")
+        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.username.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.username.placeholder.title")
+        }
+      }
+    }
     public enum Tab {
       /// Email
       public static let email = Localized.tr("Localizable", "ud.tab.email")
@@ -1251,18 +1267,6 @@ public enum Localized {
       /// Username
       public static let username = Localized.tr("Localizable", "ud.tab.username")
     }
-    public enum Username {
-      public enum Search {
-        /// Search by username
-        public static let inputPlaceholder = Localized.tr("Localizable", "ud.username.search.inputPlaceholder")
-        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.username.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.username.search.placeholder.title")
-        }
-      }
-    }
   }
 
   public enum Validator {
diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings
index e16bf6fa254602a9e7f4cfaa9f977af25490517f..fb5ab0e271d09d6da94605606504f46ba9a757df 100644
--- a/Sources/Shared/Resources/en.lproj/Localizable.strings
+++ b/Sources/Shared/Resources/en.lproj/Localizable.strings
@@ -950,24 +950,20 @@
 
 "ud.title"
 = "Search";
-"ud.placeholder.title"
-= "Searching is private by nature. The network cannot identify who a search request came from.";
+"ud.tab.username"
+= "Username";
+"ud.tab.email"
+= "Email";
+"ud.tab.phone"
+= "Phone";
+"ud.tab.qr"
+= "QR Code";
 "ud.placeholder.drawer.title"
 = "Search";
 "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.";
 "ud.placeholder.drawer.action"
 = "Got it";
-"ud.tab.phone"
-= "Phone";
-"ud.tab.email"
-= "Email";
-"ud.tab.username"
-= "Username";
-"ud.tab.qr"
-= "QR Code";
-"ud.sectionTitle"
-= "User";
 "ud.noneFound"
 = "There are no users with that %@.";
 "ud.requestDrawer.title"
@@ -986,12 +982,16 @@
 = "Edit your new contact’s nickname so you know who they are.";
 "ud.nicknameDrawer.save"
 = "Save";
-
-"ud.username.search.inputPlaceholder"
+"ud.search.username.input"
 = "Search by username";
-"ud.username.search.placeholder.title"
+"ud.search.email.input"
+= "Search by email";
+"ud.search.phone.input"
+= "Search by phone number";
+
+"ud.search.username.placeholder.title"
 = "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel.";
-"ud.username.search.placeholder.subtitle"
+"ud.search.username.placeholder.subtitle"
 = "Your searches are anonymous. Search information is never linked to your account or personally identifiable.";
 
 // LaunchFeature