From 55f40b894ff49bc98dc19c29d8037a3295fe6068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 13:49:21 +0200 Subject: [PATCH 1/4] fix(seed): block screenshots on the verify-seed screen VerifySeedView was a StatelessWidget with no screenshot protection, unlike the sibling seed screens (create_wallet_view, settings_seed_view). The user enters real seed words here, so they could land in the OS app-switcher snapshot or a screen recording. Convert to StatefulWidget and disable screenshots on init / re-enable on dispose, matching the sibling screens. Adds a widget test asserting the no_screenshot channel receives screenshotOff on init and screenshotOn on dispose. (Native app-switcher blanking itself is not unit-testable; this pins the contract the protection relies on.) Refs #612 (S1) --- lib/screens/verify_seed/verify_seed_page.dart | 24 +++++++++++++- .../verify_seed/verify_seed_page_test.dart | 31 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/screens/verify_seed/verify_seed_page.dart b/lib/screens/verify_seed/verify_seed_page.dart index 17804a1a6..113f69dfe 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,30 @@ class VerifySeedPage extends StatelessWidget { ); } -class VerifySeedView extends StatelessWidget { +class VerifySeedView extends StatefulWidget { const VerifySeedView({super.key}); + @override + State createState() => _VerifySeedViewState(); +} + +class _VerifySeedViewState extends State { + @override + void initState() { + // Seed words are entered/visible here — block screenshots and the + // app-switcher snapshot like the sibling seed screens (create_wallet_view, + // settings_seed_view). 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/screens/verify_seed/verify_seed_page_test.dart b/test/screens/verify_seed/verify_seed_page_test.dart index a28aa564b..572dd84d9 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() { }); }); + // Regression for issue #612 S1: 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 { From 9f5e3c013287c77f04784c642f47b9b86049dead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 15:25:14 +0200 Subject: [PATCH 2/4] fix(seed): drop issue ref and sibling-file names from verify-seed comments --- lib/screens/verify_seed/verify_seed_page.dart | 5 ++--- test/screens/verify_seed/verify_seed_page_test.dart | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/screens/verify_seed/verify_seed_page.dart b/lib/screens/verify_seed/verify_seed_page.dart index 113f69dfe..6dfc5ae89 100644 --- a/lib/screens/verify_seed/verify_seed_page.dart +++ b/lib/screens/verify_seed/verify_seed_page.dart @@ -38,9 +38,8 @@ class _VerifySeedViewState extends State { @override void initState() { // Seed words are entered/visible here — block screenshots and the - // app-switcher snapshot like the sibling seed screens (create_wallet_view, - // settings_seed_view). Re-enabled on dispose so other screens stay - // screenshot-able. + // app-switcher snapshot like the other seed screens. Re-enabled on dispose + // so other screens stay screenshot-able. NoScreenshot.instance.screenshotOff(); super.initState(); } diff --git a/test/screens/verify_seed/verify_seed_page_test.dart b/test/screens/verify_seed/verify_seed_page_test.dart index 572dd84d9..abd72e885 100644 --- a/test/screens/verify_seed/verify_seed_page_test.dart +++ b/test/screens/verify_seed/verify_seed_page_test.dart @@ -183,10 +183,10 @@ void main() { }); }); - // Regression for issue #612 S1: 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. + // 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'); From 46ea9a1ea28c397f4cb30b9f12b37676ef73758c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 15:51:19 +0200 Subject: [PATCH 3/4] test(seed): stub no_screenshot channel in verify-seed golden test The golden harness rendered VerifySeedView without mocking the com.flutterplaza.no_screenshot_methods channel, so the screenshotOff() call in initState threw MissingPluginException and the goldens could not even be generated. Stub the channel (as the widget test already does). --- .../verify_seed/verify_seed_golden_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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( From 7afd40efdb8d840e0825bf1549ec84c40cdef419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Mon, 1 Jun 2026 16:02:01 +0200 Subject: [PATCH 4/4] docs(seed): annotate screenshot platform path with @no-integration-test Per CONTRIBUTING.md, platform-specific code paths must carry the // @no-integration-test annotation while no integration_test/ dir exists. --- lib/screens/verify_seed/verify_seed_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/screens/verify_seed/verify_seed_page.dart b/lib/screens/verify_seed/verify_seed_page.dart index 6dfc5ae89..6017923d6 100644 --- a/lib/screens/verify_seed/verify_seed_page.dart +++ b/lib/screens/verify_seed/verify_seed_page.dart @@ -35,6 +35,9 @@ class VerifySeedView extends StatefulWidget { } 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