diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bd793d78..78b4b8d8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,6 +26,9 @@ PODS: - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS + - mobile_scanner (7.0.0): + - Flutter + - FlutterMacOS - no_screenshot (0.10.0): - Flutter - open_file_ios (1.0.3): @@ -49,6 +52,7 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -78,6 +82,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/darwin" no_screenshot: :path: ".symlinks/plugins/no_screenshot/ios" open_file_ios: @@ -99,6 +105,7 @@ SPEC CHECKSUMS: IdensicMobileSDK: a8ec2cf5c216ae138b00e2ff32e0e31c5e366cec image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 no_screenshot: 03c8ac6586f9652cd45e3d12d74e5992256403ac open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 diff --git a/lib/screens/pay/pay_scan_page.dart b/lib/screens/pay/pay_scan_page.dart index 8ca3f74d..01affc73 100644 --- a/lib/screens/pay/pay_scan_page.dart +++ b/lib/screens/pay/pay_scan_page.dart @@ -55,7 +55,7 @@ class PayScanView extends StatelessWidget { appBar: AppBar(title: Text(S.of(context).payScanTitle)), body: QrScannerView( onDetect: (raw) => context.read().onCodeDetected(raw), - errorBuilder: (context, error, child) { + errorBuilder: (context, error) { final message = error.errorCode == MobileScannerErrorCode.permissionDenied ? S.of(context).payScanCameraPermissionDenied : S.of(context).payScanCameraUnavailable; diff --git a/lib/widgets/scanner/qr_scanner_view.dart b/lib/widgets/scanner/qr_scanner_view.dart index 6ef8f793..6d5e9c38 100644 --- a/lib/widgets/scanner/qr_scanner_view.dart +++ b/lib/widgets/scanner/qr_scanner_view.dart @@ -4,8 +4,26 @@ // pay flow (LNURL decode) and the wallet-to-wallet send flow (EVM address // decode); the per-flow decode logic it feeds is unit-tested in the respective // cubit tests. -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:realunit_wallet/styles/colors.dart'; + +/// Compact icon-only error placeholder used when [QrScannerView.errorBuilder] +/// is null. Replaces mobile_scanner 7.x's own (taller, textScale-scaling) +/// default to avoid overflow in bounded layouts like send_recipient_page.dart's +/// Expanded. +Widget _defaultErrorBuilder(BuildContext context, MobileScannerException error) { + return ColoredBox( + color: RealUnitColors.basic.black, + child: Center( + child: Icon( + Icons.error_outline, + size: 48, + color: RealUnitColors.status.red600, + ), + ), + ); +} /// Thin wrapper around [MobileScanner] that surfaces the first raw barcode /// value of each capture via [onDetect]. Keeping the camera/MethodChannel @@ -17,8 +35,10 @@ class QrScannerView extends StatelessWidget { final ValueChanged onDetect; /// Optional error UI builder forwarded to [MobileScanner.errorBuilder]. - /// When null, MobileScanner's own default error handling is used. - final Widget Function(BuildContext, MobileScannerException, Widget?)? errorBuilder; + /// When null, this compact icon-only placeholder is used instead of + /// mobile_scanner 7.x's own (taller, textScale-scaling) default, to avoid + /// overflow in bounded layouts like send_recipient_page.dart's Expanded. + final Widget Function(BuildContext, MobileScannerException)? errorBuilder; const QrScannerView({ super.key, @@ -33,7 +53,7 @@ class QrScannerView extends StatelessWidget { final raw = capture.barcodes.firstOrNull?.rawValue; if (raw != null) onDetect(raw); }, - errorBuilder: errorBuilder, + errorBuilder: errorBuilder ?? _defaultErrorBuilder, ); } } diff --git a/pubspec.lock b/pubspec.lock index 5c204b0f..759158cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,7 +78,7 @@ packages: description: path: "." ref: "v0.0.10" - resolved-ref: "cd99ce656410e8df6c6585076d6ee0205a67b34c" + resolved-ref: cd99ce656410e8df6c6585076d6ee0205a67b34c url: "https://github.com/DFXswiss/bitbox_flutter.git" source: git version: "0.0.1" @@ -955,10 +955,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + sha256: ce3f059ebd6dbfab7292bba0e893e354b46730636820d3c9ef69005ce2d55bce url: "https://pub.dev" source: hosted - version: "5.2.3" + version: "7.4.0" mocktail: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 67fd1d5c..c6634fa5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: http: ^1.1.0 intl: any local_auth: ^3.0.0 - mobile_scanner: ^5.2.3 + mobile_scanner: ^7.4.0 no_screenshot: ^1.1.0 open_file: ^3.5.11 path: ^1.9.0 diff --git a/test/goldens/screens/send/goldens/macos/send_recipient_page_empty.png b/test/goldens/screens/send/goldens/macos/send_recipient_page_empty.png index 7b64f40a..850d2b97 100644 Binary files a/test/goldens/screens/send/goldens/macos/send_recipient_page_empty.png and b/test/goldens/screens/send/goldens/macos/send_recipient_page_empty.png differ diff --git a/test/helper/golden_plugin_stubs.dart b/test/helper/golden_plugin_stubs.dart index d20b47b1..d9141c94 100644 --- a/test/helper/golden_plugin_stubs.dart +++ b/test/helper/golden_plugin_stubs.dart @@ -68,4 +68,10 @@ void stubMobileScannerChannel() { const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'), MockStreamHandler.inline(onListen: (arguments, sink) {}), ); + // mobile_scanner 7.x additionally subscribes to a device-orientation event + // stream; stub it as a no-op so `listen`/`cancel` don't throw MissingPluginException. + messenger.setMockStreamHandler( + const EventChannel('dev.steenbakker.mobile_scanner/scanner/deviceOrientation'), + MockStreamHandler.inline(onListen: (arguments, sink) {}), + ); } diff --git a/test/screens/pay/pay_scan_page_test.dart b/test/screens/pay/pay_scan_page_test.dart index 4689fd5b..b1718d94 100644 --- a/test/screens/pay/pay_scan_page_test.dart +++ b/test/screens/pay/pay_scan_page_test.dart @@ -132,7 +132,6 @@ void main() { final errorWidget = scanner.errorBuilder!( context, MobileScannerException(errorCode: nonPermissionCode), - null, ); await tester.pumpWidget(MaterialApp(home: errorWidget)); diff --git a/test/widgets/scanner/qr_scanner_view_test.dart b/test/widgets/scanner/qr_scanner_view_test.dart new file mode 100644 index 00000000..ac9c0b5b --- /dev/null +++ b/test/widgets/scanner/qr_scanner_view_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:realunit_wallet/widgets/scanner/qr_scanner_view.dart'; + +import '../../helper/helper.dart'; + +void main() { + setUpAll(stubMobileScannerChannel); + + group('$QrScannerView default error placeholder', () { + testWidgets( + 'renders the compact icon placeholder without overflow at high textScale in a tight box', + (tester) async { + await tester.pumpApp( + MediaQuery( + data: const MediaQueryData(size: Size(400, 800)) + .copyWith(textScaler: const TextScaler.linear(3.0)), + child: Center( + child: SizedBox( + width: 60, + height: 60, + child: QrScannerView(onDetect: (_) {}), + ), + ), + ), + ); + + // Let the stubbed permission handshake resolve into the + // permission-denied error state so the default error builder paints. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + expect(tester.takeException(), isNull); + expect(find.byIcon(Icons.error_outline), findsOneWidget); + final icon = tester.widget(find.byIcon(Icons.error_outline)); + expect(icon.size, 48); + }, + ); + }); +}