suggestly_flutter 0.7.0
suggestly_flutter: ^0.7.0 copied to clipboard
Themeable dialogs in flutter for Suggestly feedback flows: feature requests, bug reports, and ratings.
suggestly_flutter #
Embeddable Material 3 forms and ready-made dialogs for Suggestly feedback: feature requests, bug reports (with screenshots and hidden version and device metadata), and star ratings. The forms inherit your app's theme, take translated strings, and can be placed in any page or shown through the bundled dialog wrappers.
Features #
SuggestlyBugReportForm,SuggestlyFeatureRequestFormandSuggestlyRatingForm- fields-only widgets that you can embed in your own pages, driven by aSuggestlyFormController.SuggestlyBugReportPopup,SuggestlyFeatureRequestPopupandSuggestlyRatingPopup- one-call wrappers that open the matching form in a centred dialog, or a full-height bottom sheet on phones, with Cancel and Submit buttons.SuggestlyFormLabels- every user-visible string with an English default, so translated apps present the forms as their own.- Fields inherit the host's
InputDecorationTheme, severity and priority use aSegmentedButton, and optional bug report fields sit behind an "Add more detail" expander. - Identity, eligibility, name capture, payload building and submission stay inside the package for both presentations.
Getting started #
Add the dependency to your pubspec.yaml (path assumes the monorepo layout):
dependencies:
suggestly_flutter:
path: ../suggestly.packages/suggestly_flutter
Then import the package:
import 'package:suggestly_flutter/suggestly_flutter.dart';
Before showing any Suggestly dialogs, initialise the shared client with your API key:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Suggestly.initialize(apiKey: 'YOUR-API-KEY-HERE');
runApp(const MyApp());
}
Initialisation now validates the API key by fetching your application details. If the key is invalid, a SuggestlyApiException is thrown.
If you do not already store a Suggestly identity for the signed-in customer, call Suggestly.ensureSubmissionIdentity to create or retrieve the user and mint a short-lived submission token:
final SubmissionIdentity identity = await Suggestly.ensureSubmissionIdentity(
email: currentUser.email,
name: currentUser.displayName, // Optional; stored only when provided.
);
Keep the returned SubmissionIdentity in memory and pass it to each popup. The SDK automatically refreshes tokens when they expire, so you can reuse the identity across multiple submissions during the same session.
The Flutter SDK no longer depends on legacy public profile endpoints (/public/users/:userId and /public/users/profile). User identity and token flows run through the submission-token endpoints only.
For private applications, users of your app do not need a Suggestly account or a reviewer invite before they can submit. Submissions signed with the SDK's submission token are accepted straight away. After the first submission the backend enrols the user as a pending reviewer and emails them a link to accept the invite, which lets them view the private application and replies inside Suggestly. Feedback submitted through the Suggestly web portal still requires collaborator or reviewer access.
Pass the signed-in user's name when you have it. The backend stores it on the Suggestly user the first time it is seen, and the dialogs reuse it instead of asking the user to type a name.
Usage #
Feature requests #
final SubmissionIdentity identity = await Suggestly.ensureSubmissionIdentity(
email: currentUser.email,
name: currentUser.displayName,
);
final SuggestlyPopupResult<FeedbackSubmissionResult> featureResult =
await SuggestlyFeatureRequestPopup.show(
context,
title: 'Request a feature',
message: 'Share what would make Suggestly more helpful.',
identity: identity,
submitLabel: 'Submit request',
);
if (featureResult.didSubmit) {
debugPrint('We logged the feature request.');
} else if (featureResult.hasError) {
debugPrint('Feature request failed: ${featureResult.errorMessage}');
}
Bug reports (with attachments) #
final SuggestlyPopupResult<FeedbackSubmissionResult> bugResult =
await SuggestlyBugReportPopup.show(
context,
title: 'Report a bug',
message: 'Screenshots are optional but recommended.',
submitLabel: 'Send report',
identity: identity,
);
if (bugResult.didSubmit) {
debugPrint('Thanks for the report!');
} else if (bugResult.hasError) {
debugPrint('Bug report failed: ${bugResult.errorMessage}');
}
The popup guides users through adding up to five screenshots using the built-in file_selector integration. Attachments are validated for file type and size, renamed deterministically, and uploaded alongside the report.
Ratings #
final SuggestlyPopupResult<FeedbackSubmissionResult> ratingResult =
await SuggestlyRatingPopup.show(
context,
title: 'Rate your experience',
message: 'Stars help us prioritise improvements.',
identity: identity,
ratingLabelBuilder: (value) => '$value / 5',
submitLabel: 'Submit rating',
);
if (ratingResult.didSubmit) {
debugPrint('Thanks for the feedback!');
} else if (ratingResult.hasError) {
debugPrint('Rating failed: ${ratingResult.errorMessage}');
}
Collaborators can submit internal ratings while an application remains in Draft. Once it is live, the popups automatically prevent team members from rating their own product.
What the dialog shows #
The dialog renders only the title, the optional message, the form and the Cancel and Submit buttons. There is no application header and no "processed via Suggestly" notice; the reviewer invite email sent after the first submission tells users where to manage their feedback. On screens narrower than 600dp the popup opens as a full-height bottom sheet that grows with the keyboard; wider screens get a centred dialog capped at 720dp. Pass TextInputPopupThemeData(presentation: SuggestlyPopupPresentation.dialog) through themeOverrides or a TextInputPopupTheme to keep the dialog everywhere.
Submit stays disabled until the required fields are valid. The bug report dialog hides the version and environment fields by default; pass showVersionField: true or showEnvironmentFields: true to show if you still want users to type them, or pre-fill them through initialData so they are sent without being shown.
Embedding the form in your own page #
Each popup is a thin wrapper around a form widget. Use the form directly when you want your own app bar, footer buttons or navigation. The form renders only its fields plus the preparing, blocked and error states; it draws no buttons and never pops a route. You trigger submission through a SuggestlyFormController and receive the outcome once through onResult. The form is a Column sized to its content, so it can sit inside a ListView, SingleChildScrollView or Scaffold body.
class ReportProblemPage extends StatefulWidget {
const ReportProblemPage({super.key, required this.identity});
final SubmissionIdentity identity;
@override
State<ReportProblemPage> createState() => _ReportProblemPageState();
}
class _ReportProblemPageState extends State<ReportProblemPage> {
final SuggestlyFormController _controller = SuggestlyFormController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Report a problem')),
// Keep the buttons inside the body so they stay above the keyboard.
// A Scaffold.bottomNavigationBar would be covered by it.
body: Column(
children: <Widget>[
Expanded(
child: SingleChildScrollView(
// The form pads itself 16px above and below its fields.
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SuggestlyBugReportForm(
identity: widget.identity,
controller: _controller,
onResult: (SuggestlyPopupResult<FeedbackSubmissionResult> result) {
Navigator.of(context).pop(result);
},
// Sent in the payload without being shown to the user.
initialData: const BugReportData(
title: '',
description: '',
severity: BugReportSeverity.medium,
reportedVersion: '2.3.1+45',
environment: BugReportEnvironment(
model: 'Pixel 8',
os: 'Android',
osVersion: '15',
),
),
),
),
),
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: ListenableBuilder(
listenable: _controller,
builder: (BuildContext context, Widget? child) {
return Row(
children: <Widget>[
Expanded(
child: OutlinedButton(
onPressed: _controller.isSubmitting
? null
: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed:
_controller.canSubmit ? _controller.submit : null,
child: Text(
_controller.isSubmitting ? 'Sending' : 'Send',
),
),
),
],
);
},
),
),
),
],
),
);
}
}
The form pads itself 16px above and below its fields so the first floating label and the last control clear your header and footer; pass padding to override that. The controller exposes canSubmit, isSubmitting, isPreparing, isBlocked and errorMessage, and notifies listeners whenever any of them changes. canSubmit is false while preparing, blocked, submitting, after a successful submission, or while a required field is empty or invalid.
SuggestlyFeatureRequestForm and SuggestlyRatingForm take the same shape, with FeatureRequestData and RatingData as initialData. defaultSeverity and defaultPriority preselect the segmented control so an untouched form is valid. Pass pickAttachments to the bug report form to show an attach action backed by your own picker; the package adds no image picker dependency, and the dialog wrapper uses file_selector.
Translated apps #
Every string the forms render has an English default. Hosts that translate their app should pass labels so the form reads as part of the host:
SuggestlyBugReportForm(
identity: identity,
onResult: handleResult,
labels: SuggestlyFormLabels(
common: SuggestlyCommonFormLabels(
titleLabel: t.feedback.title,
descriptionLabel: t.feedback.description,
titleRequired: t.feedback.titleRequired,
),
bugReport: SuggestlyBugReportFormLabels(
severityLabel: t.feedback.severity,
severityLow: t.feedback.severityLow,
moreDetailLabel: t.feedback.moreDetail,
),
),
);
The same labels parameter is accepted by the show methods.
Field styling #
Fields are built as InputDecoration(labelText:, hintText:) and nothing else, so your InputDecorationTheme supplies the border, radius, fill, padding and error style. When you pass TextInputPopupThemeData.inputDecoration, that override stays authoritative and the package fills the gaps it leaves with its own outline border and padding.
Example host app #
An interactive host application lives under example/ so you can exercise the dialogs without wiring them into your product first. Launch it with:
cd suggestly.packages/suggestly_flutter/example
flutter run
Demo mode boots instantly using the bundled in-memory client, and you can toggle eligibility for each popup to validate error paths. Switch to Live mode, paste a real Suggestly API key, and press Initialise to talk to your own backend.
The host shows both presentations: the three buttons call the show methods, and Report a problem pushes a page with its own app bar and Cancel and Send buttons around SuggestlyBugReportForm, pre-filled with the app version from package_info_plus and the device details from device_info_plus. The host theme sets a filled, 12px-radius InputDecorationTheme that the forms inherit.
Publishing #
Publish from the package root:
cd suggestly.packages/suggestly_flutter
flutter pub get
flutter analyze
flutter test
dart pub publish --dry-run
dart pub publish
Before running dart pub publish, make sure pubspec.yaml has the next semantic version, CHANGELOG.md documents that version, and the dry run lists only files that should be public on pub.flutter-io.cn. Pub.dev package uploads are permanent; publish a new version for fixes rather than relying on removal.
Theming #
Wrap a subtree with TextInputPopupTheme to provide defaults that apply to every popup, or pass themeOverrides to individual dialogs:
TextInputPopupTheme(
data: TextInputPopupThemeData(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
contentPadding: const EdgeInsets.fromLTRB(28, 28, 28, 20),
loadingIndicatorColor: Colors.deepPurple,
loadingTextStyle: Theme.of(context).textTheme.bodyMedium,
errorTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
child: Builder(
builder: (context) => FilledButton(
onPressed: () {
SuggestlyFeatureRequestPopup.show(
context,
title: 'Request a feature',
message: 'Tell us what would make Suggestly better.',
identity: identity,
themeOverrides: const TextInputPopupThemeData(
primaryButtonStyle: FilledButton.styleFrom(
minimumSize: Size(140, 40),
),
),
);
},
child: const Text('Request feature'),
),
),
);
Additional information #
flutter analyzeandflutter testrun clean.SuggestlyBugReportPopupowns the attachment workflow withfile_selector, enforcing the same limits as the Suggestly web app (image formats only, up to five files, 5 MB each). Embedded forms enforce the same limits on files returned bypickAttachments.- Forms prompt for a missing user name, save it before submission and include it in the payload, allowing the backend to persist that profile name as part of the secure submission flow.
- Contributions, issues, and suggestions are welcome.
Direct API access #
If you need to interact with the Suggestly service outside the prebuilt dialogs, the Suggestly class exposes the underlying client:
Suggestly.ensureSubmissionIdentitynormalises email/name input, creates the user if necessary, stores the name when the user has none, and issues a signed submission token plus user ID for downstream calls. Reviewer enrolment for private applications is handled entirely by the backend after submissions, so no additional parameters or dialogs are needed on the client.Suggestly.fetchFeedbackEligibilityreads cached eligibility for one or more submission kinds, whileSuggestly.ensureFeedbackEligibilityrefreshes it on demand. Both accept aSubmissionIdentityso the signed submission token (and embeddeduserId) accompanies every lookup.Suggestly.getApplicationSummaryreturns the application metadata (name, logo, status).Suggestly.initializeuses it to validate the API key; the forms no longer request it.Suggestly.submitBugReport,Suggestly.submitFeatureRequest, andSuggestly.submitRatinglet you send feedback from fully custom flows using theSubmissionIdentityyou already issued.Suggestly.listMySubmissions,Suggestly.getMySubmissionandSuggestly.addSubmissionCommentback an in-app "My feedback" screen: the user's own bug reports and suggestions with their status, the full detail with the team's comments, and a way to add more context. They use the sameSubmissionIdentity, so no Suggestly account or portal sign-in is needed. EachFeedbackSubmissionalso carries aportalUrlfor hosts that prefer to hand off to the portal.Suggestly.disposeclears in-memory caches and closes the internal HTTP client; call it when tearing down long-lived isolates or cleaning up tests.Suggestly.debugOverrideClientaccepts aSuggestlyTestingClient(or your own fake) to intercept outbound traffic during automated testing.
User name capture is handled automatically within the forms and is not part of the public Flutter API surface.