diff --git a/lib/screens/verify_seed/verify_seed_page.dart b/lib/screens/verify_seed/verify_seed_page.dart index 17804a1a6..6017923d6 100644 --- a/lib/screens/verify_seed/verify_seed_page.dart +++ b/lib/screens/verify_seed/verify_seed_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:no_screenshot/no_screenshot.dart'; import 'package:realunit_wallet/generated/i18n.dart'; import 'package:realunit_wallet/packages/service/wallet_service.dart'; import 'package:realunit_wallet/packages/wallet/wallet.dart'; @@ -26,9 +27,32 @@ class VerifySeedPage extends StatelessWidget { ); } -class VerifySeedView extends StatelessWidget { +class VerifySeedView extends StatefulWidget { const VerifySeedView({super.key}); + @override + State createState() => _VerifySeedViewState(); +} + +class _VerifySeedViewState extends State { + // @no-integration-test: no_screenshot suppresses the OS screenshot / + // app-switcher thumbnail / screen-recording via a platform channel — the + // real effect is only observable on a device, not in a widget/golden test. + @override + void initState() { + // Seed words are entered/visible here — block screenshots and the + // app-switcher snapshot like the other seed screens. Re-enabled on dispose + // so other screens stay screenshot-able. + NoScreenshot.instance.screenshotOff(); + super.initState(); + } + + @override + void dispose() { + NoScreenshot.instance.screenshotOn(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/test/goldens/screens/verify_seed/verify_seed_golden_test.dart b/test/goldens/screens/verify_seed/verify_seed_golden_test.dart index fa9ca8590..48b3ccd12 100644 --- a/test/goldens/screens/verify_seed/verify_seed_golden_test.dart +++ b/test/goldens/screens/verify_seed/verify_seed_golden_test.dart @@ -1,5 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -16,6 +17,12 @@ void main() { late _MockVerifySeedCubit verifySeedCubit; late MockHomeBloc homeBloc; + // VerifySeedView toggles screenshot protection in initState/dispose via the + // no_screenshot method channel. Stub it so the render does not throw a + // MissingPluginException in the headless golden harness. + const noScreenshotChannel = + MethodChannel('com.flutterplaza.no_screenshot_methods'); + setUp(() { verifySeedCubit = _MockVerifySeedCubit(); homeBloc = MockHomeBloc(); @@ -26,6 +33,13 @@ void main() { ), ); when(() => homeBloc.state).thenReturn(const HomeState()); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(noScreenshotChannel, (_) async => true); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(noScreenshotChannel, null); }); Widget buildSubject() => MultiBlocProvider( diff --git a/test/screens/verify_seed/verify_seed_page_test.dart b/test/screens/verify_seed/verify_seed_page_test.dart index a28aa564b..abd72e885 100644 --- a/test/screens/verify_seed/verify_seed_page_test.dart +++ b/test/screens/verify_seed/verify_seed_page_test.dart @@ -1,5 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -182,6 +183,36 @@ void main() { }); }); + // The verify-seed screen (where the user re-enters real seed words) had no + // screenshot protection, unlike the sibling seed screens. It must disable + // screenshots on init and re-enable on dispose so it never lands in the + // app-switcher snapshot / a recording. + testWidgets('disables screenshots on init and re-enables on dispose', + (tester) async { + const channel = MethodChannel('com.flutterplaza.no_screenshot_methods'); + final calls = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (call) async { + calls.add(call.method); + return true; + }, + ); + addTearDown( + () => tester.binding.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null), + ); + + await tester.pumpApp(buildSubject(const VerifySeedView())); + expect(calls, contains('screenshotOff'), + reason: 'seed screen must block screenshots on init'); + + // Replace the screen so VerifySeedView is disposed. + await tester.pumpApp(buildSubject(const SizedBox.shrink())); + expect(calls, contains('screenshotOn'), + reason: 'leaving the seed screen must re-enable screenshots'); + }); + group('$BlocListener', () { testWidgets('sends $LoadWalletEvent with the committed wallet when verified', (tester) async {