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

@sendoracloud/sdk-react-native

v1.16.0

Published

Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push token registration requires a Dev Client (EAS Build or `np

Readme

@sendoracloud/sdk-react-native

Official React Native + Expo SDK for SendoraCloud.

📖 First time on React Native? Read the 5-minute quickstart — it covers the Hermes CSPRNG polyfill, anonymous-first auth, push token receipts, and the demo end-to-end push send.

Expo Go vs Dev Client — pick the right runner

| Feature | Expo Go (SDK 53+) | Dev Client / EAS Build | Bare RN | | --- | --- | --- | --- | | Analytics (track, screen, identify) | ✅ | ✅ | ✅ | | Auth (signUp, signIn, MFA, passkeys) | ✅ | ✅ | ✅ | | Deep links (warm path) | ✅ | ✅ | ✅ | | Deferred deep links (fingerprint) | ✅ | ✅ | ✅ | | Deferred deep links (Play Install Referrer) | ❌ — needs react-native-play-install-referrer | ✅ | ✅ | | Push token registration (iOS APNs / FCM) | ❌ — Expo Go dropped remote push in SDK 53 | ✅ | ✅ |

If push notifications matter to you, you MUST move off Expo Go. Plain Expo Go cannot receive remote push tokens since SDK 53 (Expo deprecated the shared APNs cert). Run a Dev Client instead:

# One-time: build the dev client locally
npx expo prebuild --clean
npx expo run:ios       # or run:android
# Day-to-day: just `npx expo start` — same DX as Expo Go

Or use EAS Build for cloud builds. Auth, analytics, deep links all work fine in plain Expo Go — only push registration requires Dev Client.

💡 If registerPushToken returns null or sign-up succeeds but the dashboard never shows your device token, you're almost certainly on Expo Go. Switch to a Dev Client.

Install

npx expo install @sendoracloud/sdk-react-native @react-native-async-storage/async-storage

That's it. Since 0.17.0 the SDK bundles react-native-get-random-values as a hard dependency and auto-applies the polyfill at SDK load — no manual import "react-native-get-random-values" in your entry file required.

@react-native-async-storage/async-storage is a required peer dependency used to persist the anonymous device id across app restarts.

Upgrading from 0.16.x? You can delete the import "react-native-get-random-values"; line from your index.js / index.tsx entry file — it's now a no-op (the SDK side-effect-loads the polyfill itself). Leaving it in does no harm.

Upgrading from 0.x to 1.0? Delete the orgId (and any legacy publicKey) fields from your SendoraCloud.init({...}) call — the backend now resolves the org from the API key server-side. All public method signatures are unchanged.

  await SendoraCloud.init({
    apiKey: "pk_prod_...",
-   orgId: "<ORG_ID>",
  });

Optional peers (deep links)

| Peer | When to install | | --- | --- | | expo-localization | Improves device-fingerprint locale accuracy. Most Expo apps already have it. | | expo-crypto | Hardens the SHA-256 used by computeDeviceFingerprint(). Auto-used when installed; SDK falls back to web crypto and then a pure-JS impl if not. | | react-native-play-install-referrer | Android-only. Enables 100%-accurate deferred deep-link matching via Play Install Referrer. SDK auto-probes — no-op on iOS or when uninstalled. |

Quick start

import SendoraCloud from "@sendoracloud/sdk-react-native";

// App.tsx or wherever you initialise app-level services
await SendoraCloud.init({
  apiKey: "pk_prod_...",          // from Settings → API Keys in the dashboard
  iosBundleId: "com.yourapp",     // bundle-id gate — required for SDK link mint
  androidPackageName: "com.yourapp",
  // linkDomain: "pulse.link",    // optional — only if you use a custom share host
});

// Identify the signed-in user
SendoraCloud.identify("user-123", { email: "[email protected]" });

// Track product events + screen views
SendoraCloud.track("signup.completed", { plan: "growth" });
SendoraCloud.screen("Pricing");

// Push tokens
const receipt = await SendoraCloud.registerPushToken({
  token: expoPushToken,
  platform: Platform.OS as "ios" | "android",
  bundleId: "com.yourapp",
});

// On logout
await SendoraCloud.reset();

API surface

| Method | Description | | --- | --- | | init(config) | Initialize the SDK. Call once at app startup. | | identify(userId, traits?) | Associate a user id (+ optional traits) with the device. | | track(event, props?) | Fire a custom product event. | | screen(name, props?) | Track a screen view. | | registerPushToken(reg) | Register an APNs or FCM push token. | | auth.* | Sign-in / sign-up / MFA / passkeys / OIDC / SSO + auth.getRefreshToken() + auth.merge(anonRefresh, targetRefresh?). See auth.ts. | | links.create<T>(input, opts?) | Mint a Sendora deep link. Generic T for typed linkData. | | links.prewarm<T>(input, opts?) | Background-mint + cache so share-tap is instant. | | links.handleUniversalLink(url) | Resolve a warm-path Universal Link / App Link delivery. | | links.matchDeferred(input?) | Cold-launch deferred match. Auto-probes Play Install Referrer + fingerprint. | | links.attachLinkingApi({ onLegacyUrl }) | One-call replacement for Linking.getInitialURL + addEventListener + extract-pre-check. | | links.onLinkOpened<T>(cb) | Register a callback fired for both warm + deferred opens. | | links.revoke(shortcode) | Soft-delete a link (private-content unsend). Idempotent. | | links.getStats(shortcode) | { totalClicks, uniqueClicks, deferredMatches, byOs, byCountry, byDevice }. | | computeDeviceFingerprint() | Standalone — returns canonical hex SHA-256. Identical recipe across all 3 mobile SDKs. | | reset() | Clear identity + rotate anonymous id. Call on logout. | | getAnonymousId() | Stable device id (persisted across restarts). | | getUserId() | Currently identified user id, or null. |

Deep links (Branch / Firebase Dynamic Links parity)

import SendoraCloud, { LinkError } from "@sendoracloud/sdk-react-native";

// 1. Typed linkData — define your app's deep-link shape once.
interface ArticleLink extends Record<string, unknown> {
  type: "article";
  articleId: string;
  category: "tech" | "design" | "policy";
  sharedBy: string;
}

// 2. Prewarm on row mount → share tap completes instantly.
SendoraCloud.links.prewarm<ArticleLink>(
  {
    title: `Share: ${article.title}`,
    iosDeepLinkPath: `/articles/${article.id}`,
    androidDeepLinkPath: `/articles/${article.id}`,
    ogTitle: article.title,
    linkData: { type: "article", articleId: article.id, category: article.category, sharedBy: currentUser.id },
    // fallbackUrl optional — backend defaults from your project's apps registry.
  },
  { key: `article:${article.id}` },
);

// 3. Mint on tap — returns from prewarm cache when keys match.
try {
  const link = await SendoraCloud.links.create<ArticleLink>(input, { key: `article:${article.id}` });
  await Share.share({ message: link.url });
} catch (err) {
  if (err instanceof LinkError) {
    switch (err.code) {
      case "BUNDLE_MISMATCH": /* register bundle in Dashboard → Apps */ break;
      case "PLAN_LIMIT":      /* upgrade plan */ break;
      case "RATE_LIMITED":    /* back off + retry */ break;
      case "FALLBACK_REQUIRED": /* configure App Store URL in apps registry */ break;
    }
  }
}

// 4. App-root router — one call replaces the Linking boilerplate.
useEffect(() => {
  const offOpen = SendoraCloud.links.onLinkOpened<ArticleLink>((event) => {
    if (event.linkData.type === "article") navigateToArticle(event.linkData.articleId);
  });
  let detach: (() => void) | undefined;
  void SendoraCloud.links
    .attachLinkingApi({ onLegacyUrl: (url) => myExistingRouter.handle(url) })
    .then((d) => { detach = d; });
  // Cold-launch deferred match — auto-picks installReferrer / fingerprint.
  void SendoraCloud.links.matchDeferred();
  return () => { detach?.(); offOpen(); };
}, []);

// 5. Revoke + stats — no dashboard scraping.
await SendoraCloud.links.revoke("ab3xk9p");
const stats = await SendoraCloud.links.getStats("ab3xk9p");
// → { totalClicks, uniqueClicks, deferredMatches, byOs, byCountry, byDevice }

Full paste-ready recipes live in examples/links-share.tsx and examples/links-router.tsx.

Typed errors

LinkError is thrown for every backend rejection. Branch on err.code (a typed LinkErrorCode) rather than string-matching err.message:

"BUNDLE_MISMATCH" | "DATA_TOO_LARGE" | "EXPIRED" | "NETWORK" | "RATE_LIMITED"
| "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_INPUT" | "PLAN_LIMIT"
| "FALLBACK_REQUIRED" | "SERVER" | "UNKNOWN"

Bundle-id gate

init() accepts iosBundleId / androidPackageName. The SDK auto-forwards them on every links.create() / links.matchDeferred() call. The backend cross-checks against your project's apps registry (Dashboard → Apps); a leaked public key plus a wrong bundle returns LinkError(code: "BUNDLE_MISMATCH", statusCode: 422). Fail-closed when zero apps are registered — register your iOS bundle id + Android package name + SHA-256 fingerprint before going to prod.

Auth

SendoraCloud.auth covers anonymous sign-in, email + password, magic link, email OTP, TOTP MFA, recovery codes, OIDC / SAML SSO, Sign in with Apple, Google, Microsoft, LinkedIn, Facebook, Discord. See auth quickstart.

Anonymous → identified lifecycle

The SDK creates an anonymous user on first init(). Every track() / identify() attaches to that user_id until the user signs up or signs in. Two transitions exist; both preserve analytics continuity, but the second one requires one extra line of code from your app:

Case 1 — fresh signup. auth.signUp(email, password) detects the anonymous refresh token and calls /auth-service/upgrade first, which flips is_anonymous=false + sets the password on the SAME row. The user_id is preserved — no funnel break. Falls back to a fresh /signup if the bound user is already identified (rare race).

const { user } = await SendoraCloud.auth.signUp(email, password);
// user.id === the same id the anon user had. Done.

Case 2 — email already exists. signUp() throws EmailAlreadyTakenError without consuming the anon refresh. You fall back to signIn() — but signIn() wipes the local anon identity, so anon events stay orphaned on the backend unless you transfer them with auth.merge(anonRefresh). Capture the anon refresh BEFORE the signIn:

try {
  const { user } = await SendoraCloud.auth.signUp(email, password);
} catch (err) {
  if (!(err instanceof EmailAlreadyTakenError)) throw err;

  const anonRefresh = await SendoraCloud.auth.getRefreshToken();
  const { user } = await SendoraCloud.auth.signIn(email, password);

  // Best-effort, silent-fail-tolerant — anon row may be already drained.
  if (anonRefresh) {
    SendoraCloud.auth.merge(anonRefresh).catch(() => undefined);
  }
}

Why merge isn't automatic on signIn. Two people sharing a device (browser kiosk, family iPad) would silently get their anon-session events combined with a third party's real account. Forcing the merge call into your app keeps the data-mixing decision explicit. Wrap signIn() with the snippet above if you want auto behaviour.

Full reference + edge-case writeup: /docs/quickstart-react-native#anon-existing-email.

Config

{
  apiKey: string;              // Required. pk_(prod|staging|dev)_<16+ alphanumerics>.
  apiUrl?: string;             // Defaults to https://api.sendoracloud.com
  environment?: "prod" | "staging" | "dev";
  projectId?: string;
  consentedByDefault?: boolean; // Defaults to true.
  autoTrack?: boolean | { appOpen?: boolean; sessionStart?: boolean; appBackground?: boolean };
  sessionIdleMs?: number;       // Idle threshold for session expiry. Default 30 min.
  iosBundleId?: string;         // Bundle-id gate input for links.create / matchDeferred.
  androidPackageName?: string;
  linkDomain?: string;          // Custom share host (e.g. "pulse.link"). Defaults to Sendora's.
  debug?: boolean;
}

Stability

This SDK is on the 1.x line. Patch bumps (1.x.y → 1.x.z) are backwards-compatible; minor bumps may include opt-in feature additions. Breaking changes only on major bumps — always read the CHANGELOG before upgrading.

JWT verification (custom backend)

If your backend verifies Sendora-issued access tokens directly (rather than calling Sendora APIs that re-verify on every hit), use the OIDC-standard JWKS auto-discovery pattern — never hardcode the JWKS URL:

import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose";

const JWKS_BY_ISS = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
function getJwks(iss: string) {
  if (!JWKS_BY_ISS.has(iss)) {
    JWKS_BY_ISS.set(iss, createRemoteJWKSet(new URL(`${iss}/.well-known/jwks.json`)));
  }
  return JWKS_BY_ISS.get(iss)!;
}

const claims = decodeJwt(token);
const { payload } = await jwtVerify(token, getJwks(claims.iss as string));

The iss claim is a per-org Sendora URL (e.g. https://api.sendoracloud.com/api/v1/auth-service/<orgId>); appending /.well-known/jwks.json resolves on the same host. This means a customer can rotate signing keys without your code redeploying.

Security

  • Secret keys refused on the client. Only pk_* keys accepted; sk_* throws at init.
  • HTTPS enforced outside local dev. Non-https: apiUrl values are rejected unless localhost / 127.0.0.1 + environment != "prod".
  • AsyncStorage backs the anonymous id for cross-restart identity; never cross-device. reset() awaits the storage writes.
  • Use opaque userIds, not emails. Persisted to AsyncStorage plaintext. Put email in traits instead.
  • Push token + metadata capped at 4 KB each.
  • Deep-link bundle-id gate — a leaked public key cannot mint links pointed at someone else's app.

License

MIT