feedbackjar 1.5.0 copy "feedbackjar: ^1.5.0" to clipboard
feedbackjar: ^1.5.0 copied to clipboard

Flutter SDK for FeedbackJar — collect user feedback with automatic device metadata. You build the UI; the SDK handles submission and listing.

FeedbackJar Flutter SDK #

pub package

A lightweight Flutter SDK for collecting user feedback. You build your own form — the SDK handles submission (enriched with device metadata) and fetching the public feedback list.

  • Min SDK: Dart 3.0 / Flutter 3.10
  • Platforms: Android, iOS
  • Package: pub.dev
  • License: MIT

Installation #

flutter pub add feedbackjar

Setup #

Initialize once before use — call configure in main() before runApp, or in your root widget's constructor. You need your widget ID from the FeedbackJar dashboard.

import 'package:feedbackjar/feedbackjar.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FeedbackJar.configure('your-widget-id');
  runApp(const MyApp());
}

Prebuilt UI #

Don't want to build a form? Drop in FeedbackJarBoard and you get a working feedback board — list, upvoting, a detail screen with comments, and a new-feedback form — built only on Flutter's own Material widgets (no extra package).

import 'package:feedbackjar/feedbackjar.dart';

// Anywhere in your widget tree:
const FeedbackJarBoard()

Or push it as a full screen from a button:

showFeedbackJar(context);

Recolour the vote state, primary button and links with accentColor (defaults to FeedbackJar red). Both entry points also take an optional boardId filter:

FeedbackJarBoard(accentColor: const Color(0xFFE5484D), boardId: 'board-id')

showFeedbackJar(context, accentColor: const Color(0xFFE5484D));

The board follows the OS light/dark setting and respects your dashboard config: vote pills hide when guest voting is off, the comment composer hides when guest commenting is off, and the Name/Email fields appear on the new-feedback form only when "Ask for Name" / "Ask for Email" are enabled (prefilled from a remembered identity). Every call is handled as a FeedbackResult — the UI never throws and shows the server's error message inline with a retry.

Prefer to build your own UI? The rest of this README covers the data API it's built on.

Submitting feedback #

Submissions can be anonymous, or include a submitter name/email if you collect them in your own form. Each submission automatically carries device metadata (OS version, device model, screen size, app version, locale).

final result = await FeedbackJar.shared.submit(userText);

switch (result) {
  case FeedbackSuccess(:final value):
    print('Submitted: ${value.postId} (${value.type})');
    // show a success state in your UI
  case FeedbackFailure(:final error):
    print('Failed: $error');
    // show an error state
}

Callback (no async context needed) #

FeedbackJar.shared.submitCallback(userText, (result) {
  switch (result) {
    case FeedbackSuccess(:final value):
      // show success state
    case FeedbackFailure(:final error):
      // show error state
  }
});

Note: the server applies rate limiting (5 submissions per 15 minutes per IP). Handle the failure case in your UI.

Custom properties #

Attach your own key/value context to a submission — merged into the auto-collected app metadata (alongside packageName, versionName, versionCode). Values should be String, num, or bool; nested maps/lists aren't supported.

final result = await FeedbackJar.shared.submit(
  userText,
  properties: {'flavor': 'foss', 'plan': 'pro'},
);

Checking config #

getConfig() returns the org's dashboard settings. "Ask for Name" / "Ask for Email" control whether submitters should be prompted; allowVotes / allowComments control whether guest voting and commenting are enabled. The SDK doesn't render any UI itself, so read this before building your own form or vote/comment UI:

final result = await FeedbackJar.shared.getConfig();
if (result case FeedbackSuccess(:final value)) {
  showNameField = value.collectName;
  showEmailField = value.collectEmail;
  showVoteButton = value.allowVotes;
  showCommentBox = value.allowComments;
}

Remembering submitter identity #

Name/email passed to submit are automatically remembered and reused on later calls, so you only need to ask once. Manage this directly with setIdentity / getIdentity / clearIdentity:

await FeedbackJar.shared.setIdentity(name: 'Ada Lovelace', email: 'ada@example.com');

final identity = await FeedbackJar.shared.getIdentity();
print(identity.name);

// e.g. on logout
await FeedbackJar.shared.clearIdentity();

Listing feedback #

Fetch the public feedback feed for your organization. Supports pagination via a cursor.

async/await #

final result = await FeedbackJar.shared.listFeedback(limit: 20);

if (result case FeedbackSuccess(:final value)) {
  for (final post in value.posts) {
    print('${post.title} — ${post.upvotes} upvotes, ${post.status}');
  }
  // value.nextCursor is non-null when more pages exist
}

Pagination #

String? cursor;

Future<void> loadNextPage() async {
  final result = await FeedbackJar.shared.listFeedback(
    limit: 20,
    cursor: cursor,
  );
  if (result case FeedbackSuccess(:final value)) {
    renderPosts(value.posts);
    cursor = value.nextCursor; // pass this back in for the next page
  }
}

Callback #

FeedbackJar.shared.listFeedbackCallback(
  limit: 20,
  callback: (result) {
    if (result case FeedbackSuccess(:final value)) {
      // render value.posts
    }
  },
);

Voting #

Guests can upvote a post without signing in. Each install generates a random anonymous id once (a UUIDv4, persisted via shared_preferences — not a device identifier) and sends it with every vote, so the same install's votes are counted once and hasVoted is populated on listFeedback results.

Voting must be enabled for the project — check WidgetConfig.allowVotes (see Checking config) before showing your vote UI.

// Toggle a vote
final state = await FeedbackJar.shared.getVoteState(post.id);
if (state case FeedbackSuccess(:final value)) {
  final result = value.hasVoted
      ? await FeedbackJar.shared.unvote(post.id)
      : await FeedbackJar.shared.vote(post.id);

  if (result case FeedbackSuccess(:final value)) {
    print('${value.upvotes} upvotes, hasVoted=${value.hasVoted}');
  }
}

vote and unvote are idempotent — calling them twice is a no-op. Both return the new VoteState. If guest voting is disabled the result is a FeedbackFailure carrying the server's message (e.g. Guest voting is disabled for this project.).

Comments #

Fetch and add public comments on a post. Comment threads are two levels deep — each root comment carries its replies, and a reply's parentId points at the root.

final result = await FeedbackJar.shared.listComments(post.id, limit: 20);

if (result case FeedbackSuccess(:final value)) {
  for (final comment in value.comments) {
    print('${comment.authorName}: ${comment.content}');
    for (final reply in comment.replies) {
      print('  ↳ ${reply.authorName}: ${reply.content}');
    }
  }
  // value.nextCursor is non-null when more pages exist
}

Adding a comment or reply uses this install's anonymous id. name/email fall back to the remembered identity (see Remembering submitter identity); email is used only for reply notifications and is never linked to a real account. Commenting must be enabled — check WidgetConfig.allowComments.

// A top-level comment
final result = await FeedbackJar.shared.addComment(post.id, 'Please add this!');
if (result case FeedbackSuccess(:final value)) {
  print('New comment id: $value');
}

// A reply to a root comment
await FeedbackJar.shared.addComment(
  post.id,
  'Agreed — this would help a lot.',
  parentId: rootComment.id,
);

parentId must be a root comment; you cannot reply to a reply.

API reference #

FeedbackJar #

Method Description
configure(widgetId) Configure the SDK. Call once before anything else.
shared.submit(content, {email?, name?, properties?}) Submit feedback, optionally with custom properties merged into app metadata. Returns Future<FeedbackResult<FeedbackResponse>>.
shared.submitCallback(content, callback, {email?, name?, properties?}) Callback variant.
shared.listFeedback({boardId?, limit = 20, cursor?}) List public feedback. limit is clamped to 1–50.
shared.listFeedbackCallback({boardId?, limit, cursor?, callback}) Callback variant.
shared.getConfig() Fetch the org config (name/email prompts, allowVotes, allowComments). Returns Future<FeedbackResult<WidgetConfig>>.
shared.getConfigCallback(callback) Callback variant.
shared.vote(postId) / shared.unvote(postId) Add/remove this install's guest upvote. Idempotent. Returns Future<FeedbackResult<VoteState>>.
shared.getVoteState(postId) Current upvote count and whether this install voted.
shared.voteCallback / unvoteCallback / getVoteStateCallback Callback variants.
shared.listComments(postId, {limit = 20, cursor?}) List public comments (two-level threads). limit clamped to 1–50.
shared.addComment(postId, content, {parentId?, name?, email?}) Add a guest comment or reply. Returns Future<FeedbackResult<String>> (the new comment id).
shared.listCommentsCallback / addCommentCallback Callback variants.
shared.setIdentity({name?, email?}) Remember a submitter's name/email for future submit calls; also best-effort synced to the server.
shared.getIdentity() The currently remembered identity, if any. Returns Future<FeedbackIdentity>.
shared.clearIdentity() Forget the remembered identity.

FeedbackResult<T> #

A sealed class with two subtypes. Use Dart 3 pattern matching:

switch (result) {
  case FeedbackSuccess(:final value):
    // T value
  case FeedbackFailure(:final error):
    // Object error
}

FeedbackResponse #

class FeedbackResponse {
  final String postId;
  final String title;    // AI-generated title for the submission
  final String type;     // e.g. FEEDBACK, BUG, FEATURE_REQUEST
  final String boardId;
}

FeedbackPost #

class FeedbackPost {
  final String id;
  final String title;
  final String content;
  final String type;
  final String status;       // OPEN, IN_PROGRESS, COMPLETED, ...
  final String slug;
  final String boardId;
  final int voteCount;
  final int commentCount;
  final int upvotes;
  final bool hasVoted;       // whether this install's anon id upvoted
  final String? authorName;
  final String createdAt;    // ISO-8601
  final String updatedAt;    // ISO-8601
}

FeedbackListResult #

class FeedbackListResult {
  final List<FeedbackPost> posts;
  final String? nextCursor;  // null when there are no more pages
}

WidgetConfig #

class WidgetConfig {
  final bool collectName;    // org asks for the submitter's name
  final bool collectEmail;   // org asks for the submitter's email
  final bool allowVotes;     // guest upvoting enabled for the project
  final bool allowComments;  // guest commenting enabled for the project
}

FeedbackIdentity #

class FeedbackIdentity {
  final String? name;
  final String? email;
}

VoteState #

class VoteState {
  final int upvotes;    // current upvote count
  final bool hasVoted;  // whether this install's anon id upvoted
}

FeedbackComment #

class FeedbackComment {
  final String id;
  final String content;
  final String authorName;
  final String? authorRole;  // owner / admin / member, or null for a guest
  final bool isBot;
  final String? parentId;    // root comment id when this is a reply
  final String createdAt;    // ISO-8601
  final List<FeedbackComment> replies;  // one level; empty on a reply
}

FeedbackCommentListResult #

class FeedbackCommentListResult {
  final List<FeedbackComment> comments;
  final String? nextCursor;  // null when there are no more pages
}

Notes #

  • Feedback can be submitted anonymously, or with a name/email — the SDK never requires either.
  • Name/email are persisted on-device via shared_preferences so they survive app restarts.
  • Guest votes/comments are attributed to a per-install anonymous id (a random UUIDv4, also stored in shared_preferences). It is not a device identifier and resets on reinstall or clear-data.
  • The server rate-limits vote/unvote (60/min) and comment creation (10/min) per IP. Handle the failure case in your UI.
  • Private boards and non-public posts are never returned by listFeedback.
  • Every request carries an X-FeedbackJar-SDK: flutter/<version> header; submissions also include sdk / sdkVersion in metadata.
  • All methods return a FeedbackResult; nothing throws on network or HTTP errors.
  • Requires WidgetsFlutterBinding.ensureInitialized() before configure if called before runApp.
1
likes
0
points
283
downloads

Publisher

verified publisherfeedbackjar.com

Weekly Downloads

Flutter SDK for FeedbackJar — collect user feedback with automatic device metadata. You build the UI; the SDK handles submission and listing.

Homepage

License

unknown (license)

Dependencies

device_info_plus, flutter, http, package_info_plus, shared_preferences

More

Packages that depend on feedbackjar