Fix QR verification sending multiple notifications (#773)

The camera scanner was continuously detecting the same QR code (10-30+ times/second), causing multiple verification notifications to be sent. This happened because:

1. AVCaptureMetadataOutput fires repeatedly while QR is visible
2. Each detection triggered a new verification flow
3. After receiving response, pendingQRVerifications was cleared, allowing duplicate scans

Changes:
- Add deduplication using lastValid state to ignore re-scans of same QR code
- Only set lastValid after successful verification initiation
- Add onSuccess callback to close scanner after successful scan
- Automatically return to "My QR" view after verification starts
- Apply same logic to both iOS camera and macOS manual input paths

This ensures exactly one verification request per scan session.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-10-07 11:32:57 +02:00
committed by GitHub
co-authored by jack
parent d75700fa2b
commit c583949031
+31 -5
View File
@@ -109,6 +109,7 @@ struct ImageWrapper: View {
struct QRScanView: View { struct QRScanView: View {
@EnvironmentObject var viewModel: ChatViewModel @EnvironmentObject var viewModel: ChatViewModel
var isActive: Bool = true var isActive: Bool = true
var onSuccess: (() -> Void)? = nil // Called when verification succeeds
@State private var input = "" @State private var input = ""
@State private var result: String = "" // not shown for iOS scanner @State private var result: String = "" // not shown for iOS scanner
@State private var lastValid: String = "" @State private var lastValid: String = ""
@@ -131,10 +132,18 @@ struct QRScanView: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
#if os(iOS) #if os(iOS)
CameraScannerView(isActive: isActive) { code in CameraScannerView(isActive: isActive) { code in
// Deduplicate: ignore if we just processed this exact QR code
guard code != lastValid else { return }
if let qr = VerificationService.shared.verifyScannedQR(code) { if let qr = VerificationService.shared.verifyScannedQR(code) {
let ok = viewModel.beginQRVerification(with: qr) let ok = viewModel.beginQRVerification(with: qr)
if !ok { /* already pending; continue scanning */ } if ok {
lastValid = code // Successfully initiated verification; remember this QR to prevent re-scanning
lastValid = code
// Close scanner and return to "My QR" view
onSuccess?()
}
// If !ok, peer not found or already pending - don't set lastValid so user can retry
} else { } else {
// ignore invalid reads; continue scanning // ignore invalid reads; continue scanning
} }
@@ -148,9 +157,22 @@ struct QRScanView: View {
.frame(height: 100) .frame(height: 100)
.border(Color.gray.opacity(0.4)) .border(Color.gray.opacity(0.4))
Button(Strings.validate) { Button(Strings.validate) {
// Deduplicate: ignore if we just processed this exact QR
guard input != lastValid else {
result = Strings.requested("") // Already processed
return
}
if let qr = VerificationService.shared.verifyScannedQR(input) { if let qr = VerificationService.shared.verifyScannedQR(input) {
let ok = viewModel.beginQRVerification(with: qr) let ok = viewModel.beginQRVerification(with: qr)
result = ok ? Strings.requested(qr.nickname) : Strings.notFound if ok {
result = Strings.requested(qr.nickname)
lastValid = input
// Close scanner and return to "My QR" view
onSuccess?()
} else {
result = Strings.notFound
}
} else { } else {
result = Strings.invalid result = Strings.invalid
} }
@@ -308,12 +330,16 @@ struct VerificationSheetView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundColor(accentColor) .foregroundColor(accentColor)
#if os(iOS) #if os(iOS)
QRScanView(isActive: showingScanner) QRScanView(isActive: showingScanner, onSuccess: {
showingScanner = false
})
.environmentObject(viewModel) .environmentObject(viewModel)
.frame(height: 280) .frame(height: 280)
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
#else #else
QRScanView() QRScanView(onSuccess: {
showingScanner = false
})
.environmentObject(viewModel) .environmentObject(viewModel)
#endif #endif
} }