Feddy Docs
React Native SDKCommon Flows

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:

  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',
});
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:

  1. Step one asks whether the user is enjoying the app.
  2. 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 };
CaseWhenWhat to do
kind: 'network'DNS / TLS / offline / timeoutShow retry UI; the user's network is unhappy.
kind: 'http'Server 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 {
  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 casePick
Roadmap fits a modal with default theming<RoadmapView />
Mixed feedback list with a board picker<RequestListView />
Embedded inside another screen / custom themeProgrammatic fetchRequests
Different empty state, hero, or onboardingProgrammatic
Showing votes / comments inline elsewhereProgrammatic
Minimal code, no design workBundled views

On this page