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

@privacysuraksha/react-native-consent

v0.2.0

Published

The bare React Native / Expo shell for the Compliant DPDP consent notice. It hosts the same embedded web core (`packages/banner`) that `mobile/ios` and `mobile/android` host natively, inside a real `react-native-webview`.

Readme

@privacysuraksha/react-native-consent

The bare React Native / Expo shell for the Compliant DPDP consent notice. It hosts the same embedded web core (packages/banner) that mobile/ios and mobile/android host natively, inside a real react-native-webview.

Every snippet below is copied verbatim from example/App.tsx, which is a real RN app this package is wired into with "@privacysuraksha/react-native-consent": "file:.." — nothing here is untested prose.

Install

npm install @privacysuraksha/react-native-consent react-native-webview
# and ONE of:
npm install react-native-keychain @react-native-async-storage/async-storage   # bare RN
npm install expo-secure-store @react-native-async-storage/async-storage       # Expo

react-native-webview, react-native-keychain, expo-secure-store and @react-native-async-storage/async-storage are peer dependencies — this package installs no native code of its own, so there is nothing to link and nothing to conflict with a version a customer already has in their tree.

Bundle size: the published dist/index.js is ~581 KB. That is not a mistake — scripts/build.mjs bundles both the embedded web core (spec §4.2) and all 23 locale catalogs (spec §4.6) into this one file at publish time (spec §4.8), rather than shipping them as separate assets. The catalogs still parse lazily at runtime, only the requested locale ever gets JSON-parsed (assets.test.ts asserts this), so this size does not translate one-to-one into launch-time cost — but it is the real number a bundle-size budget should account for, not the web core's own ~46 KB.

Integration

import { CompliantConsentProvider } from '@privacysuraksha/react-native-consent';
import { keychainStorage } from '@privacysuraksha/react-native-consent/adapters/keychain';

function App() {
  return (
    <CompliantConsentProvider
      config={{
        siteKey: 'site_demo_0000000000000000',
        apiBase: 'https://api.privacysuraksha.com',
        // Optional. Present here only so the example exercises the
        // header this SDK attaches to every httpRequest for an
        // app-sourced consent record (EMBEDDED.md §5 gap 2, ledger row
        // 384). Omit entirely for a site with no registered mobile app.
        appKey: 'app_00000000000000000000000000000000',
        storage: keychainStorage(),
      }}
    >
      <AppContent />
    </CompliantConsentProvider>
  );
}

That is the whole integration: mount CompliantConsentProvider once, near the app's root, and supply a storage adapter. It shows the notice itself when one is due; there is nothing else to call to make that happen. Call useCompliantConsent().show('prefs') from anywhere inside the provider to open the preferences view on demand (a "Manage consent" menu item, for example).

Pass a stable config object. config is read once, at mount — a changed config prop is intentionally ignored afterward (spec §9 risk 5), so the notice does not re-fetch or re-render mid-decision because a parent re-rendered. Pass a useMemo-wrapped object or a module-level constant, not an inline object literal — an inline literal is a new reference on every render, which is harmless here only because the effect that reads it never re-runs, but it invites the same trap the next time this file is edited.

Where the keys come from

  • siteKey — dashboard → your site. It is public; it is already in your website's page source.
  • appKey — dashboard → your site → Mobile apps → register this app by its bundle identifier (iOS build) or application id (Android build). You are shown the key once.

If your site has registered no apps, omit appKey and everything works. The moment you register one, consent requests from apps must carry a valid key — including from a build you shipped before registering, so register before you release. See "The app key is extractable from the JS bundle" below for what this key does and does not prove.

Storage adapters — one is mandatory

CompliantConsentProvider throws (through config.onError, not a render crash) if config.storage is missing. There is no unencrypted fallback. The visitor id this package generates is the only thing standing between a consent record and an anonymous one, and it must survive an app restart in storage a customer's own device backup does not silently exfiltrate.

Two adapters ship, and the split between them is deliberate:

| Export | Backing store | Use for | |---|---|---| | keychainStorage() from /adapters/keychain | react-native-keychain (iOS Keychain / Android Keystore) + @react-native-async-storage/async-storage | bare React Native apps | | expoStorage() from /adapters/expo | expo-secure-store + @react-native-async-storage/async-storage | Expo apps |

Both adapters expose two surfaces under the hood, matching mobile/ios exactly:

  • secureGet/secureSet — the Keychain/Keystore/SecureStore. Survives app uninstall. Holds the visitor id only.
  • get/set/delete — plain AsyncStorage. Cleared on uninstall. Holds prefs, language, and the cached config.

That split is why a reinstall shows the notice again (see below) — Keychain survival across reinstall would resurrect a decision the visitor believed they had removed, so this package never puts the decision there, only the id.

Neither adapter raises a biometric prompt. Both pin AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY accessibility (readable at launch after the first unlock, never migrated to a new device via backup) and, on the bare-RN adapter, STORAGE_TYPE.AES_GCM_NO_AUTH specifically because react-native-keychain's default cipher selection on Android can gate on biometrics — a consent banner that raises a fingerprint prompt at launch would be an absurd failure mode. mobile/react-native/maestro/transport.yaml relaunches the app after accepting and asserts the notice does not reappear, which is the one signal that would actually catch a regression here; no unit test running against in-memory storage ever would.

Writing your own adapter

Any object implementing CompliantStorage from @privacysuraksha/react-native-consent works — the two shipped adapters are a convenience, not a requirement:

export interface CompliantStorage {
  secureGet(key: string): Promise<string | null>;
  secureSet(key: string, value: string): Promise<void>;
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  delete(key: string): Promise<void>;
}

Consent Mode v2 (signalWriter)

This SDK ships no Firebase dependency of its own — if a customer's app already uses @react-native-firebase/analytics for Google Consent Mode v2, this SDK's dependency would only fight it. Instead, supply a callback:

<CompliantConsentProvider
  config={{
    // ...
    signalWriter: (signals) => {
      // Swap for `analytics().setConsent(signals)` once
      // @react-native-firebase/analytics is installed — the shape matches
      // that call's argument exactly: ad_storage, analytics_storage,
      // ad_user_data, ad_personalization, security_storage,
      // functionality_storage and personalization_storage, each
      // 'granted' | 'denied'.
      console.log('[compliant] consent-mode signals', signals);
    },
  }}
>

A signalWriter that throws never costs the consent record — the record is already persisted before this callback runs, and CompliantConsentProvider swallows and reports the throw through onError rather than propagating it.

The provider must never be conditionally unmounted

Do not write {showConsent && <CompliantConsentProvider>...}. Mount it once and leave it mounted for the life of the app.

React Native's idiomatic reflex for "hide this" is to stop rendering it, and that reflex is wrong here. A successful consent POST fires a second persist/decision pair carrying the server's recordId and atServer, and it can arrive well after the notice has already visually hidden. A provider that unmounts on hide passes an eleven-item conformance suite and silently drops those two fields — they never reach the API a second time, and the record it produced is missing the fields renewal detection depends on. Internally the provider hides the WebView behind an always-mounted overlay View (opacity/pointerEvents), never a real RN Modal (which unmounts its children on hide), for exactly this reason.

The app key is extractable from the JS bundle

config.appKey, if supplied, is sent as X-Compliant-App on every httpRequest this SDK makes, and is issued once per registered app from the dashboard (Site → Mobile apps). It authenticates a POST as coming from a registered app surface for that site — not from a verified genuine install of that app. The key lives in the app binary (this SDK's own bundle, or the app's) and can be extracted from it by anyone who decompiles the APK/IPA or dumps the JS bundle; it is not a secret in the cryptographic sense. State that weaker claim in customer-facing material, never the stronger one. Device attestation (Apple App Attest, Google Play Integrity) would close that gap and is tracked separately (docs/superpowers/backlog/dpdp-ledger.md row 399) — not built by this package.

Reinstalling the app shows the notice again

The visitor id this SDK generates is a fresh v4 UUID created on first launch after install and stored only in the Keychain/Keystore/SecureStore surface (secureGet/secureSet), never in a backed-up location and never derived from IDFA/AAID. An uninstall clears that store, so a reinstall gets a new id and no prior decision to read — the notice shows again. This is correct behaviour, not a bug: it mirrors what a browser does when a visitor clears cookies, and avoiding it would mean either using an advertising identifier (which would make the consent record itself a tracking identifier) or storing the decision somewhere that survives an uninstall the visitor initiated on purpose.

No figure here was measured on a physical device

Every latency number this package's tests or this README could quote — timeToReady, timeToPaint, cold-WebView-boot cost — comes from a Chromium harness (tier 1) or from maestro/transport.yaml running against an iOS Simulator or an Android Emulator (tier 2), both of which run on the host machine's own CPU and are optimistic by construction. Neither this package nor mobile/ios/mobile/android has ever reported a number produced on real hardware. Re-running on physical devices — including one low-end Android phone, since Android's per-process WebView boot cost varies with hardware in a way a simulator or an emulator does not — is tracked separately (docs/superpowers/backlog/dpdp-ledger.md row 396) and blocks customer ship, not this package landing. Do not quote anything from this package as a device SLA.

Testing strategy

  • Tier 1 (npx vitest run --project mobile-react-native) — unit and component tests against a real Chromium instance (Playwright), covering everything except what only a real react-native-webview can prove.
  • Tier 2 (maestro test mobile/react-native/maestro/transport.yaml, run against example/) — four assertions on a real device/simulator/ emulator: the inlined HTML paints in a real WebView, injectJavaScript delivered init, onMessage carried the decision back, and the Keychain adapter round-tripped a visitor id with no biometric prompt. This is the only layer that exercises a genuine react-native-webview rather than Chromium — the Android shell's own plan found two product bugs invisible to all 117 of its unit/instrumented tests, because nothing in that module consumed it the way a customer does. example/ exists so this package has the same guarantee.

Publishing

This is the first package in this monorepo published as an npm tarball (docs/superpowers/backlog/dpdp-ledger.md row 397) rather than consumed by git tag or Gradle composite build. npm pack -w @privacysuraksha/react-native-consent produces a tarball whose dist/ is bundled with esbuild (scripts/build.mjs) so that @compliant/banner's private, unpublished TypeScript sources are inlined rather than left as an unresolvable import — CI's packed-tarball job extracts the packed tarball and greps dist/ to enforce this on every change.