1// ContactScannerView.swift
2// SophaxChat
3//
4// Camera QR scanner for adding contacts from a Contact Card.
5// Uses VisionKit DataScannerViewController (iOS 16+).
6
7import SwiftUI
8import VisionKit
9import SophaxChatCore
10
11struct ContactScannerView: View {
12 @EnvironmentObject var appState: AppState
13 @Environment(\.dismiss) private var dismiss
14 @State private var scannedResult: String? = nil
15 @State private var showError = false
16
17 var body: some View {
18 NavigationStack {
19 Group {
20 if DataScannerViewController.isSupported && DataScannerViewController.isAvailable {
21 DataScannerRepresentable(scannedResult: $scannedResult)
22 .ignoresSafeArea()
23 .overlay(alignment: .bottom) {
24 Text("Point at a SophaxChat QR code")
25 .font(.subheadline)
26 .foregroundStyle(.white)
27 .padding(.horizontal, 16)
28 .padding(.vertical, 10)
29 .background(.black.opacity(0.5))
30 .clipShape(Capsule())
31 .padding(.bottom, 40)
32 }
33 } else {
34 ContentUnavailableView(
35 "Scanner Unavailable",
36 systemImage: "qrcode.viewfinder",
37 description: Text("Camera scanning is not available on this device.")
38 )
39 }
40 }
41 .navigationBarTitleDisplayMode(.inline)
42 .toolbar {
43 ToolbarItem(placement: .cancellationAction) {
44 Button("Cancel") { dismiss() }
45 }
46 }
47 .onChange(of: scannedResult) { _, result in
48 guard let str = result, let url = URL(string: str) else { return }
49 appState.handleIncomingLink(url)
50 dismiss()
51 }
52 .alert("Invalid QR Code", isPresented: $showError) {
53 Button("OK", role: .cancel) { }
54 }
55 }
56 }
57}
58
59private struct DataScannerRepresentable: UIViewControllerRepresentable {
60 @Binding var scannedResult: String?
61
62 func makeUIViewController(context: Context) -> DataScannerViewController {
63 let scanner = DataScannerViewController(
64 recognizedDataTypes: [.barcode(symbologies: [.qr])],
65 isHighlightingEnabled: true
66 )
67 scanner.delegate = context.coordinator
68 try? scanner.startScanning()
69 return scanner
70 }
71
72 func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {}
73
74 func makeCoordinator() -> Coordinator { Coordinator(scannedResult: $scannedResult) }
75
76 final class Coordinator: NSObject, DataScannerViewControllerDelegate {
77 @Binding var scannedResult: String?
78 init(scannedResult: Binding<String?>) { _scannedResult = scannedResult }
79
80 func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
81 if case .barcode(let barcode) = item {
82 scannedResult = barcode.payloadStringValue
83 }
84 }
85
86 func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
87 // Auto-scan first found QR without requiring tap
88 guard scannedResult == nil else { return }
89 for item in addedItems {
90 if case .barcode(let barcode) = item, let payload = barcode.payloadStringValue {
91 scannedResult = payload
92 return
93 }
94 }
95 }
96 }
97}