From 9d4bbaf59032dd0a2a7ba7d86b058f75e43726b6 Mon Sep 17 00:00:00 2001 From: JakesMD <71591438+JakesMD@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:04:56 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Add=20redaction=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/example/lib/main.dart | 5 ++ feedback/lib/feedback.dart | 2 + feedback/lib/src/feedback_controller.dart | 8 +++ feedback/lib/src/feedback_redacted.dart | 67 +++++++++++++++++++ .../src/feedback_redaction_controller.dart | 29 ++++++++ feedback/lib/src/feedback_widget.dart | 14 ++++ 6 files changed, 125 insertions(+) create mode 100644 feedback/lib/src/feedback_redacted.dart create mode 100644 feedback/lib/src/feedback_redaction_controller.dart diff --git a/feedback/example/lib/main.dart b/feedback/example/lib/main.dart index 20cc49b4..61af1ed2 100644 --- a/feedback/example/lib/main.dart +++ b/feedback/example/lib/main.dart @@ -58,6 +58,7 @@ class _MyAppState extends State { GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], + localeOverride: const Locale('en'), mode: FeedbackMode.draw, pixelRatio: 1, @@ -205,6 +206,10 @@ class MyHomePage extends StatelessWidget { ); }, ), + SizedBox(height: 10), + FeedbackRedacted( + child: const Text('This is some sensitive information.'), + ), ], ), ), diff --git a/feedback/lib/feedback.dart b/feedback/lib/feedback.dart index 121146be..1d8c4e93 100644 --- a/feedback/lib/feedback.dart +++ b/feedback/lib/feedback.dart @@ -5,6 +5,8 @@ library feedback; export 'src/better_feedback.dart'; export 'src/feedback_controller.dart'; export 'src/feedback_mode.dart'; +export 'src/feedback_redacted.dart'; +export 'src/feedback_redaction_controller.dart'; export 'src/l18n/translation.dart'; export 'src/theme/feedback_theme.dart' show FeedbackThemeData; export 'src/user_feedback.dart'; diff --git a/feedback/lib/src/feedback_controller.dart b/feedback/lib/src/feedback_controller.dart index f8f68f9b..69edb25a 100644 --- a/feedback/lib/src/feedback_controller.dart +++ b/feedback/lib/src/feedback_controller.dart @@ -25,6 +25,7 @@ class FeedbackController extends ChangeNotifier { /// Typically, this does not need to be called by the user of this library void hide() { _isVisible = false; + redactionController.unredact(); notifyListeners(); } @@ -34,4 +35,11 @@ class FeedbackController extends ChangeNotifier { /// true and feedback is currently displayed. final DraggableScrollableController sheetController = DraggableScrollableController(); + + /// Controller for managing redaction of sensitive content. + /// + // Having a seperate controller for redaction ensures that the entire widget + // tree isn't rebuilt when the redaction state changes. + final FeedbackRedactionController redactionController = + FeedbackRedactionController(); } diff --git a/feedback/lib/src/feedback_redacted.dart b/feedback/lib/src/feedback_redacted.dart new file mode 100644 index 00000000..2a755c19 --- /dev/null +++ b/feedback/lib/src/feedback_redacted.dart @@ -0,0 +1,67 @@ +import 'dart:ui'; + +import 'package:feedback/src/better_feedback.dart'; +import 'package:flutter/material.dart'; + +/// A widget that applies a blur effect to its child when the [BetterFeedback] +/// draw mode is active or the screenshot is being taken. This is useful for +/// redacting sensitive information from the screenshot. +class FeedbackRedacted extends StatefulWidget { + /// Creates a [FeedbackRedacted] widget. + const FeedbackRedacted({required this.child, this.blurAmount = 5, super.key}); + + /// The child widget to which the blur effect will be applied. + final Widget child; + + /// The amount of blur to apply when redaction mode is enabled. + final double blurAmount; + + @override + State createState() => _FeedbackRedactedState(); +} + +class _FeedbackRedactedState extends State { + bool isListenerAdded = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // Ensure that the listener is only added once. + if (!isListenerAdded) { + BetterFeedback.of(context) + .redactionController + .addListener(onUpdateOfController); + isListenerAdded = true; + } + } + + @override + void dispose() { + super.dispose(); + BetterFeedback.of(context) + .redactionController + .removeListener(onUpdateOfController); + } + + @override + Widget build(BuildContext context) { + if (BetterFeedback.of(context).redactionController.isRedacted) { + // Wrapping the child with a ImageFiltered instead of just changing the + // sigma values to 0 makes testing easier. + return ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: widget.blurAmount, + sigmaY: widget.blurAmount, + ), + child: widget.child, + ); + } + + return widget.child; + } + + void onUpdateOfController() { + setState(() {}); + } +} diff --git a/feedback/lib/src/feedback_redaction_controller.dart b/feedback/lib/src/feedback_redaction_controller.dart new file mode 100644 index 00000000..7f86dba2 --- /dev/null +++ b/feedback/lib/src/feedback_redaction_controller.dart @@ -0,0 +1,29 @@ +import 'package:feedback/feedback.dart'; +import 'package:flutter/material.dart'; + +/// Controls the state of the feedback redaction. +class FeedbackRedactionController extends ChangeNotifier { + bool _isRedacted = false; + + /// Whether sensitive content (wrapped in [FeedbackRedacted]) is currently + /// redacted. + bool get isRedacted => _isRedacted; + + /// Redacts all sensitive content (wrapped in [FeedbackRedacted]). + /// After draw mode is enabled or the screenshot is being taken. + /// Typically, this does not need to be called by the user of this library. + void redact() { + if (_isRedacted) return; + _isRedacted = true; + notifyListeners(); + } + + /// Unredacts all sensitive content (wrapped in [FeedbackRedacted]). + /// After navigation mode is enabled or the screenshot has been taken. + /// Typically, this does not need to be called by the user of this library. + void unredact() { + if (!_isRedacted) return; + _isRedacted = false; + notifyListeners(); + } +} diff --git a/feedback/lib/src/feedback_widget.dart b/feedback/lib/src/feedback_widget.dart index 5a9a2715..b99cb61a 100644 --- a/feedback/lib/src/feedback_widget.dart +++ b/feedback/lib/src/feedback_widget.dart @@ -110,10 +110,13 @@ class FeedbackWidgetState extends State super.didUpdateWidget(oldWidget); // update feedback mode with the initial value mode = widget.mode; + if (oldWidget.mode != widget.mode) _toggleRedaction(); + if (oldWidget.isFeedbackVisible != widget.isFeedbackVisible && oldWidget.isFeedbackVisible == false) { // Feedback is now visible, // start animation to show it. + _toggleRedaction(); _controller.forward(); } @@ -239,6 +242,7 @@ class FeedbackWidgetState extends State this.mode = mode; _hideKeyboard(context); }); + _toggleRedaction(); }, onCloseFeedback: () { _hideKeyboard(context); @@ -325,6 +329,14 @@ class FeedbackWidgetState extends State ); } + void _toggleRedaction() { + if (mode == FeedbackMode.draw) { + BetterFeedback.of(context).redactionController.redact(); + } else { + BetterFeedback.of(context).redactionController.unredact(); + } + } + static Future _sendFeedback( BuildContext context, OnFeedbackCallback onFeedbackSubmitted, @@ -335,6 +347,8 @@ class FeedbackWidgetState extends State bool showKeyboard = false, Map? extras, }) async { + BetterFeedback.of(context).redactionController.redact(); + if (!showKeyboard) { _hideKeyboard(context); } From 0812a44d7bb33f33caa82062a431aff750a6f1ad Mon Sep 17 00:00:00 2001 From: JakesMD <71591438+JakesMD@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:46:13 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A7=AA=20Add=20missing=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/lib/src/feedback_redacted.dart | 22 +++++------ feedback/test/feedback_controller_test.dart | 15 ++++++++ .../feedback_redaction_controller_test.dart | 38 +++++++++++++++++++ feedback/test/feedback_test.dart | 16 +++++++- feedback/test/test_app.dart | 8 ++-- 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 feedback/test/feedback_redaction_controller_test.dart diff --git a/feedback/lib/src/feedback_redacted.dart b/feedback/lib/src/feedback_redacted.dart index 2a755c19..4973f219 100644 --- a/feedback/lib/src/feedback_redacted.dart +++ b/feedback/lib/src/feedback_redacted.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:feedback/src/better_feedback.dart'; +import 'package:feedback/src/feedback_redaction_controller.dart'; import 'package:flutter/material.dart'; /// A widget that applies a blur effect to its child when the [BetterFeedback] @@ -21,35 +22,30 @@ class FeedbackRedacted extends StatefulWidget { } class _FeedbackRedactedState extends State { - bool isListenerAdded = false; + FeedbackRedactionController? controller; @override void didChangeDependencies() { super.didChangeDependencies(); // Ensure that the listener is only added once. - if (!isListenerAdded) { - BetterFeedback.of(context) - .redactionController - .addListener(onUpdateOfController); - isListenerAdded = true; - } + controller ??= BetterFeedback.of(context).redactionController + ..addListener(onUpdateOfController); } @override void dispose() { super.dispose(); - BetterFeedback.of(context) - .redactionController - .removeListener(onUpdateOfController); + controller?.removeListener(onUpdateOfController); } @override Widget build(BuildContext context) { - if (BetterFeedback.of(context).redactionController.isRedacted) { - // Wrapping the child with a ImageFiltered instead of just changing the - // sigma values to 0 makes testing easier. + if (controller?.isRedacted == true) { + // Re-wrapping the child instead of just changing the sigma values makes + // testing easier. return ImageFiltered( + key: const Key('redaction_blur'), imageFilter: ImageFilter.blur( sigmaX: widget.blurAmount, sigmaY: widget.blurAmount, diff --git a/feedback/test/feedback_controller_test.dart b/feedback/test/feedback_controller_test.dart index d8a3f659..62c341a0 100644 --- a/feedback/test/feedback_controller_test.dart +++ b/feedback/test/feedback_controller_test.dart @@ -32,5 +32,20 @@ void main() { expect(controller.isVisible, false); expect(listenerWasCalled, true); }); + + test(' change redaction from redacted to unredacted when hidden', () { + final controller = FeedbackController(); + controller.show((_) {}); + controller.redactionController.redact(); + + var listenerWasCalled = false; + controller.redactionController.addListener(() { + listenerWasCalled = true; + }); + + controller.hide(); + expect(controller.redactionController.isRedacted, false); + expect(listenerWasCalled, true); + }); }); } diff --git a/feedback/test/feedback_redaction_controller_test.dart b/feedback/test/feedback_redaction_controller_test.dart new file mode 100644 index 00000000..fa1afb9b --- /dev/null +++ b/feedback/test/feedback_redaction_controller_test.dart @@ -0,0 +1,38 @@ +import 'package:feedback/src/feedback_redaction_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FeedbackRedactionController', () { + test(' default is unredacted', () { + final controller = FeedbackRedactionController(); + expect(controller.isRedacted, false); + }); + + test(' change redaction from unredacted to redacted', () { + final controller = FeedbackRedactionController(); + + var listenerWasCalled = false; + controller.addListener(() { + listenerWasCalled = true; + }); + + controller.redact(); + expect(controller.isRedacted, true); + expect(listenerWasCalled, true); + }); + + test(' change redaction from redacted to unredacted', () { + final controller = FeedbackRedactionController(); + controller.redact(); + + var listenerWasCalled = false; + controller.addListener(() { + listenerWasCalled = true; + }); + + controller.unredact(); + expect(controller.isRedacted, false); + expect(listenerWasCalled, true); + }); + }); +} diff --git a/feedback/test/feedback_test.dart b/feedback/test/feedback_test.dart index d55e950a..a165f51d 100644 --- a/feedback/test/feedback_test.dart +++ b/feedback/test/feedback_test.dart @@ -25,8 +25,10 @@ void main() { // feedback is closed var userInputFields = find.byKey(const Key('feedback_bottom_sheet')); + final redactionBlur = find.byKey(const Key('redaction_blur')); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); // open feedback final openFeedbackButton = find.text('open feedback'); @@ -55,8 +57,10 @@ void main() { // feedback is closed var userInputFields = find.byKey(const Key('feedback_bottom_sheet')); + final redactionBlur = find.byKey(const Key('redaction_blur')); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); // open feedback final openFeedbackButton = find.text('open feedback'); @@ -67,6 +71,7 @@ void main() { final activeDrawingColor = getActiveColorButton(); expect(userInputFields, findsOneWidget); + expect(redactionBlur, findsOneWidget); expect(activeDrawingColor.evaluate().length, 4); }); @@ -85,8 +90,10 @@ void main() { // feedback is closed var userInputFields = find.byKey(const Key('feedback_bottom_sheet')); + final redactionBlur = find.byKey(const Key('redaction_blur')); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); // open feedback final openFeedbackButton = find.text('open feedback'); @@ -97,6 +104,7 @@ void main() { final activeDrawingColor = getActiveColorButton(); expect(userInputFields, findsOneWidget); + expect(redactionBlur, findsNothing); expect(activeDrawingColor, findsNothing); }); @@ -110,8 +118,10 @@ void main() { // feedback is closed final userInputFields = find.byKey(const Key('feedback_bottom_sheet')); + final redactionBlur = find.byKey(const Key('redaction_blur')); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); // open feedback final openFeedbackButton = find.byKey(const Key('open_feedback')); @@ -127,10 +137,11 @@ void main() { await tester.pumpAndSettle(); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); }); testWidgets( - 'back button in drawing mode reverses drawings and ' + 'back button in drawing mode reverses drawings and redaction and ' 'then leaves the feedback interface', (tester) async { const widget = BetterFeedback( mode: FeedbackMode.draw, @@ -142,6 +153,7 @@ void main() { // feedback is closed final userInputFields = find.byKey(const Key('feedback_bottom_sheet')); + final redactionBlur = find.byKey(const Key('redaction_blur')); // open feedback final openFeedbackButton = find.byKey(const Key('open_feedback')); @@ -149,6 +161,7 @@ void main() { await tester.pumpAndSettle(); expect(userInputFields, findsOneWidget); + expect(redactionBlur, findsOneWidget); // add fake step to test reversing final feedbackWidgetState = @@ -167,6 +180,7 @@ void main() { await tester.pumpAndSettle(); expect(userInputFields, findsNothing); + expect(redactionBlur, findsNothing); }); testWidgets('feedback callback gets called', (tester) async { diff --git a/feedback/test/test_app.dart b/feedback/test/test_app.dart index 68b32f33..7d6b6d9d 100644 --- a/feedback/test/test_app.dart +++ b/feedback/test/test_app.dart @@ -66,9 +66,11 @@ class MyTestPageState extends State { const Text( 'You have pushed the button this many times:', ), - Text( - '$counter', - style: Theme.of(context).textTheme.headlineMedium, + FeedbackRedacted( + child: Text( + '$counter', + style: Theme.of(context).textTheme.headlineMedium, + ), ), const TextField(), TextButton( From 25d274c37b29205d3f26d9c22678c00d646be343 Mon Sep 17 00:00:00 2001 From: JakesMD <71591438+JakesMD@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:59:10 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=93=9D=20Add=20"Hiding=20sensitive=20?= =?UTF-8?q?content"=20to=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/feedback/README.md b/feedback/README.md index a6ae3af8..6b915014 100644 --- a/feedback/README.md +++ b/feedback/README.md @@ -226,6 +226,17 @@ void main() { } ``` +## Hiding sensitive content in the screenshot + +Any widget that contains sensitive content can be wrapped with a `FeedbackRedacted` widget. This will ensure that the content is blurred in the screenshot. + +```dart +FeedbackRedacted( + blurAmount: 10 // Optional, defaults to 5. + child: Text('sensitive information') +); +``` + ## 💡 Tips, tricks and usage scenarios - You can combine this with [device_info_plus](https://pub.dev/packages/device_info_plus) From 481c4852e00b2569f8ee7ce5df765f94416e257c Mon Sep 17 00:00:00 2001 From: JakesMD <71591438+JakesMD@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:50:38 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20Fix=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/lib/src/feedback_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feedback/lib/src/feedback_controller.dart b/feedback/lib/src/feedback_controller.dart index 69edb25a..d8823c2b 100644 --- a/feedback/lib/src/feedback_controller.dart +++ b/feedback/lib/src/feedback_controller.dart @@ -38,8 +38,8 @@ class FeedbackController extends ChangeNotifier { /// Controller for managing redaction of sensitive content. /// - // Having a seperate controller for redaction ensures that the entire widget - // tree isn't rebuilt when the redaction state changes. + // Having a seperate controller for redaction ensures that the entire feedback + // widget isn't rebuilt when redaction state changes. final FeedbackRedactionController redactionController = FeedbackRedactionController(); } From b6657b8bcaa48af2030d273a78997e0dbb5e6a4c Mon Sep 17 00:00:00 2001 From: JakesMD <71591438+JakesMD@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:51:11 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feedback/README.md b/feedback/README.md index 6b915014..7e7ac3dc 100644 --- a/feedback/README.md +++ b/feedback/README.md @@ -232,8 +232,8 @@ Any widget that contains sensitive content can be wrapped with a `FeedbackRedact ```dart FeedbackRedacted( - blurAmount: 10 // Optional, defaults to 5. - child: Text('sensitive information') + blurAmount: 10, // Optional, defaults to 5. + child: Text('sensitive information'), ); ```