@redacto.io/consent-sdk-react 9.5.0-beta.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.
- package/CHANGELOG.md +3 -3
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +122 -15
- package/dist/index.mjs +122 -15
- package/dist/privacy-center.d.mts +31 -7
- package/dist/privacy-center.d.ts +31 -7
- package/dist/privacy-center.js +173 -38
- package/dist/privacy-center.mjs +173 -38
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -4
- package/src/RedactoNoticeConsent/api/index.ts +131 -2
- package/src/RedactoNoticeConsent/api/types.ts +33 -1
- package/src/RedactoNoticeConsent/types.ts +1 -0
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -1
- package/src/RedactoNoticeConsentInline/api/index.ts +5 -2
- package/src/RedactoNoticeConsentInline/api/types.ts +1 -1
- package/src/RedactoNoticeConsentInline/types.ts +1 -0
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
- package/src/RedactoPrivacyCenter/api/actions.ts +10 -3
- package/src/RedactoPrivacyCenter/api/client.ts +67 -21
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +58 -6
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
- package/src/RedactoPrivacyCenter/context/types.ts +1 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +8 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +9 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +11 -1
- package/tests/Form.test.tsx +5 -0
- package/tests/mocks.ts +1 -0
package/dist/privacy-center.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
|
@@ -9693,7 +9695,9 @@ var manageConsent = async ({
|
|
|
9693
9695
|
consentClient,
|
|
9694
9696
|
nominatorContact,
|
|
9695
9697
|
dataElementUuids,
|
|
9696
|
-
productUuid
|
|
9698
|
+
productUuid,
|
|
9699
|
+
noticeUuid,
|
|
9700
|
+
dataElements
|
|
9697
9701
|
}) => {
|
|
9698
9702
|
try {
|
|
9699
9703
|
const accessToken = await ensureValidToken(organizationId, workspaceId, void 0, authContext, consentClient);
|
|
@@ -9710,7 +9714,9 @@ var manageConsent = async ({
|
|
|
9710
9714
|
consentClient,
|
|
9711
9715
|
nominatorContact,
|
|
9712
9716
|
dataElementUuids,
|
|
9713
|
-
productUuid
|
|
9717
|
+
productUuid,
|
|
9718
|
+
noticeUuid,
|
|
9719
|
+
dataElements
|
|
9714
9720
|
});
|
|
9715
9721
|
return result;
|
|
9716
9722
|
} catch (error) {
|
|
@@ -9730,7 +9736,9 @@ var manageConsent = async ({
|
|
|
9730
9736
|
consentClient,
|
|
9731
9737
|
nominatorContact,
|
|
9732
9738
|
dataElementUuids,
|
|
9733
|
-
productUuid
|
|
9739
|
+
productUuid,
|
|
9740
|
+
noticeUuid,
|
|
9741
|
+
dataElements
|
|
9734
9742
|
});
|
|
9735
9743
|
}
|
|
9736
9744
|
console.error(`Error ${action} consent:`, error);
|
|
@@ -10039,7 +10047,11 @@ var useAuth = () => {
|
|
|
10039
10047
|
import { createContext as createContext3, useContext as useContext3, useMemo as useMemo2 } from "react";
|
|
10040
10048
|
|
|
10041
10049
|
// src/RedactoPrivacyCenter/api/client.ts
|
|
10042
|
-
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(/\/$/, "");
|
|
10043
10055
|
async function privacyCenterApiFetch(url, options) {
|
|
10044
10056
|
const { authToken, getAuthToken, headers = {}, ...fetchOptions } = options;
|
|
10045
10057
|
const requestHeaders = new Headers();
|
|
@@ -10132,6 +10144,9 @@ function createConsentClient(baseUrl) {
|
|
|
10132
10144
|
function privacyCenterUrl(baseRequest, path) {
|
|
10133
10145
|
return `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center${path}`;
|
|
10134
10146
|
}
|
|
10147
|
+
function ledgerUrl(baseRequest, path) {
|
|
10148
|
+
return `${ledgerBase}/public/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}${path}`;
|
|
10149
|
+
}
|
|
10135
10150
|
function caseUrl(baseRequest, caseUuid, path = "") {
|
|
10136
10151
|
return privacyCenterUrl(baseRequest, `/cases/${caseUuid}${path}`);
|
|
10137
10152
|
}
|
|
@@ -10265,7 +10280,8 @@ function createConsentClient(baseUrl) {
|
|
|
10265
10280
|
if (params.product_uuid) queryParams.append("product_uuid", params.product_uuid);
|
|
10266
10281
|
if (params.language) queryParams.append("language", params.language);
|
|
10267
10282
|
const queryString = queryParams.toString();
|
|
10268
|
-
const
|
|
10283
|
+
const suffix = `/user-consents${queryString ? `?${queryString}` : ""}`;
|
|
10284
|
+
const url = ledgerUrl(baseRequest, suffix);
|
|
10269
10285
|
return privacyCenterApiFetch(url, {
|
|
10270
10286
|
...options,
|
|
10271
10287
|
method: "GET",
|
|
@@ -10286,10 +10302,8 @@ function createConsentClient(baseUrl) {
|
|
|
10286
10302
|
if (params.language) queryParams.append("language", params.language);
|
|
10287
10303
|
if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
|
|
10288
10304
|
const queryString = queryParams.toString();
|
|
10289
|
-
const
|
|
10290
|
-
|
|
10291
|
-
`/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`
|
|
10292
|
-
);
|
|
10305
|
+
const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
|
|
10306
|
+
const url = ledgerUrl(baseRequest, suffix);
|
|
10293
10307
|
return privacyCenterApiFetch(url, {
|
|
10294
10308
|
...options,
|
|
10295
10309
|
method: "GET",
|
|
@@ -10298,20 +10312,48 @@ function createConsentClient(baseUrl) {
|
|
|
10298
10312
|
});
|
|
10299
10313
|
},
|
|
10300
10314
|
/**
|
|
10301
|
-
* 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.
|
|
10302
10325
|
*/
|
|
10303
|
-
async manageConsent(baseRequest, purposeId, contact, status, authToken, options, nominatorContact, dataElementUuids, productUuid) {
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
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");
|
|
10315
10357
|
return privacyCenterApiFetch(url, {
|
|
10316
10358
|
...options,
|
|
10317
10359
|
method: "POST",
|
|
@@ -10348,7 +10390,7 @@ function createConsentClient(baseUrl) {
|
|
|
10348
10390
|
if (params.created_before) queryParams.append("created_before", params.created_before);
|
|
10349
10391
|
if (params.language) queryParams.append("language", params.language);
|
|
10350
10392
|
const queryString = queryParams.toString();
|
|
10351
|
-
const url =
|
|
10393
|
+
const url = ledgerUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
|
|
10352
10394
|
return privacyCenterApiFetch(url, {
|
|
10353
10395
|
...options,
|
|
10354
10396
|
method: "GET",
|
|
@@ -10363,7 +10405,7 @@ function createConsentClient(baseUrl) {
|
|
|
10363
10405
|
* and the Authorization header must be sent (anchor navigation wouldn't carry it).
|
|
10364
10406
|
*/
|
|
10365
10407
|
async getReceiptPdf(baseRequest, receiptUuid, authToken) {
|
|
10366
|
-
const url =
|
|
10408
|
+
const url = ledgerUrl(baseRequest, `/receipts/${receiptUuid}/download`);
|
|
10367
10409
|
const token = resolveAuthToken(authToken, baseRequest);
|
|
10368
10410
|
const requestHeaders = new Headers();
|
|
10369
10411
|
requestHeaders.set("Accept", "application/pdf");
|
|
@@ -10549,15 +10591,20 @@ import { jsx as jsx4 } from "react/jsx-runtime";
|
|
|
10549
10591
|
var BaseUrlContext = createContext3(null);
|
|
10550
10592
|
var BaseUrlProvider = ({
|
|
10551
10593
|
children,
|
|
10552
|
-
baseUrl
|
|
10594
|
+
baseUrl,
|
|
10595
|
+
ledgerBaseUrl
|
|
10553
10596
|
}) => {
|
|
10554
|
-
const consentClient = useMemo2(
|
|
10597
|
+
const consentClient = useMemo2(
|
|
10598
|
+
() => createConsentClient(baseUrl, ledgerBaseUrl),
|
|
10599
|
+
[baseUrl, ledgerBaseUrl]
|
|
10600
|
+
);
|
|
10555
10601
|
const value = useMemo2(
|
|
10556
10602
|
() => ({
|
|
10557
10603
|
baseUrl,
|
|
10604
|
+
ledgerBaseUrl,
|
|
10558
10605
|
consentClient
|
|
10559
10606
|
}),
|
|
10560
|
-
[baseUrl, consentClient]
|
|
10607
|
+
[baseUrl, ledgerBaseUrl, consentClient]
|
|
10561
10608
|
);
|
|
10562
10609
|
return /* @__PURE__ */ jsx4(BaseUrlContext.Provider, { value, children });
|
|
10563
10610
|
};
|
|
@@ -18112,6 +18159,7 @@ function ConsentManager({ variant }) {
|
|
|
18112
18159
|
);
|
|
18113
18160
|
const handleManageConsent = useCallback13(
|
|
18114
18161
|
async (purposeId, action, nominatorContact, dataElementUuids, productUuid) => {
|
|
18162
|
+
var _a2, _b2;
|
|
18115
18163
|
if (!organizationId || !workspaceId || !contact) {
|
|
18116
18164
|
toast.error(t("missingInfoToManage"));
|
|
18117
18165
|
return;
|
|
@@ -18128,6 +18176,11 @@ function ConsentManager({ variant }) {
|
|
|
18128
18176
|
renew: t("unableToRenewConsent")
|
|
18129
18177
|
};
|
|
18130
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
|
+
}));
|
|
18131
18184
|
const response = await manageConsent({
|
|
18132
18185
|
organizationId,
|
|
18133
18186
|
workspaceId,
|
|
@@ -18138,7 +18191,9 @@ function ConsentManager({ variant }) {
|
|
|
18138
18191
|
consentClient,
|
|
18139
18192
|
nominatorContact,
|
|
18140
18193
|
dataElementUuids,
|
|
18141
|
-
productUuid
|
|
18194
|
+
productUuid,
|
|
18195
|
+
noticeUuid,
|
|
18196
|
+
dataElements
|
|
18142
18197
|
});
|
|
18143
18198
|
if (response) {
|
|
18144
18199
|
toast.success(successMessages[action]);
|
|
@@ -18160,7 +18215,7 @@ function ConsentManager({ variant }) {
|
|
|
18160
18215
|
handleCloseModal();
|
|
18161
18216
|
}
|
|
18162
18217
|
},
|
|
18163
|
-
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient]
|
|
18218
|
+
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient, selectedRecord]
|
|
18164
18219
|
);
|
|
18165
18220
|
const getVar26 = (varName) => `var(${varName})`;
|
|
18166
18221
|
const directBadge = (_c = (_b = consentDetail == null ? void 0 : consentDetail.status_summary) == null ? void 0 : _b.direct_purposes) != null ? _c : "";
|
|
@@ -18685,6 +18740,43 @@ import { AiOutlineClose } from "react-icons/ai";
|
|
|
18685
18740
|
import { PiUploadBold } from "react-icons/pi";
|
|
18686
18741
|
|
|
18687
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
|
+
});
|
|
18688
18780
|
var buildGroupPurposes = (group) => group.purposes.filter((p) => p !== null && p !== void 0).map((p) => {
|
|
18689
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 })) : [];
|
|
18690
18782
|
return {
|
|
@@ -19913,6 +20005,44 @@ function Form({
|
|
|
19913
20005
|
},
|
|
19914
20006
|
[]
|
|
19915
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
|
+
);
|
|
19916
20046
|
const fetchAndApplyTranslations = useCallback14(async (langValue) => {
|
|
19917
20047
|
if (!organizationId || !workspaceId || !data) {
|
|
19918
20048
|
return;
|
|
@@ -19941,7 +20071,7 @@ function Form({
|
|
|
19941
20071
|
toast.info(t("translationNotAvailable"));
|
|
19942
20072
|
return;
|
|
19943
20073
|
}
|
|
19944
|
-
setData(translatedFormData);
|
|
20074
|
+
setData(await withLedgerPickerGroups(translatedFormData, langValue));
|
|
19945
20075
|
if (translatedFormData.grievance_options) {
|
|
19946
20076
|
const currentGrievanceSelections = { ...selectedGrievances };
|
|
19947
20077
|
setGrievanceOptions(translatedFormData.grievance_options);
|
|
@@ -19957,7 +20087,7 @@ function Form({
|
|
|
19957
20087
|
} finally {
|
|
19958
20088
|
setIsTranslating(false);
|
|
19959
20089
|
}
|
|
19960
|
-
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t]);
|
|
20090
|
+
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t, withLedgerPickerGroups]);
|
|
19961
20091
|
useEffect22(() => {
|
|
19962
20092
|
if (!skipProductStep) {
|
|
19963
20093
|
setSelectedProductKey(null);
|
|
@@ -20063,7 +20193,11 @@ function Form({
|
|
|
20063
20193
|
console.warn("No purposes with data elements found in API response");
|
|
20064
20194
|
}
|
|
20065
20195
|
}
|
|
20066
|
-
|
|
20196
|
+
const pickerData = await withLedgerPickerGroups(
|
|
20197
|
+
displayData,
|
|
20198
|
+
langParam || void 0
|
|
20199
|
+
);
|
|
20200
|
+
setData(pickerData);
|
|
20067
20201
|
setGrievanceOptions(displayData.grievance_options || []);
|
|
20068
20202
|
const initialGrievances = (displayData.grievance_options || []).reduce(
|
|
20069
20203
|
(acc, option) => {
|
|
@@ -20083,7 +20217,7 @@ function Form({
|
|
|
20083
20217
|
}
|
|
20084
20218
|
};
|
|
20085
20219
|
void initializePrivacyCenterForm();
|
|
20086
|
-
}, [organizationId, workspaceId, isPopup, authContext, consentClient]);
|
|
20220
|
+
}, [organizationId, workspaceId, isPopup, authContext, consentClient, withLedgerPickerGroups]);
|
|
20087
20221
|
const getEnglishDataElementName = useCallback14((translatedName) => {
|
|
20088
20222
|
var _a2, _b2, _c2;
|
|
20089
20223
|
if (!((_a2 = englishDataRef.current) == null ? void 0 : _a2.purposes)) {
|
|
@@ -27502,6 +27636,7 @@ import { jsx as jsx90 } from "react/jsx-runtime";
|
|
|
27502
27636
|
injectPCStyles();
|
|
27503
27637
|
function RedactoPrivacyCenter({
|
|
27504
27638
|
baseUrl,
|
|
27639
|
+
ledgerBaseUrl,
|
|
27505
27640
|
slug,
|
|
27506
27641
|
accessToken,
|
|
27507
27642
|
refreshToken,
|
|
@@ -27509,7 +27644,7 @@ function RedactoPrivacyCenter({
|
|
|
27509
27644
|
initialPage = "consent-manager",
|
|
27510
27645
|
onBack
|
|
27511
27646
|
}) {
|
|
27512
|
-
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(
|
|
27513
27648
|
AuthProvider,
|
|
27514
27649
|
{
|
|
27515
27650
|
accessToken,
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|