Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions feedback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions feedback/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class _MyAppState extends State<MyApp> {
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],

localeOverride: const Locale('en'),
mode: FeedbackMode.draw,
pixelRatio: 1,
Expand Down Expand Up @@ -205,6 +206,10 @@ class MyHomePage extends StatelessWidget {
);
},
),
SizedBox(height: 10),
FeedbackRedacted(
child: const Text('This is some sensitive information.'),
),
],
),
),
Expand Down
2 changes: 2 additions & 0 deletions feedback/lib/feedback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 8 additions & 0 deletions feedback/lib/src/feedback_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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 feedback
// widget isn't rebuilt when redaction state changes.
final FeedbackRedactionController redactionController =
FeedbackRedactionController();
}
63 changes: 63 additions & 0 deletions feedback/lib/src/feedback_redacted.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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]
/// 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<FeedbackRedacted> createState() => _FeedbackRedactedState();
}

class _FeedbackRedactedState extends State<FeedbackRedacted> {
FeedbackRedactionController? controller;

@override
void didChangeDependencies() {
super.didChangeDependencies();

// Ensure that the listener is only added once.
controller ??= BetterFeedback.of(context).redactionController
..addListener(onUpdateOfController);
}

@override
void dispose() {
super.dispose();
controller?.removeListener(onUpdateOfController);
}

@override
Widget build(BuildContext context) {
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,
),
child: widget.child,
);
}

return widget.child;
}

void onUpdateOfController() {
setState(() {});
}
}
29 changes: 29 additions & 0 deletions feedback/lib/src/feedback_redaction_controller.dart
Original file line number Diff line number Diff line change
@@ -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();
}
}
14 changes: 14 additions & 0 deletions feedback/lib/src/feedback_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,13 @@ class FeedbackWidgetState extends State<FeedbackWidget>
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();
}

Expand Down Expand Up @@ -239,6 +242,7 @@ class FeedbackWidgetState extends State<FeedbackWidget>
this.mode = mode;
_hideKeyboard(context);
});
_toggleRedaction();
},
onCloseFeedback: () {
_hideKeyboard(context);
Expand Down Expand Up @@ -325,6 +329,14 @@ class FeedbackWidgetState extends State<FeedbackWidget>
);
}

void _toggleRedaction() {
if (mode == FeedbackMode.draw) {
BetterFeedback.of(context).redactionController.redact();
} else {
BetterFeedback.of(context).redactionController.unredact();
}
}

static Future<void> _sendFeedback(
BuildContext context,
OnFeedbackCallback onFeedbackSubmitted,
Expand All @@ -335,6 +347,8 @@ class FeedbackWidgetState extends State<FeedbackWidget>
bool showKeyboard = false,
Map<String, dynamic>? extras,
}) async {
BetterFeedback.of(context).redactionController.redact();

if (!showKeyboard) {
_hideKeyboard(context);
}
Expand Down
15 changes: 15 additions & 0 deletions feedback/test/feedback_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}
38 changes: 38 additions & 0 deletions feedback/test/feedback_redaction_controller_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
16 changes: 15 additions & 1 deletion feedback/test/feedback_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -67,6 +71,7 @@ void main() {
final activeDrawingColor = getActiveColorButton();

expect(userInputFields, findsOneWidget);
expect(redactionBlur, findsOneWidget);
expect(activeDrawingColor.evaluate().length, 4);
});

Expand All @@ -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');
Expand All @@ -97,6 +104,7 @@ void main() {
final activeDrawingColor = getActiveColorButton();

expect(userInputFields, findsOneWidget);
expect(redactionBlur, findsNothing);
expect(activeDrawingColor, findsNothing);
});

Expand All @@ -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'));
Expand All @@ -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,
Expand All @@ -142,13 +153,15 @@ 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'));
await tester.tap(openFeedbackButton);
await tester.pumpAndSettle();

expect(userInputFields, findsOneWidget);
expect(redactionBlur, findsOneWidget);

// add fake step to test reversing
final feedbackWidgetState =
Expand All @@ -167,6 +180,7 @@ void main() {
await tester.pumpAndSettle();

expect(userInputFields, findsNothing);
expect(redactionBlur, findsNothing);
});

testWidgets('feedback callback gets called', (tester) async {
Expand Down
8 changes: 5 additions & 3 deletions feedback/test/test_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ class MyTestPageState extends State<MyTestPage> {
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(
Expand Down