@redacto.io/consent-sdk-react 9.5.0-beta.0 → 10.0.0-beta.1
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/.turbo/turbo-build.log +22 -25
- package/CHANGELOG.md +16 -2
- package/dist/index.d.mts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +116 -11
- package/dist/index.mjs +116 -11
- package/dist/privacy-center.d.mts +76 -19
- package/dist/privacy-center.d.ts +76 -19
- package/dist/privacy-center.js +2022 -1378
- package/dist/privacy-center.mjs +1946 -1297
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -3
- package/src/RedactoNoticeConsent/api/index.ts +130 -1
- package/src/RedactoNoticeConsent/api/types.ts +33 -0
- package/src/RedactoNoticeConsent/types.ts +1 -0
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +4 -1
- package/src/RedactoNoticeConsentInline/api/types.ts +1 -0
- package/src/RedactoNoticeConsentInline/types.ts +1 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
- package/src/RedactoPrivacyCenter/api/actions.ts +15 -2
- package/src/RedactoPrivacyCenter/api/client.ts +146 -20
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +63 -7
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +83 -0
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +193 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
- 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 +63 -1
- package/src/RedactoPrivacyCenter/locales/as/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +7 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +105 -0
- package/tests/Form.test.tsx +58 -7
- package/tests/consentClient.test.ts +171 -0
- package/tests/manageConsentServer.test.ts +38 -0
- package/tests/mocks.ts +1 -0
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
|
|
@@ -2121,6 +2146,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2121
2146
|
await submitConsentEvent({
|
|
2122
2147
|
accessToken,
|
|
2123
2148
|
baseUrl,
|
|
2149
|
+
ledgerBaseUrl,
|
|
2124
2150
|
noticeUuid: content.notice_uuid,
|
|
2125
2151
|
purposes: content.purposes.map((purpose) => {
|
|
2126
2152
|
// 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,
|
|
@@ -128,6 +132,7 @@ export const fetchConsentContent = async ({
|
|
|
128
132
|
export const submitConsentEvent = async ({
|
|
129
133
|
accessToken,
|
|
130
134
|
baseUrl,
|
|
135
|
+
ledgerBaseUrl,
|
|
131
136
|
noticeUuid,
|
|
132
137
|
purposes,
|
|
133
138
|
declined,
|
|
@@ -151,7 +156,9 @@ export const submitConsentEvent = async ({
|
|
|
151
156
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
|
|
159
|
+
// Fall back to the consent server when no ledger is configured, so existing
|
|
160
|
+
// integrations that only pass baseUrl remain backwards compatible.
|
|
161
|
+
const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
|
|
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,
|
|
@@ -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;
|
|
@@ -199,6 +231,7 @@ export type Purpose = {
|
|
|
199
231
|
export type SubmitConsentEventParams = {
|
|
200
232
|
accessToken: string;
|
|
201
233
|
baseUrl?: string;
|
|
234
|
+
ledgerBaseUrl?: string;
|
|
202
235
|
noticeUuid: string;
|
|
203
236
|
purposes: Array<Purpose>;
|
|
204
237
|
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,
|
|
@@ -320,6 +321,7 @@ const RedactoConsentInlineComponent = ({
|
|
|
320
321
|
workspace_uuid,
|
|
321
322
|
accessToken: tokenToUse,
|
|
322
323
|
baseUrl,
|
|
324
|
+
ledgerBaseUrl,
|
|
323
325
|
noticeUuid: content.notice_uuid,
|
|
324
326
|
purposes: content.purposes.map((purpose) => {
|
|
325
327
|
// Check if any required data element is selected
|
|
@@ -382,6 +384,7 @@ const RedactoConsentInlineComponent = ({
|
|
|
382
384
|
org_uuid,
|
|
383
385
|
workspace_uuid,
|
|
384
386
|
baseUrl,
|
|
387
|
+
ledgerBaseUrl,
|
|
385
388
|
selectedDataElements,
|
|
386
389
|
selectedPurposes,
|
|
387
390
|
applicationId,
|
|
@@ -48,12 +48,15 @@ export const submitConsentEvent = async ({
|
|
|
48
48
|
workspace_uuid,
|
|
49
49
|
accessToken,
|
|
50
50
|
baseUrl,
|
|
51
|
+
ledgerBaseUrl,
|
|
51
52
|
noticeUuid,
|
|
52
53
|
purposes,
|
|
53
54
|
declined,
|
|
54
55
|
meta_data,
|
|
55
56
|
}: SubmitConsentEventParams): Promise<void> => {
|
|
56
|
-
|
|
57
|
+
// Fall back to the consent server when no ledger is configured, so existing
|
|
58
|
+
// integrations that only pass baseUrl remain backwards compatible.
|
|
59
|
+
const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
|
|
57
60
|
|
|
58
61
|
const payload: ConsentEventPayload = {
|
|
59
62
|
notice_uuid: noticeUuid,
|
|
@@ -12,6 +12,7 @@ import { useAuth } from './context/AuthContext';
|
|
|
12
12
|
import { PCLoader } from './ui';
|
|
13
13
|
import { pcStyles } from './styles/pcStyles';
|
|
14
14
|
import AppShell from './components/AppShell/AppShell';
|
|
15
|
+
import { SelectUserGate } from './components/SelectUserGate';
|
|
15
16
|
|
|
16
17
|
interface PrivacyCenterLayoutProps {
|
|
17
18
|
children: ReactNode;
|
|
@@ -164,11 +165,13 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
|
|
|
164
165
|
|
|
165
166
|
return (
|
|
166
167
|
<OrganizationProvider organization={organization}>
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
<SelectUserGate>
|
|
169
|
+
<DataAvailabilityProvider>
|
|
170
|
+
<AppShell onBack={onBack}>
|
|
171
|
+
{children}
|
|
172
|
+
</AppShell>
|
|
173
|
+
</DataAvailabilityProvider>
|
|
174
|
+
</SelectUserGate>
|
|
172
175
|
</OrganizationProvider>
|
|
173
176
|
);
|
|
174
177
|
}
|
|
@@ -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; omit to use the legacy Python privacy-center consent endpoints
|
|
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}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
GetCaseMessagesParams,
|
|
25
25
|
MessageDocument,
|
|
26
26
|
GetNotificationsServerParams,
|
|
27
|
+
LegacyManageConsentResponse,
|
|
27
28
|
NotificationActionBaseParams,
|
|
28
29
|
NotificationActionResponse,
|
|
29
30
|
NotificationByUuidServerParams,
|
|
@@ -351,6 +352,8 @@ export async function manageConsentServer(params: {
|
|
|
351
352
|
nominatorContact?: string;
|
|
352
353
|
dataElementUuids?: string[];
|
|
353
354
|
productUuid?: string;
|
|
355
|
+
noticeUuid?: string;
|
|
356
|
+
dataElements?: Array<{ uuid: string; selected: boolean }>;
|
|
354
357
|
}): Promise<boolean | null> {
|
|
355
358
|
try {
|
|
356
359
|
if (!params.consentClient) {
|
|
@@ -359,6 +362,11 @@ export async function manageConsentServer(params: {
|
|
|
359
362
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
360
363
|
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
361
364
|
|
|
365
|
+
// Ledger mode routes to the Go ledger's submit-consent (partial), which
|
|
366
|
+
// resolves on 2xx and throws otherwise — a resolved call is success (the
|
|
367
|
+
// ledger returns a ConsentEventResponse, no `success` flag). Legacy mode
|
|
368
|
+
// hits the Python manage-consent endpoint, whose unwrapped response
|
|
369
|
+
// carries a `success` flag that must be honored even on 2xx.
|
|
362
370
|
const result = await params.consentClient.manageConsent(
|
|
363
371
|
baseRequest,
|
|
364
372
|
params.purposeId,
|
|
@@ -368,9 +376,14 @@ export async function manageConsentServer(params: {
|
|
|
368
376
|
undefined,
|
|
369
377
|
params.nominatorContact,
|
|
370
378
|
params.dataElementUuids,
|
|
371
|
-
params.productUuid
|
|
379
|
+
params.productUuid,
|
|
380
|
+
params.noticeUuid,
|
|
381
|
+
params.dataElements
|
|
372
382
|
);
|
|
373
|
-
|
|
383
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
384
|
+
return Boolean((result as LegacyManageConsentResponse).success);
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
374
387
|
} catch (error: any) {
|
|
375
388
|
// Normalize nominator-specific errors into stable error codes
|
|
376
389
|
const errorMsg = error?.message || '';
|