@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.
- package/CHANGELOG.md +6 -0
- 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 +187 -44
- package/dist/privacy-center.mjs +187 -44
- 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/PrivacyCenterLayout.tsx +6 -4
- 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/AuthContext.tsx +7 -5
- 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/src/RedactoPrivacyCenter/lib/utils.ts +28 -0
- package/tests/Form.test.tsx +5 -0
- package/tests/mocks.ts +1 -0
- package/tests/resolveContactFromJwt.test.ts +44 -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;
|
|
@@ -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
|
|
9936
|
-
if (
|
|
9937
|
-
sessionStorage.setItem("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
|
|
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
|
|
10282
|
-
|
|
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
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
15089
|
-
if (
|
|
15090
|
-
sessionStorage.setItem("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
|
-
|
|
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
|
@@ -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,
|