Skip to content

feat(settings): add Screen Privacy toggle - #2693

Merged
ethicnology merged 6 commits into
developfrom
feat/screenshot-preference
Aug 20, 2026
Merged

feat(settings): add Screen Privacy toggle#2693
ethicnology merged 6 commits into
developfrom
feat/screenshot-preference

Conversation

@wired-pasteque

@wired-pasteque wired-pasteque commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Adds a user-controllable Screen Privacy toggle (in app settings) that turns OS-level screenshot/recording blocking on sensitive screens (recovery phrase, seed views) on or off. Defaults on, so existing installs keep protection until they opt out.

Also, fixed a bug where leaving a protected screen could leave screenshots blocked across the entire app.

Screen_Recording_20260819_170352.mp4

@wired-pasteque wired-pasteque self-assigned this Aug 19, 2026
@claude

This comment was marked as outdated.

@wired-pasteque
wired-pasteque force-pushed the feat/screenshot-preference branch from ed5afc2 to e18fcd8 Compare August 19, 2026 15:26
@wired-pasteque

Copy link
Copy Markdown
Collaborator Author

@claude, check the fixes

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @wired-pasteque's task in 4m 34s —— View job


Review: fixes check out ✅

  • Read repo guidelines (no CLAUDE.md in repo)
  • Diff PR against origin/develop
  • Review screen-capture protection lifecycle (the "stuck blocked" bug fix)
  • Review settings/DB migration changes (v14 → v15)
  • Review tests
  • Post review findings

The "stuck protection" bug fix is correct

The old code in AllSeedViewScreen called enableScreenPrivacy() in build and only disabled it via PopScope.onPopInvokedWithResult, so any non-pop navigation left the whole app blocked. The fix addresses this at both layers:

  • lib/features/all_seed_view/ui/all_seed_view_screen.dart is now a StatefulWidget that acquires in initState and releases in disposedispose fires on any route removal, not just pop.
  • lib/core/utils/screen_capture_protection.dart reference-counts mounted protected screens, which correctly handles overlapping screens (push replacement where the new screen's initState runs before the old screen's dispose no longer flickers or drops protection). _sync() computes the desired state synchronously before the platform call, so the last invocation always wins.
  • The _privacyAcquired guard in lib/core/mixins/privacy_screen.dart makes acquire/release idempotent per screen, so the count can't be corrupted by double calls.
  • All 7 users of PrivacyScreen now follow the acquire-in-initState/late final, release-in-dispose pattern — I checked each one.

The toggle plumbing is also sound: SettingsCubit is app-scoped and init() runs at startup (lib/main.dart:270), so the persisted preference reaches the singleton before any sensitive screen can mount, and the default is fail-safe (true) until then. toggleScreenCaptureProtection applies the change immediately to already-mounted screens. Tests in test/core_test/screen_capture_protection_test.dart cover the ref-counting, opt-out-while-mounted, and opt-back-in paths.

One thing to flag before merge

Dev installs already on schema v15 won't get the new column. On develop, Schema14To15 previously existed (it added only the Tor columns), so any internal/dev device that already upgraded to v15 will never re-run the 14→15 step — screen_capture_protection_enabled will be missing from their settings table and settings reads will fail at runtime. The PR handles the analogous problem for released v14 devices (the idempotency guard + the new test), but not this one. Since v15 is unreleased this may be acceptable ("dev devices reinstall"), but it should be a conscious decision — alternatives are bumping to v16 for the new column, or guarding the settings read. Please confirm this is intended.

Minor notes (non-blocking)

  • toggleScreenCaptureProtection (settings_cubit.dart:223): if storedSettings is somehow still null, the DB and singleton get updated but copyWith on null leaves state unchanged, so the switch would visually snap back to "on" while protection is actually off. Practically unreachable given init() runs at startup, but an early-return or reload would make it airtight.
  • Import ordering in settings_cubit.dart (screen_capture_protection.dart before result.dart) and settings_locator.dart (set_screen_capture... before set_payjoin...) isn't alphabetical — not lint-enforced here (package:lints/recommended doesn't include directives_ordering), so purely cosmetic.
  • The Podfile change disables CocoaPods' static-transitive-dependency check globally rather than per-pod; the in-code comment already documents this trade-off, so just noting it's a known blind spot for future dependencies.

What I verified

Migration consistency (v14 JSON/generated schema no longer carry the Tor columns; v15 carries Tor + screen-capture; both migration tests exercise the released-v14 and dev-device-with-tor-columns paths), the seed row default, all .arb files carry both new keys, the private-named-parameter constructor style matches the repo (Dart 3.12), and the cubit test wiring. I could not run flutter analyze or the test suite — no Flutter toolchain is available in this environment, so please rely on CI for that.

@wired-pasteque
wired-pasteque force-pushed the feat/screenshot-preference branch from e18fcd8 to 87cdfe6 Compare August 19, 2026 16:18
Comment thread lib/core/storage/migrations/schema_14_to_15.dart Outdated
Comment thread lib/core/storage/migrations/schema_14_to_15.dart Outdated
Comment thread packages/screen_privacy/lib/src/screen_capture_protection.dart
Comment thread packages/screen_privacy/lib/src/privacy_screen.dart
context.select is only valid inside build; calling it in
didChangeDependencies threw 'Tried to use context.select outside of the
build method' when opening the show/verify mnemonic screens. Read the
fingerprint with context.read instead — build still watches the bloc, so
state changes continue to drive reloads.
develop moved the Tor columns from the released v14 into the 14->15 step,
but an earlier unreleased develop build had briefly added them in 13->14.
A dev device upgraded by that build already has the columns, so the
unguarded 14->15 adds threw 'duplicate column' on the next launch.

Wrap the tor adds in _addColumnIfNotExists so the migration is idempotent,
and correct the schema-14 doc comments (schema 14 shipped in v6.13.0).
Screenshot/recording blocking on sensitive screens (recovery phrase,
seed views) is now user-controllable from App settings, defaulting on.

- ScreenCaptureProtection: process-wide, reference-counted controller for
  the app-wide FLAG_SECURE flag, gated on the user preference. Fixes the
  leak where a single screen leaving via non-pop navigation left the whole
  app un-screenshottable; AllSeedViewScreen becomes a StatefulWidget with
  proper teardown.
- Persisted as settings.screen_capture_protection_enabled (v14->v15),
  default true so existing installs keep protection until opting out.
- Toggle sits after Logs in App settings; disabling shows a snackbar.
- Localised in all supported languages.
The onion (embedded Tor) plugin depends on IPtProxy, a static
xcframework it force-loads itself. use_frameworks! (dynamic) is required
so the flutter_rust_bridge plugins expose loadable frameworks, but that
made pod install abort on the static transitive dependency, and the link
failed on IPtProxy's Go resolver symbols.

- pre_install: skip the false-positive static-transitive-dependency check.
- post_install: link libresolv into onion/IPtProxy for res_9_ninit/
  nclose/nsearch.
@wired-pasteque
wired-pasteque force-pushed the feat/screenshot-preference branch from eb061c4 to 626e41f Compare August 19, 2026 22:44
@ethicnology
ethicnology merged commit a8d0725 into develop Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants