What changed, and why it matters
This commit fixes a bug where scanning a QR code could accidentally close the same screen multiple times in rapid succession. The change wraps the scan handler so it only runs once, and also cancels the camera stream before closing the screen. Repeated pops could cause navigation glitches or minor app instability, but there is no direct evidence it could be used to steal funds or keys.
Treat as a routine bug-fix / hardening patch. Review whether other async callbacks that trigger navigation or sensitive actions (e.g., signing, sending) are similarly guarded against double invocation. No urgent security response is indicated by the diff alone.
Security signals we found
Race-condition-like duplicate UI action prevented (multiple Navigator.pop calls)
Barcode subscription cancellation added to stop further scan events after first successful scan
Generic single-execution guard introduced for async callbacks with arguments
Evidence from the diff
The patch makes IfNotAlreadyAsync generic and adds a withArgs constructor so a single-execution lock can be used with parameters. It then uses IfNotAlreadyAsync
Changed components
lib/widgets/qr_scanner.dartlib/utilities/if_not_already.dartlib/pages/signing/sub_widgets/sign_message_tab.dartlib/pages/signing/sub_widgets/verify_message_tab.dartInspect captured patch +27 / −12
diff --git a/lib/pages/signing/sub_widgets/sign_message_tab.dart b/lib/pages/signing/sub_widgets/sign_message_tab.dart
index a4b0d92..e797048 100644
--- a/lib/pages/signing/sub_widgets/sign_message_tab.dart
+++ b/lib/pages/signing/sub_widgets/sign_message_tab.dart
@@ -90,7 +90,7 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
messageController.text = ref.read(_pSignState).message;
- _chooseAddress = IfNotAlreadyAsync(() async {
+ _chooseAddress = IfNotAlreadyAsync<void>(() async {
final Address? address;
if (Util.isDesktop) {
@@ -166,7 +166,7 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
}
}).execute;
- _sign = IfNotAlreadyAsync(() async {
+ _sign = IfNotAlreadyAsync<void>(() async {
Exception? ex;
final state = ref.read(_pSignState);
diff --git a/lib/pages/signing/sub_widgets/verify_message_tab.dart b/lib/pages/signing/sub_widgets/verify_message_tab.dart
index 08b4e59..0a33183 100644
--- a/lib/pages/signing/sub_widgets/verify_message_tab.dart
+++ b/lib/pages/signing/sub_widgets/verify_message_tab.dart
@@ -82,7 +82,7 @@ class _VerifyMessageFormState extends ConsumerState<VerifyMessageForm> {
messageController.text = ref.read(_pVerifyState).message;
signatureController.text = ref.read(_pVerifyState).signature;
- _verify = IfNotAlreadyAsync(() async {
+ _verify = IfNotAlreadyAsync<void>(() async {
Exception? ex;
final verified = await showLoading(
diff --git a/lib/utilities/if_not_already.dart b/lib/utilities/if_not_already.dart
index 664fc9a..41da52c 100644
--- a/lib/utilities/if_not_already.dart
+++ b/lib/utilities/if_not_already.dart
@@ -18,18 +18,24 @@ class IfNotAlready {
}
}
-class IfNotAlreadyAsync {
- final Future<void> Function() _function;
+class IfNotAlreadyAsync<T> {
+ final Future<void> Function()? _function;
+ final Future<void> Function(T? args)? _functionWithArgs;
bool _locked = false;
- IfNotAlreadyAsync(this._function);
+ IfNotAlreadyAsync(this._function) : _functionWithArgs = null;
+ IfNotAlreadyAsync.withArgs(this._functionWithArgs) : _function = null;
- Future<void> execute() async {
+ Future<void> execute([T? args]) async {
if (!_locked) {
_locked = true;
try {
- await _function();
+ if (_function == null) {
+ await _function!();
+ } else {
+ await _functionWithArgs!(args);
+ }
} finally {
_locked = false;
}
diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart
index 7f3c428..cd19c83 100644
--- a/lib/widgets/qr_scanner.dart
+++ b/lib/widgets/qr_scanner.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import '../themes/stack_colors.dart';
+import '../utilities/if_not_already.dart';
import '../utilities/text_styles.dart';
import 'background.dart';
import 'custom_buttons/app_bar_icon_button.dart';
@@ -23,10 +24,18 @@ class _QrScannerState extends State<QrScanner> {
StreamSubscription<Barcode>? sub;
- void _onScanned(String? data) {
- if (data != null && mounted) {
- Navigator.of(context).pop(data);
- }
+ late final Future<void> Function(String?) _onScanned;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _onScanned = IfNotAlreadyAsync<String>.withArgs((data) async {
+ await sub?.cancel();
+ if (mounted) {
+ Navigator.of(context).pop(data);
+ }
+ }).execute;
}
// In order to get hot reload to work we need to pause the camera if the platform
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.