FeedbackJar

React Native

The FeedbackJar React Native SDK is a data layer plus an optional prebuilt board. Use just the data layer if you’re building your own UI, or drop in FeedbackJarBoard for a complete list → detail → new feedback flow with voting and comments already wired up.

Installation

sh
npm install @feedbackjar/react-native-sdk@0.7.1

The SDK ships two small autolinked native helpers (app id + on-device storage). No manual linking. Networking uses React Native’s built-in fetch, Platform, and Dimensions APIs.

Expo Go: custom native modules don’t load in Expo Go. Install the optional @react-native-async-storage/async-storage as a fallback, or use a dev/bare build.

Setup

Configure once before use, typically in your root App.tsx:

ts
import { FeedbackJar } from '@feedbackjar/react-native-sdk';

FeedbackJar.configure({ widgetId: 'your-widget-id' });

Get your widget ID from Settings → Widgets in your FeedbackJar project.

Prebuilt board (optional)

Skip building your own screens — FeedbackJarBoard is a complete board built only on React Native primitives (no extra UI dependency):

tsx
import { FeedbackJarBoard } from '@feedbackjar/react-native-sdk';

function FeedbackScreen() {
  return <FeedbackJarBoard accentColor="#e5484d" />;
}
PropDescription
accentColor?Colour for the vote state, primary button and links. Default #e5484d.
boardId?Restrict the feed to one board.
onClose?When set, a “Close” action appears in the header.

Gives you list, upvote, detail, comments, and a new-feedback form. Nothing throws; failures render inline. Follows system light/dark.

Identify the submitter (optional)

Remember a name/email so later submit, vote, and addComment calls attribute to the same person, and so their name/email are synced to the server against this device’s anonymous id:

ts
await FeedbackJar.setIdentity({ name: user.name, email: user.email });
const identity = await FeedbackJar.getIdentity();

// On logout:
await FeedbackJar.clearIdentity();

Skip this and submissions stay fully anonymous — setIdentity is optional, not required.

Verified identity (signed-in users)

If your app has its own signed-in users, attribute their votes/comments to a real, verifiable account — consistent across every device they use — instead of the unverified name/email above. Your backend computes the signature:

ts
// Your backend, once the user is authenticated:
//   timestamp = Date.now()
//   signature = HMAC-SHA256(orgSecretKey, `${userId}:${email}:${timestamp}`)
// (org secret key is in the FeedbackJar dashboard)

await FeedbackJar.setIdentity({
  userId: user.id,
  email: user.email,
  signature: signatureFromYourBackend,
  timestamp: timestampFromYourBackend,
});

Once set, vote, addComment, submit, and hasVoted all attach to this real user on every device — immediately, and any votes/comments already made anonymously on this device are folded in automatically. timestamp must be the exact value your backend signed, and expires after 7 days — call setIdentity again on each sign-in.

Submit feedback

Each submission automatically includes device metadata (OS version, screen size, locale, timezone).

ts
const result = await FeedbackJar.submit(userText);

if (result.ok) {
  console.log('Submitted:', result.value.postId);
} else {
  console.error('Failed:', result.error.message);
}

A callback variant is available when you are not in an async context:

ts
FeedbackJar.submit(userText, undefined, (result) => {
  if (result.ok) {
    // show success state
  }
});

Pass email/name for a headless submission where identity isn’t already known, and properties for your own key/value context merged into the auto-collected app metadata:

ts
await FeedbackJar.submit(userText, { properties: { plan: 'pro' } });

List feedback

Fetch the public feedback feed with optional pagination:

ts
const result = await FeedbackJar.listFeedback({ limit: 20 });

if (result.ok) {
  for (const post of result.value.posts) {
    console.log(`${post.title} — ${post.upvotes} upvotes`);
  }
}

Pass boardId to filter by board, or cursor for pagination.

FeedbackJar.getPost(postId) fetches a single public post the same way.

Voting

Guest upvoting as this device’s anonymous guest — idempotent, and gated by the project’s Allow guest votes setting (WidgetConfig.allowVotes, see Get widget config):

ts
await FeedbackJar.vote(postId);          // upvote
await FeedbackJar.unvote(postId);        // remove the upvote
const state = await FeedbackJar.getVoteState(postId); // current count + hasVoted

Comments

Anonymous, two-level threads — gated by Allow guest comments (WidgetConfig.allowComments):

ts
const comments = await FeedbackJar.listComments(postId, { limit: 20 });

// Reply to a post, or to a root comment via parentId (replies-to-replies aren't allowed)
await FeedbackJar.addComment(postId, 'Same issue here!');

name/email fall back to the remembered identity (setIdentity) when omitted.

Get widget config

Check whether the org asks for name/email, and whether voting/comments are enabled, before building your own UI around them:

ts
const result = await FeedbackJar.getConfig();
if (result.ok) {
  if (result.value.collectEmail) { /* show an email field */ }
  if (result.value.allowVotes) { /* show your vote button */ }
}
FieldMeaning
collectNameOrg asks submitters for their name (“Ask for Name”)
collectEmailOrg asks submitters for their email (“Ask for Email”)
allowVotesGuest upvoting is enabled for this project
allowCommentsGuest commenting is enabled for this project

API reference

MethodDescription
configure({ widgetId })Configure the SDK. Call once before anything else.
setIdentity({ name?, email? })Remember a submitter’s name/email for later calls.
setIdentity({ userId, email, signature, timestamp, ... })Set a verified, org-signed identity — see Verified identity.
getIdentity()The currently remembered identity, if any.
clearIdentity()Forget the remembered identity (e.g. on logout).
submit(content, options?)Submit feedback. options: email?, name?, properties?. Returns FeedbackJarResult<FeedbackResponse>.
listFeedback(options?)List public feedback. options: boardId?, limit?, cursor?. limit is clamped to 1–50.
getPost(postId)Fetch a single public post.
getConfig()Fetch this project’s WidgetConfig.
vote(postId) / unvote(postId)Cast or remove this device’s upvote. Idempotent.
getVoteState(postId)Current upvote count and whether this device voted.
listComments(postId, options?)List a post’s comments (two levels). options: limit?, cursor?.
addComment(postId, content, options?)Add a comment or reply. options: parentId?, name?, email?.

submit and listFeedback also accept a trailing callback for use outside an async context. All methods return a discriminated union — nothing throws on network errors:

ts
type FeedbackJarResult<T> =
  | { ok: true; value: T }
  | { ok: false; error: Error };

Notes

  • Submission, voting, and comments are all anonymous by default — setIdentity attaches a name/email, it does not require a full account or portal login.
  • Voting and commenting only work when the project has them enabled — check getConfig() before showing that UI.
  • The server rate-limits submissions (5 per 15 minutes per IP).
  • Private boards and non-public posts are never returned by listFeedback.