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

@testerpaykit/expo

v0.4.1

Published

TesterPayKit Expo / React-Native bindings — shake-to-report, device info, bug-report hook

Readme

@testerpaykit/expo

TesterPayKit bindings for Expo / React Native — anonymous bug-report submission, device snapshots, shake-to-report hook, and a small package-owned bug-report UI shell.

Install

npx expo install \
  @testerpaykit/expo \
  @react-native-async-storage/async-storage \
  expo-sensors expo-device expo-application expo-file-system expo-audio

Native modules are peer-optional. Without them the related feature no-ops or falls back: shake detection (expo-sensors), snapshots (expo-device, expo-application), file upload helpers (expo-file-system), and voice recording (expo-audio).

Quick start

import { TpkExpo, useShake } from '@testerpaykit/expo';
import { useEffect, useState } from 'react';
import { Alert } from 'react-native';

export function App() {
  const [tpk, setTpk] = useState<TpkExpo | null>(null);

  useEffect(() => {
    (async () => {
      const client = await TpkExpo.init({
        projectId: '8980ca35-8697-4d8c-ae3f-0677c7d177d1',
        testerCode: 'demo-tester-01',
        // Point at local Mac during dev, omit for prod:
        apiBaseUrl: __DEV__ ? 'http://192.168.178.69:18080' : undefined,
      });
      setTpk(client);
    })();
  }, []);

  useShake({
    enabled: !!tpk,
    onShake: async () => {
      if (!tpk) return;
      const result = await tpk.submitBug({
        title: 'Shake-reported bug',
        description: 'User shook the device on the home screen.',
        currentScreen: 'HomeScreen',
        severity: 'medium',
      });
      Alert.alert('Bug reported', `Server id: ${result.id}`);
    },
  });

  return null; // your real UI here
}

End the session when the tester finishes:

await tpk.endSession({
  reason: 'completed',
  bugsFound: 1,
  tasksCompleted: 2,
});

For a logical restart, rotate to a fresh server session:

await tpk.rotateSession();

Package-owned UI shell

Use the provider when you want the package to own initialization:

import {
  BugReportSheet,
  FloatingFeedbackButton,
  TesterPayKitProvider,
  useTesterPayKit,
} from '@testerpaykit/expo';
import { useState } from 'react';

function FeedbackControls() {
  const { client, ready } = useTesterPayKit();
  const [open, setOpen] = useState(false);

  return (
    <>
      <FloatingFeedbackButton
        disabled={!ready}
        onPress={() => setOpen(true)}
      />
      <BugReportSheet
        currentScreen="Home"
        onClose={() => setOpen(false)}
        tpk={client}
        visible={open}
      />
    </>
  );
}

export function App() {
  return (
    <TesterPayKitProvider
      config={{
        projectId: '8980ca35-8697-4d8c-ae3f-0677c7d177d1',
        testerCode: 'demo-tester-01',
      }}
    >
      <FeedbackControls />
    </TesterPayKitProvider>
  );
}

If your app already owns TpkExpo.init(), pass the initialized client into TesterPayKitProvider instead of config.

What TpkExpo.init() does

  1. Reads cached anonymous-tester JWT from AsyncStorage (key @testerpaykit/auth). If the cached entry matches the current projectId + testerCode, reuse it — skip the network round trip.
  2. Otherwise POST /auth/tester/anonymous with the { projectId, testerCode } pair, store the response, and use the returned access token for subsequent submits.
  3. Starts a real server-side tester session via POST /v1/testers/sessions. submitBug() uses that server session id so coordinator session timelines and fair-pay counters stay connected.

Pass storage to use an app-owned persistence adapter (MMKV/SQLite) instead of AsyncStorage for auth, queues, and cached crashes:

await TpkExpo.init({
  projectId,
  testerCode,
  storage: myStorageAdapter, // { get, set, remove, keys? }
});

What submitBug() does

tpk.submitBug({ title, description, … }):

  • Auto-gathers deviceSnapshot via expo-device (model, manufacturer, OS version, isPhysicalDevice) plus appSnapshot via expo-application (app version, build number, bundle id).
  • POSTs to /v1/testers/me/bug-reports with all required fields.
  • Returns { id, status, createdAt, submittedAt } from the server.

Server-side enum validation:

  • type ∈ {bug, crash, uiIssue, performance, featureRequest, generalFeedback}
  • severity ∈ {critical, high, medium, low}
  • category ∈ {functional, ui, performance, security, compatibility, localization, accessibility}

Defaults: type=bug, severity=medium, category=functional.

Local-first dev

Point apiBaseUrl at your Mac's LAN IP when developing against the local TPK stack. iOS simulator can use http://localhost:18080; real device needs http://<mac-lan-ip>:18080 (get via ipconfig getifaddr en0 on macOS).

The Flutter SDK's TPK_LOCAL flag has the same effect in Bonblick — they share the same tpk CLI + same Docker stack defined under moinsen-workspace/TesterPayKit/.

Run the package-level local API smoke test when the Docker stack is up:

TPK_EXPO_RUN_LOCAL_API=1 \
TPK_EXPO_API_BASE_URL=http://localhost:18080 \
pnpm --filter @testerpaykit/expo exec vitest run src/__tests__/local-api.integration.test.ts

Feature parity with the Flutter SDK

All three tiers ship today. Every peer-optional native module is auto-detected at runtime — install only the ones you actually use.

Tier 1 — wired by TpkExpo.init()

| Feature | What it does | |---|---| | Anonymous-tester JWT | First call to /auth/tester/anonymous, cached in AsyncStorage | | Server sessions | init() starts /v1/testers/sessions; endSession() closes it | | submitBug() | POSTs /v1/testers/me/bug-reports with device + app + breadcrumbs | | Breadcrumbs | Ring buffer (default 50) auto-attached to every bug report | | trackScreenView / trackTap / trackEvent | Push breadcrumbs and session events | | flushTelemetry() | POSTs pending timeline data to /events and /breadcrumbs | | Crash handler | Global ErrorUtils hook + unhandledrejection listener, caches crash context before retry | | getPendingCrashes() / flushCachedCrashes() | Post-crash review and retry surface | | Offline queues | Failed bug submits and attachments (5xx / network) queued and retried | | Device + app snapshot | Auto-gathered via expo-device + expo-application | | Shake-to-report | useShake({ onShake }) hook |

Tier 2 — task fetching, network, screenshots, dashboard

| Feature | API | Peer dep | |---|---|---| | Network capture | enabled by default in init; toggle via captureNetwork: false | none | | Task fetching | tpk.api.getMyTasks() / enrollInProject() / unenrollFromProject() | none | | Tester profile + points | tpk.api.getMyProfile() / getMyPoints() | none | | Dashboard hook | useTpkDashboard(tpk){tasks, summary, profile, points} | none | | Screenshot capture | captureView(viewRef) → base64 PNG | react-native-view-shot | | Attachment upload | tpk.attachToBugReport(...) queues transient failures | expo-file-system for file helpers |

Tier 3 — annotations, recording, earnings

| Feature | API | Peer dep | |---|---|---| | Widget-annotation registry | <TpkAnnotated task="…" labels={…}> + useTaskAnnotation() + tpk.annotations.subscribe(…) | none | | Annotated screenshot editor | <TpkAnnotationOverlay tool="freehand" …> with finalize() → base64 | react-native-svg, react-native-view-shot | | Screen recording | const rec = new ScreenRecorder(); await rec.start(); await rec.stop() then uploadAsAttachment(...) | react-native-record-screen, expo-file-system | | Voice notes | VoiceNoteRecorder wraps Expo Audio recorder output and uploads voice_note attachments | expo-audio, expo-file-system | | Earnings summary | tpk.api.getEarningsSummary() / getMyPayouts() + useTpkEarnings(tpk) hook | none |

Annotated-screenshot example

import {
  captureView, uploadScreenshotAttachment,
  TpkAnnotationOverlay, type TpkAnnotationOverlayHandle,
} from '@testerpaykit/expo';

const rootRef = useRef<View>(null);
const editor = useRef<TpkAnnotationOverlayHandle>(null);

// 1. Capture the screen
const shot = await captureView(rootRef);
// 2. Show the editor with `imageUri={`data:image/png;base64,${shot.base64}`}`
// 3. When the user taps "Send":
const flat = await editor.current?.finalize();
const bug = await tpk.submitBug({ title: '...', description: '...' });
if (flat && bug) await uploadScreenshotAttachment(tpk.api, bug.id, flat);

Screen-recording example

import { ScreenRecorder } from '@testerpaykit/expo';

const rec = new ScreenRecorder();
await rec.start({ maxDurationSeconds: 30 });
// …user reproduces the bug, then taps "Stop"…
const clip = await rec.stop();
if (clip) {
  const bug = await tpk.submitBug({ title: '...', description: '...' });
  await rec.uploadAsAttachment(tpk.api, bug.id, clip);
}

Voice-note example

import { RecordingPresets, useAudioRecorder } from 'expo-audio';
import { VoiceNoteRecorder } from '@testerpaykit/expo';

const expoRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
const voice = new VoiceNoteRecorder({ recorder: expoRecorder });

await voice.start();
// ...tester describes the bug...
const note = await voice.stop();
if (note) {
  const bug = await tpk.submitBug({ title: '...', description: '...' });
  await voice.uploadAsAttachment(tpk.api, bug.id, note);
}

What this package is NOT

  • Not a full console app. The provider, floating button, and bug-report sheet are package primitives; app-specific navigation, task workflows, and branding stay in your app.
  • Not a video-editing tool. Recordings are uploaded as-is; if you need trimming or thumbnails do it client-side with expo-video-thumbnails before calling uploadAsAttachment.