From a6ec3e1cd88657b5b9b7b376067c8f0beb30b695 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 17:27:42 +0200 Subject: [PATCH 01/18] move from upserfeedback to feedback --- ...dback_dialog.dart => feedback_widget.dart} | 37 +++++++++--------- flutter/example/lib/main.dart | 38 +++---------------- 2 files changed, 23 insertions(+), 52 deletions(-) rename flutter/example/lib/{user_feedback_dialog.dart => feedback_widget.dart} (94%) diff --git a/flutter/example/lib/user_feedback_dialog.dart b/flutter/example/lib/feedback_widget.dart similarity index 94% rename from flutter/example/lib/user_feedback_dialog.dart rename to flutter/example/lib/feedback_widget.dart index a219db6376..774d43ef8a 100644 --- a/flutter/example/lib/user_feedback_dialog.dart +++ b/flutter/example/lib/feedback_widget.dart @@ -3,24 +3,24 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class UserFeedbackDialog extends StatefulWidget { - const UserFeedbackDialog({ +class FeedbackWidget extends StatefulWidget { + const FeedbackWidget({ super.key, - required this.eventId, + required this.associatedEventId, this.hub, - }) : assert(eventId != const SentryId.empty()); + }) : assert(associatedEventId != const SentryId.empty()); - final SentryId eventId; + final SentryId? associatedEventId; final Hub? hub; @override - _UserFeedbackDialogState createState() => _UserFeedbackDialogState(); + _FeedbackWidgetState createState() => _FeedbackWidgetState(); } -class _UserFeedbackDialogState extends State { +class _FeedbackWidgetState extends State { TextEditingController nameController = TextEditingController(); TextEditingController emailController = TextEditingController(); - TextEditingController commentController = TextEditingController(); + TextEditingController messageController = TextEditingController(); @override Widget build(BuildContext context) { @@ -66,14 +66,14 @@ class _UserFeedbackDialogState extends State { ), const SizedBox(height: 8), TextField( - key: const ValueKey('sentry_comment_textfield'), + key: const ValueKey('sentry_message_textfield'), minLines: 5, maxLines: null, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'What happened?', ), - controller: commentController, + controller: messageController, keyboardType: TextInputType.multiline, ), const SizedBox(height: 8), @@ -85,14 +85,14 @@ class _UserFeedbackDialogState extends State { ElevatedButton( key: const ValueKey('sentry_submit_feedback_button'), onPressed: () async { - // ignore: deprecated_member_use - final feedback = SentryUserFeedback( - eventId: widget.eventId, - comments: commentController.text, - email: emailController.text, + + final feedback = SentryFeedback( + message: messageController.text, + contactEmail: emailController.text, name: nameController.text, + associatedEventId: widget.associatedEventId, ); - await _submitUserFeedback(feedback); + await _captureFeedback(feedback); // ignore: use_build_context_synchronously Navigator.pop(context); }, @@ -108,10 +108,9 @@ class _UserFeedbackDialogState extends State { ); } - // ignore: deprecated_member_use - Future _submitUserFeedback(SentryUserFeedback feedback) { + Future _captureFeedback(SentryFeedback feedback) { // ignore: deprecated_member_use - return (widget.hub ?? HubAdapter()).captureUserFeedback(feedback); + return (widget.hub ?? HubAdapter()).captureFeedback(feedback); } } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 9d85a89b9a..d126ac3ef3 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -28,7 +28,7 @@ import 'auto_close_screen.dart'; import 'drift/connection/connection.dart'; import 'drift/database.dart'; import 'isar/user.dart'; -import 'user_feedback_dialog.dart'; +import 'feedback_widget.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String exampleDsn = @@ -82,7 +82,7 @@ Future setupSentry( // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - options.spotlight = Spotlight(enabled: true); + // options.spotlight = Spotlight(enabled: true); options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; @@ -450,7 +450,7 @@ class MainScaffold extends StatelessWidget { Sentry.captureMessage( 'This message has an attachment', withScope: (scope) { - const txt = 'Lorem Ipsum dolar sit amet'; + const txt = 'Lorem Ipsum dolor sit amet'; scope.addAttachment( SentryAttachment.fromIntList( utf8.encode(txt), @@ -501,40 +501,12 @@ class MainScaffold extends StatelessWidget { await showDialog( context: context, builder: (context) { - return UserFeedbackDialog(eventId: id); + return FeedbackWidget(associatedEventId: id); }, ); }, text: - 'Shows a custom user feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', - buttonTitle: 'Capture User Feedback', - ), - TooltipButton( - onPressed: () async { - await showDialog( - context: context, - builder: (context) { - return UserFeedbackDialog(eventId: SentryId.newId()); - }, - ); - }, - text: '', - buttonTitle: 'Show UserFeedback Dialog without event', - ), - TooltipButton( - onPressed: () async { - final associatedEventId = - await Sentry.captureMessage('Associated Event'); - await Sentry.captureFeedback( - SentryFeedback( - message: 'message', - contactEmail: 'john.appleseed@apple.com', - name: 'John Appleseed', - associatedEventId: associatedEventId, - ), - ); - }, - text: '', + 'Shows a custom feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', buttonTitle: 'Capture Feedback', ), TooltipButton( From e8813a20335ecd1db44933afb6bca9c52eb97490 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 19 Aug 2024 17:54:01 +0200 Subject: [PATCH 02/18] add basic validation --- flutter/example/lib/feedback_widget.dart | 72 +++++++++++++++++++----- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/flutter/example/lib/feedback_widget.dart b/flutter/example/lib/feedback_widget.dart index 774d43ef8a..28c49441a7 100644 --- a/flutter/example/lib/feedback_widget.dart +++ b/flutter/example/lib/feedback_widget.dart @@ -18,9 +18,13 @@ class FeedbackWidget extends StatefulWidget { } class _FeedbackWidgetState extends State { - TextEditingController nameController = TextEditingController(); - TextEditingController emailController = TextEditingController(); - TextEditingController messageController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _messageController = TextEditingController(); + + String? _name; + String? _email; + String? _message; @override Widget build(BuildContext context) { @@ -47,34 +51,40 @@ class _FeedbackWidgetState extends State { const Divider(height: 24), TextField( key: const ValueKey('sentry_name_textfield'), - decoration: const InputDecoration( - border: OutlineInputBorder(), + decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'Name', + errorText: _errorText(_name), ), - controller: nameController, + controller: _nameController, keyboardType: TextInputType.text, + onChanged: (text) => setState(() => _name = text ), ), const SizedBox(height: 8), TextField( key: const ValueKey('sentry_email_textfield'), - decoration: const InputDecoration( - border: OutlineInputBorder(), + decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'E-Mail', + errorText: _errorText(_email), ), - controller: emailController, + controller: _emailController, keyboardType: TextInputType.emailAddress, + onChanged: (text) => setState(() => _email = text ), ), const SizedBox(height: 8), TextField( key: const ValueKey('sentry_message_textfield'), minLines: 5, maxLines: null, - decoration: const InputDecoration( - border: OutlineInputBorder(), + decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'What happened?', + errorText: _errorText(_message), ), - controller: messageController, + controller: _messageController, keyboardType: TextInputType.multiline, + onChanged: (text) => setState(() => _message = text ), ), const SizedBox(height: 8), const _PoweredBySentryMessage(), @@ -86,10 +96,22 @@ class _FeedbackWidgetState extends State { key: const ValueKey('sentry_submit_feedback_button'), onPressed: () async { + if (_name == null || _email == null || _message == null) { + setState(() { + _name ??= ''; + _email ??= ''; + _message ??= ''; + }); + } + + if (!_valid(_name) || !_valid(_email) || !_valid(_message)) { + return; + } + final feedback = SentryFeedback( - message: messageController.text, - contactEmail: emailController.text, - name: nameController.text, + message: _messageController.text, + contactEmail: _emailController.text, + name: _nameController.text, associatedEventId: widget.associatedEventId, ); await _captureFeedback(feedback); @@ -108,10 +130,30 @@ class _FeedbackWidgetState extends State { ); } + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _messageController.dispose(); + super.dispose(); + } + Future _captureFeedback(SentryFeedback feedback) { // ignore: deprecated_member_use return (widget.hub ?? HubAdapter()).captureFeedback(feedback); } + + String? _errorText(String? text) { + if (text != null && text.isEmpty) { + return 'Can\'t be empty'; + } else { + return null; + } + } + + bool _valid(String? text) { + return text != null && text.isNotEmpty; + } } class _PoweredBySentryMessage extends StatelessWidget { From f3eecbb9d445f4325deb32a3fa85d1789b5145eb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 11:35:58 +0200 Subject: [PATCH 03/18] change to fullscreen widget, add new text --- flutter/example/lib/feedback_widget.dart | 617 +++++++---------------- flutter/example/lib/main.dart | 13 +- 2 files changed, 186 insertions(+), 444 deletions(-) diff --git a/flutter/example/lib/feedback_widget.dart b/flutter/example/lib/feedback_widget.dart index 28c49441a7..0456087d99 100644 --- a/flutter/example/lib/feedback_widget.dart +++ b/flutter/example/lib/feedback_widget.dart @@ -8,16 +8,46 @@ class FeedbackWidget extends StatefulWidget { super.key, required this.associatedEventId, this.hub, + this.title = 'Report a Bug', + this.nameLabel = 'Name', + this.namePlaceholder = 'Your Name', + this.emailLabel = 'Email', + this.emailPlaceholder = 'your.email@example.org', + this.messageLabel = 'Description', + this.messagePlaceholder = 'What\'s the bug? What did you expect?', + this.submitButtonLabel = 'Send Bug Report', + this.cancelButtonLabel = 'Cancel', + this.isRequiredLabel = '(required)', + this.isNameRequired = false, + this.isEmailRequired = false, }) : assert(associatedEventId != const SentryId.empty()); final SentryId? associatedEventId; final Hub? hub; + final String title; + + final String nameLabel; + final String namePlaceholder; + final String emailLabel; + final String emailPlaceholder; + final String messageLabel; + final String messagePlaceholder; + + final String submitButtonLabel; + final String cancelButtonLabel; + + final String isRequiredLabel; + + final bool isNameRequired; + final bool isEmailRequired; + @override _FeedbackWidgetState createState() => _FeedbackWidgetState(); } class _FeedbackWidgetState extends State { + final TextEditingController _nameController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _messageController = TextEditingController(); @@ -28,105 +58,162 @@ class _FeedbackWidgetState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( + return Scaffold( + appBar: AppBar( + title: const Text('Report a Bug'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisSize: MainAxisSize.min, children: [ - Text( - "It looks like we're having some internal issues.", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 4), - Text( - 'Our team has been notified. ' - "If you'd like to help, tell us what happened below.", - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: Colors.grey), - ), - const Divider(height: 24), - TextField( - key: const ValueKey('sentry_name_textfield'), - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: 'Name', - errorText: _errorText(_name), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_name_label'), + widget.nameLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + if (widget.isNameRequired) + Text( + key: const ValueKey('sentry_feedback_name_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextField( + key: const ValueKey('sentry_feedback_name_textfield'), + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.namePlaceholder, + errorText: _errorText(_name), + ), + controller: _nameController, + keyboardType: TextInputType.text, + onChanged: (text) => setState(() => _name = text ), + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_email_label'), + widget.emailLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + if (widget.isEmailRequired) + Text( + key: const ValueKey('sentry_feedback_email_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextField( + key: const ValueKey('sentry_feedback_email_textfield'), + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.emailPlaceholder, + errorText: _errorText(_email), + ), + controller: _emailController, + keyboardType: TextInputType.emailAddress, + onChanged: (text) => setState(() => _email = text ), + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_message_label'), + widget.messageLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + Text( + key: const ValueKey('sentry_feedback_message_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextField( + key: const ValueKey('sentry_feedback_message_textfield'), + style: Theme.of(context).textTheme.bodyLarge, + minLines: 5, + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.messagePlaceholder, + errorText: _errorText(_message), + ), + controller: _messageController, + keyboardType: TextInputType.multiline, + onChanged: (text) => setState(() => _message = text ), + ), + ], + ), ), - controller: _nameController, - keyboardType: TextInputType.text, - onChanged: (text) => setState(() => _name = text ), ), - const SizedBox(height: 8), - TextField( - key: const ValueKey('sentry_email_textfield'), - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: 'E-Mail', - errorText: _errorText(_email), - ), - controller: _emailController, - keyboardType: TextInputType.emailAddress, - onChanged: (text) => setState(() => _email = text ), + Column( + children: [ + SizedBox( + width: double.infinity, + child: ElevatedButton( + key: const ValueKey('sentry_feedback_submit_button'), + onPressed: () async { + + if (_name == null || _email == null || _message == null) { + setState(() { + _name ??= ''; + _email ??= ''; + _message ??= ''; + }); + } + + if (!_valid(_name) || !_valid(_email) || !_valid(_message)) { + return; + } + + final feedback = SentryFeedback( + message: _messageController.text, + contactEmail: _emailController.text, + name: _nameController.text, + associatedEventId: widget.associatedEventId, + ); + await _captureFeedback(feedback); + // ignore: use_build_context_synchronously + Navigator.pop(context); + }, + child: Text(widget.submitButtonLabel), + ), + ), + SizedBox( + width: double.infinity, + child: TextButton( + key: const ValueKey('sentry_feedback_close_button'), + onPressed: () { + Navigator.pop(context); + }, + child: Text(widget.cancelButtonLabel), + ), + ), + ], ), - const SizedBox(height: 8), - TextField( - key: const ValueKey('sentry_message_textfield'), - minLines: 5, - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: 'What happened?', - errorText: _errorText(_message), - ), - controller: _messageController, - keyboardType: TextInputType.multiline, - onChanged: (text) => setState(() => _message = text ), - ), - const SizedBox(height: 8), - const _PoweredBySentryMessage(), ], ), ), - actions: [ - ElevatedButton( - key: const ValueKey('sentry_submit_feedback_button'), - onPressed: () async { - - if (_name == null || _email == null || _message == null) { - setState(() { - _name ??= ''; - _email ??= ''; - _message ??= ''; - }); - } - - if (!_valid(_name) || !_valid(_email) || !_valid(_message)) { - return; - } - - final feedback = SentryFeedback( - message: _messageController.text, - contactEmail: _emailController.text, - name: _nameController.text, - associatedEventId: widget.associatedEventId, - ); - await _captureFeedback(feedback); - // ignore: use_build_context_synchronously - Navigator.pop(context); - }, - child: const Text('Submit Crash Report')), - TextButton( - key: const ValueKey('sentry_close_button'), - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Close'), - ) - ], ); } @@ -155,351 +242,3 @@ class _FeedbackWidgetState extends State { return text != null && text.isNotEmpty; } } - -class _PoweredBySentryMessage extends StatelessWidget { - const _PoweredBySentryMessage(); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Crash reports powered by'), - const SizedBox(width: 8), - SizedBox( - height: 30, - child: _SentryLogo(), - ), - ], - ), - ); - } -} - -class _SentryLogo extends StatelessWidget { - @override - Widget build(BuildContext context) { - var color = Colors.white; - final brightenss = Theme.of(context).brightness; - if (brightenss == Brightness.light) { - color = const Color(0xff362d59).withOpacity(1.0); - } - - return FittedBox( - fit: BoxFit.contain, - child: CustomPaint( - size: const Size(222, 66), - painter: _SentryLogoCustomPainter(color), - ), - ); - } -} - -/// Created with https://fluttershapemaker.com/ -/// Sentry Logo comes from https://sentry.io/branding/ -class _SentryLogoCustomPainter extends CustomPainter { - final Color color; - - _SentryLogoCustomPainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final path_0 = Path(); - path_0.moveTo(size.width * 0.1306306, size.height * 0.03424242); - path_0.arcToPoint(Offset(size.width * 0.09459459, size.height * 0.03424242), - radius: Radius.elliptical( - size.width * 0.02103604, size.height * 0.07075758), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.06495495, size.height * 0.2050000); - path_0.arcToPoint(Offset(size.width * 0.1449099, size.height * 0.6089394), - radius: - Radius.elliptical(size.width * 0.1450901, size.height * 0.4880303), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.1240991, size.height * 0.6089394); - path_0.arcToPoint(Offset(size.width * 0.05445946, size.height * 0.2646970), - radius: - Radius.elliptical(size.width * 0.1246847, size.height * 0.4193939), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.02702703, size.height * 0.4242424); - path_0.arcToPoint(Offset(size.width * 0.06860360, size.height * 0.6086364), - radius: - Radius.elliptical(size.width * 0.07171171, size.height * 0.2412121), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.02081081, size.height * 0.6086364); - path_0.arcToPoint(Offset(size.width * 0.01801802, size.height * 0.5918182), - radius: Radius.elliptical( - size.width * 0.003423423, size.height * 0.01151515), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.03126126, size.height * 0.5160606); - path_0.arcToPoint(Offset(size.width * 0.01612613, size.height * 0.4872727), - radius: - Radius.elliptical(size.width * 0.04837838, size.height * 0.1627273), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.003018018, size.height * 0.5630303); - path_0.arcToPoint(Offset(size.width * 0.01063063, size.height * 0.6575758), - radius: Radius.elliptical( - size.width * 0.02045045, size.height * 0.06878788), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.arcToPoint(Offset(size.width * 0.02081081, size.height * 0.6666667), - radius: Radius.elliptical( - size.width * 0.02099099, size.height * 0.07060606), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.08626126, size.height * 0.6666667); - path_0.arcToPoint(Offset(size.width * 0.05022523, size.height * 0.4043939), - radius: - Radius.elliptical(size.width * 0.08738739, size.height * 0.2939394), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.06063063, size.height * 0.3437879); - path_0.arcToPoint(Offset(size.width * 0.1070270, size.height * 0.6666667), - radius: - Radius.elliptical(size.width * 0.1075225, size.height * 0.3616667), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.1624775, size.height * 0.6666667); - path_0.arcToPoint(Offset(size.width * 0.08855856, size.height * 0.1848485), - radius: - Radius.elliptical(size.width * 0.1616216, size.height * 0.5436364), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.lineTo(size.width * 0.1095946, size.height * 0.06363636); - path_0.arcToPoint(Offset(size.width * 0.1143243, size.height * 0.05954545), - radius: Radius.elliptical( - size.width * 0.003468468, size.height * 0.01166667), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.cubicTo( - size.width * 0.1167117, - size.height * 0.06393939, - size.width * 0.2057207, - size.height * 0.5863636, - size.width * 0.2073874, - size.height * 0.5924242); - path_0.arcToPoint(Offset(size.width * 0.2043243, size.height * 0.6095455), - radius: Radius.elliptical( - size.width * 0.003423423, size.height * 0.01151515), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.1828829, size.height * 0.6095455); - path_0.quadraticBezierTo(size.width * 0.1832883, size.height * 0.6384848, - size.width * 0.1828829, size.height * 0.6672727); - path_0.lineTo(size.width * 0.2044144, size.height * 0.6672727); - path_0.arcToPoint(Offset(size.width * 0.2252252, size.height * 0.5974242), - radius: Radius.elliptical( - size.width * 0.02067568, size.height * 0.06954545), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.arcToPoint(Offset(size.width * 0.2224324, size.height * 0.5628788), - radius: Radius.elliptical( - size.width * 0.02022523, size.height * 0.06803030), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.close(); - path_0.moveTo(size.width * 0.5600000, size.height * 0.4284848); - path_0.lineTo(size.width * 0.4935135, size.height * 0.1396970); - path_0.lineTo(size.width * 0.4769369, size.height * 0.1396970); - path_0.lineTo(size.width * 0.4769369, size.height * 0.5268182); - path_0.lineTo(size.width * 0.4937387, size.height * 0.5268182); - path_0.lineTo(size.width * 0.4937387, size.height * 0.2301515); - path_0.lineTo(size.width * 0.5621171, size.height * 0.5268182); - path_0.lineTo(size.width * 0.5768018, size.height * 0.5268182); - path_0.lineTo(size.width * 0.5768018, size.height * 0.1396970); - path_0.lineTo(size.width * 0.5600000, size.height * 0.1396970); - path_0.close(); - path_0.moveTo(size.width * 0.3925676, size.height * 0.3566667); - path_0.lineTo(size.width * 0.4521622, size.height * 0.3566667); - path_0.lineTo(size.width * 0.4521622, size.height * 0.3063636); - path_0.lineTo(size.width * 0.3925225, size.height * 0.3063636); - path_0.lineTo(size.width * 0.3925225, size.height * 0.1898485); - path_0.lineTo(size.width * 0.4597748, size.height * 0.1898485); - path_0.lineTo(size.width * 0.4597748, size.height * 0.1395455); - path_0.lineTo(size.width * 0.3754054, size.height * 0.1395455); - path_0.lineTo(size.width * 0.3754054, size.height * 0.5268182); - path_0.lineTo(size.width * 0.4606306, size.height * 0.5268182); - path_0.lineTo(size.width * 0.4606306, size.height * 0.4765152); - path_0.lineTo(size.width * 0.3925225, size.height * 0.4765152); - path_0.close(); - path_0.moveTo(size.width * 0.3224775, size.height * 0.3075758); - path_0.lineTo(size.width * 0.3224775, size.height * 0.3075758); - path_0.cubicTo( - size.width * 0.2992793, - size.height * 0.2887879, - size.width * 0.2927928, - size.height * 0.2739394, - size.width * 0.2927928, - size.height * 0.2378788); - path_0.cubicTo( - size.width * 0.2927928, - size.height * 0.2054545, - size.width * 0.3013063, - size.height * 0.1834848, - size.width * 0.3140090, - size.height * 0.1834848); - path_0.arcToPoint(Offset(size.width * 0.3458559, size.height * 0.2221212), - radius: - Radius.elliptical(size.width * 0.05432432, size.height * 0.1827273), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.3548649, size.height * 0.1792424); - path_0.arcToPoint(Offset(size.width * 0.3143243, size.height * 0.1337879), - radius: - Radius.elliptical(size.width * 0.06351351, size.height * 0.2136364), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.cubicTo( - size.width * 0.2915315, - size.height * 0.1337879, - size.width * 0.2756306, - size.height * 0.1792424, - size.width * 0.2756306, - size.height * 0.2439394); - path_0.cubicTo( - size.width * 0.2756306, - size.height * 0.3136364, - size.width * 0.2891441, - size.height * 0.3377273, - size.width * 0.3137387, - size.height * 0.3578788); - path_0.cubicTo( - size.width * 0.3356306, - size.height * 0.3748485, - size.width * 0.3423423, - size.height * 0.3906061, - size.width * 0.3423423, - size.height * 0.4259091); - path_0.cubicTo( - size.width * 0.3423423, - size.height * 0.4612121, - size.width * 0.3333333, - size.height * 0.4830303, - size.width * 0.3194144, - size.height * 0.4830303); - path_0.arcToPoint(Offset(size.width * 0.2820270, size.height * 0.4336364), - radius: - Radius.elliptical(size.width * 0.05558559, size.height * 0.1869697), - rotation: 0, - largeArc: false, - clockwise: true); - path_0.lineTo(size.width * 0.2718919, size.height * 0.4743939); - path_0.arcToPoint(Offset(size.width * 0.3188288, size.height * 0.5327273), - radius: - Radius.elliptical(size.width * 0.07180180, size.height * 0.2415152), - rotation: 0, - largeArc: false, - clockwise: false); - path_0.cubicTo( - size.width * 0.3435135, - size.height * 0.5327273, - size.width * 0.3593694, - size.height * 0.4880303, - size.width * 0.3593694, - size.height * 0.4189394); - path_0.cubicTo( - size.width * 0.3592342, - size.height * 0.3604545, - size.width * 0.3489640, - size.height * 0.3290909, - size.width * 0.3224775, - size.height * 0.3075758); - path_0.close(); - path_0.moveTo(size.width * 0.8815315, size.height * 0.1396970); - path_0.lineTo(size.width * 0.8468919, size.height * 0.3215152); - path_0.lineTo(size.width * 0.8124775, size.height * 0.1396970); - path_0.lineTo(size.width * 0.7923874, size.height * 0.1396970); - path_0.lineTo(size.width * 0.8378378, size.height * 0.3737879); - path_0.lineTo(size.width * 0.8378378, size.height * 0.5269697); - path_0.lineTo(size.width * 0.8551351, size.height * 0.5269697); - path_0.lineTo(size.width * 0.8551351, size.height * 0.3719697); - path_0.lineTo(size.width * 0.9009009, size.height * 0.1396970); - path_0.close(); - path_0.moveTo(size.width * 0.5904054, size.height * 0.1921212); - path_0.lineTo(size.width * 0.6281081, size.height * 0.1921212); - path_0.lineTo(size.width * 0.6281081, size.height * 0.5269697); - path_0.lineTo(size.width * 0.6454054, size.height * 0.5269697); - path_0.lineTo(size.width * 0.6454054, size.height * 0.1921212); - path_0.lineTo(size.width * 0.6831081, size.height * 0.1921212); - path_0.lineTo(size.width * 0.6831081, size.height * 0.1396970); - path_0.lineTo(size.width * 0.5904505, size.height * 0.1396970); - path_0.close(); - path_0.moveTo(size.width * 0.7631081, size.height * 0.3757576); - path_0.cubicTo( - size.width * 0.7804955, - size.height * 0.3595455, - size.width * 0.7901351, - size.height * 0.3186364, - size.width * 0.7901351, - size.height * 0.2601515); - path_0.cubicTo( - size.width * 0.7901351, - size.height * 0.1857576, - size.width * 0.7739640, - size.height * 0.1389394, - size.width * 0.7478829, - size.height * 0.1389394); - path_0.lineTo(size.width * 0.6967117, size.height * 0.1389394); - path_0.lineTo(size.width * 0.6967117, size.height * 0.5266667); - path_0.lineTo(size.width * 0.7138288, size.height * 0.5266667); - path_0.lineTo(size.width * 0.7138288, size.height * 0.3875758); - path_0.lineTo(size.width * 0.7428829, size.height * 0.3875758); - path_0.lineTo(size.width * 0.7720721, size.height * 0.5269697); - path_0.lineTo(size.width * 0.7920721, size.height * 0.5269697); - path_0.lineTo(size.width * 0.7605405, size.height * 0.3781818); - path_0.close(); - path_0.moveTo(size.width * 0.7137838, size.height * 0.3378788); - path_0.lineTo(size.width * 0.7137838, size.height * 0.1909091); - path_0.lineTo(size.width * 0.7460811, size.height * 0.1909091); - path_0.cubicTo( - size.width * 0.7629279, - size.height * 0.1909091, - size.width * 0.7725676, - size.height * 0.2177273, - size.width * 0.7725676, - size.height * 0.2642424); - path_0.cubicTo( - size.width * 0.7725676, - size.height * 0.3107576, - size.width * 0.7622523, - size.height * 0.3378788, - size.width * 0.7462613, - size.height * 0.3378788); - path_0.close(); - - final paint0Fill = Paint()..style = PaintingStyle.fill; - paint0Fill.color = color; - canvas.drawPath(path_0, paint0Fill); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index d126ac3ef3..461d13618b 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -498,11 +498,14 @@ class MainScaffold extends StatelessWidget { onPressed: () async { final id = await Sentry.captureMessage('UserFeedback'); if (!context.mounted) return; - await showDialog( - context: context, - builder: (context) { - return FeedbackWidget(associatedEventId: id); - }, + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return FeedbackWidget(associatedEventId: id); + }, + fullscreenDialog: true, + ) ); }, text: From d62e13ce7182ca3a54206d36a3dccb436fa3c5e5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 11:47:40 +0200 Subject: [PATCH 04/18] move to flutter package --- flutter/example/lib/main.dart | 3 +-- flutter/lib/sentry_flutter.dart | 1 + .../lib => lib/src/feedback}/feedback_widget.dart | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename flutter/{example/lib => lib/src/feedback}/feedback_widget.dart (97%) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 461d13618b..ff6c9fad05 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -28,7 +28,6 @@ import 'auto_close_screen.dart'; import 'drift/connection/connection.dart'; import 'drift/database.dart'; import 'isar/user.dart'; -import 'feedback_widget.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String exampleDsn = @@ -502,7 +501,7 @@ class MainScaffold extends StatelessWidget { context, MaterialPageRoute( builder: (context) { - return FeedbackWidget(associatedEventId: id); + return SentryFeedbackWidget(associatedEventId: id); }, fullscreenDialog: true, ) diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index d15c8b7a70..cee4479d71 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -17,3 +17,4 @@ export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; export 'src/navigation/sentry_display_widget.dart'; +export 'src/feedback/feedback_widget.dart'; diff --git a/flutter/example/lib/feedback_widget.dart b/flutter/lib/src/feedback/feedback_widget.dart similarity index 97% rename from flutter/example/lib/feedback_widget.dart rename to flutter/lib/src/feedback/feedback_widget.dart index 0456087d99..7ba368c214 100644 --- a/flutter/example/lib/feedback_widget.dart +++ b/flutter/lib/src/feedback/feedback_widget.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class FeedbackWidget extends StatefulWidget { - const FeedbackWidget({ +class SentryFeedbackWidget extends StatefulWidget { + const SentryFeedbackWidget({ super.key, - required this.associatedEventId, + this.associatedEventId, this.hub, this.title = 'Report a Bug', this.nameLabel = 'Name', @@ -43,10 +43,10 @@ class FeedbackWidget extends StatefulWidget { final bool isEmailRequired; @override - _FeedbackWidgetState createState() => _FeedbackWidgetState(); + _SentryFeedbackWidgetState createState() => _SentryFeedbackWidgetState(); } -class _FeedbackWidgetState extends State { +class _SentryFeedbackWidgetState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); From 86cd2e5334171c6c0f52dd525676c0a31268076d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:24:38 +0200 Subject: [PATCH 05/18] add tests for feedback widget --- flutter/example/lib/main.dart | 3 +- flutter/lib/sentry_flutter.dart | 2 +- ...idget.dart => sentry_feedback_widget.dart} | 60 ++++-- .../feedback/sentry_feedback_widget_test.dart | 193 ++++++++++++++++++ flutter/test/mocks.mocks.dart | 58 +++++- 5 files changed, 288 insertions(+), 28 deletions(-) rename flutter/lib/src/feedback/{feedback_widget.dart => sentry_feedback_widget.dart} (84%) create mode 100644 flutter/test/feedback/sentry_feedback_widget_test.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index ff6c9fad05..bdc39df52e 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -504,8 +504,7 @@ class MainScaffold extends StatelessWidget { return SentryFeedbackWidget(associatedEventId: id); }, fullscreenDialog: true, - ) - ); + )); }, text: 'Shows a custom feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index cee4479d71..65b7fc7b54 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -17,4 +17,4 @@ export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; export 'src/navigation/sentry_display_widget.dart'; -export 'src/feedback/feedback_widget.dart'; +export 'src/feedback/sentry_feedback_widget.dart'; diff --git a/flutter/lib/src/feedback/feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart similarity index 84% rename from flutter/lib/src/feedback/feedback_widget.dart rename to flutter/lib/src/feedback/sentry_feedback_widget.dart index 7ba368c214..11b040a789 100644 --- a/flutter/lib/src/feedback/feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -47,7 +47,6 @@ class SentryFeedbackWidget extends StatefulWidget { } class _SentryFeedbackWidgetState extends State { - final TextEditingController _nameController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _messageController = TextEditingController(); @@ -60,7 +59,7 @@ class _SentryFeedbackWidgetState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Report a Bug'), + title: Text(widget.title), ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -82,7 +81,8 @@ class _SentryFeedbackWidgetState extends State { const SizedBox(width: 4), if (widget.isNameRequired) Text( - key: const ValueKey('sentry_feedback_name_required_label'), + key: const ValueKey( + 'sentry_feedback_name_required_label'), widget.isRequiredLabel, style: Theme.of(context).textTheme.labelMedium, ), @@ -99,7 +99,7 @@ class _SentryFeedbackWidgetState extends State { ), controller: _nameController, keyboardType: TextInputType.text, - onChanged: (text) => setState(() => _name = text ), + onChanged: (text) => setState(() => _name = text), ), const SizedBox(height: 16), Row( @@ -112,7 +112,8 @@ class _SentryFeedbackWidgetState extends State { const SizedBox(width: 4), if (widget.isEmailRequired) Text( - key: const ValueKey('sentry_feedback_email_required_label'), + key: const ValueKey( + 'sentry_feedback_email_required_label'), widget.isRequiredLabel, style: Theme.of(context).textTheme.labelMedium, ), @@ -129,7 +130,7 @@ class _SentryFeedbackWidgetState extends State { ), controller: _emailController, keyboardType: TextInputType.emailAddress, - onChanged: (text) => setState(() => _email = text ), + onChanged: (text) => setState(() => _email = text), ), const SizedBox(height: 16), Row( @@ -141,7 +142,8 @@ class _SentryFeedbackWidgetState extends State { ), const SizedBox(width: 4), Text( - key: const ValueKey('sentry_feedback_message_required_label'), + key: const ValueKey( + 'sentry_feedback_message_required_label'), widget.isRequiredLabel, style: Theme.of(context).textTheme.labelMedium, ), @@ -160,7 +162,7 @@ class _SentryFeedbackWidgetState extends State { ), controller: _messageController, keyboardType: TextInputType.multiline, - onChanged: (text) => setState(() => _message = text ), + onChanged: (text) => setState(() => _message = text), ), ], ), @@ -173,16 +175,33 @@ class _SentryFeedbackWidgetState extends State { child: ElevatedButton( key: const ValueKey('sentry_feedback_submit_button'), onPressed: () async { - - if (_name == null || _email == null || _message == null) { + if (_name == null && widget.isNameRequired) { setState(() { _name ??= ''; + }); + } + + if (_email == null && widget.isEmailRequired) { + setState(() { _email ??= ''; + }); + } + + if (_message == null) { + setState(() { _message ??= ''; }); } - if (!_valid(_name) || !_valid(_email) || !_valid(_message)) { + if (!_valid(_name, widget.isNameRequired)) { + return; + } + + if (!_valid(_email, widget.isEmailRequired)) { + return; + } + + if (!_valid(_message, true)) { return; } @@ -201,13 +220,13 @@ class _SentryFeedbackWidgetState extends State { ), SizedBox( width: double.infinity, - child: TextButton( - key: const ValueKey('sentry_feedback_close_button'), - onPressed: () { - Navigator.pop(context); - }, - child: Text(widget.cancelButtonLabel), - ), + child: TextButton( + key: const ValueKey('sentry_feedback_close_button'), + onPressed: () { + Navigator.pop(context); + }, + child: Text(widget.cancelButtonLabel), + ), ), ], ), @@ -238,7 +257,10 @@ class _SentryFeedbackWidgetState extends State { } } - bool _valid(String? text) { + bool _valid(String? text, bool required) { + if (!required) { + return true; + } return text != null && text.isNotEmpty; } } diff --git a/flutter/test/feedback/sentry_feedback_widget_test.dart b/flutter/test/feedback/sentry_feedback_widget_test.dart new file mode 100644 index 0000000000..5f1a506e2f --- /dev/null +++ b/flutter/test/feedback/sentry_feedback_widget_test.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../mocks.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$SentryFeedbackWidget validation', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('does not call hub on submit if not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget(hub: hub), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + verifyNever( + fixture.hub.captureFeedback( + captureAny, + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + ), + ); + }); + + testWidgets('shows error on submit if message not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget(hub: hub), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsOne); + expect(find.text('(required)'), findsOne); + }); + + testWidgets('shows error on submit if name not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isNameRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(2)); + expect(find.text('(required)'), findsExactly(2)); + }); + + testWidgets('shows error on submit if email not valid', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isEmailRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(2)); + expect(find.text('(required)'), findsExactly(2)); + }); + + testWidgets('shows error on submit if name and email not valid', + (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + isNameRequired: true, + isEmailRequired: true, + ), + ); + + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + expect(find.text('Can\'t be empty'), findsExactly(3)); + expect(find.text('(required)'), findsExactly(3)); + }); + }); + + group('$SentryFeedbackWidget submit', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('does call hub captureFeedback on submit', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget(hub: hub), + ); + + when(fixture.hub.captureFeedback( + any, + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + )).thenAnswer( + (_) async => SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea')); + + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_name_textfield')), + "fixture-name"); + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_email_textfield')), + "fixture-email"); + await tester.enterText( + find.byKey(ValueKey('sentry_feedback_message_textfield')), + "fixture-message"); + await tester.tap(find.text('Send Bug Report')); + await tester.pumpAndSettle(); + + verify(fixture.hub.captureFeedback( + argThat(predicate((feedback) => + feedback.name == 'fixture-name' && + feedback.contactEmail == 'fixture-email' && + feedback.message == 'fixture-message')), + hint: anyNamed('hint'), + withScope: anyNamed('withScope'), + )).called(1); + }); + }); + + group('$SentryFeedbackWidget localization', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('sets labels and hints from parameters', (tester) async { + await fixture.pumpFeedbackWidget( + tester, + (hub) => SentryFeedbackWidget( + hub: hub, + title: 'fixture-title', + nameLabel: 'fixture-nameLabel', + namePlaceholder: 'fixture-namePlaceholder', + emailLabel: 'fixture-emailLabel', + emailPlaceholder: 'fixture-emailPlaceholder', + messageLabel: 'fixture-messageLabel', + messagePlaceholder: 'fixture-messagePlaceholder', + submitButtonLabel: 'fixture-submitButtonLabel', + cancelButtonLabel: 'fixture-cancelButtonLabel', + isRequiredLabel: 'fixture-isRequiredLabel', + ), + ); + + expect(find.text('fixture-title'), findsOne); + expect(find.text('fixture-nameLabel'), findsOne); + expect(find.text('fixture-namePlaceholder'), findsOne); + expect(find.text('fixture-emailLabel'), findsOne); + expect(find.text('fixture-emailPlaceholder'), findsOne); + expect(find.text('fixture-messageLabel'), findsOne); + expect(find.text('fixture-messagePlaceholder'), findsOne); + expect(find.text('fixture-submitButtonLabel'), findsOne); + expect(find.text('fixture-cancelButtonLabel'), findsOne); + expect(find.text('fixture-isRequiredLabel'), findsOne); + }); + }); +} + +class Fixture { + var hub = MockHub(); + + Future pumpFeedbackWidget( + WidgetTester tester, Widget Function(Hub) builder) async { + await tester.pumpWidget( + MaterialApp( + home: builder(hub), + ), + ); + } +} diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 01d2127efe..fd5b6c9cbe 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -263,6 +263,12 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { returnValueForMissingStub: null, ); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override _i2.SentrySpanContext get context => (super.noSuchMethod( Invocation.getter(#context), @@ -332,12 +338,6 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { returnValue: {}, ) as Map); - @override - Map get measurements => (super.noSuchMethod( - Invocation.getter(#measurements), - returnValue: {}, - ) as Map); - @override _i8.Future finish({ _i3.SpanStatus? status, @@ -502,6 +502,24 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { returnValueForMissingStub: null, ); + @override + void setMeasurementFromChild( + String? name, + num? value, { + _i2.SentryMeasurementUnit? unit, + }) => + super.noSuchMethod( + Invocation.method( + #setMeasurementFromChild, + [ + name, + value, + ], + {#unit: unit}, + ), + returnValueForMissingStub: null, + ); + @override void scheduleFinish() => super.noSuchMethod( Invocation.method( @@ -1517,6 +1535,34 @@ class MockHub extends _i1.Mock implements _i2.Hub { returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); + @override + _i8.Future<_i3.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + { + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + this, + Invocation.method( + #captureFeedback, + [feedback], + { + #hint: hint, + #withScope: withScope, + }, + ), + )), + ) as _i8.Future<_i3.SentryId>); + @override _i8.Future addBreadcrumb( _i3.Breadcrumb? crumb, { From 047721f04828a2d63f79795f97ba7df7ea71fc05 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:34:33 +0200 Subject: [PATCH 06/18] add changelog entry --- CHANGELOG.md | 10 ++++++++++ flutter/example/lib/main.dart | 16 ++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16aadcd9fe..9324dba266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. +- Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) +```dart +Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SentryFeedbackWidget(associatedEventId: id), + fullscreenDialog: true, + ), +); +``` ### Improvements diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index bdc39df52e..0c500c5477 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -81,7 +81,7 @@ Future setupSentry( // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - // options.spotlight = Spotlight(enabled: true); + options.spotlight = Spotlight(enabled: true); options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; @@ -498,13 +498,13 @@ class MainScaffold extends StatelessWidget { final id = await Sentry.captureMessage('UserFeedback'); if (!context.mounted) return; Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SentryFeedbackWidget(associatedEventId: id); - }, - fullscreenDialog: true, - )); + context, + MaterialPageRoute( + builder: (context) => + SentryFeedbackWidget(associatedEventId: id), + fullscreenDialog: true, + ), + ); }, text: 'Shows a custom feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', From 9bb28a7f159f131fbbc93f0b5a18e16cc7d8d3c1 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:39:24 +0200 Subject: [PATCH 07/18] test event id on submit --- flutter/test/feedback/sentry_feedback_widget_test.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/test/feedback/sentry_feedback_widget_test.dart b/flutter/test/feedback/sentry_feedback_widget_test.dart index 5f1a506e2f..e267eeee49 100644 --- a/flutter/test/feedback/sentry_feedback_widget_test.dart +++ b/flutter/test/feedback/sentry_feedback_widget_test.dart @@ -107,7 +107,10 @@ void main() { testWidgets('does call hub captureFeedback on submit', (tester) async { await fixture.pumpFeedbackWidget( tester, - (hub) => SentryFeedbackWidget(hub: hub), + (hub) => SentryFeedbackWidget( + hub: hub, + associatedEventId: SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'), + ), ); when(fixture.hub.captureFeedback( @@ -133,7 +136,8 @@ void main() { argThat(predicate((feedback) => feedback.name == 'fixture-name' && feedback.contactEmail == 'fixture-email' && - feedback.message == 'fixture-message')), + feedback.message == 'fixture-message' && + feedback.associatedEventId == SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'))), hint: anyNamed('hint'), withScope: anyNamed('withScope'), )).called(1); From a25f9432141ffaf5c9e2c5ffc1a535c358ffbeb1 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:42:59 +0200 Subject: [PATCH 08/18] add localizable validationErrorLabel --- flutter/lib/src/feedback/sentry_feedback_widget.dart | 4 +++- flutter/test/feedback/sentry_feedback_widget_test.dart | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index 11b040a789..a9f7bffa56 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -17,6 +17,7 @@ class SentryFeedbackWidget extends StatefulWidget { this.messagePlaceholder = 'What\'s the bug? What did you expect?', this.submitButtonLabel = 'Send Bug Report', this.cancelButtonLabel = 'Cancel', + this.validationErrorLabel = 'Can\'t be empty', this.isRequiredLabel = '(required)', this.isNameRequired = false, this.isEmailRequired = false, @@ -36,6 +37,7 @@ class SentryFeedbackWidget extends StatefulWidget { final String submitButtonLabel; final String cancelButtonLabel; + final String validationErrorLabel; final String isRequiredLabel; @@ -251,7 +253,7 @@ class _SentryFeedbackWidgetState extends State { String? _errorText(String? text) { if (text != null && text.isEmpty) { - return 'Can\'t be empty'; + return widget.validationErrorLabel; } else { return null; } diff --git a/flutter/test/feedback/sentry_feedback_widget_test.dart b/flutter/test/feedback/sentry_feedback_widget_test.dart index e267eeee49..d78a5ed503 100644 --- a/flutter/test/feedback/sentry_feedback_widget_test.dart +++ b/flutter/test/feedback/sentry_feedback_widget_test.dart @@ -166,6 +166,7 @@ void main() { submitButtonLabel: 'fixture-submitButtonLabel', cancelButtonLabel: 'fixture-cancelButtonLabel', isRequiredLabel: 'fixture-isRequiredLabel', + validationErrorLabel: 'fixture-validationErrorLabel', ), ); @@ -179,6 +180,11 @@ void main() { expect(find.text('fixture-submitButtonLabel'), findsOne); expect(find.text('fixture-cancelButtonLabel'), findsOne); expect(find.text('fixture-isRequiredLabel'), findsOne); + + await tester.tap(find.text('fixture-submitButtonLabel')); + await tester.pumpAndSettle(); + + expect(find.text('fixture-validationErrorLabel'), findsOne); }); }); } From 54912d4aa88699549b26319460e8d6ea05367ed5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:47:31 +0200 Subject: [PATCH 09/18] format --- flutter/test/feedback/sentry_feedback_widget_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flutter/test/feedback/sentry_feedback_widget_test.dart b/flutter/test/feedback/sentry_feedback_widget_test.dart index d78a5ed503..668b4e247e 100644 --- a/flutter/test/feedback/sentry_feedback_widget_test.dart +++ b/flutter/test/feedback/sentry_feedback_widget_test.dart @@ -109,7 +109,8 @@ void main() { tester, (hub) => SentryFeedbackWidget( hub: hub, - associatedEventId: SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'), + associatedEventId: + SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'), ), ); @@ -137,7 +138,8 @@ void main() { feedback.name == 'fixture-name' && feedback.contactEmail == 'fixture-email' && feedback.message == 'fixture-message' && - feedback.associatedEventId == SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'))), + feedback.associatedEventId == + SentryId.fromId('1988bb1b6f0d4c509e232f0cb9aaeaea'))), hint: anyNamed('hint'), withScope: anyNamed('withScope'), )).called(1); From 940a8521f2759bfef7db89ebe9306b8017a99bc7 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 14:55:57 +0200 Subject: [PATCH 10/18] init hub in ctor --- .../lib/src/feedback/sentry_feedback_widget.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index a9f7bffa56..3d3b056542 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -4,10 +4,10 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class SentryFeedbackWidget extends StatefulWidget { - const SentryFeedbackWidget({ + SentryFeedbackWidget({ super.key, this.associatedEventId, - this.hub, + Hub? hub, this.title = 'Report a Bug', this.nameLabel = 'Name', this.namePlaceholder = 'Your Name', @@ -21,10 +21,11 @@ class SentryFeedbackWidget extends StatefulWidget { this.isRequiredLabel = '(required)', this.isNameRequired = false, this.isEmailRequired = false, - }) : assert(associatedEventId != const SentryId.empty()); + }) : assert(associatedEventId != const SentryId.empty()), + _hub = hub ?? HubAdapter(); final SentryId? associatedEventId; - final Hub? hub; + final Hub _hub; final String title; @@ -246,9 +247,8 @@ class _SentryFeedbackWidgetState extends State { super.dispose(); } - Future _captureFeedback(SentryFeedback feedback) { - // ignore: deprecated_member_use - return (widget.hub ?? HubAdapter()).captureFeedback(feedback); + Future _captureFeedback(SentryFeedback feedback) { + return widget._hub.captureFeedback(feedback); } String? _errorText(String? text) { From a4c0cfbd47f073b5bce8e425f5f1295036c4cc01 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 20 Aug 2024 15:23:48 +0200 Subject: [PATCH 11/18] use form and TextFormField --- .../src/feedback/sentry_feedback_widget.dart | 82 +++++++------------ 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index 3d3b056542..4da1eb93cf 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -54,6 +54,8 @@ class _SentryFeedbackWidgetState extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _messageController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + String? _name; String? _email; String? _message; @@ -69,7 +71,8 @@ class _SentryFeedbackWidgetState extends State { child: Column( children: [ Expanded( - child: SingleChildScrollView( + child: Form( + key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -92,17 +95,19 @@ class _SentryFeedbackWidgetState extends State { ], ), const SizedBox(height: 4), - TextField( + TextFormField( key: const ValueKey('sentry_feedback_name_textfield'), style: Theme.of(context).textTheme.bodyLarge, + controller: _nameController, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: widget.namePlaceholder, - errorText: _errorText(_name), ), - controller: _nameController, keyboardType: TextInputType.text, - onChanged: (text) => setState(() => _name = text), + validator: (String? value) { + return _errorText(value, widget.isNameRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 16), Row( @@ -123,17 +128,19 @@ class _SentryFeedbackWidgetState extends State { ], ), const SizedBox(height: 4), - TextField( + TextFormField( key: const ValueKey('sentry_feedback_email_textfield'), + controller: _emailController, style: Theme.of(context).textTheme.bodyLarge, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: widget.emailPlaceholder, - errorText: _errorText(_email), ), - controller: _emailController, keyboardType: TextInputType.emailAddress, - onChanged: (text) => setState(() => _email = text), + validator: (String? value) { + return _errorText(value, widget.isEmailRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 16), Row( @@ -153,19 +160,21 @@ class _SentryFeedbackWidgetState extends State { ], ), const SizedBox(height: 4), - TextField( + TextFormField( key: const ValueKey('sentry_feedback_message_textfield'), + controller: _messageController, style: Theme.of(context).textTheme.bodyLarge, minLines: 5, maxLines: null, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: widget.messagePlaceholder, - errorText: _errorText(_message), ), - controller: _messageController, keyboardType: TextInputType.multiline, - onChanged: (text) => setState(() => _message = text), + validator: (String? value) { + return _errorText(value, true); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, ), ], ), @@ -178,36 +187,9 @@ class _SentryFeedbackWidgetState extends State { child: ElevatedButton( key: const ValueKey('sentry_feedback_submit_button'), onPressed: () async { - if (_name == null && widget.isNameRequired) { - setState(() { - _name ??= ''; - }); - } - - if (_email == null && widget.isEmailRequired) { - setState(() { - _email ??= ''; - }); - } - - if (_message == null) { - setState(() { - _message ??= ''; - }); - } - - if (!_valid(_name, widget.isNameRequired)) { + if (!_formKey.currentState!.validate()) { return; } - - if (!_valid(_email, widget.isEmailRequired)) { - return; - } - - if (!_valid(_message, true)) { - return; - } - final feedback = SentryFeedback( message: _messageController.text, contactEmail: _emailController.text, @@ -247,22 +229,14 @@ class _SentryFeedbackWidgetState extends State { super.dispose(); } - Future _captureFeedback(SentryFeedback feedback) { - return widget._hub.captureFeedback(feedback); - } - - String? _errorText(String? text) { - if (text != null && text.isEmpty) { + String? _errorText(String? value, bool isRequired) { + if (isRequired && (value == null || value.isEmpty)) { return widget.validationErrorLabel; - } else { - return null; } + return null; } - bool _valid(String? text, bool required) { - if (!required) { - return true; - } - return text != null && text.isNotEmpty; + Future _captureFeedback(SentryFeedback feedback) { + return widget._hub.captureFeedback(feedback); } } From 9fcb5ad5b10ff862aba0a4425f820d20d2f23196 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 21 Aug 2024 10:46:51 +0200 Subject: [PATCH 12/18] fix overflow, maybePop --- .../src/feedback/sentry_feedback_widget.dart | 193 +++++++++--------- 1 file changed, 99 insertions(+), 94 deletions(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index 4da1eb93cf..dd62cc0317 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -71,115 +71,120 @@ class _SentryFeedbackWidgetState extends State { child: Column( children: [ Expanded( - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text( - key: const ValueKey('sentry_feedback_name_label'), - widget.nameLabel, - style: Theme.of(context).textTheme.labelMedium, + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + key: const ValueKey('sentry_feedback_name_label'), + widget.nameLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), + if (widget.isNameRequired) + Text( + key: const ValueKey( + 'sentry_feedback_name_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const SizedBox(height: 4), + TextFormField( + key: const ValueKey('sentry_feedback_name_textfield'), + style: Theme.of(context).textTheme.bodyLarge, + controller: _nameController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.namePlaceholder, ), - const SizedBox(width: 4), - if (widget.isNameRequired) + keyboardType: TextInputType.text, + validator: (String? value) { + return _errorText(value, widget.isNameRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 16), + Row( + children: [ Text( - key: const ValueKey( - 'sentry_feedback_name_required_label'), - widget.isRequiredLabel, + key: const ValueKey('sentry_feedback_email_label'), + widget.emailLabel, style: Theme.of(context).textTheme.labelMedium, ), - ], - ), - const SizedBox(height: 4), - TextFormField( - key: const ValueKey('sentry_feedback_name_textfield'), - style: Theme.of(context).textTheme.bodyLarge, - controller: _nameController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: widget.namePlaceholder, + const SizedBox(width: 4), + if (widget.isEmailRequired) + Text( + key: const ValueKey( + 'sentry_feedback_email_required_label'), + widget.isRequiredLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + ], ), - keyboardType: TextInputType.text, - validator: (String? value) { - return _errorText(value, widget.isNameRequired); - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - const SizedBox(height: 16), - Row( - children: [ - Text( - key: const ValueKey('sentry_feedback_email_label'), - widget.emailLabel, - style: Theme.of(context).textTheme.labelMedium, + const SizedBox(height: 4), + TextFormField( + key: const ValueKey('sentry_feedback_email_textfield'), + controller: _emailController, + style: Theme.of(context).textTheme.bodyLarge, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.emailPlaceholder, ), - const SizedBox(width: 4), - if (widget.isEmailRequired) + keyboardType: TextInputType.emailAddress, + validator: (String? value) { + return _errorText(value, widget.isEmailRequired); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + key: + const ValueKey('sentry_feedback_message_label'), + widget.messageLabel, + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(width: 4), Text( key: const ValueKey( - 'sentry_feedback_email_required_label'), + 'sentry_feedback_message_required_label'), widget.isRequiredLabel, style: Theme.of(context).textTheme.labelMedium, ), - ], - ), - const SizedBox(height: 4), - TextFormField( - key: const ValueKey('sentry_feedback_email_textfield'), - controller: _emailController, - style: Theme.of(context).textTheme.bodyLarge, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: widget.emailPlaceholder, + ], ), - keyboardType: TextInputType.emailAddress, - validator: (String? value) { - return _errorText(value, widget.isEmailRequired); - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - const SizedBox(height: 16), - Row( - children: [ - Text( - key: const ValueKey('sentry_feedback_message_label'), - widget.messageLabel, - style: Theme.of(context).textTheme.labelMedium, - ), - const SizedBox(width: 4), - Text( - key: const ValueKey( - 'sentry_feedback_message_required_label'), - widget.isRequiredLabel, - style: Theme.of(context).textTheme.labelMedium, + const SizedBox(height: 4), + TextFormField( + key: + const ValueKey('sentry_feedback_message_textfield'), + controller: _messageController, + style: Theme.of(context).textTheme.bodyLarge, + minLines: 5, + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.messagePlaceholder, ), - ], - ), - const SizedBox(height: 4), - TextFormField( - key: const ValueKey('sentry_feedback_message_textfield'), - controller: _messageController, - style: Theme.of(context).textTheme.bodyLarge, - minLines: 5, - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: widget.messagePlaceholder, + keyboardType: TextInputType.multiline, + validator: (String? value) { + return _errorText(value, true); + }, + autovalidateMode: AutovalidateMode.onUserInteraction, ), - keyboardType: TextInputType.multiline, - validator: (String? value) { - return _errorText(value, true); - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - ], + ], + ), ), ), ), + const SizedBox(height: 8), Column( children: [ SizedBox( @@ -197,8 +202,8 @@ class _SentryFeedbackWidgetState extends State { associatedEventId: widget.associatedEventId, ); await _captureFeedback(feedback); - // ignore: use_build_context_synchronously - Navigator.pop(context); + + Navigator.maybePop(context); }, child: Text(widget.submitButtonLabel), ), From c6e3c83b34bc0d7c760d378a9ef4027f77626e46 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 21 Aug 2024 10:55:16 +0200 Subject: [PATCH 13/18] fix import --- flutter/lib/src/feedback/sentry_feedback_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index dd62cc0317..6d2f3e648b 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -1,7 +1,7 @@ // ignore_for_file: library_private_types_in_public_api import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import '../../sentry_flutter.dart'; class SentryFeedbackWidget extends StatefulWidget { SentryFeedbackWidget({ From 652842f411541f5289bf159310be8aff1df97b52 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 11:49:45 +0200 Subject: [PATCH 14/18] fix cl --- CHANGELOG.md | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71ba90d89..cfe6819dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ ## Unreleased +## Features + +- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) + - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. + - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. + - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. + - Ignored routes will also create no TTID and TTFD spans. +- Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) +```dart +Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SentryFeedbackWidget(associatedEventId: id), + fullscreenDialog: true, + ), +); +``` + ### Enhancements - Improve app start integration ([#2266](https://github.com/getsentry/sentry-dart/pull/2266)) @@ -38,14 +58,6 @@ appRunner: () => runApp(MyApp()), ); ``` - -- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. - - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - - Ignored routes will also create no TTID and TTFD spans. - Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242)) - Add `enableDartSymbolication` option to Sentry.init() for **Flutter iOS, macOS and Android** ([#2256](https://github.com/getsentry/sentry-dart/pull/2256)) - This flag enables symbolication of Dart stack traces when native debug images are not available. @@ -75,21 +87,6 @@ ```dart SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ``` -- Support `captureFeedback` ([#2230](https://github.com/getsentry/sentry-dart/pull/2230)) - - Deprecated `Sentry.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. -- Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) -```dart -Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SentryFeedbackWidget(associatedEventId: id), - fullscreenDialog: true, - ), -); -``` ### Improvements From 007959513773180a9a7210c12a1f83223a7b2dab Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 11:52:23 +0200 Subject: [PATCH 15/18] fix cl --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe6819dc2..39377dee75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,6 @@ - Deprecated `Hub.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryClient.captureUserFeedback`, use `captureFeedback` instead. - Deprecated `SentryUserFeedback`, use `SentryFeedback` instead. - - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - - Ignored routes will also create no TTID and TTFD spans. - Add `SentryFeedbackWidget` ([#2240](https://github.com/getsentry/sentry-dart/pull/2240)) ```dart Navigator.push( From 5fd32f28f679187e7018fcb5ea441cd9e839c926 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 12:04:14 +0200 Subject: [PATCH 16/18] fix mock.mocks --- flutter/test/mocks.mocks.dart | 39 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 2df9355ae5..f2cf885b18 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1172,6 +1172,34 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + @override + _i7.Future<_i2.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Scope? scope, + _i2.Hint? hint, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + { + #scope: scope, + #hint: hint, + }, + ), + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( + this, + Invocation.method( + #captureFeedback, + [feedback], + { + #scope: scope, + #hint: hint, + }, + ), + )), + ) as _i7.Future<_i2.SentryId>); + @override _i7.Future<_i2.SentryId> captureMetrics( Map>? metricsBuckets) => @@ -1760,8 +1788,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i7.Future); @override -<<<<<<< HEAD - _i8.Future<_i3.SentryId> captureFeedback( + _i7.Future<_i2.SentryId> captureFeedback( _i2.SentryFeedback? feedback, { _i2.Hint? hint, _i2.ScopeCallback? withScope, @@ -1775,7 +1802,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureFeedback, @@ -1786,15 +1813,11 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override - _i8.Future addBreadcrumb( - _i3.Breadcrumb? crumb, { -======= _i7.Future addBreadcrumb( _i2.Breadcrumb? crumb, { ->>>>>>> feat/capture-feedback _i2.Hint? hint, }) => (super.noSuchMethod( From 55b85e06002958e7b3155ab6ec7c33d642044f5b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 12:25:35 +0200 Subject: [PATCH 17/18] fix analyzer issues --- flutter/lib/src/feedback/sentry_feedback_widget.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index 6d2f3e648b..abee49ea45 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -56,10 +56,6 @@ class _SentryFeedbackWidgetState extends State { final GlobalKey _formKey = GlobalKey(); - String? _name; - String? _email; - String? _message; - @override Widget build(BuildContext context) { return Scaffold( @@ -203,7 +199,9 @@ class _SentryFeedbackWidgetState extends State { ); await _captureFeedback(feedback); - Navigator.maybePop(context); + if (context.mounted) { + await Navigator.maybePop(context); + } }, child: Text(widget.submitButtonLabel), ), From 9645324a7e60b16ac2165498279e65bb504d03c7 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 23 Sep 2024 12:36:36 +0200 Subject: [PATCH 18/18] fix access to mounted for lower flutter versions --- flutter/lib/src/feedback/sentry_feedback_widget.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/flutter/lib/src/feedback/sentry_feedback_widget.dart b/flutter/lib/src/feedback/sentry_feedback_widget.dart index abee49ea45..3112bb8cee 100644 --- a/flutter/lib/src/feedback/sentry_feedback_widget.dart +++ b/flutter/lib/src/feedback/sentry_feedback_widget.dart @@ -199,7 +199,14 @@ class _SentryFeedbackWidgetState extends State { ); await _captureFeedback(feedback); - if (context.mounted) { + bool mounted; + try { + mounted = (this as dynamic).mounted as bool; + } on NoSuchMethodError catch (_) { + mounted = false; + } + if (mounted) { + // ignore: use_build_context_synchronously await Navigator.maybePop(context); } },