@stackra/consent
v2.0.2
Published
Client-side GDPR/CCPA consent management for the Stackra framework — categories, reactive preferences, pluggable storage adapters, React hooks, and a HeroUI consent banner. Backend-agnostic.
Maintainers
Readme
@stackra/consent
Client-side GDPR / CCPA / COPPA consent management for the Stackra framework — consent categories, a reactive preference store, pluggable storage adapters, cross-platform React + React Native hooks, a HeroUI consent banner (web), a HeroUI Native bottom sheet + preferences screen + COPPA-compliant age gate (native), and an iOS App Tracking Transparency wrapper. Backend-agnostic.
Subpaths
| Import | Contents |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @stackra/consent | Core: ConsentModule, ConsentManager, ConsentRegistry, MemoryConsentAdapter, hooks, tokens |
| @stackra/consent/react | Web: <ConsentBanner>, useConsent, useConsentGate, useConsentBanner, LocalStorageConsentAdapter |
| @stackra/consent/native | Native: NativeConsentModule, SecureStoreConsentAdapter, AttService, <ConsentBottomSheet>, <ConsentPreferencesScreen>, <AgeGateScreen>, useConsent, useConsentBottomSheet, useAgeGate, useAttStatus |
| @stackra/consent/testing | In-memory MockConsentManager + test doubles for consumer tests |
Installation
// package.json
"@stackra/consent": "workspace:*"Quick start (web)
import { Module } from "@stackra/container";
import { ConsentModule } from "@stackra/consent";
@Module({
imports: [
ConsentModule.forRoot({
categories: [
{
slug: "necessary",
label: "Necessary",
description: "Essential cookies",
required: true,
default: true,
},
{
slug: "analytics",
label: "Analytics",
description: "Usage analytics",
required: false,
default: false,
},
],
defaultMode: "opt-in",
// Persistence: delegate to @stackra/storage. Wire
// WebStorageModule.forRoot() at the app root; pick a store
// instance name here.
storage: "localStorage",
}),
],
})
export class AppModule {}import { ConsentBanner } from "@stackra/consent/react";
import { useConsentGate } from "@stackra/consent";
function App() {
const { allowed } = useConsentGate("analytics");
return (
<>
{allowed ? <Analytics /> : null}
<ConsentBanner locale="en" />
</>
);
}Quick start (native)
import { Module } from "@stackra/container";
import { NativeStorageModule } from "@stackra/storage/native";
import { NativeConsentModule } from "@stackra/consent/native";
@Module({
imports: [
NativeStorageModule.forRoot({
default: "asyncStorage",
stores: {
asyncStorage: { driver: "asyncStorage", prefix: "app:" },
},
}),
NativeConsentModule.forRoot({
categories: [
{
slug: "analytics",
label: "Analytics",
description: "Usage analytics",
required: false,
default: false,
},
],
defaultMode: "opt-in",
// Defaults to 'asyncStorage'. Pass 'secureStore' with a
// matching driver registered on the storage manager for
// GDPR/COPPA-grade durability.
}),
],
})
export class AppModule {}import {
ConsentBottomSheet,
useConsentBottomSheet,
} from "@stackra/consent/native";
function ConsentGate() {
const { isOpen, setOpen } = useConsentBottomSheet();
return (
<ConsentBottomSheet isOpen={isOpen} onOpenChange={setOpen} locale="en" />
);
}React Native ATT setup
App Tracking Transparency is iOS 14.5+ only. @stackra/consent/native ships an
AttService that wraps expo-tracking-transparency (an OPTIONAL peer) and
fails soft on every other platform.
1. Install the peer
pnpm --filter <your-app> add expo-tracking-transparency2. Add the iOS purpose string
Every iOS app that calls ATT MUST declare NSUserTrackingUsageDescription in
Info.plist — the App Store rejects apps missing this key on iOS 14.5+.
For Expo apps, add to app.json / app.config.ts:
{
"expo": {
"plugins": [
[
"expo-tracking-transparency",
{
"userTrackingPermission": "This identifier will be used to measure how the app is used and improve your experience."
}
]
]
}
}For bare React Native apps, edit ios/<AppName>/Info.plist directly:
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to measure how the app is used and improve your experience.</string>3. Wire the ordering — request ATT BEFORE analytics
Analytics libraries (Sentry, Firebase, Segment, ...) capture the ATT decision at
their bootstrap time. The parent app owns the ordering — call
AttService.requestPermission() before you register any analytics module.
// apps/mobile/src/bootstrap.tsx
import { useAttStatus, AttStatus } from "@stackra/consent/native";
import { useEffect, useState } from "react";
export function AppBootstrap({ children }: { children: React.ReactNode }) {
const { status, isAvailable, requestPermission } = useAttStatus();
const [ready, setReady] = useState<boolean>(!isAvailable);
useEffect(() => {
if (!isAvailable) return;
if (status !== AttStatus.Unknown) {
setReady(true);
return;
}
// Fire the ATT prompt on cold start. Apple recommends a
// foreground gesture — real apps often show a "pre-prompt"
// screen with a Continue button that fires `requestPermission()`.
void requestPermission().then(() => setReady(true));
}, [status, isAvailable, requestPermission]);
if (!ready) return null; // splash / spinner
return <>{children}</>;
}4. Gate analytics on the result
import { useAttStatus, AttStatus } from "@stackra/consent/native";
function MaybeMountAnalytics() {
const { status } = useAttStatus();
if (status !== AttStatus.Authorized) return null;
return <AnalyticsProvider />;
}AttService.getStatus() and AttService.requestPermission() both resolve to
AttStatus.Authorized on non-iOS platforms and iOS < 14.5 — the gate reads as
"unrestricted" everywhere ATT doesn't apply.
SecureStore-backed persistence
For GDPR / COPPA regulated markets that require the strongest durability, wire a
secureStore driver into your NativeStorageModule and set
storage: 'secureStore' on NativeConsentModule.forRoot(...). The
SecureStoreConsentAdapter reads / writes under the fixed storage key
stackra.consent.decisions.v1.
Age gating
<AgeGateScreen> + useAgeGate() implement a COPPA / GDPR-K compliant
birthdate gate. Default minimum age is 13 (US COPPA); pass minAge={16} for
GDPR-K states.
import { AgeGateScreen, useAgeGate } from "@stackra/consent/native";
function AppWithAgeGate({ children }: { children: React.ReactNode }) {
const { isVerified } = useAgeGate({ minAge: 16 });
if (isVerified === null) return null; // storage read pending
if (!isVerified) return <AgeGateScreen minAge={16} />;
return <>{children}</>;
}The persisted record contains only the ISO date-of-birth + verification timestamp — no computed age (it would drift) and no other PII.
Lifecycle
ConsentRegistryimplementsOnModuleInitand seeds the configured categories during module init.ConsentManagerimplementsOnApplicationBootstrap— it reads the populated registry, hydrates from the storage adapter, and applies defaults after every module has initialised.
There are no bootstrap classes or side-effect provider factories.
Events
When an EVENT_EMITTER (@stackra/contracts) is registered, the manager emits
CONSENT_EVENTS (consent.granted, consent.revoked, consent.decided,
consent.preferences.updated) on a fail-open basis. The emitter is injected as
optional — the package works without it.
License
MIT © Figentra L.L.C.
