@consentera/react-native-consent 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Consentera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # @consentera/react-native-consent
2
+
3
+ DPDP (India, Digital Personal Data Protection Act 2023) consent for React Native.
4
+
5
+ The app **never holds a secret key**. Your own backend holds `tiq_live_*` and
6
+ proxies to Consentera; this SDK talks to your backend, opens Consentera's hosted
7
+ notice in an in-app browser, and reads the decision back.
8
+
9
+ - **Version** 2.0.0 · **peers** react ≥18.2, react-native ≥0.73
10
+ - **Pure JS.** No native module of its own, no codegen, nothing to link.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm i @consentera/react-native-consent
16
+ # Two peers this SDK REFUSES to work without. It does not silently degrade.
17
+ npm i react-native-inappbrowser-reborn react-native-get-random-values
18
+ cd ios && pod install
19
+ ```
20
+
21
+ Add `import 'react-native-get-random-values';` as the **first line of
22
+ `index.js`**, and register your callback scheme in `AndroidManifest.xml`
23
+ (`<data android:scheme="yourapp" android:host="consent" />`) and in
24
+ `Info.plist` (`CFBundleURLTypes`). The scheme must also be registered
25
+ server-side in `ALLOWED_CALLBACK_APP_SCHEMES`, or session create is refused 400.
26
+
27
+ Expo, or any host that already has a browser: skip
28
+ `react-native-inappbrowser-reborn` and inject your own.
29
+
30
+ ```ts
31
+ import * as WebBrowser from 'expo-web-browser';
32
+ import { setInAppBrowser } from '@consentera/react-native-consent';
33
+
34
+ setInAppBrowser({
35
+ openAuth: async (url, scheme) => {
36
+ const r = await WebBrowser.openAuthSessionAsync(url, scheme);
37
+ return r.type === 'success' ? r.url : null;
38
+ },
39
+ });
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```ts
45
+ import {
46
+ ConsenteraSession,
47
+ ConsentGate,
48
+ type PrincipalRef,
49
+ } from '@consentera/react-native-consent';
50
+
51
+ const session = new ConsenteraSession({
52
+ // YOUR backend's route to Consentera. Required — there is no default server,
53
+ // and a blank or scheme-less value throws ConsenteraError
54
+ // (code BACKEND_BASE_URL_REQUIRED).
55
+ backendBaseUrl: 'https://api.yourbank.in/consentera',
56
+ callbackScheme: 'yourapp',
57
+ });
58
+
59
+ // The ONE shape the lifecycle roads accept: an OPEN map keyed by YOUR
60
+ // organisation's locked integration key (F015), the same shape and the same
61
+ // vocabulary `dataPrincipal` uses on create. A bare string
62
+ // (`data_principal_ref`) is refused outright by the API.
63
+ const who: PrincipalRef = { dataPrincipalIdentifiers: { email: 'riya@example.in' } };
64
+
65
+ // 1 — create the session through YOUR backend
66
+ const s = await session.createSession({
67
+ // Keyed by YOUR organisation's locked integration key — the SAME vocabulary
68
+ // the lifecycle roads take. The mobile atom is `mobile` on both.
69
+ dataPrincipal: { email: 'riya@example.in' },
70
+ noticeInternalName: 'your_notice_code',
71
+ dateOfBirth: '1998-04-12', // omit and the person's age is UNKNOWN
72
+ });
73
+ if (s.warnings?.length) console.log(s.warnings); // arrive with a 200
74
+ if (s.guardianVerification) { /* §9(1): a child, invitation sent, not yet consented */ }
75
+
76
+ // 2 — present the hosted notice in the in-app browser and check the callback
77
+ const returned = await session.presentConsent(s);
78
+ if (returned) {
79
+ const cb = session.parseCallback(returned, s.callbackState ?? null);
80
+ // cb is a HINT. It proves the callback came back to the deep link this SDK
81
+ // asked for with the nonce this SDK generated — not that anyone consented.
82
+ //
83
+ // The platform returns
84
+ // <callback>?artifact_id=…&pending=1&session_id=…&state=…&status=granted|partial|denied
85
+ // cb.callbackStatus is 'granted' | 'partial' | 'denied', else 'unknown' (never
86
+ // a grant). cb.pending is true when the record is still being written: your
87
+ // backend's read-back may answer 202 + Retry-After — wait, do not conclude
88
+ // "no consent". Confirm by reading the consent back through your backend.
89
+ }
90
+
91
+ // 3 — validate. THIS is the only source of consent truth, and it is also what
92
+ // covers the person who simply closed the tab (no callback ever arrives).
93
+ const d = await session.validate(who, 'product_analytics');
94
+ if (d.decision === 'ALLOW') { /* process */ }
95
+
96
+ // 4 — withdrawal must be as easy as giving (DPDP §6(4))
97
+ await session.withdraw(who, ['product_analytics']);
98
+
99
+ // 5 — the full rights portal (access, erasure, nomination, grievance), one tap.
100
+ // The same identifier map as everywhere else; the key is the kind.
101
+ await session.openPortal({ email: 'riya@example.in' });
102
+ ```
103
+
104
+ Re-validate on `AppState` `'active'`: Chrome blocks gesture-less custom-scheme
105
+ redirects, so a person who closes the tab produces **no callback at all**.
106
+
107
+ ## One identifier vocabulary (F015)
108
+
109
+ `dataPrincipal` on create and the lifecycle roads' identifiers are the **same
110
+ open map**, keyed by the fields your organisation locked. This SDK does **not**
111
+ allow-list them: the admissible set is a per-tenant fact the platform reads at
112
+ request time, so send what your key defines and let the server answer.
113
+
114
+ | | |
115
+ |---|---|
116
+ | the mobile atom | **`mobile`** on both roads. `phone` is refused **by name** — `UNKNOWN_IDENTIFIER_FIELD`, whose message says "…use mobile". |
117
+ | `pan` | **not a scheme field at all.** It is evidence-class and can never be one. |
118
+ | the portal (`openPortal`) | **the same map**, and the key is the kind: `{ mobile: '+91…' }` is sent as a `mobile` reference, never as an email. The portal takes ONE identifier: pass one, or set `identifierScheme` so the SDK can choose. `phone` is refused before the wire. |
119
+ | `aadhaar` | the Aadhaar-**linked token**, never the number. A raw 12-digit value is refused — but **the code differs by road**: `INVALID_IDENTIFIER_FORMAT` on the lifecycle roads (F015 folded the old token into it there), and still `AADHAAR_RAW_REFUSED` on session create. Switch on both, or on the 400 alone. |
120
+
121
+ The four refusals you can switch on: `UNKNOWN_IDENTIFIER_FIELD` (names the field
122
+ you sent and the ones that are allowed), `IDENTIFIER_REQUIRED` (you named
123
+ nobody), `INVALID_IDENTIFIER_FORMAT` (right field, wrong value — this is also where a raw
124
+ Aadhaar number lands on these roads), and `SCHEME_NOT_CONFIGURED` (this tenant has no locked key yet — an onboarding
125
+ problem, not a request problem).
126
+
127
+ Only the wire KEY differs between the two roads: `data_principal` on create may
128
+ mint a person, `data_principal_identifiers` on the lifecycle roads resolves only.
129
+
130
+ ## ConsentGate — the mobile equivalent of cookie consent
131
+
132
+ ```ts
133
+ const gate = new ConsentGate(session, who);
134
+ gate.register({
135
+ purposeCode: 'product_analytics',
136
+ name: 'Analytics SDK',
137
+ init: () => analytics.start(),
138
+ revoke: () => { analytics.stop(); analytics.clearIdentifiers(); },
139
+ });
140
+ await gate.refresh(); // on start and on every resume
141
+ ```
142
+
143
+ `init` runs at most once per allowed-transition; `revoke` runs when a purpose
144
+ stops validating. Validation failures are **fail-closed**. Never "initialise
145
+ then opt out".
146
+
147
+ ## What this SDK will not do
148
+
149
+ It **refuses rather than degrading**, in four places, and each refusal is a
150
+ decision rather than an omission:
151
+
152
+ | Missing | It does | It will not |
153
+ |---|---|---|
154
+ | in-app browser | throws with the install line | fall back to `Linking.openURL` — the external browser leaves the callback to any app that claims the scheme |
155
+ | CSPRNG | throws | fall back to `Math.random` for the callback nonce |
156
+ | callback whose scheme, host, path or `state` does not match | throws | accept it |
157
+ | a withdrawal response it cannot decode | throws | report success |
158
+
159
+ It has **no advertising function**: no advertising identifier, no IABTCF keys,
160
+ no ATT. That is a separate concern and would be a separate package.
161
+
162
+ ## Errors
163
+
164
+ Every failure is a `ConsenteraError` with `status`, `code` (the platform's
165
+ canonical code), `platformMessage`, `requestId` and `retryable`.
166
+
167
+ `platformMessage` is the platform's own sentence for a refusal, verbatim, for
168
+ example *"\"phone\" is not one of this organisation's identifier fields —
169
+ send email"*. Show it to the person: it says **why**. It is kept out of
170
+ `message`, as is the rest of the response body, because a body can carry the
171
+ person's own identifiers and `message` ends up wherever you log.
172
+
173
+ ```ts
174
+ try { await session.validate(who, 'product_analytics'); }
175
+ catch (e) {
176
+ if (e instanceof ConsenteraError) show(e.platformMessage ?? 'Something went wrong');
177
+ report({ code: e.code, status: e.status, requestId: e.requestId }); // for support
178
+ }
179
+ ```
180
+
181
+ Mutations carry an `Idempotency-Key` minted once per operation and reused across
182
+ retries; 429 and 5xx are retried with full jitter, honouring `Retry-After`; a
183
+ 4xx is never retried.
184
+
185
+ ## Configuration
186
+
187
+ | Option | Default | |
188
+ |---|---|---|
189
+ | `requestTimeoutMs` | 30000 | per ATTEMPT |
190
+ | `totalTimeoutMs` | 2× the above | ceiling on the whole call including retries |
191
+ | `maxAttempts` | 3 | including the first; 1 disables retrying |
192
+ | `identifierScheme` | — | your integration key's fields, in the order `openPortal` should prefer when you pass several identifiers |
193
+ | `retryBaseDelayMs` | 250 | full-jitter base |
194
+ | `onDiagnostic` | — | opt-in sink; the SDK is otherwise SILENT |
195
+ | `callbackHost` / `callbackPath` | `consent` / `/callback` | both are CHECKED on the callback |
196
+
197
+ ## Support
198
+
199
+ Security issues: see `SECURITY.md` at the repository root.
200
+ Full integration guide: <https://docs.consentera.in/docs/developer/df-integration/apps/mobile-03-react-native>
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ConsentGate = void 0;
7
+ /**
8
+ * ConsentGate — the mobile equivalent of cookie consent (RN twin of the
9
+ * Android SDK's gate package). Gate third-party SDKs (analytics, ads,
10
+ * attribution) and advertising identifiers on the DP's live consent state:
11
+ * `init` runs only after the purpose validates ALLOW; `revoke` runs when a
12
+ * previously-allowed purpose stops validating (stop the SDK, flush queues,
13
+ * clear stored identifiers). Never "initialize then opt out".
14
+ *
15
+ * Call refresh() on app start and whenever the app returns to the foreground
16
+ * (AppState 'active') — the deep-link callback is best-effort only.
17
+ */
18
+
19
+ class ConsentGate {
20
+ trackers = [];
21
+ started = new Set();
22
+ listeners = new Set();
23
+ states = [];
24
+
25
+ /**
26
+ * @param who the person this gate is about, in the ONE shape the lifecycle
27
+ * roads accept. It was a bare `dataPrincipalRef: string` until 2026-09-22,
28
+ * which no longer type-checked against `validate` (tsc: "Argument of type
29
+ * 'string' is not assignable to parameter of type 'PrincipalRef'") and could
30
+ * not express the identifiers the API now requires.
31
+ */
32
+ constructor(session, who) {
33
+ this.session = session;
34
+ this.who = who;
35
+ }
36
+ register(tracker) {
37
+ this.trackers.push(tracker);
38
+ this.states = [...this.states, {
39
+ name: tracker.name,
40
+ purposeCode: tracker.purposeCode,
41
+ allowed: false
42
+ }];
43
+ }
44
+ onChange(fn) {
45
+ this.listeners.add(fn);
46
+ return () => this.listeners.delete(fn);
47
+ }
48
+
49
+ /** Re-validate every registered purpose; fail-closed on errors. */
50
+ async refresh() {
51
+ const next = [];
52
+ for (const t of this.trackers) {
53
+ let allowed = false;
54
+ try {
55
+ const d = await this.session.validate(this.who, t.purposeCode);
56
+ allowed = (d.decision ?? '').toUpperCase() === 'ALLOW';
57
+ } catch {
58
+ allowed = false;
59
+ }
60
+ const wasStarted = this.started.has(t.name);
61
+ if (allowed && !wasStarted) {
62
+ try {
63
+ t.init();
64
+ } catch {/* tracker init must never crash the app */}
65
+ this.started.add(t.name);
66
+ } else if (!allowed && wasStarted) {
67
+ try {
68
+ t.revoke();
69
+ } catch {/* ditto */}
70
+ this.started.delete(t.name);
71
+ }
72
+ next.push({
73
+ name: t.name,
74
+ purposeCode: t.purposeCode,
75
+ allowed
76
+ });
77
+ }
78
+ this.states = next;
79
+ this.listeners.forEach(fn => fn(next));
80
+ return next;
81
+ }
82
+ isAllowed(purposeCode) {
83
+ return this.states.some(s => s.purposeCode === purposeCode && s.allowed);
84
+ }
85
+ }
86
+ exports.ConsentGate = ConsentGate;
87
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ConsentGate","trackers","started","Set","listeners","states","constructor","session","who","register","tracker","push","name","purposeCode","allowed","onChange","fn","add","delete","refresh","next","t","d","validate","decision","toUpperCase","wasStarted","has","init","revoke","forEach","isAllowed","some","s","exports"],"sourceRoot":"../../src","sources":["gate.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAcO,MAAMA,WAAW,CAAC;EACfC,QAAQ,GAAc,EAAE;EACxBC,OAAO,GAAG,IAAIC,GAAG,CAAS,CAAC;EAC3BC,SAAS,GAAG,IAAID,GAAG,CAAgC,CAAC;EAC5DE,MAAM,GAAgB,EAAE;;EAExB;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAASC,OAA0B,EAAUC,GAAiB,EAAE;IAAA,KAAvDD,OAA0B,GAA1BA,OAA0B;IAAA,KAAUC,GAAiB,GAAjBA,GAAiB;EAAG;EAE5EC,QAAQA,CAACC,OAAgB,EAAQ;IAC/B,IAAI,CAACT,QAAQ,CAACU,IAAI,CAACD,OAAO,CAAC;IAC3B,IAAI,CAACL,MAAM,GAAG,CAAC,GAAG,IAAI,CAACA,MAAM,EAAE;MAAEO,IAAI,EAAEF,OAAO,CAACE,IAAI;MAAEC,WAAW,EAAEH,OAAO,CAACG,WAAW;MAAEC,OAAO,EAAE;IAAM,CAAC,CAAC;EAC1G;EAEAC,QAAQA,CAACC,EAAiC,EAAc;IACtD,IAAI,CAACZ,SAAS,CAACa,GAAG,CAACD,EAAE,CAAC;IACtB,OAAO,MAAM,IAAI,CAACZ,SAAS,CAACc,MAAM,CAACF,EAAE,CAAC;EACxC;;EAEA;EACA,MAAMG,OAAOA,CAAA,EAAyB;IACpC,MAAMC,IAAiB,GAAG,EAAE;IAC5B,KAAK,MAAMC,CAAC,IAAI,IAAI,CAACpB,QAAQ,EAAE;MAC7B,IAAIa,OAAO,GAAG,KAAK;MACnB,IAAI;QACF,MAAMQ,CAAC,GAAG,MAAM,IAAI,CAACf,OAAO,CAACgB,QAAQ,CAAC,IAAI,CAACf,GAAG,EAAEa,CAAC,CAACR,WAAW,CAAC;QAC9DC,OAAO,GAAG,CAACQ,CAAC,CAACE,QAAQ,IAAI,EAAE,EAAEC,WAAW,CAAC,CAAC,KAAK,OAAO;MACxD,CAAC,CAAC,MAAM;QACNX,OAAO,GAAG,KAAK;MACjB;MACA,MAAMY,UAAU,GAAG,IAAI,CAACxB,OAAO,CAACyB,GAAG,CAACN,CAAC,CAACT,IAAI,CAAC;MAC3C,IAAIE,OAAO,IAAI,CAACY,UAAU,EAAE;QAC1B,IAAI;UAAEL,CAAC,CAACO,IAAI,CAAC,CAAC;QAAE,CAAC,CAAC,MAAM,CAAE;QAC1B,IAAI,CAAC1B,OAAO,CAACe,GAAG,CAACI,CAAC,CAACT,IAAI,CAAC;MAC1B,CAAC,MAAM,IAAI,CAACE,OAAO,IAAIY,UAAU,EAAE;QACjC,IAAI;UAAEL,CAAC,CAACQ,MAAM,CAAC,CAAC;QAAE,CAAC,CAAC,MAAM,CAAE;QAC5B,IAAI,CAAC3B,OAAO,CAACgB,MAAM,CAACG,CAAC,CAACT,IAAI,CAAC;MAC7B;MACAQ,IAAI,CAACT,IAAI,CAAC;QAAEC,IAAI,EAAES,CAAC,CAACT,IAAI;QAAEC,WAAW,EAAEQ,CAAC,CAACR,WAAW;QAAEC;MAAQ,CAAC,CAAC;IAClE;IACA,IAAI,CAACT,MAAM,GAAGe,IAAI;IAClB,IAAI,CAAChB,SAAS,CAAC0B,OAAO,CAAEd,EAAE,IAAKA,EAAE,CAACI,IAAI,CAAC,CAAC;IACxC,OAAOA,IAAI;EACb;EAEAW,SAASA,CAAClB,WAAmB,EAAW;IACtC,OAAO,IAAI,CAACR,MAAM,CAAC2B,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACpB,WAAW,KAAKA,WAAW,IAAIoB,CAAC,CAACnB,OAAO,CAAC;EAC5E;AACF;AAACoB,OAAA,CAAAlC,WAAA,GAAAA,WAAA","ignoreList":[]}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "ConsentGate", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _gate.ConsentGate;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "ConsenteraError", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _session.ConsenteraError;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "ConsenteraSession", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _session.ConsenteraSession;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "SDK_IDENTIFIER", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _session.SDK_IDENTIFIER;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "SDK_USER_AGENT", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _session.SDK_USER_AGENT;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "SDK_VERSION", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _session.SDK_VERSION;
40
+ }
41
+ });
42
+ Object.defineProperty(exports, "callbackStatusOf", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _session.callbackStatusOf;
46
+ }
47
+ });
48
+ Object.defineProperty(exports, "callbackUrlFor", {
49
+ enumerable: true,
50
+ get: function () {
51
+ return _session.callbackUrlFor;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, "newCallbackState", {
55
+ enumerable: true,
56
+ get: function () {
57
+ return _session.newCallbackState;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, "parseRetryAfter", {
61
+ enumerable: true,
62
+ get: function () {
63
+ return _session.parseRetryAfter;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, "portalIdentifier", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _session.portalIdentifier;
70
+ }
71
+ });
72
+ Object.defineProperty(exports, "readErrorEnvelope", {
73
+ enumerable: true,
74
+ get: function () {
75
+ return _session.readErrorEnvelope;
76
+ }
77
+ });
78
+ Object.defineProperty(exports, "resolveInAppBrowser", {
79
+ enumerable: true,
80
+ get: function () {
81
+ return _session.resolveInAppBrowser;
82
+ }
83
+ });
84
+ Object.defineProperty(exports, "setInAppBrowser", {
85
+ enumerable: true,
86
+ get: function () {
87
+ return _session.setInAppBrowser;
88
+ }
89
+ });
90
+ var _session = require("./session");
91
+ var _gate = require("./gate");
92
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_session","require","_gate"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,IAAAA,QAAA,GAAAC,OAAA;AA4BA,IAAAC,KAAA,GAAAD,OAAA","ignoreList":[]}