Common Flows
Open the feedback modal, render the roadmap, trigger Smart Review, build custom UI on top of the async API, and handle errors. Covers openFeedback, RequestListView, RoadmapView, 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.
import { Pressable, Text } from 'react-native';
import { Feddy } from '@feddyapp/react-native';
export function SuggestButton() {
return (
<Pressable onPress={() => Feddy.openFeedback({ boardKey: 'features' })}>
<Text>Suggest a feature</Text>
</Pressable>
);
}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: {
'roadmap-2026': {
en: 'Roadmap 2026',
ja: 'ロードマップ 2026',
es: 'Hoja de ruta 2026',
},
design: { ja: 'デザインフィードバック' },
},
});Resolution order for any custom board key:
boardTranslations[key][deviceLocale]if set- The server's
board.name(whatever the admin typed in the dashboard) - 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',
});Feddy.submitRequest({
title: 'Layout broken in landscape',
description: 'Bottom tab bar overlaps the FAB',
imageUris: ['file:///…/screenshot.jpg'],
});Image attachments require expo-image-picker and
expo-image-manipulator to be installed.
The two system boards every workspace ships with are 'features' and
'bugs'. Omit boardKey to land in the workspace's primary board.
Surface the roadmap
Default — <RoadmapView /> and <RequestListView />
Two full-screen modals with built-in pagination, pull-to-refresh, inline
voting, and tap-to-detail navigation. Both control their own visibility
via the visible / onDismiss props:
import { useState } from 'react';
import { Button } from 'react-native';
import {
RequestListView,
RoadmapView,
} from '@feddyapp/react-native';
export function FeedbackTab() {
const [listVisible, setListVisible] = useState(false);
const [roadmapVisible, setRoadmapVisible] = useState(false);
return (
<>
<Button title="All feedback" onPress={() => setListVisible(true)} />
<Button title="Roadmap" onPress={() => setRoadmapVisible(true)} />
<RequestListView
visible={listVisible}
onDismiss={() => setListVisible(false)}
/>
<RoadmapView
visible={roadmapVisible}
onDismiss={() => setRoadmapVisible(false)}
/>
</>
);
}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 views don't fit (custom theming, embedded list, custom empty state), build your own UI on top of the async read methods.
const page = await Feddy.fetchRequests({
boardKey: 'features',
status: 'planned',
limit: 20,
});
for (const item of page.items) {
console.log(item.title, item.voteCount, item.attachments.length);
}
// Single request detail (with attachments + official reply)
const 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_completed', // surfaces in your dashboard funnel
});The SDK's built-in gates decide whether to actually present the prompt — install-age, session-count, cooldown, and per-year cap. If they pass, a two-step sheet appears:
- Step one asks whether the user is enjoying the app.
- Step two confirms before invoking the system review prompt
(
expo-store-review).
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 reject with 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:
const state = await Feddy.upvote({ requestId: 'req_xyz' });
console.log(`voted=${state.voted} total=${state.voteCount}`);Read comments
Oldest-first, paginated:
const thread = await Feddy.fetchComments({
requestId: 'req_xyz',
limit: 50,
});Append a comment
const 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
By default the SDK reads the host app's currently-active subscription
from expo-iap (StoreKit 2 on iOS, Play Billing on Android) once at
configure(...) and again on each identify(...), so feedback rows in
your dashboard carry up-to-date plan info with zero extra wiring.
When expo-iap is absent the SDK silently skips detection. After a
purchase, restore, or subscription state change, ask the SDK to re-read
the entitlement:
Feddy.refreshSubscription();If your source-of-truth for paid state is RevenueCat, Adapty, or your own server, disable auto-detection and push the state explicitly:
Feddy.configure({
apiKey: 'fed_xxxxxxxxxxxx',
autoDetectSubscription: false,
});
Feddy.setSubscription({
isPaid: true,
status: 'active',
productId: 'com.foo.pro_monthly',
expiresAt: '2026-12-31T00:00:00Z',
});
// Pass null to clear and let auto-detection take over again.
Feddy.setSubscription(null);Manual override always wins over the auto-detected snapshot. Both
persist across launches; the next Feddy.identify(...) call attaches
whichever takes precedence automatically.
Handle errors
Async read / vote / comment methods reject with FeddyError. Two
shapes:
type FeddyError =
| { kind: 'network' }
| { kind: 'http'; status: number; code: string; message: string };| Case | When | What to do |
|---|---|---|
kind: 'network' | DNS / TLS / offline / timeout | Show retry UI; the user's network is unhappy. |
kind: 'http' | Server responded with 4xx / 5xx | Inspect 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 {
const page = await Feddy.fetchRequests({ boardKey: 'features' });
// ...
} catch (err) {
const e = err as FeddyError;
if (e.kind === 'network') {
showOfflineToast();
} else if (e.kind === 'http') {
switch (e.code) {
case 'rate_limited':
scheduleRetry({ seconds: 30 });
break;
case 'invalid_request':
console.error('invalid request:', e.message);
break;
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 AsyncStorage) 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 views
| Use case | Pick |
|---|---|
| Roadmap fits a modal with default theming | <RoadmapView /> |
| Mixed feedback list with a board picker | <RequestListView /> |
| Embedded inside another screen / custom theme | Programmatic fetchRequests |
| Different empty state, hero, or onboarding | Programmatic |
| Showing votes / comments inline elsewhere | Programmatic |
| Minimal code, no design work | Bundled views |