Feddy Docs
Flutter SDKCommon Flows

Common Flows

Open the feedback modal, push the roadmap as a route, trigger Smart Review, build custom UI on top of the async API, and handle errors. Covers openFeedback, RequestListView, RoadmapView, FeedbackComposeView, SmartReview, programmatic submit / fetch / vote / comment, and FeddyError patterns.

Once the SDK is installed and configured with FeddyProvider mounted, this page covers the five patterns most apps need: collecting feedback, displaying the roadmap, triggering Smart Review, building custom UI on top of the async API, and handling errors.

Submit feedback

Default — Feddy.openFeedback(...)

The bundled compose modal is fully localized in 7 languages (en / zh-Hans / zh-Hant / es / ja / de / fr, auto-picked from the device locale) and handles validation, board picking, async submit, and the success state itself.

ElevatedButton(
  onPressed: () => Feddy.openFeedback(boardKey: 'features'),
  child: const Text('Suggest a feature'),
)

By default the modal shows the workspace's two system boards — Feature and Bug.

Custom workspace boards

The bundled views fetch the workspace's full board set from GET /v1/boards (1 h cached), so any board you've added under dashboard.feddy.app/w/<ws>/boards appears without redeploying. For custom boards, supply per-locale display names via boardTranslations so each device locale renders the right label:

Feddy.configure(
  apiKey: 'fed_xxxxxxxxxxxx',
  boardTranslations: const {
    'roadmap-2026': {
      'en': 'Roadmap 2026',
      'ja': 'ロードマップ 2026',
      'es': 'Hoja de ruta 2026',
    },
    'design': {'ja': 'デザインフィードバック'},
  },
);

Resolution order for any custom board key:

  1. boardTranslations[key][deviceLocale] if set
  2. The server's board.name (whatever the admin typed in the dashboard)
  3. Capitalized key as a last-ditch label

System keys (features / bugs) always use the SDK's bundled translations.

Programmatic submit

If you have your own UI, call Feddy.submitRequest(...) directly. Like all writes, it's synchronous, fire-and-forget, and never throws.

Feddy.submitRequest(title: 'Add dark mode');
Feddy.submitRequest(
  title: 'Crash on launch',
  description: 'Happens after entering passcode on iPhone 15 Pro / iOS 17.4',
);
Feddy.submitRequest(
  title: 'Confusing onboarding step 3',
  boardKey: 'ux-research',
);

The two system boards every workspace ships with are 'features' and 'bugs'. Omit boardKey to land in the workspace's primary board.

Direct compose widget

Mount FeedbackComposeView yourself for embedded use cases — useful when you want the compose form inside a custom container, a settings page, or a bottom sheet you control:

showModalBottomSheet<void>(
  context: context,
  isScrollControlled: true,
  builder: (_) => const FeedbackComposeView(),
);

Surface the roadmap

Default — RoadmapView and RequestListView

Both render as full screens. Push as a MaterialPageRoute from any button:

Navigator.push(
  context,
  MaterialPageRoute<void>(builder: (_) => const RoadmapView()),
);
Navigator.push(
  context,
  MaterialPageRoute<void>(builder: (_) => const RequestListView()),
);

RequestListView shows all requests with a board picker. RoadmapView renders three tabs (Planned / In Progress / Completed) populated by GET /v1/requests?status=…. Detail screens have inline comments and an upvote button.

Custom UI — async fetch

When the bundled widgets don't fit (custom theming, embedded list, custom empty state), build your own UI on top of the async read methods.

final page = await Feddy.fetchRequests(
  boardKey: 'features',
  status: 'planned',
  limit: 20,
);
for (final item in page.items) {
  print('${item.title} ${item.voteCount} ${item.attachments.length}');
}

// Single request detail (with attachments + official reply)
final detail = await Feddy.fetchRequest('req_xyz');

Smart Review

Smart Review routes happy users to the App Store / Play Store and unhappy users to a private feedback form, so you stop seeing 1-star reviews from frustrated users who would have engaged constructively if asked.

Call Feddy.requestReviewIfAppropriate(...) from any "user just had a good moment" hook — onboarding completed, save succeeded, level cleared:

Feddy.requestReviewIfAppropriate(trigger: 'task_50_complete');

The SDK's built-in gates decide whether to actually present the prompt (≥7 days install age, ≥5 sessions, ≥90d cooldown, ≤3 prompts per 365d window). If they pass, a two-step sheet appears:

  1. Step one asks whether the user is enjoying the app.
  2. Step two confirms before invoking the system review prompt.

A negative answer in step one routes straight to the compose modal so the feedback is captured privately instead of as a public 1-star App Store review.

Vote and comment

All vote / comment methods are async and throw FeddyError on failure.

Toggle a vote

The server is the source of truth — upvote returns the post-toggle state so you can reconcile against an optimistic UI update:

final state = await Feddy.upvote(requestId: 'req_xyz');
print('voted=${state.voted} total=${state.voteCount}');

Read comments

Oldest-first, paginated:

final thread = await Feddy.fetchComments(
  requestId: 'req_xyz',
  limit: 50,
);

Append a comment

final posted = await Feddy.addComment(
  requestId: 'req_xyz',
  body: 'Looking forward to this!',
);

The returned posted is the new comment, so you can append to your local thread without re-fetching.

Subscription state

Feddy.configure(...) enables automatic subscription detection by default — no product IDs required:

  • iOS: reads SK2Transaction.transactions() (StoreKit 2)
  • Android: subscribes to Play Billing's purchaseStream and triggers restorePurchases()

The detected snapshot is attached to the next Feddy.identify(...) call. Call Feddy.refreshSubscription() after a purchase / restore to re-read state. Pass autoDetectSubscription: false to disable.

Limitations:

  • Android subscriptions surface no expiration timestamp (expiresAt is always null); Play Billing's client-side API does not expose it.
  • Trial / introductory offer detection is not performed; not-yet-expired entitlements are reported as active.

If your source-of-truth for paid state is RevenueCat, Adapty, or your own server, push the snapshot manually — manual overrides always win:

Feddy.setSubscription(const Subscription(
  isPaid: true,
  status: SubscriptionStatus.active,
  productId: 'com.foo.pro_yearly',
  expiresAt: '2027-01-01T00:00:00Z',
));

// Pass null to clear the override.
Feddy.setSubscription(null);

Both manual and auto values persist across launches via shared_preferences; the next Feddy.identify(...) call attaches whichever takes precedence automatically.

Handle errors

Async read / vote / comment methods throw FeddyError. Two shapes:

sealed class FeddyError implements Exception {}
class FeddyNetworkError extends FeddyError {}
class FeddyHttpError extends FeddyError {
  final int status;
  final String code;
  final String message;
}
CaseWhenWhat to do
FeddyNetworkErrorDNS / TLS / offline / timeoutShow retry UI; the user's network is unhappy.
FeddyHttpErrorServer responded with 4xx / 5xxInspect code for typed handling.

The code field is a stable string contract — values like rate_limited, invalid_request, not_found. Match on it rather than parsing message:

try {
  final page = await Feddy.fetchRequests(boardKey: 'features');
  // ...
} on FeddyNetworkError {
  showOfflineToast();
} on FeddyHttpError catch (e) {
  switch (e.code) {
    case 'rate_limited':
      scheduleRetry(const Duration(seconds: 30));
    case 'invalid_request':
      debugPrint('invalid request: ${e.message}');
    default:
      showGenericError(e.message);
  }
}

Offline behavior (writes)

Writes — submitRequest(...) — never throw. On network failure or 5xx the payload is persisted to a local FIFO queue (via shared_preferences) and replayed on the next Feddy.configure(...) call. 4xx responses are dropped with a console log (replaying bad payloads would loop forever).

The queue survives app restarts. The host app does not need to manage it.

When to choose programmatic over the bundled widgets

Use casePick
Roadmap fits a full screen with default themingRoadmapView
Mixed feedback list with a board pickerRequestListView
Embedded inside another screen / custom themeProgrammatic fetchRequests
Different empty state, hero, or onboardingProgrammatic
Showing votes / comments inline elsewhereProgrammatic
Minimal code, no design workBundled widgets

On this page