@redacto.io/consent-sdk-react 9.4.0 → 10.0.0-beta.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.d.mts +4 -2
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +122 -15
  5. package/dist/index.mjs +122 -15
  6. package/dist/privacy-center.d.mts +31 -7
  7. package/dist/privacy-center.d.ts +31 -7
  8. package/dist/privacy-center.js +187 -44
  9. package/dist/privacy-center.mjs +187 -44
  10. package/package.json +1 -1
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -4
  13. package/src/RedactoNoticeConsent/api/index.ts +131 -2
  14. package/src/RedactoNoticeConsent/api/types.ts +33 -1
  15. package/src/RedactoNoticeConsent/types.ts +1 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -1
  17. package/src/RedactoNoticeConsentInline/api/index.ts +5 -2
  18. package/src/RedactoNoticeConsentInline/api/types.ts +1 -1
  19. package/src/RedactoNoticeConsentInline/types.ts +1 -0
  20. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -4
  21. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
  22. package/src/RedactoPrivacyCenter/api/actions.ts +10 -3
  23. package/src/RedactoPrivacyCenter/api/client.ts +67 -21
  24. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
  25. package/src/RedactoPrivacyCenter/components/Form.tsx +58 -6
  26. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
  27. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +7 -5
  28. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
  29. package/src/RedactoPrivacyCenter/context/types.ts +1 -0
  30. package/src/RedactoPrivacyCenter/lib/api.ts +8 -0
  31. package/src/RedactoPrivacyCenter/lib/constants.ts +9 -0
  32. package/src/RedactoPrivacyCenter/lib/types.ts +11 -1
  33. package/src/RedactoPrivacyCenter/lib/utils.ts +28 -0
  34. package/tests/Form.test.tsx +5 -0
  35. package/tests/mocks.ts +1 -0
  36. package/tests/resolveContactFromJwt.test.ts +44 -0
@@ -8576,13 +8576,12 @@ async function fetchGroupPurposesServer(params) {
8576
8576
  }
8577
8577
  }
8578
8578
  async function manageConsentServer(params) {
8579
- var _a;
8580
8579
  try {
8581
8580
  if (!params.consentClient) {
8582
8581
  throw new Error("Consent client is required");
8583
8582
  }
8584
8583
  const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
8585
- const result = await params.consentClient.manageConsent(
8584
+ await params.consentClient.manageConsent(
8586
8585
  baseRequest,
8587
8586
  params.purposeId,
8588
8587
  params.contact,
@@ -8591,9 +8590,11 @@ async function manageConsentServer(params) {
8591
8590
  void 0,
8592
8591
  params.nominatorContact,
8593
8592
  params.dataElementUuids,
8594
- params.productUuid
8593
+ params.productUuid,
8594
+ params.noticeUuid,
8595
+ params.dataElements
8595
8596
  );
8596
- return (_a = result == null ? void 0 : result.success) != null ? _a : null;
8597
+ return true;
8597
8598
  } catch (error) {
8598
8599
  const errorMsg = (error == null ? void 0 : error.message) || "";
8599
8600
  if (errorMsg.includes("nominator_not_found")) {
@@ -9030,6 +9031,7 @@ var MIN_REFRESH_BACKOFF_MS = 3e4;
9030
9031
  var MAX_REFRESH_BACKOFF_MS = 3e5;
9031
9032
  var REFRESH_TOKEN_STORAGE_KEY = "redacto_pc_refresh_token";
9032
9033
  var LEGACY_REFRESH_TOKEN_COOKIE = "refresh_token";
9034
+ var FORM_PICKER_GROUP_LIMIT = 100;
9033
9035
  var POLL_INTERVAL = 3e4;
9034
9036
  var NOTIFICATION_PAGE_SIZE = 10;
9035
9037
  var NOTIFICATION_DROPDOWN_LIMIT = 5;
@@ -9205,6 +9207,14 @@ var decodeJwtToken = (token) => {
9205
9207
  return null;
9206
9208
  }
9207
9209
  };
9210
+ var resolveContactFromJwt = (decoded) => {
9211
+ if (!decoded) {
9212
+ return null;
9213
+ }
9214
+ const userData = decoded.user_data || {};
9215
+ const candidate = decoded.contact || decoded.primary_email || userData.primary_email || userData.primary_mobile || userData.org_user_id;
9216
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
9217
+ };
9208
9218
  var isJwtExpired = (token, bufferSeconds = 5 * 60) => {
9209
9219
  if (!token || typeof token !== "string") {
9210
9220
  return true;
@@ -9685,7 +9695,9 @@ var manageConsent = async ({
9685
9695
  consentClient,
9686
9696
  nominatorContact,
9687
9697
  dataElementUuids,
9688
- productUuid
9698
+ productUuid,
9699
+ noticeUuid,
9700
+ dataElements
9689
9701
  }) => {
9690
9702
  try {
9691
9703
  const accessToken = await ensureValidToken(organizationId, workspaceId, void 0, authContext, consentClient);
@@ -9702,7 +9714,9 @@ var manageConsent = async ({
9702
9714
  consentClient,
9703
9715
  nominatorContact,
9704
9716
  dataElementUuids,
9705
- productUuid
9717
+ productUuid,
9718
+ noticeUuid,
9719
+ dataElements
9706
9720
  });
9707
9721
  return result;
9708
9722
  } catch (error) {
@@ -9722,7 +9736,9 @@ var manageConsent = async ({
9722
9736
  consentClient,
9723
9737
  nominatorContact,
9724
9738
  dataElementUuids,
9725
- productUuid
9739
+ productUuid,
9740
+ noticeUuid,
9741
+ dataElements
9726
9742
  });
9727
9743
  }
9728
9744
  console.error(`Error ${action} consent:`, error);
@@ -9932,9 +9948,9 @@ var syncJwtDataToSessionStorage = (accessToken) => {
9932
9948
  if (!decoded) {
9933
9949
  return;
9934
9950
  }
9935
- const email = decoded.contact || decoded.primary_email || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
9936
- if (email && typeof email === "string") {
9937
- sessionStorage.setItem("email", email);
9951
+ const contact = resolveContactFromJwt(decoded) || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
9952
+ if (contact && typeof contact === "string") {
9953
+ sessionStorage.setItem("email", contact);
9938
9954
  }
9939
9955
  const orgUuid = decoded.organisation_uuid;
9940
9956
  if (orgUuid && typeof orgUuid === "string") {
@@ -10031,7 +10047,11 @@ var useAuth = () => {
10031
10047
  import { createContext as createContext3, useContext as useContext3, useMemo as useMemo2 } from "react";
10032
10048
 
10033
10049
  // src/RedactoPrivacyCenter/api/client.ts
10034
- function createConsentClient(baseUrl) {
10050
+ function createConsentClient(baseUrl, ledgerBaseUrl) {
10051
+ if (!ledgerBaseUrl) {
10052
+ throw new Error("ledgerBaseUrl is required to create the consent client");
10053
+ }
10054
+ const ledgerBase = ledgerBaseUrl.replace(/\/$/, "");
10035
10055
  async function privacyCenterApiFetch(url, options) {
10036
10056
  const { authToken, getAuthToken, headers = {}, ...fetchOptions } = options;
10037
10057
  const requestHeaders = new Headers();
@@ -10124,6 +10144,9 @@ function createConsentClient(baseUrl) {
10124
10144
  function privacyCenterUrl(baseRequest, path) {
10125
10145
  return `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center${path}`;
10126
10146
  }
10147
+ function ledgerUrl(baseRequest, path) {
10148
+ return `${ledgerBase}/public/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}${path}`;
10149
+ }
10127
10150
  function caseUrl(baseRequest, caseUuid, path = "") {
10128
10151
  return privacyCenterUrl(baseRequest, `/cases/${caseUuid}${path}`);
10129
10152
  }
@@ -10257,7 +10280,8 @@ function createConsentClient(baseUrl) {
10257
10280
  if (params.product_uuid) queryParams.append("product_uuid", params.product_uuid);
10258
10281
  if (params.language) queryParams.append("language", params.language);
10259
10282
  const queryString = queryParams.toString();
10260
- const url = privacyCenterUrl(baseRequest, `/user-consents${queryString ? `?${queryString}` : ""}`);
10283
+ const suffix = `/user-consents${queryString ? `?${queryString}` : ""}`;
10284
+ const url = ledgerUrl(baseRequest, suffix);
10261
10285
  return privacyCenterApiFetch(url, {
10262
10286
  ...options,
10263
10287
  method: "GET",
@@ -10278,10 +10302,8 @@ function createConsentClient(baseUrl) {
10278
10302
  if (params.language) queryParams.append("language", params.language);
10279
10303
  if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
10280
10304
  const queryString = queryParams.toString();
10281
- const url = privacyCenterUrl(
10282
- baseRequest,
10283
- `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`
10284
- );
10305
+ const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
10306
+ const url = ledgerUrl(baseRequest, suffix);
10285
10307
  return privacyCenterApiFetch(url, {
10286
10308
  ...options,
10287
10309
  method: "GET",
@@ -10290,20 +10312,48 @@ function createConsentClient(baseUrl) {
10290
10312
  });
10291
10313
  },
10292
10314
  /**
10293
- * Manage consent (privacy center) - revoke, regrant, or renew
10315
+ * Manage consent (privacy center) - revoke, regrant, or renew.
10316
+ *
10317
+ * The Go ledger has no manage-consent route, so this is expressed as a
10318
+ * `partial` submit-consent on the ledger — mirroring the Python dual-write
10319
+ * relay (`build_consent_management_payload`): the affected purpose is
10320
+ * re-submitted with `selected: false` to revoke or `selected: true` to
10321
+ * regrant/renew, and `partial: true` so the rest of the notice's purposes
10322
+ * are left untouched. The principal is resolved from the JWT, so no
10323
+ * `contact`/identifiers are sent in the body, and `consent_event_uuid` is
10324
+ * omitted so the ledger mints a fresh event.
10294
10325
  */
10295
- async manageConsent(baseRequest, purposeId, contact, status, authToken, options, nominatorContact, dataElementUuids, productUuid) {
10296
- const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
10297
- const body = { purpose_uuid: purposeId, contact };
10298
- if (nominatorContact) {
10299
- body.nominator_contact = nominatorContact;
10300
- }
10301
- if (Array.isArray(dataElementUuids) && dataElementUuids.length > 0) {
10302
- body.data_element_uuids = dataElementUuids;
10303
- }
10304
- if (productUuid) {
10305
- body.product_uuid = productUuid;
10306
- }
10326
+ async manageConsent(baseRequest, purposeId, contact, status, authToken, options, nominatorContact, dataElementUuids, productUuid, noticeUuid, dataElements) {
10327
+ if (!noticeUuid) {
10328
+ throw new Error("notice_uuid is required to manage consent via the ledger");
10329
+ }
10330
+ const purposeSelected = status !== "revoke";
10331
+ const elements = (dataElements != null ? dataElements : []).map((de) => {
10332
+ let selected;
10333
+ if (status === "revoke") {
10334
+ selected = false;
10335
+ } else if (status === "regrant") {
10336
+ selected = Array.isArray(dataElementUuids) ? dataElementUuids.includes(de.uuid) : de.selected;
10337
+ } else {
10338
+ selected = de.selected;
10339
+ }
10340
+ return { uuid: de.uuid, selected };
10341
+ });
10342
+ const body = {
10343
+ notice_uuid: noticeUuid,
10344
+ partial: true,
10345
+ declined: false,
10346
+ select_all_mandatory: false,
10347
+ self_declared_adult: false,
10348
+ purposes: [
10349
+ {
10350
+ purpose_uuid: purposeId,
10351
+ selected: purposeSelected,
10352
+ data_elements: elements
10353
+ }
10354
+ ]
10355
+ };
10356
+ const url = ledgerUrl(baseRequest, "/submit-consent");
10307
10357
  return privacyCenterApiFetch(url, {
10308
10358
  ...options,
10309
10359
  method: "POST",
@@ -10340,7 +10390,7 @@ function createConsentClient(baseUrl) {
10340
10390
  if (params.created_before) queryParams.append("created_before", params.created_before);
10341
10391
  if (params.language) queryParams.append("language", params.language);
10342
10392
  const queryString = queryParams.toString();
10343
- const url = privacyCenterUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
10393
+ const url = ledgerUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
10344
10394
  return privacyCenterApiFetch(url, {
10345
10395
  ...options,
10346
10396
  method: "GET",
@@ -10355,7 +10405,7 @@ function createConsentClient(baseUrl) {
10355
10405
  * and the Authorization header must be sent (anchor navigation wouldn't carry it).
10356
10406
  */
10357
10407
  async getReceiptPdf(baseRequest, receiptUuid, authToken) {
10358
- const url = privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
10408
+ const url = ledgerUrl(baseRequest, `/receipts/${receiptUuid}/download`);
10359
10409
  const token = resolveAuthToken(authToken, baseRequest);
10360
10410
  const requestHeaders = new Headers();
10361
10411
  requestHeaders.set("Accept", "application/pdf");
@@ -10541,15 +10591,20 @@ import { jsx as jsx4 } from "react/jsx-runtime";
10541
10591
  var BaseUrlContext = createContext3(null);
10542
10592
  var BaseUrlProvider = ({
10543
10593
  children,
10544
- baseUrl
10594
+ baseUrl,
10595
+ ledgerBaseUrl
10545
10596
  }) => {
10546
- const consentClient = useMemo2(() => createConsentClient(baseUrl), [baseUrl]);
10597
+ const consentClient = useMemo2(
10598
+ () => createConsentClient(baseUrl, ledgerBaseUrl),
10599
+ [baseUrl, ledgerBaseUrl]
10600
+ );
10547
10601
  const value = useMemo2(
10548
10602
  () => ({
10549
10603
  baseUrl,
10604
+ ledgerBaseUrl,
10550
10605
  consentClient
10551
10606
  }),
10552
- [baseUrl, consentClient]
10607
+ [baseUrl, ledgerBaseUrl, consentClient]
10553
10608
  );
10554
10609
  return /* @__PURE__ */ jsx4(BaseUrlContext.Provider, { value, children });
10555
10610
  };
@@ -15085,9 +15140,9 @@ function PrivacyCenterLayout({ children, slug, onBack }) {
15085
15140
  if (orgId) sessionStorage.setItem("pc_org_id", orgId);
15086
15141
  if (workspaceId) sessionStorage.setItem("pc_workspace_id", workspaceId);
15087
15142
  if (orgSlug) sessionStorage.setItem("pc_org_slug", orgSlug);
15088
- const email = decoded.contact || decoded.primary_email;
15089
- if (email && typeof email === "string") {
15090
- sessionStorage.setItem("email", email);
15143
+ const contact = resolveContactFromJwt(decoded);
15144
+ if (contact) {
15145
+ sessionStorage.setItem("email", contact);
15091
15146
  }
15092
15147
  }
15093
15148
  }
@@ -18104,6 +18159,7 @@ function ConsentManager({ variant }) {
18104
18159
  );
18105
18160
  const handleManageConsent = useCallback13(
18106
18161
  async (purposeId, action, nominatorContact, dataElementUuids, productUuid) => {
18162
+ var _a2, _b2;
18107
18163
  if (!organizationId || !workspaceId || !contact) {
18108
18164
  toast.error(t("missingInfoToManage"));
18109
18165
  return;
@@ -18120,6 +18176,11 @@ function ConsentManager({ variant }) {
18120
18176
  renew: t("unableToRenewConsent")
18121
18177
  };
18122
18178
  try {
18179
+ const noticeUuid = (_a2 = selectedRecord == null ? void 0 : selectedRecord.notice_uuid) != null ? _a2 : void 0;
18180
+ const dataElements = ((_b2 = selectedRecord == null ? void 0 : selectedRecord.data_elements) != null ? _b2 : []).map((el) => ({
18181
+ uuid: el.uuid,
18182
+ selected: el.selected
18183
+ }));
18123
18184
  const response = await manageConsent({
18124
18185
  organizationId,
18125
18186
  workspaceId,
@@ -18130,7 +18191,9 @@ function ConsentManager({ variant }) {
18130
18191
  consentClient,
18131
18192
  nominatorContact,
18132
18193
  dataElementUuids,
18133
- productUuid
18194
+ productUuid,
18195
+ noticeUuid,
18196
+ dataElements
18134
18197
  });
18135
18198
  if (response) {
18136
18199
  toast.success(successMessages[action]);
@@ -18152,7 +18215,7 @@ function ConsentManager({ variant }) {
18152
18215
  handleCloseModal();
18153
18216
  }
18154
18217
  },
18155
- [organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient]
18218
+ [organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient, selectedRecord]
18156
18219
  );
18157
18220
  const getVar26 = (varName) => `var(${varName})`;
18158
18221
  const directBadge = (_c = (_b = consentDetail == null ? void 0 : consentDetail.status_summary) == null ? void 0 : _b.direct_purposes) != null ? _c : "";
@@ -18677,6 +18740,43 @@ import { AiOutlineClose } from "react-icons/ai";
18677
18740
  import { PiUploadBold } from "react-icons/pi";
18678
18741
 
18679
18742
  // src/RedactoPrivacyCenter/components/groupHelpers.ts
18743
+ var consentGroupToFormProductGroup = (group) => {
18744
+ var _a;
18745
+ return {
18746
+ product_uuid: group.product_uuid,
18747
+ product_name: group.product_name,
18748
+ product_description: group.product_description,
18749
+ nominator: group.nominator,
18750
+ purposes: ((_a = group.purposes) != null ? _a : []).map(purposeItemToFormPurpose)
18751
+ };
18752
+ };
18753
+ var purposeItemToFormPurpose = (purpose) => {
18754
+ var _a, _b;
18755
+ return {
18756
+ uuid: purpose.purpose_uuid,
18757
+ purpose_uuid: purpose.purpose_uuid,
18758
+ name: purpose.name,
18759
+ description: (_a = purpose.description) != null ? _a : "",
18760
+ industries: "",
18761
+ selected: false,
18762
+ given_consent: false,
18763
+ status: purpose.status,
18764
+ validity: null,
18765
+ enabled: true,
18766
+ data_elements: ((_b = purpose.data_elements) != null ? _b : []).map(
18767
+ consentDataElementToFormDataElement
18768
+ )
18769
+ };
18770
+ };
18771
+ var consentDataElementToFormDataElement = (element) => ({
18772
+ uuid: element.uuid,
18773
+ name: element.name,
18774
+ description: "",
18775
+ enabled: element.enabled,
18776
+ required: element.required,
18777
+ given_consent: element.selected,
18778
+ selected: element.selected
18779
+ });
18680
18780
  var buildGroupPurposes = (group) => group.purposes.filter((p) => p !== null && p !== void 0).map((p) => {
18681
18781
  const validDataElements = Array.isArray(p.data_elements) ? p.data_elements.filter((de) => de !== null && de !== void 0 && Boolean(de.uuid)).map((de) => ({ ...de, selected: false })) : [];
18682
18782
  return {
@@ -19905,6 +20005,44 @@ function Form({
19905
20005
  },
19906
20006
  []
19907
20007
  );
20008
+ const withLedgerPickerGroups = useCallback14(
20009
+ async (form, language) => {
20010
+ var _a2, _b2;
20011
+ if (isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
20012
+ return form;
20013
+ }
20014
+ const consents = await fetchUserConsents({
20015
+ organizationId,
20016
+ workspaceId,
20017
+ authContext,
20018
+ consentClient,
20019
+ // No status filter: include every status (active / expired / withdrawn
20020
+ // / declined), matching Python form/data, so a DSAR can target products
20021
+ // the user no longer actively consents to.
20022
+ limit: FORM_PICKER_GROUP_LIMIT,
20023
+ nominatedLimit: FORM_PICKER_GROUP_LIMIT,
20024
+ language
20025
+ });
20026
+ if (!consents) {
20027
+ return form;
20028
+ }
20029
+ return {
20030
+ ...form,
20031
+ direct_groups: ((_a2 = consents.direct) != null ? _a2 : []).map(consentGroupToFormProductGroup),
20032
+ nominated_groups: ((_b2 = consents.nominated) != null ? _b2 : []).map(
20033
+ consentGroupToFormProductGroup
20034
+ )
20035
+ };
20036
+ },
20037
+ [
20038
+ isPopup,
20039
+ isCaseManagementContext,
20040
+ organizationId,
20041
+ workspaceId,
20042
+ authContext,
20043
+ consentClient
20044
+ ]
20045
+ );
19908
20046
  const fetchAndApplyTranslations = useCallback14(async (langValue) => {
19909
20047
  if (!organizationId || !workspaceId || !data) {
19910
20048
  return;
@@ -19933,7 +20071,7 @@ function Form({
19933
20071
  toast.info(t("translationNotAvailable"));
19934
20072
  return;
19935
20073
  }
19936
- setData(translatedFormData);
20074
+ setData(await withLedgerPickerGroups(translatedFormData, langValue));
19937
20075
  if (translatedFormData.grievance_options) {
19938
20076
  const currentGrievanceSelections = { ...selectedGrievances };
19939
20077
  setGrievanceOptions(translatedFormData.grievance_options);
@@ -19949,7 +20087,7 @@ function Form({
19949
20087
  } finally {
19950
20088
  setIsTranslating(false);
19951
20089
  }
19952
- }, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t]);
20090
+ }, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t, withLedgerPickerGroups]);
19953
20091
  useEffect22(() => {
19954
20092
  if (!skipProductStep) {
19955
20093
  setSelectedProductKey(null);
@@ -20055,7 +20193,11 @@ function Form({
20055
20193
  console.warn("No purposes with data elements found in API response");
20056
20194
  }
20057
20195
  }
20058
- setData(displayData);
20196
+ const pickerData = await withLedgerPickerGroups(
20197
+ displayData,
20198
+ langParam || void 0
20199
+ );
20200
+ setData(pickerData);
20059
20201
  setGrievanceOptions(displayData.grievance_options || []);
20060
20202
  const initialGrievances = (displayData.grievance_options || []).reduce(
20061
20203
  (acc, option) => {
@@ -20075,7 +20217,7 @@ function Form({
20075
20217
  }
20076
20218
  };
20077
20219
  void initializePrivacyCenterForm();
20078
- }, [organizationId, workspaceId, isPopup, authContext, consentClient]);
20220
+ }, [organizationId, workspaceId, isPopup, authContext, consentClient, withLedgerPickerGroups]);
20079
20221
  const getEnglishDataElementName = useCallback14((translatedName) => {
20080
20222
  var _a2, _b2, _c2;
20081
20223
  if (!((_a2 = englishDataRef.current) == null ? void 0 : _a2.purposes)) {
@@ -27494,6 +27636,7 @@ import { jsx as jsx90 } from "react/jsx-runtime";
27494
27636
  injectPCStyles();
27495
27637
  function RedactoPrivacyCenter({
27496
27638
  baseUrl,
27639
+ ledgerBaseUrl,
27497
27640
  slug,
27498
27641
  accessToken,
27499
27642
  refreshToken,
@@ -27501,7 +27644,7 @@ function RedactoPrivacyCenter({
27501
27644
  initialPage = "consent-manager",
27502
27645
  onBack
27503
27646
  }) {
27504
- return /* @__PURE__ */ jsx90(I18nextProvider, { i18n: i18n_default, children: /* @__PURE__ */ jsx90(BrandingProvider, { slug, baseUrl, children: /* @__PURE__ */ jsx90(BaseUrlProvider, { baseUrl, children: /* @__PURE__ */ jsx90(
27647
+ return /* @__PURE__ */ jsx90(I18nextProvider, { i18n: i18n_default, children: /* @__PURE__ */ jsx90(BrandingProvider, { slug, baseUrl, children: /* @__PURE__ */ jsx90(BaseUrlProvider, { baseUrl, ledgerBaseUrl, children: /* @__PURE__ */ jsx90(
27505
27648
  AuthProvider,
27506
27649
  {
27507
27650
  accessToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "9.4.0",
3
+ "version": "10.0.0-beta.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,11 @@ vi.mock("./api", () => ({
26
26
  fetchTTSAudioUrls: vi.fn(),
27
27
  initiateGuardianVerification: vi.fn(),
28
28
  verifyGuardianStatus: vi.fn(),
29
+ // Defaults to resolving undefined, so the ledger consent-state overlay is
30
+ // skipped and the banner falls back to the Python-provided values the
31
+ // existing tests assert on.
32
+ fetchNoticeConsentStatus: vi.fn(),
33
+ buildLedgerConsentOverlay: vi.fn(),
29
34
  }));
30
35
 
31
36
  // Mock jwt-decode
@@ -11,8 +11,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
11
11
 
12
12
  // API imports
13
13
  import {
14
+ buildLedgerConsentOverlay,
14
15
  clearApiCache,
15
16
  fetchConsentContent,
17
+ fetchNoticeConsentStatus,
16
18
  fetchTTSAudioUrls,
17
19
  initiateGuardianVerification,
18
20
  submitConsentEvent,
@@ -1296,6 +1298,7 @@ export const RedactoNoticeConsent = ({
1296
1298
  accessToken,
1297
1299
  refreshToken,
1298
1300
  baseUrl,
1301
+ ledgerBaseUrl,
1299
1302
  language = "en",
1300
1303
  blockUI = true,
1301
1304
  onAccept,
@@ -1838,9 +1841,31 @@ export const RedactoNoticeConsent = ({
1838
1841
  );
1839
1842
  setCollapsedPurposes(initialCollapsedState);
1840
1843
 
1841
- // Initialize selected states - handle reconsent pre-population
1842
- const isReconsentRequired = consentContentData.detail.reconsent_required;
1843
- const purposeSelections = consentContentData.detail.purpose_selections;
1844
+ // Initialize selected states - handle reconsent pre-population.
1845
+ // Approach A: overlay the prior-consent / reconsent state from the Go
1846
+ // ledger's check-consent. Consents live in the ledger post-cutover, so
1847
+ // Python's notice-validation overlay is empty; the notice text/config
1848
+ // still comes from Python (above). Falls back to the Python-provided
1849
+ // values when the ledger is unavailable, and is skipped for the
1850
+ // magic-link flow (specific_uuid), which pre-fills from a specific event.
1851
+ let isReconsentRequired = consentContentData.detail.reconsent_required;
1852
+ let purposeSelections = consentContentData.detail.purpose_selections;
1853
+ if (ledgerBaseUrl && !applicationId) {
1854
+ const ledgerStatus = await fetchNoticeConsentStatus({
1855
+ accessToken,
1856
+ ledgerBaseUrl,
1857
+ noticeUuid: noticeId,
1858
+ signal: abortControllerRef.current?.signal,
1859
+ });
1860
+ if (ledgerStatus) {
1861
+ const overlay = buildLedgerConsentOverlay(
1862
+ ledgerStatus,
1863
+ activeConfig.purposes
1864
+ );
1865
+ isReconsentRequired = overlay?.reconsent_required ?? false;
1866
+ purposeSelections = overlay?.purpose_selections;
1867
+ }
1868
+ }
1844
1869
 
1845
1870
  // Categorize purposes for reconsent UI
1846
1871
  const newCategorizedPurposes = isReconsentRequired
@@ -2120,7 +2145,7 @@ export const RedactoNoticeConsent = ({
2120
2145
  if (content) {
2121
2146
  await submitConsentEvent({
2122
2147
  accessToken,
2123
- baseUrl,
2148
+ ledgerBaseUrl,
2124
2149
  noticeUuid: content.notice_uuid,
2125
2150
  purposes: content.purposes.map((purpose) => {
2126
2151
  // Check if this is an already-consented purpose that was modified
@@ -3,10 +3,14 @@ import { parseApiError } from "../../shared/api-errors";
3
3
  import type {
4
4
  ConsentContent,
5
5
  ConsentEventPayload,
6
+ DataElementSelection,
6
7
  FetchConsentContentParams,
8
+ FetchNoticeConsentStatusParams,
7
9
  FetchTTSAudioUrlsParams,
8
10
  InitiateGuardianVerificationParams,
9
11
  InitiateGuardianVerificationResponse,
12
+ NoticeConsentStatusResponse,
13
+ PurposeSelection,
10
14
  RedactoJwtPayload,
11
15
  SubmitConsentEventParams,
12
16
  TTSAudioUrlsResponse,
@@ -127,7 +131,7 @@ export const fetchConsentContent = async ({
127
131
 
128
132
  export const submitConsentEvent = async ({
129
133
  accessToken,
130
- baseUrl,
134
+ ledgerBaseUrl,
131
135
  noticeUuid,
132
136
  purposes,
133
137
  declined,
@@ -151,7 +155,10 @@ export const submitConsentEvent = async ({
151
155
  throw new Error("Invalid token: missing organization or workspace UUID");
152
156
  }
153
157
 
154
- const apiBaseUrl = baseUrl || BASE_URL;
158
+ if (!ledgerBaseUrl) {
159
+ throw new Error("ledgerBaseUrl is required to submit consent");
160
+ }
161
+ const apiBaseUrl = ledgerBaseUrl;
155
162
 
156
163
  const validatedPurposes = purposes.map((purpose) => {
157
164
  if (!purpose.uuid) {
@@ -209,6 +216,128 @@ export const submitConsentEvent = async ({
209
216
  apiCache.clear();
210
217
  };
211
218
 
219
+ /**
220
+ * Fetch the principal's current consent status for a notice from the Go
221
+ * ledger (`check-consent`). The principal resolves from the JWT, so the body
222
+ * is empty. Returns null on any failure so the caller can fall back to the
223
+ * Python-provided overlay (graceful degradation). This is how the banner
224
+ * learns prior-consent / reconsent state post-cutover, since Python's notice
225
+ * validation reads its own (now-empty) consent DB.
226
+ */
227
+ export const fetchNoticeConsentStatus = async ({
228
+ accessToken,
229
+ ledgerBaseUrl,
230
+ noticeUuid,
231
+ signal,
232
+ }: FetchNoticeConsentStatusParams): Promise<NoticeConsentStatusResponse | null> => {
233
+ if (!accessToken || !ledgerBaseUrl || !noticeUuid) {
234
+ return null;
235
+ }
236
+
237
+ const decodedToken = decodeTokenSafely(accessToken);
238
+ if (!decodedToken) {
239
+ return null;
240
+ }
241
+
242
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
243
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
244
+ return null;
245
+ }
246
+
247
+ try {
248
+ const response = await fetch(
249
+ `${ledgerBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/check-consent`,
250
+ {
251
+ method: "POST",
252
+ headers: {
253
+ Authorization: `Bearer ${accessToken}`,
254
+ "Content-Type": "application/json",
255
+ },
256
+ body: "{}",
257
+ signal,
258
+ }
259
+ );
260
+
261
+ if (!response.ok) {
262
+ return null;
263
+ }
264
+
265
+ const data = (await response.json()) as
266
+ | NoticeConsentStatusResponse
267
+ | { detail?: NoticeConsentStatusResponse };
268
+ // The ledger returns the response directly; tolerate a {detail} envelope too.
269
+ const body =
270
+ data && typeof data === "object" && "detail" in data && data.detail
271
+ ? data.detail
272
+ : (data as NoticeConsentStatusResponse);
273
+
274
+ if (!body || !Array.isArray(body.purposes)) {
275
+ return null;
276
+ }
277
+ return body;
278
+ } catch {
279
+ // Network / abort / parse failure — degrade to the Python overlay.
280
+ return null;
281
+ }
282
+ };
283
+
284
+ /**
285
+ * Translate the ledger's notice consent status into the
286
+ * `{ purpose_selections, reconsent_required }` shape the banner already
287
+ * consumes, so prior-consent pre-fill and reconsent categorization work off
288
+ * the ledger. Returns null for a first-time principal (no consent record —
289
+ * every purpose INACTIVE), preserving the normal first-time consent flow.
290
+ *
291
+ * `needs_reconsent` is derived from `status` (anything not ACTIVE needs
292
+ * reconsent: expired / withdrawn / declined / inactive). NOTE: notice-version
293
+ * reconsent is NOT covered — the ledger's check-consent has no version
294
+ * awareness yet, so a republished-notice version bump won't re-prompt until
295
+ * that lands on the ledger (tracked fast-follow).
296
+ */
297
+ export const buildLedgerConsentOverlay = (
298
+ status: NoticeConsentStatusResponse,
299
+ configPurposes: ConsentContent["detail"]["active_config"]["purposes"]
300
+ ): {
301
+ reconsent_required: boolean;
302
+ purpose_selections: Record<string, PurposeSelection>;
303
+ } | null => {
304
+ const byUuid = new Map(status.purposes.map((p) => [p.uuid, p]));
305
+ const hasHistory = status.purposes.some(
306
+ (p) => (p.status || "INACTIVE") !== "INACTIVE"
307
+ );
308
+ if (!hasHistory) {
309
+ return null;
310
+ }
311
+
312
+ const purpose_selections: Record<string, PurposeSelection> = {};
313
+ for (const cp of configPurposes) {
314
+ const lp = byUuid.get(cp.uuid);
315
+ const purposeStatus = lp?.status || "INACTIVE";
316
+ const selectedByUuid = new Map(
317
+ (lp?.data_elements || []).map((de) => [de.uuid, de.selected])
318
+ );
319
+ const data_elements: Record<string, DataElementSelection> = {};
320
+ for (const de of cp.data_elements) {
321
+ data_elements[de.uuid] = {
322
+ selected: selectedByUuid.get(de.uuid) ?? false,
323
+ enabled: de.enabled,
324
+ required: de.required,
325
+ };
326
+ }
327
+ purpose_selections[cp.uuid] = {
328
+ selected: lp?.selected ?? false,
329
+ status: purposeStatus,
330
+ needs_reconsent: purposeStatus !== "ACTIVE",
331
+ data_elements,
332
+ };
333
+ }
334
+
335
+ const reconsent_required = configPurposes.some(
336
+ (cp) => (purpose_selections[cp.uuid]?.status || "INACTIVE") !== "ACTIVE"
337
+ );
338
+ return { reconsent_required, purpose_selections };
339
+ };
340
+
212
341
  export const fetchTTSAudioUrls = async ({
213
342
  accessToken,
214
343
  baseUrl,