feat: add QR scanner view + 7 missing API endpoints
- QRScannerView: AVFoundation camera + barcode/QR detection with flashlight toggle, viewfinder overlay, MAC/UUID pattern recognition - New API endpoints: deleteServicePoint, updateServicePoint, listBeacons, decommissionBeacon, lookupByMac, getBeaconStatus, getProfile - Wire QR scanner into ScanView with BLE Scan + QR Scan side-by-side - MAC address lookup on scan to check if beacon already registered - Updated Xcode project file with new source
This commit is contained in:
parent
cfa78679be
commit
6832a8ad53
4 changed files with 460 additions and 10 deletions
|
|
@ -18,6 +18,7 @@
|
|||
D01000000006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000006 /* LoginView.swift */; };
|
||||
D01000000007 /* BusinessListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000007 /* BusinessListView.swift */; };
|
||||
D01000000008 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000008 /* ScanView.swift */; };
|
||||
D01000000009 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000009 /* QRScannerView.swift */; };
|
||||
D0100000000A /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0200000000A /* RootView.swift */; };
|
||||
D01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D02000000060 /* Assets.xcassets */; };
|
||||
D01000000070 /* payfrit-favicon-light-outlines.svg in Resources */ = {isa = PBXBuildFile; fileRef = D02000000070 /* payfrit-favicon-light-outlines.svg */; };
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
D02000000006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
D02000000007 /* BusinessListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessListView.swift; sourceTree = "<group>"; };
|
||||
D02000000008 /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = "<group>"; };
|
||||
D02000000009 /* QRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = "<group>"; };
|
||||
D0200000000A /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||
D02000000010 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D02000000060 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
|
@ -110,6 +112,7 @@
|
|||
D02000000006 /* LoginView.swift */,
|
||||
D02000000007 /* BusinessListView.swift */,
|
||||
D02000000008 /* ScanView.swift */,
|
||||
D02000000009 /* QRScannerView.swift */,
|
||||
D02000000010 /* Info.plist */,
|
||||
D02000000060 /* Assets.xcassets */,
|
||||
D02000000070 /* payfrit-favicon-light-outlines.svg */,
|
||||
|
|
@ -254,6 +257,7 @@
|
|||
D01000000006 /* LoginView.swift in Sources */,
|
||||
D01000000007 /* BusinessListView.swift in Sources */,
|
||||
D01000000008 /* ScanView.swift in Sources */,
|
||||
D01000000009 /* QRScannerView.swift in Sources */,
|
||||
D0100000000A /* RootView.swift in Sources */,
|
||||
D010000000B1 /* BLEBeaconScanner.swift in Sources */,
|
||||
D010000000B2 /* BeaconProvisioner.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -194,6 +194,102 @@ actor APIClient {
|
|||
return resp.data?.name ?? "Unknown"
|
||||
}
|
||||
|
||||
// MARK: - Service Point Management
|
||||
|
||||
func deleteServicePoint(servicePointId: String, businessId: String, token: String) async throws {
|
||||
let body: [String: Any] = ["ID": servicePointId, "BusinessID": businessId]
|
||||
let data = try await post(path: "/servicepoints/delete.php", body: body, token: token, businessId: businessId)
|
||||
let resp = try JSONDecoder().decode(APIResponse<EmptyData>.self, from: data)
|
||||
guard resp.success else {
|
||||
throw APIError.serverError(resp.message ?? "Failed to delete service point")
|
||||
}
|
||||
}
|
||||
|
||||
func updateServicePoint(servicePointId: String, name: String, businessId: String, token: String) async throws {
|
||||
let body: [String: Any] = ["ID": servicePointId, "Name": name, "BusinessID": businessId]
|
||||
let data = try await post(path: "/servicepoints/save.php", body: body, token: token, businessId: businessId)
|
||||
let resp = try JSONDecoder().decode(APIResponse<EmptyData>.self, from: data)
|
||||
guard resp.success else {
|
||||
throw APIError.serverError(resp.message ?? "Failed to update service point")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Beacon Management
|
||||
|
||||
struct BeaconListResponse: Codable {
|
||||
let id: String?
|
||||
let ID: String?
|
||||
let uuid: String?
|
||||
let UUID: String?
|
||||
let major: Int?
|
||||
let Major: Int?
|
||||
let minor: Int?
|
||||
let Minor: Int?
|
||||
let macAddress: String?
|
||||
let MacAddress: String?
|
||||
let beaconType: String?
|
||||
let BeaconType: String?
|
||||
let servicePointId: String?
|
||||
let ServicePointID: String?
|
||||
let isVerified: Bool?
|
||||
let IsVerified: Bool?
|
||||
}
|
||||
|
||||
func listBeacons(businessId: String, token: String) async throws -> [BeaconListResponse] {
|
||||
let body: [String: Any] = ["BusinessID": businessId]
|
||||
let data = try await post(path: "/beacon-sharding/list_beacons.php", body: body, token: token, businessId: businessId)
|
||||
let resp = try JSONDecoder().decode(APIResponse<[BeaconListResponse]>.self, from: data)
|
||||
guard resp.success else {
|
||||
throw APIError.serverError(resp.message ?? "Failed to list beacons")
|
||||
}
|
||||
return resp.data ?? []
|
||||
}
|
||||
|
||||
func decommissionBeacon(beaconId: String, businessId: String, token: String) async throws {
|
||||
let body: [String: Any] = ["ID": beaconId, "BusinessID": businessId]
|
||||
let data = try await post(path: "/beacon-sharding/decommission_beacon.php", body: body, token: token, businessId: businessId)
|
||||
let resp = try JSONDecoder().decode(APIResponse<EmptyData>.self, from: data)
|
||||
guard resp.success else {
|
||||
throw APIError.serverError(resp.message ?? "Failed to decommission beacon")
|
||||
}
|
||||
}
|
||||
|
||||
func lookupByMac(macAddress: String, token: String) async throws -> BeaconListResponse? {
|
||||
let body: [String: Any] = ["MacAddress": macAddress]
|
||||
let data = try await post(path: "/beacon-sharding/lookup_by_mac.php", body: body, token: token)
|
||||
let resp = try JSONDecoder().decode(APIResponse<BeaconListResponse>.self, from: data)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
func getBeaconStatus(uuid: String, major: Int, minor: Int, token: String) async throws -> BeaconListResponse? {
|
||||
let body: [String: Any] = ["UUID": uuid, "Major": major, "Minor": minor]
|
||||
let data = try await post(path: "/beacon-sharding/beacon_status.php", body: body, token: token)
|
||||
let resp = try JSONDecoder().decode(APIResponse<BeaconListResponse>.self, from: data)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
// MARK: - User Profile
|
||||
|
||||
struct UserProfile: Codable {
|
||||
let id: String?
|
||||
let ID: String?
|
||||
let firstName: String?
|
||||
let FirstName: String?
|
||||
let lastName: String?
|
||||
let LastName: String?
|
||||
let contactNumber: String?
|
||||
let ContactNumber: String?
|
||||
}
|
||||
|
||||
func getProfile(token: String) async throws -> UserProfile {
|
||||
let data = try await post(path: "/users/profile.php", body: [:], token: token)
|
||||
let resp = try JSONDecoder().decode(APIResponse<UserProfile>.self, from: data)
|
||||
guard resp.success, let profile = resp.data else {
|
||||
throw APIError.serverError(resp.message ?? "Failed to load profile")
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
private struct EmptyData: Codable {}
|
||||
|
|
|
|||
290
PayfritBeacon/Views/QRScannerView.swift
Normal file
290
PayfritBeacon/Views/QRScannerView.swift
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import SwiftUI
|
||||
import AVFoundation
|
||||
import Vision
|
||||
|
||||
/// QR/Barcode scanner for reading beacon MAC addresses and UUIDs
|
||||
/// Matches Android QRScannerActivity — camera preview + barcode detection
|
||||
struct QRScannerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
/// Called with the scanned string (MAC address, UUID, or other barcode)
|
||||
let onScan: (String) -> Void
|
||||
|
||||
@State private var scannedCode: String?
|
||||
@State private var isFlashOn = false
|
||||
@State private var cameraPermission: CameraPermission = .undetermined
|
||||
|
||||
enum CameraPermission {
|
||||
case undetermined, granted, denied
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
if cameraPermission == .granted {
|
||||
CameraPreview(onCodeDetected: handleDetection, isFlashOn: $isFlashOn)
|
||||
.ignoresSafeArea()
|
||||
|
||||
// Scan overlay
|
||||
scanOverlay
|
||||
} else if cameraPermission == .denied {
|
||||
cameraPermissionDenied
|
||||
} else {
|
||||
Color.black.ignoresSafeArea()
|
||||
.overlay {
|
||||
ProgressView("Requesting camera…")
|
||||
.tint(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Scan QR / Barcode")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.ultraThinMaterial, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { dismiss() }
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
isFlashOn.toggle()
|
||||
} label: {
|
||||
Image(systemName: isFlashOn ? "flashlight.on.fill" : "flashlight.off.fill")
|
||||
.foregroundStyle(isFlashOn ? .yellow : .white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await checkCameraPermission()
|
||||
}
|
||||
.alert("Code Detected", isPresented: .init(
|
||||
get: { scannedCode != nil },
|
||||
set: { if !$0 { scannedCode = nil } }
|
||||
)) {
|
||||
Button("Use This") {
|
||||
if let code = scannedCode {
|
||||
onScan(code)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Scan Again", role: .cancel) {
|
||||
scannedCode = nil
|
||||
}
|
||||
} message: {
|
||||
Text(scannedCode ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scan Overlay
|
||||
|
||||
private var scanOverlay: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
// Viewfinder frame
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(.white.opacity(0.8), lineWidth: 2)
|
||||
.frame(width: 280, height: 280)
|
||||
.overlay {
|
||||
// Corner accents
|
||||
GeometryReader { geo in
|
||||
let w = geo.size.width
|
||||
let h = geo.size.height
|
||||
let corner: CGFloat = 30
|
||||
let thickness: CGFloat = 4
|
||||
|
||||
// Top-left
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: 0, y: corner))
|
||||
p.addLine(to: CGPoint(x: 0, y: 0))
|
||||
p.addLine(to: CGPoint(x: corner, y: 0))
|
||||
}
|
||||
.stroke(.blue, lineWidth: thickness)
|
||||
|
||||
// Top-right
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: w - corner, y: 0))
|
||||
p.addLine(to: CGPoint(x: w, y: 0))
|
||||
p.addLine(to: CGPoint(x: w, y: corner))
|
||||
}
|
||||
.stroke(.blue, lineWidth: thickness)
|
||||
|
||||
// Bottom-left
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: 0, y: h - corner))
|
||||
p.addLine(to: CGPoint(x: 0, y: h))
|
||||
p.addLine(to: CGPoint(x: corner, y: h))
|
||||
}
|
||||
.stroke(.blue, lineWidth: thickness)
|
||||
|
||||
// Bottom-right
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: w - corner, y: h))
|
||||
p.addLine(to: CGPoint(x: w, y: h))
|
||||
p.addLine(to: CGPoint(x: w, y: h - corner))
|
||||
}
|
||||
.stroke(.blue, lineWidth: thickness)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Instructions
|
||||
Text("Point camera at beacon QR code or barcode")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
|
||||
private var cameraPermissionDenied: some View {
|
||||
ContentUnavailableView {
|
||||
Label("Camera Access Required", systemImage: "camera.fill")
|
||||
} description: {
|
||||
Text("Open Settings and enable camera access for Payfrit Beacon.")
|
||||
} actions: {
|
||||
Button("Open Settings") {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func checkCameraPermission() async {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
cameraPermission = .granted
|
||||
case .notDetermined:
|
||||
let granted = await AVCaptureDevice.requestAccess(for: .video)
|
||||
cameraPermission = granted ? .granted : .denied
|
||||
default:
|
||||
cameraPermission = .denied
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDetection(_ code: String) {
|
||||
guard scannedCode == nil else { return } // Debounce
|
||||
// Haptic feedback
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
generator.impactOccurred()
|
||||
scannedCode = code
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Camera Preview (UIViewRepresentable)
|
||||
|
||||
/// AVFoundation camera preview with barcode/QR detection via Vision framework
|
||||
struct CameraPreview: UIViewRepresentable {
|
||||
let onCodeDetected: (String) -> Void
|
||||
@Binding var isFlashOn: Bool
|
||||
|
||||
func makeUIView(context: Context) -> CameraPreviewUIView {
|
||||
let view = CameraPreviewUIView(onCodeDetected: onCodeDetected)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
|
||||
uiView.setFlash(isFlashOn)
|
||||
}
|
||||
}
|
||||
|
||||
final class CameraPreviewUIView: UIView {
|
||||
private var captureSession: AVCaptureSession?
|
||||
private var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
private let onCodeDetected: (String) -> Void
|
||||
private var lastDetectionTime: Date = .distantPast
|
||||
private let debounceInterval: TimeInterval = 1.5
|
||||
|
||||
init(onCodeDetected: @escaping (String) -> Void) {
|
||||
self.onCodeDetected = onCodeDetected
|
||||
super.init(frame: .zero)
|
||||
setupCamera()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
previewLayer?.frame = bounds
|
||||
}
|
||||
|
||||
func setFlash(_ on: Bool) {
|
||||
guard let device = AVCaptureDevice.default(for: .video),
|
||||
device.hasTorch else { return }
|
||||
try? device.lockForConfiguration()
|
||||
device.torchMode = on ? .on : .off
|
||||
device.unlockForConfiguration()
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
let session = AVCaptureSession()
|
||||
session.sessionPreset = .high
|
||||
|
||||
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
|
||||
let input = try? AVCaptureDeviceInput(device: device) else { return }
|
||||
|
||||
if session.canAddInput(input) {
|
||||
session.addInput(input)
|
||||
}
|
||||
|
||||
// Use AVCaptureMetadataOutput for barcode detection (more reliable than Vision for barcodes)
|
||||
let metadataOutput = AVCaptureMetadataOutput()
|
||||
if session.canAddOutput(metadataOutput) {
|
||||
session.addOutput(metadataOutput)
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
|
||||
metadataOutput.metadataObjectTypes = [
|
||||
.qr,
|
||||
.ean8, .ean13,
|
||||
.code128, .code39, .code93,
|
||||
.dataMatrix,
|
||||
.pdf417,
|
||||
.aztec
|
||||
]
|
||||
}
|
||||
|
||||
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
previewLayer.videoGravity = .resizeAspectFill
|
||||
layer.addSublayer(previewLayer)
|
||||
self.previewLayer = previewLayer
|
||||
|
||||
self.captureSession = session
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
captureSession?.stopRunning()
|
||||
setFlash(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVCaptureMetadataOutputObjectsDelegate
|
||||
|
||||
extension CameraPreviewUIView: AVCaptureMetadataOutputObjectsDelegate {
|
||||
func metadataOutput(
|
||||
_ output: AVCaptureMetadataOutput,
|
||||
didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection
|
||||
) {
|
||||
guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
||||
let value = metadata.stringValue,
|
||||
!value.isEmpty else { return }
|
||||
|
||||
// Debounce rapid detections
|
||||
let now = Date()
|
||||
guard now.timeIntervalSince(lastDetectionTime) > debounceInterval else { return }
|
||||
lastDetectionTime = now
|
||||
|
||||
onCodeDetected(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ struct ScanView: View {
|
|||
@State private var provisioningState: ProvisioningState = .idle
|
||||
@State private var statusMessage = ""
|
||||
@State private var errorMessage: String?
|
||||
@State private var showQRScanner = false
|
||||
@State private var scannedMAC: String?
|
||||
|
||||
enum ProvisioningState {
|
||||
case idle
|
||||
|
|
@ -68,6 +70,11 @@ struct ScanView: View {
|
|||
.sheet(isPresented: $showCreateServicePoint) {
|
||||
createServicePointSheet
|
||||
}
|
||||
.sheet(isPresented: $showQRScanner) {
|
||||
QRScannerView { code in
|
||||
handleQRScan(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadServicePoints()
|
||||
|
|
@ -210,18 +217,31 @@ struct ScanView: View {
|
|||
|
||||
private var beaconList: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Scan button
|
||||
Button {
|
||||
Task { await startScan() }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: bleManager.isScanning ? "antenna.radiowaves.left.and.right" : "magnifyingglass")
|
||||
Text(bleManager.isScanning ? "Scanning…" : "Scan for Beacons")
|
||||
// Scan buttons
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
Task { await startScan() }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: bleManager.isScanning ? "antenna.radiowaves.left.and.right" : "magnifyingglass")
|
||||
Text(bleManager.isScanning ? "Scanning…" : "BLE Scan")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(bleManager.isScanning || bleManager.bluetoothState != .poweredOn)
|
||||
|
||||
Button {
|
||||
showQRScanner = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "qrcode.viewfinder")
|
||||
Text("QR Scan")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(bleManager.isScanning || bleManager.bluetoothState != .poweredOn)
|
||||
.padding()
|
||||
|
||||
if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning {
|
||||
|
|
@ -613,6 +633,45 @@ struct ScanView: View {
|
|||
resetProvisioningState()
|
||||
}
|
||||
|
||||
/// Handle QR/barcode scan result — could be MAC address, UUID, or other identifier
|
||||
private func handleQRScan(_ code: String) {
|
||||
let cleaned = code.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Check if it looks like a MAC address (AA:BB:CC:DD:EE:FF or AABBCCDDEEFF)
|
||||
let macPattern = #"^([0-9A-Fa-f]{2}[:-]?){5}[0-9A-Fa-f]{2}$"#
|
||||
if cleaned.range(of: macPattern, options: .regularExpression) != nil {
|
||||
scannedMAC = cleaned.replacingOccurrences(of: "-", with: ":").uppercased()
|
||||
statusMessage = "Scanned MAC: \(scannedMAC ?? cleaned)"
|
||||
// Look up the beacon by MAC
|
||||
Task { await lookupScannedMAC() }
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it looks like a UUID
|
||||
let uuidPattern = #"^[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}$"#
|
||||
if cleaned.range(of: uuidPattern, options: .regularExpression) != nil {
|
||||
statusMessage = "Scanned UUID: \(cleaned)"
|
||||
return
|
||||
}
|
||||
|
||||
// Generic code — just show it
|
||||
statusMessage = "Scanned: \(cleaned)"
|
||||
}
|
||||
|
||||
private func lookupScannedMAC() async {
|
||||
guard let mac = scannedMAC, let token = appState.token else { return }
|
||||
do {
|
||||
if let existing = try await APIClient.shared.lookupByMac(macAddress: mac, token: token) {
|
||||
let beaconType = existing.beaconType ?? existing.BeaconType ?? "Unknown"
|
||||
statusMessage = "MAC \(mac) — already registered as \(beaconType)"
|
||||
} else {
|
||||
statusMessage = "MAC \(mac) — not yet registered. Scan BLE to find and provision."
|
||||
}
|
||||
} catch {
|
||||
statusMessage = "MAC \(mac) — lookup failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
private func resetProvisioningState() {
|
||||
provisioningState = .idle
|
||||
statusMessage = ""
|
||||
|
|
@ -620,6 +679,7 @@ struct ScanView: View {
|
|||
selectedBeacon = nil
|
||||
pendingConfig = nil
|
||||
pendingProvisioner = nil
|
||||
scannedMAC = nil
|
||||
}
|
||||
|
||||
private func makeProvisioner(for beacon: DiscoveredBeacon) -> any BeaconProvisioner {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue