@redacto.io/consent-sdk-react 10.0.0-beta.7 → 10.0.0-beta.9
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 +16 -0
- package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
- package/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +162 -56
- package/dist/index.mjs +161 -57
- package/dist/privacy-center.d.mts +54 -12
- package/dist/privacy-center.d.ts +54 -12
- package/dist/privacy-center.js +265 -77
- package/dist/privacy-center.mjs +264 -78
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +55 -5
- package/src/RedactoNoticeConsent/api/index.ts +78 -48
- package/src/RedactoNoticeConsent/api/sandbox.ts +150 -0
- package/src/RedactoNoticeConsent/api/types.ts +57 -11
- package/src/RedactoNoticeConsent/types.ts +28 -2
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +37 -2
- package/src/RedactoPrivacyCenter/api/actions.ts +3 -1
- package/src/RedactoPrivacyCenter/api/client.ts +122 -25
- package/src/RedactoPrivacyCenter/api/fetcher.ts +24 -9
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +2 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +55 -30
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +8 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +42 -6
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +131 -39
- package/src/RedactoPrivacyCenter/context/types.ts +38 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +3 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +12 -5
- package/src/RedactoPrivacyCenter/lib/sandbox.ts +104 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +20 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +15 -0
- package/src/shared/sandbox.ts +6 -0
- package/tests/Form.test.tsx +142 -5
- package/tests/consentClient.test.ts +46 -0
package/tests/Form.test.tsx
CHANGED
|
@@ -524,12 +524,14 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
524
524
|
expect(submitButton.disabled).toBe(true);
|
|
525
525
|
});
|
|
526
526
|
|
|
527
|
-
it('
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
527
|
+
it('runs the picker overlay against the legacy endpoint when no ledgerBaseUrl is configured', async () => {
|
|
528
|
+
// `getUserConsents` serves the same grouped payload from either backend
|
|
529
|
+
// (ledger `/public/...` or Python `/dsar/privacy-center/...`), so the
|
|
530
|
+
// overlay must run in both modes — Python's form/data no longer emits
|
|
531
|
+
// product groups, and skipping the read leaves the picker empty.
|
|
531
532
|
baseUrlStub.ledgerBaseUrl = undefined;
|
|
532
533
|
setupSession();
|
|
534
|
+
fetchUserConsents.mockResolvedValue(null);
|
|
533
535
|
getFormData.mockResolvedValue(
|
|
534
536
|
makeFormData({
|
|
535
537
|
direct_groups: [
|
|
@@ -545,12 +547,12 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
545
547
|
|
|
546
548
|
render(<Form onCreateRequest={vi.fn()} />);
|
|
547
549
|
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
550
|
+
await waitFor(() => expect(fetchUserConsents).toHaveBeenCalled());
|
|
548
551
|
|
|
549
552
|
pickRequestType(RequestTypeEnum.ERASURE);
|
|
550
553
|
await waitFor(() => {
|
|
551
554
|
expect(screen.getByText('Acme Loyalty')).toBeInTheDocument();
|
|
552
555
|
});
|
|
553
|
-
expect(fetchUserConsents).not.toHaveBeenCalled();
|
|
554
556
|
});
|
|
555
557
|
|
|
556
558
|
it('runs the ledger picker overlay when ledgerBaseUrl is configured', async () => {
|
|
@@ -565,6 +567,141 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
565
567
|
await waitFor(() => expect(fetchUserConsents).toHaveBeenCalled());
|
|
566
568
|
});
|
|
567
569
|
|
|
570
|
+
it('keeps the form/data groups when user-consents comes back with no groups', async () => {
|
|
571
|
+
// A successful-but-empty read must never overwrite groups form/data did
|
|
572
|
+
// supply — that turned a populated picker into "No products available".
|
|
573
|
+
setupSession();
|
|
574
|
+
fetchUserConsents.mockResolvedValue({ direct: [], nominated: [] });
|
|
575
|
+
getFormData.mockResolvedValue(
|
|
576
|
+
makeFormData({
|
|
577
|
+
direct_groups: [
|
|
578
|
+
makeGroup(),
|
|
579
|
+
makeGroup({
|
|
580
|
+
product_uuid: 'prod-direct-2',
|
|
581
|
+
product_name: 'Acme Rewards',
|
|
582
|
+
purposes: [basePurpose('purp-2', 'Analytics')],
|
|
583
|
+
}),
|
|
584
|
+
],
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
render(<Form onCreateRequest={vi.fn()} />);
|
|
589
|
+
await waitFor(() => expect(fetchUserConsents).toHaveBeenCalled());
|
|
590
|
+
|
|
591
|
+
pickRequestType(RequestTypeEnum.ERASURE);
|
|
592
|
+
await waitFor(() => {
|
|
593
|
+
expect(screen.getByText('Acme Loyalty')).toBeInTheDocument();
|
|
594
|
+
});
|
|
595
|
+
expect(screen.getByText('Acme Rewards')).toBeInTheDocument();
|
|
596
|
+
expect(screen.queryByText('No products available')).not.toBeInTheDocument();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('falls back to the legacy flat purposes list when the backend exposes no product groups', async () => {
|
|
600
|
+
// Pre-grouping backends return purposes without any product grouping.
|
|
601
|
+
// Those consents must stay reachable under one synthetic group instead
|
|
602
|
+
// of the picker claiming the principal has none.
|
|
603
|
+
setupSession();
|
|
604
|
+
fetchUserConsents.mockResolvedValue(null);
|
|
605
|
+
getFormData.mockResolvedValue(
|
|
606
|
+
makeFormData({
|
|
607
|
+
direct_groups: [],
|
|
608
|
+
nominated_groups: [],
|
|
609
|
+
purposes: [{ purpose: basePurpose('purp-flat', 'Marketing') }],
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
render(<Form onCreateRequest={vi.fn()} />);
|
|
614
|
+
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
615
|
+
|
|
616
|
+
pickRequestType(RequestTypeEnum.ACCESS);
|
|
617
|
+
|
|
618
|
+
await waitFor(() => {
|
|
619
|
+
expect(screen.getByText('Your data')).toBeInTheDocument();
|
|
620
|
+
});
|
|
621
|
+
expect(screen.getByText('Marketing')).toBeInTheDocument();
|
|
622
|
+
expect(screen.queryByText('No products available')).not.toBeInTheDocument();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('keeps the empty state when the flat purposes carry no data elements', async () => {
|
|
626
|
+
// Nothing selectable survives `buildGroupPurposes`, so the synthetic group
|
|
627
|
+
// would render an unusable panel — the empty state is the honest answer.
|
|
628
|
+
setupSession();
|
|
629
|
+
fetchUserConsents.mockResolvedValue(null);
|
|
630
|
+
getFormData.mockResolvedValue(
|
|
631
|
+
makeFormData({
|
|
632
|
+
direct_groups: [],
|
|
633
|
+
nominated_groups: [],
|
|
634
|
+
purposes: [{ purpose: basePurpose('purp-flat', 'Marketing', []) }],
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
render(<Form onCreateRequest={vi.fn()} />);
|
|
639
|
+
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
640
|
+
|
|
641
|
+
pickRequestType(RequestTypeEnum.ACCESS);
|
|
642
|
+
|
|
643
|
+
await waitFor(() => {
|
|
644
|
+
expect(screen.getByText('No products available')).toBeInTheDocument();
|
|
645
|
+
});
|
|
646
|
+
expect(screen.queryByText('Your data')).not.toBeInTheDocument();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('submits the legacy flat fallback as an org-level case without a product anchor', async () => {
|
|
650
|
+
// The synthetic group has no server-side product, so the case must go up
|
|
651
|
+
// in the pre-grouping org-level shape rather than carrying a made-up
|
|
652
|
+
// `product_uuid` the backend cannot resolve.
|
|
653
|
+
setupSession();
|
|
654
|
+
fetchUserConsents.mockResolvedValue(null);
|
|
655
|
+
getFormData.mockResolvedValue(
|
|
656
|
+
makeFormData({
|
|
657
|
+
direct_groups: [],
|
|
658
|
+
nominated_groups: [],
|
|
659
|
+
purposes: [{ purpose: basePurpose('purp-flat', 'Marketing') }],
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
662
|
+
createCaseRequest.mockResolvedValueOnce({
|
|
663
|
+
case_id: 'CASE-FLAT',
|
|
664
|
+
uuid: 'uuid-flat',
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
render(<Form onCreateRequest={vi.fn()} />);
|
|
668
|
+
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
669
|
+
|
|
670
|
+
pickRequestType(RequestTypeEnum.ACCESS);
|
|
671
|
+
|
|
672
|
+
await waitFor(() => {
|
|
673
|
+
expect(screen.getByText('Marketing')).toBeInTheDocument();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const marketingCheckbox = screen
|
|
677
|
+
.getByText('Marketing')
|
|
678
|
+
.closest('label')
|
|
679
|
+
?.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
|
|
680
|
+
expect(marketingCheckbox).toBeTruthy();
|
|
681
|
+
fireEvent.click(marketingCheckbox!);
|
|
682
|
+
|
|
683
|
+
const confirmCheckbox = (
|
|
684
|
+
screen.getAllByRole('checkbox') as HTMLInputElement[]
|
|
685
|
+
).find((c) => c.closest('label')?.textContent?.includes('I confirm'));
|
|
686
|
+
expect(confirmCheckbox).toBeTruthy();
|
|
687
|
+
fireEvent.click(confirmCheckbox!);
|
|
688
|
+
|
|
689
|
+
const submitButton = screen.getByRole('button', { name: /^submit$/i });
|
|
690
|
+
expect(submitButton).not.toBeDisabled();
|
|
691
|
+
await act(async () => {
|
|
692
|
+
fireEvent.click(submitButton);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
await waitFor(() => expect(createCaseRequest).toHaveBeenCalledTimes(1));
|
|
696
|
+
|
|
697
|
+
const payload = createCaseRequest.mock.calls[0][2];
|
|
698
|
+
expect('product_uuid' in payload).toBe(false);
|
|
699
|
+
expect('data_principal_uuid' in payload).toBe(false);
|
|
700
|
+
expect(payload.request_details.purposes).toEqual([
|
|
701
|
+
{ purpose_uuid: 'purp-flat', data_element_uuids: ['purp-flat-de1'] },
|
|
702
|
+
]);
|
|
703
|
+
});
|
|
704
|
+
|
|
568
705
|
it('renders the product step for DSAR types when direct_groups is present', async () => {
|
|
569
706
|
setupSession();
|
|
570
707
|
const direct = makeGroup({
|
|
@@ -93,6 +93,24 @@ describe('createConsentClient — legacy mode (no ledgerBaseUrl)', () => {
|
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
it('records the selected language in the legacy manage-consent body', async () => {
|
|
97
|
+
await client.manageConsent(
|
|
98
|
+
baseRequest,
|
|
99
|
+
'purp-1',
|
|
100
|
+
'jane@example.com',
|
|
101
|
+
'revoke',
|
|
102
|
+
undefined,
|
|
103
|
+
undefined,
|
|
104
|
+
undefined,
|
|
105
|
+
undefined,
|
|
106
|
+
undefined,
|
|
107
|
+
undefined,
|
|
108
|
+
undefined,
|
|
109
|
+
'doi',
|
|
110
|
+
);
|
|
111
|
+
expect(fetchedBody()).toMatchObject({ language: 'doi' });
|
|
112
|
+
});
|
|
113
|
+
|
|
96
114
|
it('routes getReceipts to the legacy dsar/privacy-center path', async () => {
|
|
97
115
|
await client.getReceipts(baseRequest, { skip: 0, limit: 10 });
|
|
98
116
|
expect(fetchedUrl()).toBe(`${LEGACY_PREFIX}/receipts?skip=0&limit=10`);
|
|
@@ -103,6 +121,16 @@ describe('createConsentClient — legacy mode (no ledgerBaseUrl)', () => {
|
|
|
103
121
|
await client.getReceiptPdf(baseRequest, 'r-1');
|
|
104
122
|
expect(fetchedUrl()).toBe(`${LEGACY_PREFIX}/receipts/r-1/pdf`);
|
|
105
123
|
});
|
|
124
|
+
|
|
125
|
+
it('appends the language query param to getActivities when provided', async () => {
|
|
126
|
+
await client.getActivities(baseRequest, 0, 15, 'doi');
|
|
127
|
+
expect(fetchedUrl()).toContain('language=doi');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('omits the language query param from getActivities for the default language', async () => {
|
|
131
|
+
await client.getActivities(baseRequest, 0, 15);
|
|
132
|
+
expect(fetchedUrl()).not.toContain('language=');
|
|
133
|
+
});
|
|
106
134
|
});
|
|
107
135
|
|
|
108
136
|
describe('createConsentClient — ledger mode (ledgerBaseUrl set)', () => {
|
|
@@ -151,6 +179,24 @@ describe('createConsentClient — ledger mode (ledgerBaseUrl set)', () => {
|
|
|
151
179
|
});
|
|
152
180
|
});
|
|
153
181
|
|
|
182
|
+
it('records the selected language in the ledger partial submit body', async () => {
|
|
183
|
+
await client.manageConsent(
|
|
184
|
+
baseRequest,
|
|
185
|
+
'purp-1',
|
|
186
|
+
'jane@example.com',
|
|
187
|
+
'revoke',
|
|
188
|
+
undefined,
|
|
189
|
+
undefined,
|
|
190
|
+
undefined,
|
|
191
|
+
undefined,
|
|
192
|
+
undefined,
|
|
193
|
+
'notice-1',
|
|
194
|
+
[{ uuid: 'de-1', selected: true }],
|
|
195
|
+
'doi',
|
|
196
|
+
);
|
|
197
|
+
expect(fetchedBody()).toMatchObject({ language: 'doi' });
|
|
198
|
+
});
|
|
199
|
+
|
|
154
200
|
it('throws from manageConsent when noticeUuid is missing', async () => {
|
|
155
201
|
await expect(
|
|
156
202
|
client.manageConsent(baseRequest, 'purp-1', 'jane@example.com', 'revoke'),
|