@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
|
@@ -19,6 +19,38 @@ export type DataElementSelection = {
|
|
|
19
19
|
required: boolean;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
// ─── Ledger notice check-consent ────────────────────────────────────────────
|
|
23
|
+
// Mirrors the Go ledger's `NoticeConsentStatusResponse`. Used to overlay the
|
|
24
|
+
// banner's prior-consent / reconsent state from the ledger, since Python's
|
|
25
|
+
// notice-validation overlay is empty after the consent cutover.
|
|
26
|
+
export type NoticeConsentStatusDataElement = {
|
|
27
|
+
uuid: string;
|
|
28
|
+
name: string;
|
|
29
|
+
selected: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type NoticeConsentStatusPurpose = {
|
|
33
|
+
uuid: string;
|
|
34
|
+
name: string;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
status: string; // ACTIVE | EXPIRED | WITHDRAW | DECLINED | INACTIVE
|
|
37
|
+
expiry_datetime?: string | null;
|
|
38
|
+
data_elements: NoticeConsentStatusDataElement[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type NoticeConsentStatusResponse = {
|
|
42
|
+
consented: boolean;
|
|
43
|
+
all_mandatory_active: boolean;
|
|
44
|
+
purposes: NoticeConsentStatusPurpose[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type FetchNoticeConsentStatusParams = {
|
|
48
|
+
accessToken: string;
|
|
49
|
+
ledgerBaseUrl: string;
|
|
50
|
+
noticeUuid: string;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
};
|
|
53
|
+
|
|
22
54
|
export type ConsentContent = {
|
|
23
55
|
code: number;
|
|
24
56
|
status: string;
|
|
@@ -198,7 +230,7 @@ export type Purpose = {
|
|
|
198
230
|
|
|
199
231
|
export type SubmitConsentEventParams = {
|
|
200
232
|
accessToken: string;
|
|
201
|
-
|
|
233
|
+
ledgerBaseUrl: string;
|
|
202
234
|
noticeUuid: string;
|
|
203
235
|
purposes: Array<Purpose>;
|
|
204
236
|
declined: boolean;
|
|
@@ -12,6 +12,7 @@ const RedactoConsentInlineComponent = ({
|
|
|
12
12
|
notice_uuid,
|
|
13
13
|
accessToken,
|
|
14
14
|
baseUrl,
|
|
15
|
+
ledgerBaseUrl,
|
|
15
16
|
language = "en",
|
|
16
17
|
onAccept,
|
|
17
18
|
onError,
|
|
@@ -319,7 +320,7 @@ const RedactoConsentInlineComponent = ({
|
|
|
319
320
|
org_uuid,
|
|
320
321
|
workspace_uuid,
|
|
321
322
|
accessToken: tokenToUse,
|
|
322
|
-
|
|
323
|
+
ledgerBaseUrl,
|
|
323
324
|
noticeUuid: content.notice_uuid,
|
|
324
325
|
purposes: content.purposes.map((purpose) => {
|
|
325
326
|
// Check if any required data element is selected
|
|
@@ -382,6 +383,7 @@ const RedactoConsentInlineComponent = ({
|
|
|
382
383
|
org_uuid,
|
|
383
384
|
workspace_uuid,
|
|
384
385
|
baseUrl,
|
|
386
|
+
ledgerBaseUrl,
|
|
385
387
|
selectedDataElements,
|
|
386
388
|
selectedPurposes,
|
|
387
389
|
applicationId,
|
|
@@ -47,13 +47,16 @@ export const submitConsentEvent = async ({
|
|
|
47
47
|
org_uuid,
|
|
48
48
|
workspace_uuid,
|
|
49
49
|
accessToken,
|
|
50
|
-
|
|
50
|
+
ledgerBaseUrl,
|
|
51
51
|
noticeUuid,
|
|
52
52
|
purposes,
|
|
53
53
|
declined,
|
|
54
54
|
meta_data,
|
|
55
55
|
}: SubmitConsentEventParams): Promise<void> => {
|
|
56
|
-
|
|
56
|
+
if (!ledgerBaseUrl) {
|
|
57
|
+
throw new Error("ledgerBaseUrl is required to submit consent");
|
|
58
|
+
}
|
|
59
|
+
const apiBaseUrl = ledgerBaseUrl;
|
|
57
60
|
|
|
58
61
|
const payload: ConsentEventPayload = {
|
|
59
62
|
notice_uuid: noticeUuid,
|
|
@@ -15,6 +15,7 @@ injectPCStyles();
|
|
|
15
15
|
|
|
16
16
|
interface RedactoPrivacyCenterProps {
|
|
17
17
|
baseUrl: string; // Consent server base URL
|
|
18
|
+
ledgerBaseUrl: string; // Go ledger base URL — serves user-consents
|
|
18
19
|
slug: string;
|
|
19
20
|
accessToken: string;
|
|
20
21
|
refreshToken: string;
|
|
@@ -25,6 +26,7 @@ interface RedactoPrivacyCenterProps {
|
|
|
25
26
|
|
|
26
27
|
export function RedactoPrivacyCenter({
|
|
27
28
|
baseUrl,
|
|
29
|
+
ledgerBaseUrl,
|
|
28
30
|
slug,
|
|
29
31
|
accessToken,
|
|
30
32
|
refreshToken,
|
|
@@ -35,7 +37,7 @@ export function RedactoPrivacyCenter({
|
|
|
35
37
|
return (
|
|
36
38
|
<I18nextProvider i18n={i18n}>
|
|
37
39
|
<BrandingProvider slug={slug} baseUrl={baseUrl}>
|
|
38
|
-
<BaseUrlProvider baseUrl={baseUrl}>
|
|
40
|
+
<BaseUrlProvider baseUrl={baseUrl} ledgerBaseUrl={ledgerBaseUrl}>
|
|
39
41
|
<AuthProvider
|
|
40
42
|
accessToken={accessToken}
|
|
41
43
|
refreshToken={refreshToken}
|
|
@@ -351,6 +351,8 @@ export async function manageConsentServer(params: {
|
|
|
351
351
|
nominatorContact?: string;
|
|
352
352
|
dataElementUuids?: string[];
|
|
353
353
|
productUuid?: string;
|
|
354
|
+
noticeUuid?: string;
|
|
355
|
+
dataElements?: Array<{ uuid: string; selected: boolean }>;
|
|
354
356
|
}): Promise<boolean | null> {
|
|
355
357
|
try {
|
|
356
358
|
if (!params.consentClient) {
|
|
@@ -359,7 +361,10 @@ export async function manageConsentServer(params: {
|
|
|
359
361
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
360
362
|
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
361
363
|
|
|
362
|
-
|
|
364
|
+
// Routes to the Go ledger's submit-consent (partial); resolves on 2xx and
|
|
365
|
+
// throws otherwise, so a resolved call is success. The ledger returns a
|
|
366
|
+
// ConsentEventResponse (no `success` flag), unlike the old Python endpoint.
|
|
367
|
+
await params.consentClient.manageConsent(
|
|
363
368
|
baseRequest,
|
|
364
369
|
params.purposeId,
|
|
365
370
|
params.contact,
|
|
@@ -368,9 +373,11 @@ export async function manageConsentServer(params: {
|
|
|
368
373
|
undefined,
|
|
369
374
|
params.nominatorContact,
|
|
370
375
|
params.dataElementUuids,
|
|
371
|
-
params.productUuid
|
|
376
|
+
params.productUuid,
|
|
377
|
+
params.noticeUuid,
|
|
378
|
+
params.dataElements
|
|
372
379
|
);
|
|
373
|
-
return
|
|
380
|
+
return true;
|
|
374
381
|
} catch (error: any) {
|
|
375
382
|
// Normalize nominator-specific errors into stable error codes
|
|
376
383
|
const errorMsg = error?.message || '';
|
|
@@ -23,9 +23,15 @@ export type BaseRequest = {
|
|
|
23
23
|
/**
|
|
24
24
|
* Consent service client factory.
|
|
25
25
|
* Creates a client instance with a specific base URL.
|
|
26
|
-
* @param baseUrl - Consent server base URL
|
|
26
|
+
* @param baseUrl - Consent server base URL (Python service, e.g. http://host/consent)
|
|
27
|
+
* @param ledgerBaseUrl - Go ledger base URL (e.g. http://host:8090/consent-ledger).
|
|
28
|
+
* Used for user-consents reads which are served from the ledger's `/public/...` paths.
|
|
27
29
|
*/
|
|
28
|
-
export function createConsentClient(baseUrl: string) {
|
|
30
|
+
export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
31
|
+
if (!ledgerBaseUrl) {
|
|
32
|
+
throw new Error("ledgerBaseUrl is required to create the consent client");
|
|
33
|
+
}
|
|
34
|
+
const ledgerBase = ledgerBaseUrl.replace(/\/$/, "");
|
|
29
35
|
/**
|
|
30
36
|
* Privacy Center API fetch wrapper that uses Authorization: Bearer header.
|
|
31
37
|
* Privacy center endpoints require JWT tokens in Authorization header, not X-Auth-Token.
|
|
@@ -170,6 +176,10 @@ export function createConsentClient(baseUrl: string) {
|
|
|
170
176
|
return `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center${path}`;
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
function ledgerUrl(baseRequest: BaseRequest, path: string): string {
|
|
180
|
+
return `${ledgerBase}/public/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}${path}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
function caseUrl(baseRequest: BaseRequest, caseUuid: string, path = ''): string {
|
|
174
184
|
return privacyCenterUrl(baseRequest, `/cases/${caseUuid}${path}`);
|
|
175
185
|
}
|
|
@@ -373,7 +383,8 @@ export function createConsentClient(baseUrl: string) {
|
|
|
373
383
|
if (params.product_uuid) queryParams.append("product_uuid", params.product_uuid);
|
|
374
384
|
if (params.language) queryParams.append("language", params.language);
|
|
375
385
|
const queryString = queryParams.toString();
|
|
376
|
-
const
|
|
386
|
+
const suffix = `/user-consents${queryString ? `?${queryString}` : ""}`;
|
|
387
|
+
const url = ledgerUrl(baseRequest, suffix);
|
|
377
388
|
return privacyCenterApiFetch(url, {
|
|
378
389
|
...options,
|
|
379
390
|
method: "GET",
|
|
@@ -408,10 +419,8 @@ export function createConsentClient(baseUrl: string) {
|
|
|
408
419
|
if (params.language) queryParams.append("language", params.language);
|
|
409
420
|
if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
|
|
410
421
|
const queryString = queryParams.toString();
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
`/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`
|
|
414
|
-
);
|
|
422
|
+
const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
|
|
423
|
+
const url = ledgerUrl(baseRequest, suffix);
|
|
415
424
|
return privacyCenterApiFetch(url, {
|
|
416
425
|
...options,
|
|
417
426
|
method: "GET",
|
|
@@ -421,7 +430,16 @@ export function createConsentClient(baseUrl: string) {
|
|
|
421
430
|
},
|
|
422
431
|
|
|
423
432
|
/**
|
|
424
|
-
* Manage consent (privacy center) - revoke, regrant, or renew
|
|
433
|
+
* Manage consent (privacy center) - revoke, regrant, or renew.
|
|
434
|
+
*
|
|
435
|
+
* The Go ledger has no manage-consent route, so this is expressed as a
|
|
436
|
+
* `partial` submit-consent on the ledger — mirroring the Python dual-write
|
|
437
|
+
* relay (`build_consent_management_payload`): the affected purpose is
|
|
438
|
+
* re-submitted with `selected: false` to revoke or `selected: true` to
|
|
439
|
+
* regrant/renew, and `partial: true` so the rest of the notice's purposes
|
|
440
|
+
* are left untouched. The principal is resolved from the JWT, so no
|
|
441
|
+
* `contact`/identifiers are sent in the body, and `consent_event_uuid` is
|
|
442
|
+
* omitted so the ledger mints a fresh event.
|
|
425
443
|
*/
|
|
426
444
|
async manageConsent(
|
|
427
445
|
baseRequest: BaseRequest,
|
|
@@ -432,19 +450,44 @@ export function createConsentClient(baseUrl: string) {
|
|
|
432
450
|
options?: Omit<ApiFetchOptions, "getAuthToken">,
|
|
433
451
|
nominatorContact?: string,
|
|
434
452
|
dataElementUuids?: string[],
|
|
435
|
-
productUuid?: string
|
|
453
|
+
productUuid?: string,
|
|
454
|
+
noticeUuid?: string,
|
|
455
|
+
dataElements?: Array<{ uuid: string; selected: boolean }>
|
|
436
456
|
): Promise<unknown> {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (nominatorContact) {
|
|
440
|
-
body.nominator_contact = nominatorContact;
|
|
441
|
-
}
|
|
442
|
-
if (Array.isArray(dataElementUuids) && dataElementUuids.length > 0) {
|
|
443
|
-
body.data_element_uuids = dataElementUuids;
|
|
444
|
-
}
|
|
445
|
-
if (productUuid) {
|
|
446
|
-
body.product_uuid = productUuid;
|
|
457
|
+
if (!noticeUuid) {
|
|
458
|
+
throw new Error("notice_uuid is required to manage consent via the ledger");
|
|
447
459
|
}
|
|
460
|
+
const purposeSelected = status !== 'revoke';
|
|
461
|
+
const elements = (dataElements ?? []).map((de) => {
|
|
462
|
+
let selected: boolean;
|
|
463
|
+
if (status === 'revoke') {
|
|
464
|
+
selected = false;
|
|
465
|
+
} else if (status === 'regrant') {
|
|
466
|
+
// Only the data elements the user chose to re-grant become selected.
|
|
467
|
+
selected = Array.isArray(dataElementUuids)
|
|
468
|
+
? dataElementUuids.includes(de.uuid)
|
|
469
|
+
: de.selected;
|
|
470
|
+
} else {
|
|
471
|
+
// renew — preserve the currently-granted set.
|
|
472
|
+
selected = de.selected;
|
|
473
|
+
}
|
|
474
|
+
return { uuid: de.uuid, selected };
|
|
475
|
+
});
|
|
476
|
+
const body = {
|
|
477
|
+
notice_uuid: noticeUuid,
|
|
478
|
+
partial: true,
|
|
479
|
+
declined: false,
|
|
480
|
+
select_all_mandatory: false,
|
|
481
|
+
self_declared_adult: false,
|
|
482
|
+
purposes: [
|
|
483
|
+
{
|
|
484
|
+
purpose_uuid: purposeId,
|
|
485
|
+
selected: purposeSelected,
|
|
486
|
+
data_elements: elements,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
};
|
|
490
|
+
const url = ledgerUrl(baseRequest, "/submit-consent");
|
|
448
491
|
return privacyCenterApiFetch(url, {
|
|
449
492
|
...options,
|
|
450
493
|
method: "POST",
|
|
@@ -504,7 +547,8 @@ export function createConsentClient(baseUrl: string) {
|
|
|
504
547
|
if (params.created_before) queryParams.append("created_before", params.created_before);
|
|
505
548
|
if (params.language) queryParams.append("language", params.language);
|
|
506
549
|
const queryString = queryParams.toString();
|
|
507
|
-
|
|
550
|
+
// Receipts are served by the Go ledger (paginated list under /public/...).
|
|
551
|
+
const url = ledgerUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
|
|
508
552
|
return privacyCenterApiFetch<unknown>(url, {
|
|
509
553
|
...options,
|
|
510
554
|
method: "GET",
|
|
@@ -524,7 +568,9 @@ export function createConsentClient(baseUrl: string) {
|
|
|
524
568
|
receiptUuid: string,
|
|
525
569
|
authToken?: string
|
|
526
570
|
): Promise<Blob> {
|
|
527
|
-
|
|
571
|
+
// The ledger serves the receipt PDF at /receipts/{uuid}/download; the
|
|
572
|
+
// receipt_uuid comes from the ledger list above, so both must be the ledger.
|
|
573
|
+
const url = ledgerUrl(baseRequest, `/receipts/${receiptUuid}/download`);
|
|
528
574
|
const token = resolveAuthToken(authToken, baseRequest);
|
|
529
575
|
const requestHeaders = new Headers();
|
|
530
576
|
requestHeaders.set("Accept", "application/pdf");
|
|
@@ -754,6 +754,15 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
754
754
|
};
|
|
755
755
|
|
|
756
756
|
try {
|
|
757
|
+
// The ledger's submit-consent needs the notice the purpose was consented
|
|
758
|
+
// under, plus the purpose's data elements with their current selected
|
|
759
|
+
// state — both live on the loaded consent record (selectedRecord).
|
|
760
|
+
const noticeUuid = selectedRecord?.notice_uuid ?? undefined;
|
|
761
|
+
const dataElements = (selectedRecord?.data_elements ?? []).map((el) => ({
|
|
762
|
+
uuid: el.uuid,
|
|
763
|
+
selected: el.selected,
|
|
764
|
+
}));
|
|
765
|
+
|
|
757
766
|
const response = await manageConsent({
|
|
758
767
|
organizationId,
|
|
759
768
|
workspaceId,
|
|
@@ -765,6 +774,8 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
765
774
|
nominatorContact,
|
|
766
775
|
dataElementUuids,
|
|
767
776
|
productUuid,
|
|
777
|
+
noticeUuid,
|
|
778
|
+
dataElements,
|
|
768
779
|
});
|
|
769
780
|
|
|
770
781
|
if (response) {
|
|
@@ -787,7 +798,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
787
798
|
handleCloseModal();
|
|
788
799
|
}
|
|
789
800
|
},
|
|
790
|
-
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient]
|
|
801
|
+
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient, selectedRecord]
|
|
791
802
|
);
|
|
792
803
|
|
|
793
804
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
@@ -10,9 +10,10 @@ import { PiUploadBold } from 'react-icons/pi';
|
|
|
10
10
|
import { toast } from '../utils/toast';
|
|
11
11
|
import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCText, PCTextarea } from '../ui';
|
|
12
12
|
import { PCModal } from '../ui/PCModal';
|
|
13
|
-
import { buildGroupDataElements, buildGroupPurposes } from './groupHelpers';
|
|
13
|
+
import { buildGroupDataElements, buildGroupPurposes, consentGroupToFormProductGroup } from './groupHelpers';
|
|
14
14
|
import { pcStyles } from '../styles/pcStyles';
|
|
15
|
-
import { createCaseRequest, getFormData } from '../lib/api';
|
|
15
|
+
import { createCaseRequest, fetchUserConsents, getFormData } from '../lib/api';
|
|
16
|
+
import { FORM_PICKER_GROUP_LIMIT } from '../lib/constants';
|
|
16
17
|
import { ensureValidToken } from '../lib/utils';
|
|
17
18
|
import { useAuth } from '../context/AuthContext';
|
|
18
19
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
@@ -274,6 +275,53 @@ export default function Form({
|
|
|
274
275
|
[],
|
|
275
276
|
);
|
|
276
277
|
|
|
278
|
+
// Approach A: source the DSAR request-form product picker from the Go
|
|
279
|
+
// ledger's user-consents. After the consent cutover, consents live in the
|
|
280
|
+
// ledger, so Python's form/data returns no product groups; we overlay the
|
|
281
|
+
// ledger's product-grouped consents onto the form data instead. Only the
|
|
282
|
+
// end-user privacy-center path is eligible — popup / case-management resolve
|
|
283
|
+
// an arbitrary contact, which the JWT-scoped ledger endpoint cannot serve,
|
|
284
|
+
// so those keep the Python form/data groups. Returns the form data unchanged
|
|
285
|
+
// when ineligible or when the ledger read fails (graceful fallback to
|
|
286
|
+
// whatever groups form/data supplied).
|
|
287
|
+
const withLedgerPickerGroups = useCallback(
|
|
288
|
+
async (form: FormData, language?: string): Promise<FormData> => {
|
|
289
|
+
if (isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
|
|
290
|
+
return form;
|
|
291
|
+
}
|
|
292
|
+
const consents = await fetchUserConsents({
|
|
293
|
+
organizationId,
|
|
294
|
+
workspaceId,
|
|
295
|
+
authContext,
|
|
296
|
+
consentClient,
|
|
297
|
+
// No status filter: include every status (active / expired / withdrawn
|
|
298
|
+
// / declined), matching Python form/data, so a DSAR can target products
|
|
299
|
+
// the user no longer actively consents to.
|
|
300
|
+
limit: FORM_PICKER_GROUP_LIMIT,
|
|
301
|
+
nominatedLimit: FORM_PICKER_GROUP_LIMIT,
|
|
302
|
+
language,
|
|
303
|
+
});
|
|
304
|
+
if (!consents) {
|
|
305
|
+
return form;
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
...form,
|
|
309
|
+
direct_groups: (consents.direct ?? []).map(consentGroupToFormProductGroup),
|
|
310
|
+
nominated_groups: (consents.nominated ?? []).map(
|
|
311
|
+
consentGroupToFormProductGroup,
|
|
312
|
+
),
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
[
|
|
316
|
+
isPopup,
|
|
317
|
+
isCaseManagementContext,
|
|
318
|
+
organizationId,
|
|
319
|
+
workspaceId,
|
|
320
|
+
authContext,
|
|
321
|
+
consentClient,
|
|
322
|
+
],
|
|
323
|
+
);
|
|
324
|
+
|
|
277
325
|
const fetchAndApplyTranslations = useCallback(async (langValue: string) => {
|
|
278
326
|
if (!organizationId || !workspaceId || !data) {
|
|
279
327
|
return;
|
|
@@ -314,7 +362,7 @@ export default function Form({
|
|
|
314
362
|
return;
|
|
315
363
|
}
|
|
316
364
|
|
|
317
|
-
setData(translatedFormData);
|
|
365
|
+
setData(await withLedgerPickerGroups(translatedFormData, langValue));
|
|
318
366
|
|
|
319
367
|
if (translatedFormData.grievance_options) {
|
|
320
368
|
const currentGrievanceSelections = { ...selectedGrievances };
|
|
@@ -333,7 +381,7 @@ export default function Form({
|
|
|
333
381
|
} finally {
|
|
334
382
|
setIsTranslating(false);
|
|
335
383
|
}
|
|
336
|
-
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t]);
|
|
384
|
+
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t, withLedgerPickerGroups]);
|
|
337
385
|
|
|
338
386
|
// Clear product / org-level selections when request type changes.
|
|
339
387
|
// Per-product state (selectedPurposes, selectedCorrectionElements,
|
|
@@ -486,7 +534,11 @@ export default function Form({
|
|
|
486
534
|
}
|
|
487
535
|
}
|
|
488
536
|
|
|
489
|
-
|
|
537
|
+
const pickerData = await withLedgerPickerGroups(
|
|
538
|
+
displayData,
|
|
539
|
+
langParam || undefined,
|
|
540
|
+
);
|
|
541
|
+
setData(pickerData);
|
|
490
542
|
setGrievanceOptions(displayData.grievance_options || []);
|
|
491
543
|
const initialGrievances = (
|
|
492
544
|
displayData.grievance_options || []
|
|
@@ -509,7 +561,7 @@ export default function Form({
|
|
|
509
561
|
};
|
|
510
562
|
|
|
511
563
|
void initializePrivacyCenterForm();
|
|
512
|
-
}, [organizationId, workspaceId, isPopup, authContext, consentClient]);
|
|
564
|
+
}, [organizationId, workspaceId, isPopup, authContext, consentClient, withLedgerPickerGroups]);
|
|
513
565
|
|
|
514
566
|
/**
|
|
515
567
|
* Maps translated correction element names back to English names using UUID lookup.
|
|
@@ -1,4 +1,67 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ConsentDataElement,
|
|
3
|
+
ConsentGroup,
|
|
4
|
+
DataElement,
|
|
5
|
+
FormProductGroup,
|
|
6
|
+
Purpose,
|
|
7
|
+
PurposeItem,
|
|
8
|
+
} from '../lib/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map a Go-ledger `ConsentGroup` (from the user-consents endpoint) onto the
|
|
12
|
+
* DSAR request form's `FormProductGroup`. Approach A: the request-form product
|
|
13
|
+
* picker is now sourced from the ledger instead of Python's `form/data`, since
|
|
14
|
+
* consents live in the ledger after the cutover.
|
|
15
|
+
*
|
|
16
|
+
* The shapes are near-identical; the only adaptations are:
|
|
17
|
+
* - purpose id rename: ledger `purpose_uuid` -> form `uuid` (which the picker
|
|
18
|
+
* keys on and the submit payload sends back as `purpose_uuid`),
|
|
19
|
+
* - the ledger data-element's `selected` stands in for the form's
|
|
20
|
+
* `given_consent`,
|
|
21
|
+
* - fields the picker never reads (industries, validity, descriptions) are
|
|
22
|
+
* defaulted.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: the ledger caps inline purposes at 50 per group (`has_more_purposes`).
|
|
25
|
+
* A product with more than 50 purposes would need the per-group purposes
|
|
26
|
+
* endpoint to assemble a complete CORRECTION data-element list (follow-up).
|
|
27
|
+
*/
|
|
28
|
+
export const consentGroupToFormProductGroup = (
|
|
29
|
+
group: ConsentGroup,
|
|
30
|
+
): FormProductGroup => ({
|
|
31
|
+
product_uuid: group.product_uuid,
|
|
32
|
+
product_name: group.product_name,
|
|
33
|
+
product_description: group.product_description,
|
|
34
|
+
nominator: group.nominator,
|
|
35
|
+
purposes: (group.purposes ?? []).map(purposeItemToFormPurpose),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const purposeItemToFormPurpose = (purpose: PurposeItem): Purpose => ({
|
|
39
|
+
uuid: purpose.purpose_uuid,
|
|
40
|
+
purpose_uuid: purpose.purpose_uuid,
|
|
41
|
+
name: purpose.name,
|
|
42
|
+
description: purpose.description ?? '',
|
|
43
|
+
industries: '',
|
|
44
|
+
selected: false,
|
|
45
|
+
given_consent: false,
|
|
46
|
+
status: purpose.status,
|
|
47
|
+
validity: null,
|
|
48
|
+
enabled: true,
|
|
49
|
+
data_elements: (purpose.data_elements ?? []).map(
|
|
50
|
+
consentDataElementToFormDataElement,
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const consentDataElementToFormDataElement = (
|
|
55
|
+
element: ConsentDataElement,
|
|
56
|
+
): DataElement => ({
|
|
57
|
+
uuid: element.uuid,
|
|
58
|
+
name: element.name,
|
|
59
|
+
description: '',
|
|
60
|
+
enabled: element.enabled,
|
|
61
|
+
required: element.required,
|
|
62
|
+
given_consent: element.selected,
|
|
63
|
+
selected: element.selected,
|
|
64
|
+
});
|
|
2
65
|
|
|
3
66
|
/**
|
|
4
67
|
* Build the per-purpose state used by `PurposeSelectionSection` for a single
|
|
@@ -9,18 +9,24 @@ export const BaseUrlContext = createContext<BaseUrlContextValue | null>(null);
|
|
|
9
9
|
export const BaseUrlProvider = ({
|
|
10
10
|
children,
|
|
11
11
|
baseUrl,
|
|
12
|
+
ledgerBaseUrl,
|
|
12
13
|
}: {
|
|
13
14
|
children: ReactNode;
|
|
14
15
|
baseUrl: string; // Consent server base URL
|
|
16
|
+
ledgerBaseUrl: string; // Go ledger base URL — serves user-consents
|
|
15
17
|
}) => {
|
|
16
|
-
const consentClient = useMemo(
|
|
18
|
+
const consentClient = useMemo(
|
|
19
|
+
() => createConsentClient(baseUrl, ledgerBaseUrl),
|
|
20
|
+
[baseUrl, ledgerBaseUrl]
|
|
21
|
+
);
|
|
17
22
|
|
|
18
23
|
const value: BaseUrlContextValue = useMemo(
|
|
19
24
|
() => ({
|
|
20
25
|
baseUrl,
|
|
26
|
+
ledgerBaseUrl,
|
|
21
27
|
consentClient,
|
|
22
28
|
}),
|
|
23
|
-
[baseUrl, consentClient]
|
|
29
|
+
[baseUrl, ledgerBaseUrl, consentClient]
|
|
24
30
|
);
|
|
25
31
|
|
|
26
32
|
return (
|
|
@@ -392,6 +392,8 @@ export const manageConsent = async ({
|
|
|
392
392
|
nominatorContact,
|
|
393
393
|
dataElementUuids,
|
|
394
394
|
productUuid,
|
|
395
|
+
noticeUuid,
|
|
396
|
+
dataElements,
|
|
395
397
|
}: {
|
|
396
398
|
organizationId: string;
|
|
397
399
|
workspaceId: string;
|
|
@@ -403,6 +405,8 @@ export const manageConsent = async ({
|
|
|
403
405
|
nominatorContact?: string;
|
|
404
406
|
dataElementUuids?: string[];
|
|
405
407
|
productUuid?: string;
|
|
408
|
+
noticeUuid?: string;
|
|
409
|
+
dataElements?: Array<{ uuid: string; selected: boolean }>;
|
|
406
410
|
}): Promise<boolean | null> => {
|
|
407
411
|
try {
|
|
408
412
|
// Ensure we have a valid token before making the API call
|
|
@@ -423,6 +427,8 @@ export const manageConsent = async ({
|
|
|
423
427
|
nominatorContact,
|
|
424
428
|
dataElementUuids,
|
|
425
429
|
productUuid,
|
|
430
|
+
noticeUuid,
|
|
431
|
+
dataElements,
|
|
426
432
|
});
|
|
427
433
|
return result;
|
|
428
434
|
} catch (error: any) {
|
|
@@ -445,6 +451,8 @@ export const manageConsent = async ({
|
|
|
445
451
|
nominatorContact,
|
|
446
452
|
dataElementUuids,
|
|
447
453
|
productUuid,
|
|
454
|
+
noticeUuid,
|
|
455
|
+
dataElements,
|
|
448
456
|
});
|
|
449
457
|
}
|
|
450
458
|
console.error(`Error ${action} consent:`, error);
|
|
@@ -196,6 +196,15 @@ export const MAX_REFRESH_BACKOFF_MS = 300_000; // 5m
|
|
|
196
196
|
export const REFRESH_TOKEN_STORAGE_KEY = 'redacto_pc_refresh_token';
|
|
197
197
|
export const LEGACY_REFRESH_TOKEN_COOKIE = 'refresh_token';
|
|
198
198
|
|
|
199
|
+
// ─── DSAR request form ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
// Max product groups requested from the Go ledger's user-consents when
|
|
202
|
+
// sourcing the DSAR request-form product picker (Approach A). The ledger caps
|
|
203
|
+
// user-consents page size at 100; the picker needs all of a principal's
|
|
204
|
+
// groups, so we request the max. A principal with >100 product groups would
|
|
205
|
+
// need group-level pagination (follow-up).
|
|
206
|
+
export const FORM_PICKER_GROUP_LIMIT = 100;
|
|
207
|
+
|
|
199
208
|
// ─── Notification Hub ──────────────────────────────────────────────────────
|
|
200
209
|
|
|
201
210
|
export const POLL_INTERVAL = 30_000;
|
|
@@ -256,6 +256,10 @@ export type PurposeItem = {
|
|
|
256
256
|
method: string;
|
|
257
257
|
data_elements: ConsentDataElement[];
|
|
258
258
|
link_reason: string | null;
|
|
259
|
+
// UUID of the notice version this purpose was consented under. Served by the
|
|
260
|
+
// Go ledger's user-consents response; required to revoke/regrant via the
|
|
261
|
+
// ledger's submit-consent endpoint (which has no manage-consent route).
|
|
262
|
+
notice_uuid: string | null;
|
|
259
263
|
};
|
|
260
264
|
|
|
261
265
|
export type ConsentGroup = {
|
|
@@ -405,9 +409,15 @@ export type Receipt = {
|
|
|
405
409
|
created_at: string;
|
|
406
410
|
};
|
|
407
411
|
|
|
412
|
+
// Mirrors the Go ledger's `ConsentReceiptListResponse` (offset-paginated page
|
|
413
|
+
// of the principal's receipts): flat `total`/`skip`/`limit`, not a nested
|
|
414
|
+
// `pagination` object. `normalizeReceipts` in ReceiptsList tolerates either
|
|
415
|
+
// envelope, but this type documents the ledger contract the SDK now calls.
|
|
408
416
|
export type ReceiptListResponse = {
|
|
409
417
|
data: Receipt[];
|
|
410
|
-
|
|
418
|
+
total: number;
|
|
419
|
+
skip: number;
|
|
420
|
+
limit: number;
|
|
411
421
|
};
|
|
412
422
|
|
|
413
423
|
export type ActivityResponse = {
|
package/tests/Form.test.tsx
CHANGED
|
@@ -98,9 +98,14 @@ vi.mock('../src/RedactoPrivacyCenter/lib/i18n', () => ({
|
|
|
98
98
|
// 1-form -> 1-case: only `createCaseRequest` is consumed by Form.tsx now.
|
|
99
99
|
const createCaseRequest = vi.fn();
|
|
100
100
|
const getFormData = vi.fn();
|
|
101
|
+
const fetchUserConsents = vi.fn();
|
|
101
102
|
vi.mock('../src/RedactoPrivacyCenter/lib/api', () => ({
|
|
102
103
|
createCaseRequest: (...args: unknown[]) => createCaseRequest(...args),
|
|
103
104
|
getFormData: (...args: unknown[]) => getFormData(...args),
|
|
105
|
+
// Approach A sources the product picker from the ledger's user-consents.
|
|
106
|
+
// Default to no ledger result so the form falls back to the getFormData
|
|
107
|
+
// product groups these tests set up.
|
|
108
|
+
fetchUserConsents: (...args: unknown[]) => fetchUserConsents(...args),
|
|
104
109
|
}));
|
|
105
110
|
|
|
106
111
|
// ─── upload stub ─────────────────────────────────────────────────────────────
|