@redacto.io/consent-sdk-react-native 4.0.1-beta.0 → 5.0.0-beta.1

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/README.md CHANGED
@@ -136,6 +136,7 @@ import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react-native";
136
136
  | `accessToken` | `string` | ✓ | JWT access token |
137
137
  | `refreshToken` | `string` | ✓ | JWT refresh token |
138
138
  | `baseUrl` | `string` | | Override API base URL |
139
+ | `ledgerBaseUrl` | `string` | | Go ledger base URL. When set, consent writes and prior-consent checks route through the ledger (falls back: ledger → baseUrl → default). Omit to keep the legacy consent-server path. |
139
140
  | `settings` | `Partial<Settings>` | | UI customisation (colours, border radius, fonts) |
140
141
  | `language` | `string` | | Initial language code (e.g. `"en"`) |
141
142
  | `blockUI` | `boolean` | | Block the modal from being dismissed |
@@ -173,6 +174,7 @@ import { RedactoNoticeConsentInline } from "@redacto.io/consent-sdk-react-native
173
174
  | `notice_uuid` | `string` | ✓ | Notice UUID |
174
175
  | `accessToken` | `string` | | JWT access token (triggers auto-submit when set) |
175
176
  | `baseUrl` | `string` | | Override API base URL |
177
+ | `ledgerBaseUrl` | `string` | | Go ledger base URL. When set, consent writes and already-consented checks route through the ledger. Omit for legacy behavior. |
176
178
  | `language` | `string` | | Language code |
177
179
  | `settings` | `Partial<Settings>` | | UI customisation |
178
180
  | `applicationId` | `string` | | Application-specific UUID |
@@ -217,6 +219,7 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react-native/priva
217
219
  | Prop | Type | Required | Default | Description |
218
220
  | -------------- | ----------------------------------------------------------------------------- | -------- | -------------------- | ---------------------------------------- |
219
221
  | `baseUrl` | `string` | ✓ | - | Consent server base URL |
222
+ | `ledgerBaseUrl` | `string` | | - | Go ledger base URL. When set, user consents, group purposes, receipts, and manage-consent route through the ledger. Omit to use the legacy `/dsar/privacy-center/*` routes. See the note below on the DSAR form picker. |
220
223
  | `slug` | `string` | ✓ | - | Workspace slug |
221
224
  | `accessToken` | `string` | ✓ | - | JWT access token |
222
225
  | `refreshToken` | `string` | ✓ | - | JWT refresh token |
@@ -274,6 +277,18 @@ For consumers who want fully custom UI, the headless core exposes the API client
274
277
  - Keep API keys and server secrets on your backend only.
275
278
  - In Expo apps, values read from `EXPO_PUBLIC_*` are bundled into the app and should only contain non-sensitive config like UUIDs and URLs.
276
279
  - If you do not pass `baseUrl`, the SDK defaults to `https://api.redacto.io/consent` for both notice and Privacy Center surfaces.
280
+ - `ledgerBaseUrl` is optional and independent of `baseUrl` — pass both when routing consent through the Go ledger. Omitting it keeps the legacy consent-server behavior.
281
+
282
+ ## Ledger mode
283
+
284
+ Passing `ledgerBaseUrl` routes consent-bearing reads and writes to the Go ledger:
285
+
286
+ - **Notice components** — `submit-consent` goes to the ledger, and the already-consented check uses the ledger's `check-consent` endpoint instead of the legacy HTTP 409 signal. Notice content, tokens, and TTS always stay on `baseUrl`.
287
+ - **Privacy Center** — user consents, group purposes, receipts (including the `/download` PDF path), and manage-consent (sent as a `partial: true` write) route to the ledger. DSAR form, cases, activities, OTP, and notifications stay on `baseUrl`.
288
+
289
+ Every ledger read degrades to the legacy path on failure, so a ledger outage does not break the consent flow.
290
+
291
+ > **Known limitation.** Unlike the React SDK, the DSAR form's product picker is **not** overlaid from ledger-served consents. Against a workspace cut over to the ledger — where the legacy `form/data` response carries no product groups — the picker renders empty. If your DSAR form depends on the product picker, stay on the legacy path until this is addressed.
277
292
 
278
293
  ## License
279
294
 
package/dist/index.d.mts CHANGED
@@ -31,6 +31,8 @@ type Props$1 = Readonly<{
31
31
  accessToken: string;
32
32
  refreshToken: string;
33
33
  baseUrl?: string;
34
+ /** Go ledger base URL — serves check-consent and the consent write; omit to use the legacy consent server. */
35
+ ledgerBaseUrl?: string;
34
36
  settings?: Partial<Settings$1>;
35
37
  language?: string;
36
38
  blockUI?: boolean;
@@ -77,6 +79,7 @@ declare const RedactoConsentInline: React__default.NamedExoticComponent<Readonly
77
79
  accessToken?: string;
78
80
  refreshToken?: string;
79
81
  baseUrl?: string;
82
+ ledgerBaseUrl?: string;
80
83
  settings?: Partial<Settings>;
81
84
  language?: string;
82
85
  onAccept?: () => void;
@@ -93,6 +96,8 @@ type Props = Readonly<{
93
96
  accessToken?: string;
94
97
  refreshToken?: string;
95
98
  baseUrl?: string;
99
+ /** Go ledger base URL — serves check-consent and the consent write; omit to use the legacy consent server. */
100
+ ledgerBaseUrl?: string;
96
101
  settings?: Partial<Settings>;
97
102
  language?: string;
98
103
  onAccept?: () => void;
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ type Props$1 = Readonly<{
31
31
  accessToken: string;
32
32
  refreshToken: string;
33
33
  baseUrl?: string;
34
+ /** Go ledger base URL — serves check-consent and the consent write; omit to use the legacy consent server. */
35
+ ledgerBaseUrl?: string;
34
36
  settings?: Partial<Settings$1>;
35
37
  language?: string;
36
38
  blockUI?: boolean;
@@ -77,6 +79,7 @@ declare const RedactoConsentInline: React__default.NamedExoticComponent<Readonly
77
79
  accessToken?: string;
78
80
  refreshToken?: string;
79
81
  baseUrl?: string;
82
+ ledgerBaseUrl?: string;
80
83
  settings?: Partial<Settings>;
81
84
  language?: string;
82
85
  onAccept?: () => void;
@@ -93,6 +96,8 @@ type Props = Readonly<{
93
96
  accessToken?: string;
94
97
  refreshToken?: string;
95
98
  baseUrl?: string;
99
+ /** Go ledger base URL — serves check-consent and the consent write; omit to use the legacy consent server. */
100
+ ledgerBaseUrl?: string;
96
101
  settings?: Partial<Settings>;
97
102
  language?: string;
98
103
  onAccept?: () => void;
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ var fetchConsentContent = async ({
147
147
  var submitConsentEvent = async ({
148
148
  accessToken,
149
149
  baseUrl,
150
+ ledgerBaseUrl,
150
151
  noticeUuid,
151
152
  purposes,
152
153
  declined,
@@ -166,7 +167,7 @@ var submitConsentEvent = async ({
166
167
  if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
167
168
  throw new Error("Invalid token: missing organization or workspace UUID");
168
169
  }
169
- const apiBaseUrl = baseUrl || DEFAULT_CONSENT_API_BASE_URL;
170
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || DEFAULT_CONSENT_API_BASE_URL;
170
171
  const validatedPurposes = purposes.map((purpose) => {
171
172
  if (!purpose.uuid) {
172
173
  throw new Error("Invalid purpose data: missing uuid");
@@ -415,6 +416,122 @@ var verifyGuardianStatus = async ({
415
416
  };
416
417
  };
417
418
 
419
+ // src/shared/notice-consent-status.ts
420
+ var import_jwt_decode2 = require("jwt-decode");
421
+ var decodeNoticeConsentToken = (token) => {
422
+ try {
423
+ return (0, import_jwt_decode2.jwtDecode)(token);
424
+ } catch {
425
+ return null;
426
+ }
427
+ };
428
+ var fetchNoticeConsentStatusForScope = async ({
429
+ accessToken,
430
+ ledgerBaseUrl,
431
+ organisationUuid,
432
+ workspaceUuid,
433
+ noticeUuid,
434
+ signal
435
+ }) => {
436
+ if (!accessToken || !ledgerBaseUrl || !organisationUuid || !workspaceUuid || !noticeUuid) {
437
+ return null;
438
+ }
439
+ try {
440
+ const response = await fetch(
441
+ `${ledgerBaseUrl}/public/organisations/${organisationUuid}/workspaces/${workspaceUuid}/notices/${noticeUuid}/check-consent`,
442
+ {
443
+ method: "POST",
444
+ headers: {
445
+ Authorization: `Bearer ${accessToken}`,
446
+ "Content-Type": "application/json"
447
+ },
448
+ body: "{}",
449
+ signal
450
+ }
451
+ );
452
+ if (!response.ok) {
453
+ return null;
454
+ }
455
+ const data = await response.json();
456
+ const body = data && typeof data === "object" && "detail" in data && data.detail ? data.detail : data;
457
+ if (!body || !Array.isArray(body.purposes)) {
458
+ return null;
459
+ }
460
+ return body;
461
+ } catch {
462
+ return null;
463
+ }
464
+ };
465
+ var fetchNoticeConsentStatus = async ({
466
+ accessToken,
467
+ ledgerBaseUrl,
468
+ noticeUuid,
469
+ signal
470
+ }) => {
471
+ if (!accessToken || !ledgerBaseUrl || !noticeUuid) {
472
+ return null;
473
+ }
474
+ const decodedToken = decodeNoticeConsentToken(accessToken);
475
+ if (!decodedToken) {
476
+ return null;
477
+ }
478
+ const {
479
+ organisation_uuid: ORGANISATION_UUID,
480
+ workspace_uuid: WORKSPACE_UUID
481
+ } = decodedToken;
482
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
483
+ return null;
484
+ }
485
+ return fetchNoticeConsentStatusForScope({
486
+ accessToken,
487
+ ledgerBaseUrl,
488
+ organisationUuid: ORGANISATION_UUID,
489
+ workspaceUuid: WORKSPACE_UUID,
490
+ noticeUuid,
491
+ signal
492
+ });
493
+ };
494
+ var isNoticeFullyConsented = (status) => status.consented && status.all_mandatory_active;
495
+ var buildLedgerConsentOverlay = (status, configPurposes) => {
496
+ var _a, _b;
497
+ const byUuid = new Map(status.purposes.map((p) => [p.uuid, p]));
498
+ const hasHistory = status.purposes.some(
499
+ (p) => (p.status || "INACTIVE") !== "INACTIVE"
500
+ );
501
+ if (!hasHistory) {
502
+ return null;
503
+ }
504
+ const purpose_selections = {};
505
+ for (const cp of configPurposes) {
506
+ const lp = byUuid.get(cp.uuid);
507
+ const purposeStatus = (lp == null ? void 0 : lp.status) || "INACTIVE";
508
+ const selectedByUuid = new Map(
509
+ ((lp == null ? void 0 : lp.data_elements) || []).map((de) => [de.uuid, de.selected])
510
+ );
511
+ const data_elements = {};
512
+ for (const de of cp.data_elements) {
513
+ data_elements[de.uuid] = {
514
+ selected: (_a = selectedByUuid.get(de.uuid)) != null ? _a : false,
515
+ enabled: de.enabled,
516
+ required: de.required
517
+ };
518
+ }
519
+ purpose_selections[cp.uuid] = {
520
+ selected: (_b = lp == null ? void 0 : lp.selected) != null ? _b : false,
521
+ status: purposeStatus,
522
+ needs_reconsent: purposeStatus !== "ACTIVE",
523
+ data_elements
524
+ };
525
+ }
526
+ const reconsent_required = configPurposes.some(
527
+ (cp) => {
528
+ var _a2;
529
+ return (((_a2 = purpose_selections[cp.uuid]) == null ? void 0 : _a2.status) || "INACTIVE") !== "ACTIVE";
530
+ }
531
+ );
532
+ return { reconsent_required, purpose_selections };
533
+ };
534
+
418
535
  // src/RedactoNoticeConsent/components/Checkbox.tsx
419
536
  var import_react_native = require("react-native");
420
537
  var import_react_native_svg = __toESM(require("react-native-svg"));
@@ -1638,6 +1755,7 @@ var RedactoNoticeConsent = ({
1638
1755
  accessToken,
1639
1756
  refreshToken: _refreshToken,
1640
1757
  baseUrl,
1758
+ ledgerBaseUrl,
1641
1759
  settings,
1642
1760
  language = "en",
1643
1761
  blockUI = false,
@@ -1697,6 +1815,7 @@ var RedactoNoticeConsent = ({
1697
1815
  const abortControllerRef = (0, import_react2.useRef)(null);
1698
1816
  const hasBootstrappedLanguageRef = (0, import_react2.useRef)(false);
1699
1817
  const fetchContent = (0, import_react2.useCallback)(async () => {
1818
+ var _a2, _b2;
1700
1819
  if (abortControllerRef.current) {
1701
1820
  abortControllerRef.current.abort();
1702
1821
  }
@@ -1715,8 +1834,23 @@ var RedactoNoticeConsent = ({
1715
1834
  signal: abortControllerRef.current.signal
1716
1835
  });
1717
1836
  const activeConfig2 = data.detail.active_config;
1718
- const purposeSelections = data.detail.purpose_selections;
1719
- const reconsentRequired = data.detail.reconsent_required || false;
1837
+ let purposeSelections = data.detail.purpose_selections;
1838
+ let reconsentRequired = data.detail.reconsent_required || false;
1839
+ if (ledgerBaseUrl && !applicationId) {
1840
+ const ledgerStatus = await fetchNoticeConsentStatus({
1841
+ accessToken,
1842
+ ledgerBaseUrl,
1843
+ noticeUuid: noticeId,
1844
+ signal: (_a2 = abortControllerRef.current) == null ? void 0 : _a2.signal
1845
+ });
1846
+ if (ledgerStatus) {
1847
+ const overlay = buildLedgerConsentOverlay(ledgerStatus, activeConfig2.purposes);
1848
+ reconsentRequired = (_b2 = overlay == null ? void 0 : overlay.reconsent_required) != null ? _b2 : false;
1849
+ purposeSelections = overlay == null ? void 0 : overlay.purpose_selections;
1850
+ data.detail.purpose_selections = purposeSelections;
1851
+ data.detail.reconsent_required = reconsentRequired;
1852
+ }
1853
+ }
1720
1854
  const noticeIsMinor = data.detail.is_minor || false;
1721
1855
  setIsMinor(noticeIsMinor);
1722
1856
  const initialCollapsed = {};
@@ -1729,9 +1863,9 @@ var RedactoNoticeConsent = ({
1729
1863
  const purposeSelected = sel ? sel.selected : false;
1730
1864
  initialPurposeState[purpose.uuid] = purposeSelected;
1731
1865
  purpose.data_elements.forEach((el) => {
1732
- var _a2, _b2, _c2;
1866
+ var _a3, _b3, _c2;
1733
1867
  const combinedId = `${purpose.uuid}-${el.uuid}`;
1734
- const elementSel = (_c2 = (_b2 = (_a2 = sel == null ? void 0 : sel.data_elements) == null ? void 0 : _a2[el.uuid]) == null ? void 0 : _b2.selected) != null ? _c2 : false;
1868
+ const elementSel = (_c2 = (_b3 = (_a3 = sel == null ? void 0 : sel.data_elements) == null ? void 0 : _a3[el.uuid]) == null ? void 0 : _b3.selected) != null ? _c2 : false;
1735
1869
  initialDataElementState[combinedId] = elementSel;
1736
1870
  initialDataElementSelectionsMap[combinedId] = elementSel;
1737
1871
  });
@@ -1779,7 +1913,7 @@ var RedactoNoticeConsent = ({
1779
1913
  } finally {
1780
1914
  setIsLoading(false);
1781
1915
  }
1782
- }, [noticeId, accessToken, baseUrl, validateAgainst, includeFullyConsentedData, applicationId, onAccept, onError]);
1916
+ }, [noticeId, accessToken, baseUrl, ledgerBaseUrl, validateAgainst, includeFullyConsentedData, applicationId, onAccept, onError]);
1783
1917
  (0, import_react2.useEffect)(() => {
1784
1918
  hasBootstrappedLanguageRef.current = false;
1785
1919
  }, [noticeId, accessToken]);
@@ -2182,6 +2316,7 @@ var RedactoNoticeConsent = ({
2182
2316
  await submitConsentEvent({
2183
2317
  accessToken,
2184
2318
  baseUrl,
2319
+ ledgerBaseUrl,
2185
2320
  noticeUuid: activeConfig2.notice_uuid,
2186
2321
  purposes: activeConfig2.purposes.map((purpose) => ({
2187
2322
  ...purpose,
@@ -2209,7 +2344,7 @@ var RedactoNoticeConsent = ({
2209
2344
  } finally {
2210
2345
  setIsSubmitting(false);
2211
2346
  }
2212
- }, [content, accessToken, baseUrl, applicationId, selectedPurposes, selectedDataElements, stopAudio, onAccept, onError]);
2347
+ }, [content, accessToken, baseUrl, ledgerBaseUrl, applicationId, selectedPurposes, selectedDataElements, stopAudio, onAccept, onError]);
2213
2348
  const handleDecline = (0, import_react2.useCallback)(() => {
2214
2349
  stopAudio();
2215
2350
  stopPolling();
@@ -2857,13 +2992,14 @@ var submitConsentEvent2 = async ({
2857
2992
  workspace_uuid,
2858
2993
  accessToken,
2859
2994
  baseUrl,
2995
+ ledgerBaseUrl,
2860
2996
  noticeUuid,
2861
2997
  purposes,
2862
2998
  declined,
2863
2999
  meta_data
2864
3000
  }) => {
2865
3001
  var _a, _b;
2866
- const apiBaseUrl = baseUrl || DEFAULT_CONSENT_API_BASE_URL;
3002
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || DEFAULT_CONSENT_API_BASE_URL;
2867
3003
  const payload = {
2868
3004
  notice_uuid: noticeUuid,
2869
3005
  purposes: purposes.map((purpose) => {
@@ -3185,6 +3321,7 @@ var RedactoConsentInlineComponent = ({
3185
3321
  notice_uuid,
3186
3322
  accessToken,
3187
3323
  baseUrl,
3324
+ ledgerBaseUrl,
3188
3325
  language = "en",
3189
3326
  onAccept,
3190
3327
  onDecline: _onDecline,
@@ -3203,6 +3340,9 @@ var RedactoConsentInlineComponent = ({
3203
3340
  const [selectedPurposes, setSelectedPurposes] = (0, import_react3.useState)({});
3204
3341
  const [selectedDataElements, setSelectedDataElements] = (0, import_react3.useState)({});
3205
3342
  const [hasAlreadyConsented, setHasAlreadyConsented] = (0, import_react3.useState)(false);
3343
+ const [isCheckingConsent, setIsCheckingConsent] = (0, import_react3.useState)(
3344
+ Boolean(ledgerBaseUrl) && !applicationId
3345
+ );
3206
3346
  const [fetchError, setFetchError] = (0, import_react3.useState)(null);
3207
3347
  const [errorMessage, setErrorMessage] = (0, import_react3.useState)(null);
3208
3348
  const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = (0, import_react3.useState)(false);
@@ -3323,6 +3463,40 @@ var RedactoConsentInlineComponent = ({
3323
3463
  controller.abort();
3324
3464
  };
3325
3465
  }, [fetchNotice]);
3466
+ (0, import_react3.useEffect)(() => {
3467
+ if (!ledgerBaseUrl || applicationId) {
3468
+ return;
3469
+ }
3470
+ if (!accessToken || !content) {
3471
+ return;
3472
+ }
3473
+ let active = true;
3474
+ const controller = new AbortController();
3475
+ const checkLedgerConsent = async () => {
3476
+ try {
3477
+ const ledgerStatus = await fetchNoticeConsentStatusForScope({
3478
+ accessToken,
3479
+ ledgerBaseUrl,
3480
+ organisationUuid: org_uuid,
3481
+ workspaceUuid: workspace_uuid,
3482
+ noticeUuid: notice_uuid,
3483
+ signal: controller.signal
3484
+ });
3485
+ if (active && ledgerStatus && isNoticeFullyConsented(ledgerStatus)) {
3486
+ setHasAlreadyConsented(true);
3487
+ }
3488
+ } finally {
3489
+ if (active) {
3490
+ setIsCheckingConsent(false);
3491
+ }
3492
+ }
3493
+ };
3494
+ void checkLedgerConsent();
3495
+ return () => {
3496
+ active = false;
3497
+ controller.abort();
3498
+ };
3499
+ }, [ledgerBaseUrl, applicationId, accessToken, content, org_uuid, workspace_uuid, notice_uuid]);
3326
3500
  const togglePurposeCollapse = (0, import_react3.useCallback)((purposeUuid) => {
3327
3501
  setCollapsedPurposes((prev) => ({ ...prev, [purposeUuid]: !prev[purposeUuid] }));
3328
3502
  }, []);
@@ -3373,6 +3547,7 @@ var RedactoConsentInlineComponent = ({
3373
3547
  workspace_uuid,
3374
3548
  accessToken: tokenToUse,
3375
3549
  baseUrl,
3550
+ ledgerBaseUrl,
3376
3551
  noticeUuid: content.notice_uuid,
3377
3552
  purposes: content.purposes.map((purpose) => {
3378
3553
  const hasSelectedRequiredElement = purpose.data_elements.some(
@@ -3411,6 +3586,7 @@ var RedactoConsentInlineComponent = ({
3411
3586
  org_uuid,
3412
3587
  workspace_uuid,
3413
3588
  baseUrl,
3589
+ ledgerBaseUrl,
3414
3590
  selectedDataElements,
3415
3591
  selectedPurposes,
3416
3592
  applicationId,
@@ -3418,7 +3594,7 @@ var RedactoConsentInlineComponent = ({
3418
3594
  ]
3419
3595
  );
3420
3596
  (0, import_react3.useEffect)(() => {
3421
- if (accessToken && content && areAllRequiredElementsChecked && !isSubmitting && !hasSubmittedRef.current) {
3597
+ if (accessToken && content && areAllRequiredElementsChecked && !isSubmitting && !isCheckingConsent && !hasAlreadyConsented && !hasSubmittedRef.current) {
3422
3598
  hasSubmittedRef.current = true;
3423
3599
  const autoSubmit = async () => {
3424
3600
  try {
@@ -3430,7 +3606,7 @@ var RedactoConsentInlineComponent = ({
3430
3606
  };
3431
3607
  autoSubmit();
3432
3608
  }
3433
- }, [accessToken, content, areAllRequiredElementsChecked, isSubmitting, handleAcceptWithToken, onError]);
3609
+ }, [accessToken, content, areAllRequiredElementsChecked, isSubmitting, isCheckingConsent, hasAlreadyConsented, handleAcceptWithToken, onError]);
3434
3610
  (0, import_react3.useEffect)(() => {
3435
3611
  if (!accessToken) {
3436
3612
  hasSubmittedRef.current = false;