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

@getbitid/react-native

v0.9.0

Published

React Native adapter for the BitID SDK — implements @getbitid/core's ports (expo-camera, SecureStore/MMKV, a pinned fetch), exposes the holder flows, and ships the <SignInWithBitID> button.

Readme

@getbitid/react-native

Sign in and sign up with BitID, inside your app. This package ships a native bottom-sheet auth flow — identifier → OTP → (sign-up: name, phone, DOB, email) → scope consent → OAuth authorization code — that you mount as a normal React component. No WebView, no browser hand-off, no redirect out of your app.

It also carries the browser-based SignInWithBitID button and the @getbitid/core React Native composition root; see Other surfaces.


Quick start

npm install @getbitid/core @getbitid/react-native
npx expo install expo-crypto
import React, { useMemo, useState } from 'react';
import { View } from 'react-native';
import * as Crypto from 'expo-crypto';
import {
  BitIDAuthFlow,
  BitIDButton,
  BitIDThemeProvider,
  partnerProxyTransport,
  useBitIDAuth,
  type BitIDFlow,
  type BitIDPkce,
} from '@getbitid/react-native';

const PARTNER_BACKEND = 'https://api.acme.example';

const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');

async function createPkce(): Promise<BitIDPkce> {
  const codeVerifier = hex(Crypto.getRandomBytes(32));
  const digest = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    codeVerifier,
    { encoding: Crypto.CryptoEncoding.BASE64 },
  );
  return {
    codeVerifier,
    codeChallenge: digest.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
    state: hex(Crypto.getRandomBytes(16)),
  };
}

export function Connect() {
  const [flow, setFlow] = useState<BitIDFlow | null>(null);

  const transport = useMemo(
    () => partnerProxyTransport({ baseUrl: PARTNER_BACKEND }),
    [],
  );

  const ports = useMemo(
    () => ({
      tokenizer: {
        async tokenize(values) {
          const r = await fetch(`${PARTNER_BACKEND}/bitid/tokenize`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(values),
          });
          return r.json();
        },
      },
      clientIp: {
        async getClientIp() {
          const r = await fetch(`${PARTNER_BACKEND}/bitid/client-ip`, { method: 'POST' });
          return (await r.json()).ip;
        },
      },
    }),
    [],
  );

  const { state, controller } = useBitIDAuth(
    {
      clientId: 'bitid-partner-acme',
      redirectUri: 'acme://bitid-callback',
      scopes: ['bitid.identity.read'],
      exchange: 'partner-backend',
    },
    {
      transport,
      createPkce,
      ports,
      onAuthorizationCode: async (payload) => {
        await fetch(`${PARTNER_BACKEND}/bitid/exchange`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
      },
    },
  );

  const open = (next: BitIDFlow) => {
    setFlow(next);
    controller.open(next);
  };

  return (
    <BitIDThemeProvider>
      <View>
        <BitIDButton label='Sign in with BitID' onPress={() => open('signin')} />
        <BitIDButton label='Sign up with BitID' variant='secondary' onPress={() => open('signup')} />
      </View>
      <BitIDAuthFlow
        visible={flow !== null}
        flow={flow ?? 'signin'}
        state={state}
        controller={controller}
        onClose={() => setFlow(null)}
      />
    </BitIDThemeProvider>
  );
}

A complete, runnable version of this — split into config.ts, pkce.ts, and partnerBackend.ts — is in example/.


Install and peer dependencies

npm install @getbitid/core @getbitid/react-native

@getbitid/core is the package's only runtime dependency. Everything else is a peer the host app provides:

| Peer | Required for | Optional | | --- | --- | --- | | react (>=18) | everything | no | | react-native (>=0.73) | everything | no | | expo-crypto | your createPkce() (the SDK never generates PKCE for you) | yes¹ | | expo-web-browser, expo-linking | the browser-based /signin button only | yes | | expo-secure-store, react-native-mmkv | tieredStorage() backends | yes | | expo-camera | liveness via @getbitid/core | yes |

¹ expo-crypto is optional to the package (it is your app that imports it), but you need some SHA-256 + CSPRNG to build a BitIDPkce. The auth sheet itself imports no native module beyond react-native, so it runs in Expo Go.

There is no Sardine peer to install. Device risk signals used to require @sardine-ai/react-native-sardine-static-sdk; that peer is gone. The Sardine binary now ships inside this package (ios/vendor/MDI.xcframework, android/vendor/m2) behind the package's own native module, so the two lines above are the whole dependency story — details below.

The bottom sheet is built from react-native primitives only — Modal, Animated, Pressable, KeyboardAvoidingView. Nothing in the sheet needs linking; the risk-signals native module does (autolinked, but it needs a native build — see below).


The transport — one path only

Every BitID call the sheet makes goes through a BitIDTransport. There is exactly one implementation a partner app may use, and it is not a preference: your app never calls the BitID backend directly. Every call goes to your backend, which attaches the service credential server-side and forwards it.

import { partnerProxyTransport } from '@getbitid/react-native';

const transport = partnerProxyTransport({
  baseUrl: 'https://api.acme.example',
  authHeaders: async () => ({ authorization: `Bearer ${await appSession()}` }),
});

Your backend fronts every call. The transport POSTs the GraphQL operation to ${baseUrl}/bitid/graphql with Content-Type: application/json plus whatever authHeaders() returns; when authorization() is supplied its value is sent as x-bitid-user-authorization.

Why: your backend is the only thing that talks to BitID, so it holds the service-account key, can log and rate-limit, and can bind the resulting BitID identity to your own user record before the device ever learns anything.

partnerProxyTransport accepts a fetchImpl if you use a pinned/instrumented fetch, and rejects with a BitIDTransportError carrying .code and .operationName.

The removed direct mode

Earlier drafts documented a second directTransport mode in which the device called BitID's GraphQL endpoint itself with an x-bitid-service-session header. That mode is gone.

  • directTransport is a tombstone: calling it throws BitIDTransportPolicyError with policy: 'first-party-only' and code: 'BITID_TRANSPORT_FIRST_PARTY_ONLY'.
  • There is no replacement to deep-import. The package ships no module that can build a device-to-BitID transport — no grant object, no flag, no internal factory — and a test asserts the string x-bitid-service-session appears in no shipped source file.
  • authHeaders() is for your app session. Any header name matching /service[-_ ]?(account|session)/i is rejected before the request is made, with code: 'BITID_TRANSPORT_FORBIDDEN_HEADER'. The offending value is never echoed into the error message.

What this does not claim: the SDK is not a security boundary. Nothing stops anyone from writing their own fetch to any endpoint. The boundary is server-side — BitID only accepts a service session minted by a registered backend, and a device never holds one. The SDK's job is to make the correct path the only path it offers, and to fail loudly and legibly when someone reaches for the old one.


The ports sign-up requires

Sign-up needs exactly two ports. Both are thin relays to your own backend.

import type { TokenizerPort, ClientIpPort } from '@getbitid/react-native';

// The platform rejects a raw phone or email, so values are tokenized first.
// The SDK tokenizes the fields `phoneNumber` and `email`.
const tokenizer: TokenizerPort = {
  async tokenize(values) {
    const r = await fetch(`${PARTNER_BACKEND}/bitid/tokenize`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...(await appAuthHeaders()) },
      body: JSON.stringify(values),
    });
    if (!r.ok) throw Object.assign(new Error('tokenize failed'), { code: 'ACME_TOKENIZE' });
    return r.json();
  },
};

// A device cannot observe its own public IP, and account creation requires one.
const clientIp: ClientIpPort = {
  async getClientIp() {
    const r = await fetch(`${PARTNER_BACKEND}/bitid/client-ip`, { method: 'POST' });
    return (await r.json()).ip;
  },
};

Omit either and the controller throws BITID_PORT_MISSING naming the one that is absent.

riskSignals is optional.

Identity prefill needs no wiring at all. The address and SSN screens are part of sign-up, and the SDK drives the lookup, the SSN check and the address save over your GraphQL proxy itself, the same way it already calls every other sign-up operation.

The service-account key stays on your server. There is no SDK API that accepts one — no config field, no header hook, nothing. npm run constraints (scripts/assert-constraints.mjs, also run by npm run verify) fails the package if a service-account credential name or a bit_sk_-style key literal appears anywhere under src/ or example/ — in tests too. Your app receives only a short-lived session token, minted per sign-up by your backend, and it travels as a GraphQL variable through your own proxy, never as a header from the device.

The endpoint contract, with copy-pasteable Node/Express and AWS Lambda implementations, is in docs/partner-integration.md.

Sign-in does not use the service session. If you only ship sign-in you can omit ports entirely.


Finishing the flow

useBitIDAuth(config, deps) drives everything. Two config fields decide who redeems the authorization code:

| config.exchange | Who exchanges the code | You must implement | | --- | --- | --- | | 'partner-backend' (default) | your backend | deps.onAuthorizationCode | | 'device' | the SDK, on-device | nothing — onCompleted gets accessToken |

onAuthorizationCode: async ({ code, codeVerifier, state, redirectUri }) => {
  await postToYourBackend('/bitid/exchange', { code, codeVerifier, state, redirectUri });
},
onCompleted: ({ code, accessToken }) => navigate('Home'),

Prefer 'partner-backend': the partner token never touches the device, and your server can persist the link to your own user in the same request.

state is the live BitIDAuthState — { step, flow, busy, error, scopes, profile, … }. You can build a completely custom UI against controller + state and skip BitIDAuthFlow; the controller is UI-free and node-tested. createBitIDAuthController is exported for use outside React.


Theming

Wrap the sheet in BitIDThemeProvider. With no props it uses bitidLightTheme.

import { BitIDThemeProvider, bitidDarkTheme } from '@getbitid/react-native';

<BitIDThemeProvider
  theme={scheme === 'dark' ? bitidDarkTheme : bitidLightTheme}
  overrides={{
    color: { brand: '#0A7C3F', onBrand: '#FFFFFF' },
    radius: { button: 12 },
    typography: { fontFamily: 'Inter', titleSize: 24 },
  }}
>
  <BitIDAuthFlow … />
</BitIDThemeProvider>

Token groups: color, radius, space, typography, sheet, motion. createBitIDTheme is the same merge, exported for pre-computing a theme outside React. The exported primitives — BitIDButton, BitIDTextField, BitIDOtpField, BitIDTitle, BitIDSubtitle, BitIDInlineError — read the same theme, so your own screens can match the sheet.

Copy

BitIDAuthFlow takes a copy prop; mergeCopy(bitidCopy, overrides) produces one.

import { bitidCopy, mergeCopy } from '@getbitid/react-native';

const copy = mergeCopy(bitidCopy, {
  steps: { intro: { title: 'Acme uses BitID to verify you', cta: 'Continue with BitID' } },
});

Note: the shipped strings contain {partner} / {destination} placeholders, and BitIDAuthFlow renders them literally — it does no interpolation today. Override the steps you show with your own brand name baked in. scopePresentation maps scope ids to human labels and descriptions if you render your own consent screen.


Errors

Failures land in state.error as { code, message, retryable }, and the sheet's error step shows the message with a retry button when retryable. Codes are stable BITID_* strings — the full taxonomy, with the recommended partner action per code, is in docs/partner-integration.md.

The taxonomy is also exported, so you can branch on descriptors instead of literals:

import { BITID_ERROR_CODES, describeBitIDError, isBitIDErrorCode } from '@getbitid/react-native';

const { category, retryable, partnerAction } = describeBitIDError(state.error.code);

describeBitIDError answers for unknown codes too (upstream GraphQL codes, your own port codes) with a retryable-backend default, so it is safe to call unconditionally.

Errors your own ports throw pass through unchanged if they carry a string .code; anything else surfaces as BITID_UNEXPECTED.


Device risk signals (Sardine): no extra install

Sign-up sends a device risk-session id to BitID. The lightweight default is localRiskSignals(), which generates a random id — the flow works, but the signals are not device-attested.

import { localRiskSignals } from '@getbitid/react-native';
import * as Crypto from 'expo-crypto';

const ports = { serviceSession, riskSignals: localRiskSignals(() => Crypto.randomUUID()) };

To send real, device-attested signals, swap in the Sardine-backed port. There is nothing to npm install — the Sardine binary is vendored inside this package (ios/vendor/MDI.xcframework and the android/vendor/m2 Maven repo), and the package's own autolinked native module (BitIDRiskSignals) drives it. You only rebuild the native app:

npx expo prebuild            # or: cd ios && pod install
import { createSardineRiskSignals } from '@getbitid/react-native/sardine';

const ports = {
  serviceSession,
  riskSignals: createSardineRiskSignals({
    clientId: BITID_SARDINE_CLIENT_ID,
    environment: 'production',
    flow: 'signup',
  }),
};

clientId is the Sardine client id for the BitID integration — a public identifier, handed to you with your clientId at onboarding. The Sardine API secret is not a device value and is never accepted by this SDK: it lives on the server that reads the session back from Sardine.

Facts worth knowing before you commit to it:

  • It is a native module. It requires pod install (iOS) / a Gradle sync (Android), so it needs an expo prebuild + dev client or a bare workflow. It cannot work in Expo Go. The auth sheet itself still runs in Expo Go — only the risk-signals port needs the native build.
  • The SDK degrades gracefully. createSardineRiskSignals looks the native module up lazily. If it is missing, unlinked, has no clientId, or setupSDK throws, it warns once and falls back to the same local session id — sign-up still completes. Nothing throws.
  • getDeviceSignals() reports { sardineSessionUUID, sardineActive, sardineEnvironment, sardineFlow }; sardineActive: false is how you detect the fallback in the field.
  • Any environment other than 'production' is normalised to 'sandbox'.
  • Clipboard tracking is off unless you pass enableClipboardTracking: true.

The SDK ships no analytics of its own — npm run constraints asserts that Mixpanel, MoEngage, AppsFlyer, and Clarity appear nowhere under src/ or example/.


Other surfaces in this package

The browser button (/signin)

import { SignInWithBitID } from '@getbitid/react-native/signin';

<SignInWithBitID clientId='bitid-partner-acme' onSuccess={(s) => app.signedIn(s.user)} />

The OAuth-in-the-system-browser flow from @getbitid/core/signin, wrapped in a button. Needs expo-web-browser, expo-crypto, expo-linking (lazily imported). Sign-in only — it has no sign-up path and no service session. Use it when you want the hosted consent page rather than the native sheet.

The engine (createBitIDReactNative)

import { createBitIDReactNative } from '@getbitid/react-native';

const bitid = createBitIDReactNative({ mode: 'live', fetch: pinnedFetch, camera });
const status = await bitid.wallets.getStatus(userId);

Returns the same object as @getbitid/core's createBitID, with the RN ports (httpClient, reactNativeClock, tieredStorage, intervalCamera, eventLogger) supplied. BitIDProvider, useBitID, useBitIDStatus, and useAgentOnboarding are exported alongside it. Most of this plane is Beem-internal today — see the partner guide.


License

UNLICENSED — © Line Financial PBC. All rights reserved.