npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@feedbackjar/react-native-sdk

v0.7.1

Published

React Native SDK for FeedbackJar — collect user feedback from iOS and Android apps

Readme

FeedbackJar React Native SDK

A lightweight React Native SDK for collecting user feedback from iOS and Android apps. You build your own form — the SDK handles submission (enriched with device metadata) and fetching the public feedback list.

  • Min React Native: 0.70.0
  • Platforms: iOS & Android
  • Package: @feedbackjar/react-native-sdk
  • License: MIT

Installation

npm install @feedbackjar/react-native-sdk
# or
yarn add @feedbackjar/react-native-sdk
# or
pnpm add @feedbackjar/react-native-sdk

The SDK ships two small native modules (autolinked automatically, no manual linking step) — one to read your app's bundle ID/package name, one to persist the anon id and submitter identity via UserDefaults/SharedPreferences. No dependencies are required.

Expo Go: custom native modules don't load in Expo Go, so on-device values won't persist there. Install the optional @react-native-async-storage/async-storage (npx expo install @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 or entry file. You need your widget ID from the FeedbackJar dashboard.

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

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

App version in metadata

The SDK's native module reports the app version/build automatically in bare and dev-client builds. It can't in Expo Go — pass the values yourself so the dashboard shows them instead of unknown:

import * as Application from 'expo-application';

FeedbackJar.configure({
  widgetId: 'your-widget-id',
  appVersion: Application.nativeApplicationVersion, // "1.4.0"
  appBuild: Application.nativeBuildVersion,         // "42"
});

A supplied value always wins over the native one.

Prebuilt UI

If you don't want to build your own screens, drop in FeedbackJarBoard — a complete feedback board (list, upvote, detail, comments, submission). It's built only on React Native primitives, so it adds no dependency.

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

function FeedbackScreen() {
  return <FeedbackJarBoard accentColor="#e5484d" />;
}

| Prop | Description | | --- | --- | | 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. |

It reads getConfig() on mount and hides the vote pills / comment composer when guest voting / commenting is disabled, and shows name/email fields on the submission form only when the org asks for them. Follows the system light/dark setting.

After a submission, the new post can take a few seconds to appear in the public list (server-side classification / approval). The board shows it right away as a "Posting…" row and swaps it for the real post once it's live — no manual refresh needed.

Everything below is the lower-level API if you'd rather build the UI yourself.

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, screen size, locale, timezone).

Promise (recommended)

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

const result = await FeedbackJar.submit(userText);

if (result.ok) {
  console.log('Submitted:', result.value.postId, '—', result.value.type);
  // show a success state in your UI
} else {
  console.error('Failed:', result.error.message);
  // show an error state
}

Callback (no async context needed)

Safe to call from the main thread — the network call runs off the main thread and the callback is invoked when done.

FeedbackJar.submit(userText, (result) => {
  if (result.ok) {
    // show success state
  } else {
    // 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 the bundle ID/package name and version). Values should be string, number, or boolean; nested objects/arrays aren't supported.

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

Checking whether to ask for name/email

The organization's dashboard settings ("Ask for Name" / "Ask for Email") control whether submitters should be prompted. The SDK doesn't render any UI itself, so read this before building your own form:

const config = await FeedbackJar.getConfig();
if (config.ok) {
  showNameField = config.value.collectName;
  showEmailField = config.value.collectEmail;
}

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.setIdentity({ name: 'Ada Lovelace', email: '[email protected]' });

const identity = await FeedbackJar.getIdentity();
console.log(identity.name);

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

Listing feedback

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

Promise

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

if (result.ok) {
  for (const post of result.value.posts) {
    console.log(`${post.title} — ${post.upvotes} upvotes, ${post.status}`);
  }
  // result.value.nextCursor is defined when more pages exist
}

Pagination

let cursor: string | undefined;

async function loadNextPage() {
  const result = await FeedbackJar.listFeedback({ limit: 20, cursor });
  if (result.ok) {
    renderPosts(result.value.posts);
    cursor = result.value.nextCursor; // pass this back in for the next page
  }
}

Filter by board

const result = await FeedbackJar.listFeedback({
  boardId: 'your-board-id',
  limit: 10,
});

Callback

FeedbackJar.listFeedback({ limit: 20 }, (result) => {
  if (result.ok) {
    renderPosts(result.value.posts);
  }
});

Each post carries hasVoted — whether this device's anonymous id has upvoted it — so you can render a filled/empty vote button without an extra call.

Voting

Guest upvoting must be enabled for your project (Settings → "Allow guest votes"). Check FeedbackJar.getConfig()allowVotes before showing a vote button.

Votes are attributed to a random per-install id the SDK stores on-device (not a device identifier — it resets if the app is reinstalled).

const result = await FeedbackJar.vote(postId);
if (result.ok) {
  console.log(result.value.upvotes, result.value.hasVoted); // 42, true
}

// toggle
await FeedbackJar.unvote(postId);

// read current state (e.g. on a detail screen)
const state = await FeedbackJar.getVoteState(postId);

vote / unvote are idempotent — calling vote twice is a no-op, not an error.

Comments

Reading comments needs no identity. Posting requires guest comments to be enabled (Settings → "Allow guest comments") — check getConfig().allowComments.

const result = await FeedbackJar.listComments(postId, { limit: 20 });
if (result.ok) {
  for (const comment of result.value.comments) {
    console.log(comment.authorName, comment.content);
    for (const reply of comment.replies) {
      console.log('  ↳', reply.authorName, reply.content);
    }
  }
}

// add a comment (name/email fall back to the remembered identity)
await FeedbackJar.addComment(postId, 'Please add dark mode!');

// reply to a top-level comment
await FeedbackJar.addComment(postId, 'Agreed', { parentId: comment.id });

Threads are two levels deep — you cannot reply to a reply. A name/email passed here (or set via setIdentity) is remembered for the submitter; the email is used only for reply notifications and is never linked to a real account unless the person later signs into the web portal with it.

Rich text

Post and comment content can contain light Markdown (bold, italic, code, [links](url), headings, lists, quotes, fenced code) and FeedbackJar mention tokens — #[Post title](postId) for a post reference and @[Name](user:id) for a person. FeedbackJarBoard renders all of this; list previews are flattened to plain text.

In FeedbackJarBoard, tapping a #[…] post reference opens that post's detail screen (fetched via getPost when it isn't already loaded). Bare links and [text](url) open in the browser; @[…] mentions are styled but not linked.

Building your own UI? The same renderer is exported — no extra dependency:

import { RichText, toPlainText, FeedbackJar } from '@feedbackjar/react-native-sdk';

<RichText
  content={post.content}
  onPostPress={async (postId) => {
    const res = await FeedbackJar.getPost(postId);
    if (res.ok) openDetail(res.value);
  }}
/>;

const preview = toPlainText(post.content); // for a truncated row

Example component

import React, { useState } from 'react';
import { Button, TextInput, View, Text } from 'react-native';
import { FeedbackJar } from '@feedbackjar/react-native-sdk';

export function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState<string | null>(null);

  async function handleSubmit() {
    if (!text.trim()) return;
    setStatus('Submitting…');
    const result = await FeedbackJar.submit(text);
    setStatus(result.ok ? 'Thanks for your feedback!' : `Error: ${result.error.message}`);
    if (result.ok) setText('');
  }

  return (
    <View>
      <TextInput
        value={text}
        onChangeText={setText}
        placeholder="Share your feedback…"
        multiline
      />
      <Button title="Submit" onPress={handleSubmit} />
      {status ? <Text>{status}</Text> : null}
    </View>
  );
}

API reference

FeedbackJar

| Method | Description | | --- | --- | | configure({ widgetId, appVersion?, appBuild? }) | Configure the SDK. Call once before anything else. appVersion/appBuild override the native app-version metadata (needed in Expo Go). | | submit(content, options?): Promise<FeedbackJarResult<FeedbackResponse>> | Submit feedback, optionally with properties merged into app metadata. Never rejects. | | submit(content, options, callback) | Callback variant, main-thread safe. | | listFeedback(options?): Promise<FeedbackJarResult<FeedbackListResult>> | List public feedback. limit is clamped to 1–50. | | listFeedback(options, callback) | Callback variant. | | getPost(postId): Promise<FeedbackJarResult<FeedbackPost>> | Fetch one public post — used to resolve #[…] mention jump-links. | | getConfig(): Promise<FeedbackJarResult<WidgetConfig>> | Fetch whether the org asks for name/email. | | setIdentity({ name?, email? }): Promise<void> | Remember a submitter's name/email for future submit calls; also synced to the server for this device. | | getIdentity(): Promise<FeedbackIdentity> | The currently remembered identity, if any. | | clearIdentity(): Promise<void> | Forget the remembered identity. | | vote(postId): Promise<FeedbackJarResult<VoteState>> | Upvote a post as an anonymous guest. Idempotent. Needs allowVotes. | | unvote(postId): Promise<FeedbackJarResult<VoteState>> | Remove this device's upvote. Idempotent. | | getVoteState(postId): Promise<FeedbackJarResult<VoteState>> | Current upvote count + whether this device voted. | | listComments(postId, options?): Promise<FeedbackJarResult<FeedbackCommentListResult>> | Public comment thread (two levels). limit clamped 1–50. | | addComment(postId, content, options?): Promise<FeedbackJarResult<{ id: string }>> | Add a comment/reply as a guest. options.parentId to reply. Needs allowComments. |

Components & helpers

| Export | Description | | --- | --- | | <FeedbackJarBoard accentColor? boardId? onClose? /> | Drop-in board — list, upvote, detail, comments, submit. | | <RichText content onPostPress? /> | Render post/comment content: light Markdown + #[…] / @[…] mentions. | | toPlainText(content): string | Flatten Markdown + mention tokens to one line (row previews). |

FeedbackJarResult<T>

All methods return a discriminated union — nothing ever throws:

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

FeedbackResponse

interface FeedbackResponse {
  postId: string;
  title: string;    // AI-generated title for the submission
  type: string;     // e.g. FEEDBACK, BUG, FEATURE_REQUEST
  boardId: string;
  status?: string;  // initial status, e.g. OPEN or PENDING (approval required)
}

FeedbackPost

interface FeedbackPost {
  id: string;
  title: string;
  content: string;
  type: string;
  status: string;       // OPEN, IN_PROGRESS, COMPLETED, ...
  slug: string;
  boardId: string;
  voteCount: number;
  commentCount: number;
  upvotes: number;
  hasVoted: boolean;    // this device's anon id has upvoted
  authorName?: string;
  createdAt: string;    // ISO-8601
  updatedAt: string;    // ISO-8601
}

FeedbackListResult

interface FeedbackListResult {
  posts: FeedbackPost[];
  nextCursor?: string;  // undefined when there are no more pages
}

WidgetConfig

interface WidgetConfig {
  collectName: boolean;   // org asks for the submitter's name
  collectEmail: boolean;  // org asks for the submitter's email
  allowVotes: boolean;    // guest upvoting enabled
  allowComments: boolean; // guest commenting enabled
}

VoteState

interface VoteState {
  upvotes: number;
  hasVoted: boolean;
}

FeedbackComment / FeedbackCommentListResult

interface FeedbackComment {
  id: string;
  content: string;
  authorName: string;
  authorRole: string | null;  // 'owner' | 'admin' | 'member' when a team member
  isBot: boolean;
  parentId: string | null;
  createdAt: string;          // ISO-8601
  replies: FeedbackComment[]; // one level only
}

interface FeedbackCommentListResult {
  comments: FeedbackComment[];
  nextCursor?: string;
}

FeedbackIdentity

interface FeedbackIdentity {
  name: string | null;
  email: string | null;
}

Device metadata

Each submission automatically includes:

| Field | Source | | --- | --- | | app.bundleId / app.packageName | Native module | | app.version / app.versionName | Native module, or configure({ appVersion }) | | app.build / app.versionCode | Native module, or configure({ appBuild }) | | os.name | "iOS" or "Android" | | os.version | Platform.Version | | screen.width / screen.height | Dimensions.get('screen') | | screen.scale | pixel ratio | | locale.language | Native locale API | | locale.region | Native locale API | | locale.timezone | Intl.DateTimeFormat | | sdk | "react-native" | | sdkVersion | SDK package version, e.g. "0.6.0" | | timestamp | ISO-8601 UTC |

Every request (reads and writes) also carries an X-FeedbackJar-SDK: react-native/<version> header identifying the client.

Notes

  • Feedback can be submitted anonymously, or with a name/email — the SDK never requires either.
  • The anon id (for vote/comment attribution) and remembered name/email persist on-device. It is not a device identifier and resets on reinstall / clear-data.
  • Storage resolves in this order: the SDK's own native module (UserDefaults/SharedPreferences, autolinked in bare / dev-client builds) → @react-native-async-storage/async-storage if installed → an in-memory fallback that logs a one-time warning and does not survive a reload.
  • Expo Go: the native module can't load there. Run npx expo install @react-native-async-storage/async-storage (bundled in Expo Go) or the anon id regenerates on every reload, and pass configure({ appVersion, appBuild }) or the dashboard shows the app version as unknown. A dev/bare build needs neither.
  • @react-native-async-storage/async-storage is an optional peer dependency — the SDK never requires it.
  • Private boards and non-public posts are never returned by listFeedback.
  • All methods return a FeedbackJarResult; nothing throws on network/HTTP errors.
  • Works out of the box with React Native ≥ 0.70 (native modules autolink).