mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-23 22:28:22 -05:00
Add native iOS QR code scanner (#1405)
This commit is contained in:
committed by
Leendert de Borst
parent
6c561e8ece
commit
5414f40c98
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -212,7 +212,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
|
||||
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -222,84 +222,13 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUtils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = Autofill;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUITests; sourceTree = "<group>"; };
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUtils; sourceTree = "<group>"; };
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = "<group>"; };
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = "<group>"; };
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = "<group>"; };
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = "<group>"; };
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -1418,10 +1347,7 @@
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1475,10 +1401,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -291,4 +291,10 @@
|
||||
[vaultManager authenticateUser:title subtitle:subtitle resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
// MARK: - QR Code Scanner
|
||||
|
||||
- (void)scanQRCode:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager scanQRCode:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,7 @@ import VaultStoreKit
|
||||
import VaultModels
|
||||
import SwiftUI
|
||||
import VaultUI
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
* This class is used as a bridge to allow React Native to interact with the VaultStoreKit class.
|
||||
@@ -913,6 +914,40 @@ public class VaultManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func scanQRCode(_ resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
// Get the root view controller from React Native
|
||||
guard let rootVC = RCTPresentedViewController() else {
|
||||
reject("NO_VIEW_CONTROLLER", "No view controller available", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create QR scanner view
|
||||
let scannerView = QRScannerView(
|
||||
onCodeScanned: { code in
|
||||
// Dismiss and return scanned code
|
||||
rootVC.dismiss(animated: true) {
|
||||
resolve(code)
|
||||
}
|
||||
},
|
||||
onCancel: {
|
||||
// Dismiss and return nil
|
||||
rootVC.dismiss(animated: true) {
|
||||
resolve(nil)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let hostingController = UIHostingController(rootView: scannerView)
|
||||
|
||||
// Present modally as full screen
|
||||
hostingController.modalPresentationStyle = .fullScreen
|
||||
rootVC.present(hostingController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func authenticateUser(_ title: String?,
|
||||
subtitle: String?,
|
||||
|
||||
@@ -2444,12 +2444,6 @@ PODS:
|
||||
- SQLite.swift/standard (0.14.1)
|
||||
- SwiftLint (0.59.1)
|
||||
- SWXMLHash (7.0.2)
|
||||
- VisionCamera (4.7.3):
|
||||
- VisionCamera/Core (= 4.7.3)
|
||||
- VisionCamera/React (= 4.7.3)
|
||||
- VisionCamera/Core (4.7.3)
|
||||
- VisionCamera/React (4.7.3):
|
||||
- React-Core
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -2567,7 +2561,6 @@ DEPENDENCIES:
|
||||
- SignalArgon2 (~> 1.3.2)
|
||||
- SQLite.swift (~> 0.14.0)
|
||||
- SwiftLint
|
||||
- VisionCamera (from `../node_modules/react-native-vision-camera`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -2798,8 +2791,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-screens"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
VisionCamera:
|
||||
:path: "../node_modules/react-native-vision-camera"
|
||||
Yoga:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -2919,7 +2910,6 @@ SPEC CHECKSUMS:
|
||||
SQLite.swift: 2992550ebf3c5b268bf4352603e3df87d2a4ed72
|
||||
SwiftLint: 3d48e2fb2a3468fdaccf049e5e755df22fb40c2c
|
||||
SWXMLHash: dd733a457e9c4fe93b1538654057aefae4acb382
|
||||
VisionCamera: 7187b3dac1ff3071234ead959ce311875748e14f
|
||||
Yoga: dc7c21200195acacb62fa920c588e7c2106de45e
|
||||
|
||||
PODFILE CHECKSUM: ac288e273086bafdd610cafff08ccca0d164f7c3
|
||||
|
||||
196
apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift
Normal file
196
apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
/// SwiftUI view for scanning QR codes using AVFoundation
|
||||
public struct QRScannerView: View {
|
||||
let onCodeScanned: (String) -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
@State private var hasScanned = false
|
||||
@State private var showFlash = false
|
||||
|
||||
public init(onCodeScanned: @escaping (String) -> Void, onCancel: @escaping () -> Void) {
|
||||
self.onCodeScanned = onCodeScanned
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
// Camera preview
|
||||
QRScannerRepresentable(
|
||||
onCodeScanned: { code in
|
||||
if !hasScanned {
|
||||
hasScanned = true
|
||||
showFlash = true
|
||||
|
||||
// Flash animation then callback
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
onCodeScanned(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
// Overlay with viewfinder
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
// Viewfinder frame
|
||||
Rectangle()
|
||||
.stroke(Color.white, lineWidth: 3)
|
||||
.frame(width: 280, height: 280)
|
||||
.overlay(
|
||||
// Flash effect
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.opacity(showFlash ? 0.7 : 0)
|
||||
.animation(.easeInOut(duration: 0.2), value: showFlash)
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Status text
|
||||
Text("Scan AliasVault QR Code")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: onCancel) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(Color.black)
|
||||
}
|
||||
}
|
||||
|
||||
/// UIViewControllerRepresentable wrapper for AVFoundation camera
|
||||
struct QRScannerRepresentable: UIViewControllerRepresentable {
|
||||
let onCodeScanned: (String) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> QRScannerViewController {
|
||||
let controller = QRScannerViewController()
|
||||
controller.onCodeScanned = onCodeScanned
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) {
|
||||
// No updates needed
|
||||
}
|
||||
}
|
||||
|
||||
/// UIViewController that handles AVFoundation QR code scanning
|
||||
class QRScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
|
||||
var captureSession: AVCaptureSession?
|
||||
var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
var onCodeScanned: ((String) -> Void)?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupCamera()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if let session = captureSession, !session.isRunning {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.startRunning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if let session = captureSession, session.isRunning {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.stopRunning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
let session = AVCaptureSession()
|
||||
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
|
||||
return
|
||||
}
|
||||
|
||||
let videoInput: AVCaptureDeviceInput
|
||||
|
||||
do {
|
||||
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if session.canAddInput(videoInput) {
|
||||
session.addInput(videoInput)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let metadataOutput = AVCaptureMetadataOutput()
|
||||
|
||||
if session.canAddOutput(metadataOutput) {
|
||||
session.addOutput(metadataOutput)
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
metadataOutput.metadataObjectTypes = [.qr]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
previewLayer.frame = view.layer.bounds
|
||||
previewLayer.videoGravity = .resizeAspectFill
|
||||
view.layer.addSublayer(previewLayer)
|
||||
|
||||
self.captureSession = session
|
||||
self.previewLayer = previewLayer
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
previewLayer?.frame = view.layer.bounds
|
||||
}
|
||||
|
||||
func metadataOutput(
|
||||
_ output: AVCaptureMetadataOutput,
|
||||
didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection
|
||||
) {
|
||||
if let metadataObject = metadataObjects.first,
|
||||
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
|
||||
let stringValue = readableObject.stringValue {
|
||||
|
||||
// Stop scanning
|
||||
captureSession?.stopRunning()
|
||||
|
||||
// Haptic feedback
|
||||
let generator = UINotificationFeedbackGenerator()
|
||||
generator.notificationOccurred(.success)
|
||||
|
||||
// Callback with scanned code
|
||||
onCodeScanned?(stringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user