feedback_kit 0.4.0 copy "feedback_kit: ^0.4.0" to clipboard
feedback_kit: ^0.4.0 copied to clipboard

In-app feedback boards, voting and a public roadmap for Flutter. Auth-agnostic: bring your own identity via a FeedbackIdentityProvider, or let the built-in anonymous device identity handle it. Pairs w [...]

feedback_kit #

In-app feedback boards, voting and a public roadmap for Flutter. Users submit feature requests, vote on each other's, and follow what you decided to build. Pairs with the platform Feedback service, and speaks nothing but its public API.

Auth-agnostic by construction. The package never holds a board's signing key and never signs an identity itself. For an authenticated board your backend signs a short-lived token and this package carries it; for an anonymous board AnonymousDeviceIdentity needs no backend at all.

Install #

dependencies:
  feedback_kit: ^0.4.0

Anonymous board — no login required #

The lowest-friction setup. A random device token is generated once and kept in shared preferences; the service stores only its HMAC, so a database dump cannot be replayed as somebody's device.

final config = FeedbackConfig(
  baseUrl: Uri.parse('https://feedback.example.com'),
  boardPublicKey: 'cms6ie1qh0001ngy27ynms88q', // from the board's settings
  identityProvider: AnonymousDeviceIdentity(),
);

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => FeedbackBoardScreen(config: config),
));

The board's public key is safe to ship in an app binary. Its signing key is not — that one belongs in a backend, and only for the mode below.

Authenticated board — your users, your identity #

Your backend holds the board's signing key and mints a token for the signed-in user. The kit asks your code for that token and sends it as a header; it never sees the key.

identityProvider: CallbackIdentityProvider(
  getToken: () async {
    final res = await myApi.get('/feedback-token');   // your endpoint
    return res.data['token'] as String?;
  },
),

Returning null is a legitimate answer — a signed-out user then gets a readable board with a sign-in hint instead of an error.

What your backend has to do #

Sign a compact HMAC token. It is deliberately not a JWT: no algorithm field means no alg: none to get wrong.

payload = base64url(json({ sub, name?, email?, exp }))   // exp: unix seconds, ≤ 24h
token   = payload + "." + base64url(HMAC-SHA256(signingKey, payload))

sub is your own user id — opaque to the service, and what one-vote-per-person is counted against. Node example:

const payload = Buffer.from(JSON.stringify({
  sub: user.id, name: user.displayName, exp: Math.floor(Date.now() / 1000) + 3600,
})).toString('base64url')

const mac = crypto.createHmac('sha256', FEEDBACK_SIGNING_KEY)
  .update(payload).digest('base64url')

res.json({ token: `${payload}.${mac}` })

Rotating the key in the platform invalidates every issued token immediately, so deploy the new key before rotating.

Screens #

Widget What it is
FeedbackBoardScreen The entry point: list, sort, vote, submit, links to roadmap and changelog
FeedbackPostScreen One request with its comment thread and a reply box
FeedbackSubmitScreen The submission form, with an opt-in for status mail
FeedbackRoadmapScreen The public roadmap, one tab per column
FeedbackChangelogScreen What shipped, newest first — read-only

Language #

The kit reads the operating system locale and sends it as Accept-Language on every request. That one value drives three things at once: the in-app strings, the locale the service resolves for the board payload, and the language of the status-change emails it sends later. Without it a German phone would show a German UI and receive an English email about the same request.

27 locales ship with full translations. An untranslated tag resolves to its base language (pt-PT → pt) and then to English — never to raw keys. Arabic is marked right-to-left and the screens set Directionality accordingly.

Override it only if your app has its own language switch that the OS does not know about:

FeedbackConfig(..., locale: 'fr')

What the report was filed from #

Every new post carries technical context the person should not have to type: which platform, which OS version, which build of your app. The names are fixed by the Feedback service and shared by all its clients, so one inbox can compare a report from your iOS app with one from your website.

Platform and OS version the kit reads by itself. App version, build and device model come from you, because reading them needs package_info_plus and device_info_plus — and bundling those would put a second copy of the plugins into an app that already has its own, the same reason the image picker is a seam:

final info = await PackageInfo.fromPlatform();

FeedbackConfig(
  // …
  appVersion: info.version,
  appBuild: info.buildNumber,
  // Anything of your own — capped at ten keys by the service.
  contextAttributes: {'environment': 'production', 'tier': user.tier},
)

What you leave unset is absent, never guessed. None of it is published: the service keeps the client context in the team's inbox and drops it from every public view, because device, OS and build together identify the person who filed the report.

If your app must not send device details at all, turn it off:

FeedbackConfig(..., collectClientContext: false)

Status mails without retyping an address #

On a board in authenticated mode the submit form offers the address that came with the identity token your backend signed, instead of asking for one. Nothing to wire up — the kit reads it from FeedbackClient.viewer() when the form opens.

What has not changed is consent: the box still has to be ticked. An address existing is not permission to use it, so submit() sends notifyMe: true only when the person asked for it, and sends nothing at all otherwise.

Screenshots #

Attaching an image needs an image picker, and the kit deliberately does not bundle one: image_picker brings platform plugins and permission entries, and most host apps already have their own. So it asks you for the bytes, the same way it asks you for an identity token.

FeedbackConfig(
  // …
  imagePicker: () async {
    final picked = await ImagePicker().pickMultiImage();
    return Future.wait(picked.map((x) async => FeedbackImageData(
          bytes: await x.readAsBytes(),
          fileName: x.name,
        )));
  },
)

Leave imagePicker null and the attach button simply does not appear — the form still works. Whether attachments are offered at all also depends on the board's own allowAttachments setting, so a team can turn them off without an app release.

FeedbackBoardScreen reads the board's configuration first, so which tabs appear and whether anyone may act follows from the board's settings rather than from what the app was compiled with — switching a board to read-only needs no app release.

Hosting it inside your own tab bar:

FeedbackBoardScreen(config: config, showAppBar: false)

Theming #

Defaults derive from the ambient ThemeData. The board's own brandColor, when set, wins over accent — a product embedding somebody else's board should look like the board.

FeedbackBoardScreen(
  config: config,
  theme: const FeedbackTheme(accent: Colors.teal, showStatusChips: false),
)

Errors worth handling #

FeedbackException.failure is an enum rather than a string, so the UI can branch:

  • identityRequired — prompt a sign-in; the board stays readable.
  • tokenExpired — handled internally by exactly one refresh-and-retry.
  • tokenInvalid — a developer error: wrong board or wrong key.
  • rejected — rate limited or over quota.
  • notFound, network, unknown.

Using the client without the screens #

Every screen is a thin layer over FeedbackClient, which is usable on its own:

final client = FeedbackClient(config);
final page = await client.posts(sort: 'top');
await client.vote(page.items.first.id, value: 1);   // -1 pushes back, 0 clears
client.dispose();

A vote may come back naming a different post than the one you voted on — the service redirects a vote on a merged duplicate to the post that now carries the discussion. Trust VoteResult.postId.

0
likes
150
points
360
downloads

Documentation

API reference

Publisher

verified publisherconapps.net

Weekly Downloads

In-app feedback boards, voting and a public roadmap for Flutter. Auth-agnostic: bring your own identity via a FeedbackIdentityProvider, or let the built-in anonymous device identity handle it. Pairs with the platform Feedback service.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, shared_preferences

More

Packages that depend on feedback_kit