ReadyNative

Consent sheet (GDPR / CCPA)

Set up Consent sheet (GDPR / CCPA) for privacy consent in an Expo app with ReadyNative: asks before analytics + crash start (EU/EEA, UK, Switzerland…

Expo Go: yes

Runs in Expo Go; no dev build needed for this module.

This asks before anything is sent: src/lib/consent.ts implements the @/lib/consent contract on top of the core consent store, and ConsentProvider (order 85, in src/components/consent-sheet.tsx) mounts the sheet above the app. The analytics/* and crash/* modules follow the consent store from import time on and only create their SDK clients once the matching category is consented to, so PostHog, Amplitude and Sentry send nothing - no events, no flags request, no crash report - until the user says yes where the law wants that. Setup picks this option automatically whenever an analytics or crash module is selected (and none when neither is); pass --consent to override.

Setup

bun run setup --consent consent

Setup installs expo-localization (for the device region), registers ConsentProvider, and adds the sheet and its tests. The only knob is in readynative.config.ts:

privacy: { askEverywhere: false }, // true: ask every user, whatever the region

How it decides

DeviceUndecided reads asSheet
Any locale region (region setting or language region) in the EU/EEA, UK, Switzerland, Canada or Brazil, a Europe/* time zone, or no region at allfalseshown once, after onboarding
Anywhere else (California included)truenever; Settings → Privacy has switches
privacy.askEverywhere: truefalseshown to everyone

CA is Canada - a device cannot tell US states apart, so California users get the CCPA opt-out ("Do not sell or share", below) rather than a sheet.

consent.needsPrompt() is true when the stored record is not for the current CONSENT_VERSION (src/stores/consent.ts) and the device is in the ask group. Once the sheet is dismissed (any way - "Only necessary" is the default for Android back / swipe down) the record is written and it does not show again. Raise CONSENT_VERSION when the sheet asks for something new: every user in the ask group is asked again, and until they answer their old choices read as undecided.

The record persists under readynative:consent and survives sign-out (wipeLocalData() keeps it):

interface ConsentState {
  analytics: boolean | null; // null = undecided
  crash: boolean | null;
  doNotSell: boolean;
  version: number | null; // CONSENT_VERSION the answer was given to
  decidedAt: string | null; // ISO dates
  updatedAt: string | null;
  source: "sheet" | "settings" | "do-not-sell" | "legacy" | null;
}

The sheet and the Settings switches only show the categories this build has (privacyCategories in src/lib/privacy-categories.ts: analytics when an analytics module is selected, crash when a crash module is). With neither, the sheet never opens.

Usage

import { consent } from "@/lib/consent";

if (consent.get().analytics) analytics.track("paywall_seen");

const { crash } = consent.useConsent(); // reactive, inside a component
consent.set({ analytics: false }); // what the Settings switches do
consent.setDoNotSell(true); // the CCPA switch: analytics reads as off while it is on

consent.reprompt() shows the sheet now, whatever the region and the stored answer, until it's answered; Settings → Developer tools uses it.

Adding a category (say marketing): add it to ConsentCategory and ConsentState in src/stores/consent.ts, a row in consent-sheet.tsx and Settings, include it in privacyCategories, and read it where you need it.

Gotchas

  • The sheet waits 400 ms after the first screen so useOnboardingRedirect can win; on the onboarding route it never opens. ConsentProvider loads the onboarding store itself, so with onboarding/off the sheet opens on the first screen.
  • Region comes from the device (locales and time zone), not IP. A French user with a US region and a US time zone sees no sheet; the Settings switches are always there. Set privacy.askEverywhere if that trade-off is not good enough for you.
  • With AsyncStorage the consent store loads asynchronously, so events fired before it has loaded (the first screen view) are dropped rather than sent without a known answer. kv-store and MMKV load it synchronously.
  • This is a consent mechanism, not legal advice: your privacy policy still has to say what you collect (see bun run gen:privacy).

Remove it

While modules/ exists (a tree set up with --keep-modules), setup does all of it:

bun run setup --consent none --yes --keep-modules

In a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:

  1. Delete the files that are still there: src/components/__tests__/consent-sheet.test.tsx, src/components/consent-sheet.tsx, src/lib/__tests__/consent.test.ts.
  2. Replace, don't delete src/lib/consent.ts: core code imports it, so swap in the no-op version from modules/consent/none/files/ of a fresh clone of your tier repo - same exports, nothing behind them.
  3. Uninstall the dependencies: bun remove expo-localization. Keep any of expo-localization (also used by i18n/i18next, i18n/lingui, analytics/posthog) that another module you picked still needs.
  4. Drop the config plugin expo-localization from .readynative.json → modules.app.expo.plugins (that is where app.config.ts reads it from), then rebuild the dev build.
  5. Unwrap the provider: delete <ConsentProvider> and its import from src/providers.tsx.
  6. Check it: bun run typecheck and bun run lint point at anything that still imports the removed files; bun run gen:graph refreshes docs/ARCHITECTURE.md.

Reference

Everything below is generated from modules/consent/consent/module.json - the same file bun run setup reads, so it is what actually lands in your repo.

Install

bun run setup --consent consent

Module id: consent/consent (the default for this category).

Dependencies

PackageVersionKind
expo-localization~57.0.2dependency (expo install)

Config plugins

Merged into app.config.ts through .readynative.json (modules.app):

  • expo-localization

Providers

Rendered in src/providers.tsx (lower order = outermost):

OrderProviderFrom
85ConsentProvider@/components/consent-sheet

After setup

  1. Consent: test it - set the simulator region to Germany (Settings → General → Language & Region) and relaunch: the sheet appears after onboarding. Set region and time zone to the US: no sheet, and the Settings → Privacy toggles are on. privacy.askEverywhere: true in readynative.config.ts asks everyone.
  2. Consent: the sheet text is yours to edit in src/components/consent-sheet.tsx; keep 'Only necessary' as easy to reach as 'Accept all' (that is what regulators check).

Files

4 files copied to the project root
  • src/components/__tests__/consent-sheet.test.tsx
  • src/components/consent-sheet.tsx
  • src/lib/__tests__/consent.test.ts
  • src/lib/consent.ts

On this page

Get ReadyNative