@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/index.d.mts +4 -2
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +122 -15
  5. package/dist/index.mjs +122 -15
  6. package/dist/privacy-center.d.mts +31 -7
  7. package/dist/privacy-center.d.ts +31 -7
  8. package/dist/privacy-center.js +173 -38
  9. package/dist/privacy-center.mjs +173 -38
  10. package/package.json +1 -1
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -4
  13. package/src/RedactoNoticeConsent/api/index.ts +131 -2
  14. package/src/RedactoNoticeConsent/api/types.ts +33 -1
  15. package/src/RedactoNoticeConsent/types.ts +1 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -1
  17. package/src/RedactoNoticeConsentInline/api/index.ts +5 -2
  18. package/src/RedactoNoticeConsentInline/api/types.ts +1 -1
  19. package/src/RedactoNoticeConsentInline/types.ts +1 -0
  20. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
  21. package/src/RedactoPrivacyCenter/api/actions.ts +10 -3
  22. package/src/RedactoPrivacyCenter/api/client.ts +67 -21
  23. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
  24. package/src/RedactoPrivacyCenter/components/Form.tsx +58 -6
  25. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
  26. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
  27. package/src/RedactoPrivacyCenter/context/types.ts +1 -0
  28. package/src/RedactoPrivacyCenter/lib/api.ts +8 -0
  29. package/src/RedactoPrivacyCenter/lib/constants.ts +9 -0
  30. package/src/RedactoPrivacyCenter/lib/types.ts +11 -1
  31. package/tests/Form.test.tsx +5 -0
  32. 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
- baseUrl?: string;
233
+ ledgerBaseUrl: string;
202
234
  noticeUuid: string;
203
235
  purposes: Array<Purpose>;
204
236
  declined: boolean;
@@ -5,6 +5,7 @@ export type Props = Readonly<{
5
5
  accessToken: string;
6
6
  refreshToken: string;
7
7
  baseUrl?: string;
8
+ ledgerBaseUrl: string;
8
9
  settings?: Partial<Settings>;
9
10
  language?: string;
10
11
  blockUI?: 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
- baseUrl,
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
- baseUrl,
50
+ ledgerBaseUrl,
51
51
  noticeUuid,
52
52
  purposes,
53
53
  declined,
54
54
  meta_data,
55
55
  }: SubmitConsentEventParams): Promise<void> => {
56
- const apiBaseUrl = baseUrl || BASE_URL;
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,
@@ -172,7 +172,7 @@ export type SubmitConsentEventParams = {
172
172
  org_uuid: string;
173
173
  workspace_uuid: string;
174
174
  accessToken: string;
175
- baseUrl?: string;
175
+ ledgerBaseUrl: string;
176
176
  noticeUuid: string;
177
177
  purposes: Array<Purpose>;
178
178
  declined: boolean;
@@ -8,6 +8,7 @@ export type Props = Readonly<{
8
8
  accessToken?: string;
9
9
  refreshToken?: string;
10
10
  baseUrl?: string;
11
+ ledgerBaseUrl: string;
11
12
  settings?: Partial<Settings>;
12
13
  language?: string;
13
14
  onAccept?: () => void;
@@ -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
- const result = await params.consentClient.manageConsent(
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 (result as { success: boolean })?.success ?? null;
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 url = privacyCenterUrl(baseRequest, `/user-consents${queryString ? `?${queryString}` : ""}`);
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 url = privacyCenterUrl(
412
- baseRequest,
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
- const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
438
- const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
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
- const url = privacyCenterUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
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
- const url = privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
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
- setData(displayData);
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 { DataElement, FormProductGroup, Purpose } from '../lib/types';
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(() => createConsentClient(baseUrl), [baseUrl]);
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 (
@@ -14,5 +14,6 @@ export type AuthContextValue = Readonly<{
14
14
 
15
15
  export type BaseUrlContextValue = Readonly<{
16
16
  baseUrl: string;
17
+ ledgerBaseUrl: string;
17
18
  consentClient: ReturnType<typeof createConsentClient>;
18
19
  }>;
@@ -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
- pagination: Pagination;
418
+ total: number;
419
+ skip: number;
420
+ limit: number;
411
421
  };
412
422
 
413
423
  export type ActivityResponse = {
@@ -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 ─────────────────────────────────────────────────────────────