@redacto.io/consent-sdk-react 5.2.0 → 6.0.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/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +149 -206
- package/dist/index.mjs +148 -206
- package/dist/privacy-center.d.mts +178 -39
- package/dist/privacy-center.d.ts +178 -39
- package/dist/privacy-center.js +10534 -6795
- package/dist/privacy-center.mjs +7922 -4181
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +16 -20
- package/src/RedactoNoticeConsent/api/index.ts +21 -103
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +5 -4
- package/src/RedactoNoticeConsentInline/api/index.ts +64 -117
- package/src/RedactoPrivacyCenter/NotificationsPage.tsx +174 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +136 -8
- package/src/RedactoPrivacyCenter/api/client.ts +176 -15
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +722 -273
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +187 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +65 -165
- package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +179 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +318 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +333 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +102 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +6 -2
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +15 -0
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +8 -5
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +160 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +18 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +54 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +57 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +19 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +92 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +20 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +58 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +10 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +112 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +27 -0
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +105 -55
- package/src/RedactoPrivacyCenter/components/types.ts +2 -5
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +21 -1
- package/src/RedactoPrivacyCenter/lib/api.ts +147 -28
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +4 -2
- package/src/RedactoPrivacyCenter/lib/constants.ts +78 -1
- package/src/RedactoPrivacyCenter/lib/types.ts +205 -36
- package/src/RedactoPrivacyCenter/lib/utils.ts +3 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +42 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +15 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +12 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1027 -0
- package/src/RedactoPrivacyCenter/styles/variables.css +5 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +36 -16
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +4 -12
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +1 -1
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +23 -1
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +51 -5
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +2 -4
- package/src/index.ts +5 -0
- package/src/privacy-center.ts +8 -3
- package/src/shared/api-errors.ts +171 -0
package/package.json
CHANGED
|
@@ -2168,11 +2168,12 @@ export const RedactoNoticeConsent = ({
|
|
|
2168
2168
|
return;
|
|
2169
2169
|
}
|
|
2170
2170
|
|
|
2171
|
-
const err = error as Error
|
|
2171
|
+
const err = error as Error;
|
|
2172
|
+
// The SDK is a passthrough: server-owned message wins. Only the
|
|
2173
|
+
// bare-Error fallback is used when no message is present (e.g. a
|
|
2174
|
+
// network failure that never produced a Response).
|
|
2172
2175
|
const message =
|
|
2173
|
-
err.
|
|
2174
|
-
? "An error occurred while submitting your consent. Please try again later."
|
|
2175
|
-
: err.message || "Failed to submit consent. Please try again.";
|
|
2176
|
+
err.message || "Failed to submit consent. Please try again.";
|
|
2176
2177
|
setErrorMessage(message);
|
|
2177
2178
|
onError?.(error as Error);
|
|
2178
2179
|
} finally {
|
|
@@ -2713,30 +2714,25 @@ export const RedactoNoticeConsent = ({
|
|
|
2713
2714
|
const availableLanguages = useMemo(() => {
|
|
2714
2715
|
if (!content) return [language];
|
|
2715
2716
|
|
|
2716
|
-
// English is always
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2717
|
+
// English is always pinned first since it's in active_config.
|
|
2718
|
+
// Non-English default_language is pinned second so users can find it quickly.
|
|
2719
|
+
// Remaining languages are sorted A→Z by English name.
|
|
2720
|
+
const pinned = ["English"];
|
|
2720
2721
|
if (
|
|
2721
2722
|
content.default_language &&
|
|
2722
2723
|
content.default_language !== "English" &&
|
|
2723
2724
|
content.default_language !== "en"
|
|
2724
2725
|
) {
|
|
2725
|
-
|
|
2726
|
+
pinned.push(content.default_language);
|
|
2726
2727
|
}
|
|
2727
2728
|
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
languages.push(lang);
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
);
|
|
2737
|
-
}
|
|
2729
|
+
const rest = content.supported_languages_and_translations
|
|
2730
|
+
? Object.keys(content.supported_languages_and_translations)
|
|
2731
|
+
.filter((lang) => !pinned.includes(lang))
|
|
2732
|
+
.sort((a, b) => a.localeCompare(b, "en"))
|
|
2733
|
+
: [];
|
|
2738
2734
|
|
|
2739
|
-
return
|
|
2735
|
+
return [...pinned, ...rest];
|
|
2740
2736
|
}, [content, language]);
|
|
2741
2737
|
|
|
2742
2738
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jwtDecode } from "jwt-decode";
|
|
2
|
+
import { parseApiError } from "../../shared/api-errors";
|
|
2
3
|
import type {
|
|
3
4
|
ConsentContent,
|
|
4
5
|
ConsentEventPayload,
|
|
@@ -109,23 +110,10 @@ export const fetchConsentContent = async ({
|
|
|
109
110
|
});
|
|
110
111
|
|
|
111
112
|
if (!response.ok) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (response.status === 403) {
|
|
117
|
-
errorMessage = "Forbidden: Access denied";
|
|
118
|
-
} else if (response.status === 404) {
|
|
119
|
-
errorMessage = "Notice not found";
|
|
120
|
-
} else if (response.status === 409) {
|
|
121
|
-
errorMessage = "User has already provided consent";
|
|
122
|
-
} else if (response.status >= 500) {
|
|
123
|
-
errorMessage = "Server error: Please try again later";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const error = new Error(errorMessage) as Error & { status?: number };
|
|
127
|
-
error.status = response.status;
|
|
128
|
-
throw error;
|
|
113
|
+
throw await parseApiError(
|
|
114
|
+
response,
|
|
115
|
+
"Could not load the consent notice. Please try again."
|
|
116
|
+
);
|
|
129
117
|
}
|
|
130
118
|
|
|
131
119
|
const data: ConsentContent = await response.json();
|
|
@@ -211,28 +199,10 @@ export const submitConsentEvent = async ({
|
|
|
211
199
|
);
|
|
212
200
|
|
|
213
201
|
if (!response.ok) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const errorData = await response.json();
|
|
219
|
-
errorMessage = errorData.message || "Invalid request data";
|
|
220
|
-
} catch {
|
|
221
|
-
errorMessage = "Invalid request data";
|
|
222
|
-
}
|
|
223
|
-
} else if (response.status === 401) {
|
|
224
|
-
errorMessage = "Unauthorized: Invalid or expired token";
|
|
225
|
-
} else if (response.status === 403) {
|
|
226
|
-
errorMessage = "Forbidden: Access denied";
|
|
227
|
-
} else if (response.status === 404) {
|
|
228
|
-
errorMessage = "Notice not found";
|
|
229
|
-
} else if (response.status >= 500) {
|
|
230
|
-
errorMessage = "Server error: Please try again later";
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const error = new Error(errorMessage) as Error & { status?: number };
|
|
234
|
-
error.status = response.status;
|
|
235
|
-
throw error;
|
|
202
|
+
throw await parseApiError(
|
|
203
|
+
response,
|
|
204
|
+
"Could not submit your consent. Please try again."
|
|
205
|
+
);
|
|
236
206
|
}
|
|
237
207
|
|
|
238
208
|
// Clear any cached consent content since user preferences have changed
|
|
@@ -274,21 +244,10 @@ export const fetchTTSAudioUrls = async ({
|
|
|
274
244
|
});
|
|
275
245
|
|
|
276
246
|
if (!response.ok) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
} else if (response.status === 403) {
|
|
282
|
-
errorMessage = "Forbidden: Access denied";
|
|
283
|
-
} else if (response.status === 404) {
|
|
284
|
-
errorMessage = "Notice or audio not found";
|
|
285
|
-
} else if (response.status >= 500) {
|
|
286
|
-
errorMessage = "Server error: Please try again later";
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const error = new Error(errorMessage) as Error & { status?: number };
|
|
290
|
-
error.status = response.status;
|
|
291
|
-
throw error;
|
|
247
|
+
throw await parseApiError(
|
|
248
|
+
response,
|
|
249
|
+
"Could not load audio narration. Please try again."
|
|
250
|
+
);
|
|
292
251
|
}
|
|
293
252
|
|
|
294
253
|
const data: TTSAudioUrlsResponse = await response.json();
|
|
@@ -378,33 +337,10 @@ export const initiateGuardianVerification = async ({
|
|
|
378
337
|
);
|
|
379
338
|
|
|
380
339
|
if (!response.ok) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const errorData = await response.json();
|
|
386
|
-
errorMessage = errorData.message || "Invalid request data";
|
|
387
|
-
} catch {
|
|
388
|
-
errorMessage = "Invalid request data";
|
|
389
|
-
}
|
|
390
|
-
} else if (response.status === 401) {
|
|
391
|
-
errorMessage = "Unauthorized: Invalid or expired token";
|
|
392
|
-
} else if (response.status === 404) {
|
|
393
|
-
errorMessage = "Organization or workspace not found";
|
|
394
|
-
} else if (response.status === 422) {
|
|
395
|
-
try {
|
|
396
|
-
const errorData = await response.json();
|
|
397
|
-
errorMessage = errorData.message || "Validation error: Please check your input";
|
|
398
|
-
} catch {
|
|
399
|
-
errorMessage = "Validation error: Please check your input";
|
|
400
|
-
}
|
|
401
|
-
} else if (response.status >= 500) {
|
|
402
|
-
errorMessage = "Server error: Please try again later";
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const error = new Error(errorMessage) as Error & { status?: number };
|
|
406
|
-
error.status = response.status;
|
|
407
|
-
throw error;
|
|
340
|
+
throw await parseApiError(
|
|
341
|
+
response,
|
|
342
|
+
"Could not start guardian verification. Please try again."
|
|
343
|
+
);
|
|
408
344
|
}
|
|
409
345
|
|
|
410
346
|
const responseData = await response.json();
|
|
@@ -486,28 +422,10 @@ export const verifyGuardianStatus = async ({
|
|
|
486
422
|
);
|
|
487
423
|
|
|
488
424
|
if (!response.ok) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const errorData = await response.json();
|
|
494
|
-
errorMessage = errorData.message || errorData.detail?.message || "Invalid session token";
|
|
495
|
-
} catch {
|
|
496
|
-
errorMessage = "Invalid session token";
|
|
497
|
-
}
|
|
498
|
-
} else if (response.status === 401) {
|
|
499
|
-
errorMessage = "Unauthorized: Invalid or expired token";
|
|
500
|
-
} else if (response.status === 404) {
|
|
501
|
-
errorMessage = "Verification session not found or expired. Please restart the verification process.";
|
|
502
|
-
} else if (response.status === 410) {
|
|
503
|
-
errorMessage = "Verification session has expired. Please restart the verification process.";
|
|
504
|
-
} else if (response.status >= 500) {
|
|
505
|
-
errorMessage = "Server error: Please try again later";
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const error = new Error(errorMessage) as Error & { status?: number };
|
|
509
|
-
error.status = response.status;
|
|
510
|
-
throw error;
|
|
425
|
+
throw await parseApiError(
|
|
426
|
+
response,
|
|
427
|
+
"Could not check verification status. Please try again."
|
|
428
|
+
);
|
|
511
429
|
}
|
|
512
430
|
|
|
513
431
|
const responseData = await response.json();
|
|
@@ -363,11 +363,12 @@ const RedactoConsentInlineComponent = ({
|
|
|
363
363
|
// Reset ref on success to allow re-submission if needed
|
|
364
364
|
hasSubmittedRef.current = false;
|
|
365
365
|
} catch (error) {
|
|
366
|
-
const err = error as Error
|
|
366
|
+
const err = error as Error;
|
|
367
|
+
// The SDK is a passthrough: server-owned message wins. Only the
|
|
368
|
+
// bare-Error fallback is used when no message is present (e.g. a
|
|
369
|
+
// network failure that never produced a Response).
|
|
367
370
|
const message =
|
|
368
|
-
err.
|
|
369
|
-
? "An error occurred while submitting your consent. Please try again later."
|
|
370
|
-
: err.message || "Failed to submit consent. Please try again.";
|
|
371
|
+
err.message || "Failed to submit consent. Please try again.";
|
|
371
372
|
setErrorMessage(message);
|
|
372
373
|
// Reset ref on error to allow retry
|
|
373
374
|
hasSubmittedRef.current = false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseApiError } from "../../shared/api-errors";
|
|
1
2
|
import type {
|
|
2
3
|
ConsentContent,
|
|
3
4
|
ConsentEventPayload,
|
|
@@ -14,51 +15,32 @@ export const fetchConsentContent = async ({
|
|
|
14
15
|
baseUrl,
|
|
15
16
|
specific_uuid,
|
|
16
17
|
}: FetchConsentContentParams): Promise<ConsentContent> => {
|
|
17
|
-
|
|
18
|
-
const apiBaseUrl = baseUrl || BASE_URL;
|
|
18
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (specific_uuid) {
|
|
25
|
-
url.searchParams.append("specific_uuid", specific_uuid);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const response = await fetch(url.toString(), {
|
|
29
|
-
method: "GET",
|
|
30
|
-
headers: {
|
|
31
|
-
"Content-Type": "application/json",
|
|
32
|
-
},
|
|
33
|
-
});
|
|
20
|
+
const url = new URL(
|
|
21
|
+
`${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices/get-notice/${notice_uuid}`
|
|
22
|
+
);
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
39
|
-
error.status = 401;
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (response.status === 409) {
|
|
44
|
-
const error = new Error("User has already provided consent") as Error & {
|
|
45
|
-
status?: number;
|
|
46
|
-
};
|
|
47
|
-
error.status = 409;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Failed to fetch consent content: ${response.statusText}`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
24
|
+
if (specific_uuid) {
|
|
25
|
+
url.searchParams.append("specific_uuid", specific_uuid);
|
|
26
|
+
}
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
28
|
+
const response = await fetch(url.toString(), {
|
|
29
|
+
method: "GET",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw await parseApiError(
|
|
37
|
+
response,
|
|
38
|
+
"Could not load the consent notice. Please try again."
|
|
39
|
+
);
|
|
61
40
|
}
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
return data;
|
|
62
44
|
};
|
|
63
45
|
|
|
64
46
|
export const submitConsentEvent = async ({
|
|
@@ -71,86 +53,51 @@ export const submitConsentEvent = async ({
|
|
|
71
53
|
declined,
|
|
72
54
|
meta_data,
|
|
73
55
|
}: SubmitConsentEventParams): Promise<void> => {
|
|
74
|
-
|
|
75
|
-
const apiBaseUrl = baseUrl || BASE_URL;
|
|
76
|
-
|
|
77
|
-
const payload: ConsentEventPayload = {
|
|
78
|
-
notice_uuid: noticeUuid,
|
|
79
|
-
purposes: purposes.map((purpose) => {
|
|
80
|
-
if (!purpose.uuid) {
|
|
81
|
-
throw new Error(`Invalid purpose data: missing uuid`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Only include enabled data elements (disabled elements are not selectable)
|
|
85
|
-
const dataElements = purpose.data_elements
|
|
86
|
-
.filter((element) => element.enabled)
|
|
87
|
-
.map((element) => ({
|
|
88
|
-
uuid: element.uuid,
|
|
89
|
-
selected: element.required ? true : Boolean(element.selected),
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
purpose_uuid: purpose.uuid,
|
|
94
|
-
selected: Boolean(purpose.selected),
|
|
95
|
-
data_elements: dataElements.length > 0 ? dataElements : undefined,
|
|
96
|
-
};
|
|
97
|
-
}),
|
|
98
|
-
select_all_mandatory: false,
|
|
99
|
-
source: "WEB",
|
|
100
|
-
declined,
|
|
101
|
-
meta_data,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const response = await fetch(
|
|
105
|
-
`${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
|
|
106
|
-
{
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: {
|
|
109
|
-
Authorization: `Bearer ${accessToken}`,
|
|
110
|
-
"Content-Type": "application/json",
|
|
111
|
-
},
|
|
112
|
-
body: JSON.stringify(payload),
|
|
113
|
-
}
|
|
114
|
-
);
|
|
56
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
115
57
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} catch {
|
|
122
|
-
// If parsing fails, use status text
|
|
123
|
-
errorData = { message: response.statusText };
|
|
58
|
+
const payload: ConsentEventPayload = {
|
|
59
|
+
notice_uuid: noticeUuid,
|
|
60
|
+
purposes: purposes.map((purpose) => {
|
|
61
|
+
if (!purpose.uuid) {
|
|
62
|
+
throw new Error(`Invalid purpose data: missing uuid`);
|
|
124
63
|
}
|
|
125
64
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const error = new Error(
|
|
142
|
-
errorData.detail?.message || errorData.message || `Failed to submit consent event: ${response.statusText}`
|
|
143
|
-
) as Error & {
|
|
144
|
-
status?: number;
|
|
145
|
-
code?: number;
|
|
146
|
-
detail?: { message?: string };
|
|
65
|
+
// Only include enabled data elements (disabled elements are not selectable)
|
|
66
|
+
const dataElements = purpose.data_elements
|
|
67
|
+
.filter((element) => element.enabled)
|
|
68
|
+
.map((element) => ({
|
|
69
|
+
uuid: element.uuid,
|
|
70
|
+
selected: element.required ? true : Boolean(element.selected),
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
purpose_uuid: purpose.uuid,
|
|
75
|
+
selected: Boolean(purpose.selected),
|
|
76
|
+
data_elements: dataElements.length > 0 ? dataElements : undefined,
|
|
147
77
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
78
|
+
}),
|
|
79
|
+
select_all_mandatory: false,
|
|
80
|
+
source: "WEB",
|
|
81
|
+
declined,
|
|
82
|
+
meta_data,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const response = await fetch(
|
|
86
|
+
`${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
|
|
87
|
+
{
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${accessToken}`,
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(payload),
|
|
152
94
|
}
|
|
153
|
-
|
|
154
|
-
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw await parseApiError(
|
|
99
|
+
response,
|
|
100
|
+
"Could not submit your consent. Please try again."
|
|
101
|
+
);
|
|
155
102
|
}
|
|
156
103
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { IoChevronBackOutline } from 'react-icons/io5';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { usePageChrome } from './context/PageChromeContext';
|
|
5
|
+
import { fetchNotifications } from './lib/api';
|
|
6
|
+
import type { Notification, NotificationSummary, NotificationCTA, FilterTab } from './lib/types';
|
|
7
|
+
import { TAB_TO_CATEGORY, POLL_INTERVAL, NOTIFICATION_PAGE_SIZE, useNotificationStatLabels } from './lib/constants';
|
|
8
|
+
import { useCurrentLanguage } from './lib/useCurrentLanguage';
|
|
9
|
+
import { StatCard } from './components/NotificationHub/StatCard';
|
|
10
|
+
import { NotificationListPanel } from './components/NotificationHub/NotificationListPanel';
|
|
11
|
+
import { useNotificationActions } from './components/NotificationHub/useNotificationActions';
|
|
12
|
+
|
|
13
|
+
export const NotificationsPage = () => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { langParam } = useCurrentLanguage();
|
|
16
|
+
const { setPageChromeOverride, clearPageChromeOverride } = usePageChrome();
|
|
17
|
+
const statLabels = useNotificationStatLabels();
|
|
18
|
+
const {
|
|
19
|
+
organizationId, workspaceId, authContext, consentClient, navigateTo,
|
|
20
|
+
hasAcknowledgeCTA, handleMarkRead, handleMarkAllRead, handleAcknowledge, handleCTAClick,
|
|
21
|
+
} = useNotificationActions();
|
|
22
|
+
|
|
23
|
+
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
24
|
+
const [summary, setSummary] = useState<NotificationSummary>({ total: 0, unread: 0, action_needed: 0, acknowledged: 0 });
|
|
25
|
+
const [activeTab, setActiveTab] = useState<FilterTab>('all');
|
|
26
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
27
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
28
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
29
|
+
const [skip, setSkip] = useState(0);
|
|
30
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
31
|
+
const loadNotificationsRef = useRef<(includeSummary?: boolean, silent?: boolean) => Promise<void>>();
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setPageChromeOverride({ titleKey: 'notificationHub', descriptionKey: 'notificationHubSubtitle' });
|
|
35
|
+
return () => clearPageChromeOverride();
|
|
36
|
+
}, [setPageChromeOverride, clearPageChromeOverride]);
|
|
37
|
+
|
|
38
|
+
const loadNotifications = useCallback(async (includeSummary = false, silent = false) => {
|
|
39
|
+
if (!organizationId || !workspaceId) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!silent) {
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await fetchNotifications(organizationId, workspaceId, {
|
|
47
|
+
category: TAB_TO_CATEGORY[activeTab], skip, limit: NOTIFICATION_PAGE_SIZE,
|
|
48
|
+
include_summary: includeSummary,
|
|
49
|
+
}, authContext, consentClient, langParam);
|
|
50
|
+
if (result) {
|
|
51
|
+
setNotifications(result.data);
|
|
52
|
+
if (result.summary) {
|
|
53
|
+
setSummary(result.summary);
|
|
54
|
+
}
|
|
55
|
+
setTotalCount(result.total);
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
setIsRefreshing(false);
|
|
60
|
+
}
|
|
61
|
+
}, [organizationId, workspaceId, activeTab, skip, authContext, consentClient, langParam]);
|
|
62
|
+
|
|
63
|
+
// Initial load with summary
|
|
64
|
+
useEffect(() => { loadNotifications(true); }, [loadNotifications]);
|
|
65
|
+
|
|
66
|
+
// Keep ref pointed at the latest loadNotifications so the polling effect
|
|
67
|
+
// can call it without depending on (and resetting on) tab/pagination changes.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
loadNotificationsRef.current = loadNotifications;
|
|
70
|
+
}, [loadNotifications]);
|
|
71
|
+
|
|
72
|
+
// Silent poll without summary (counts don't change on pagination)
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
pollRef.current = setInterval(() => {
|
|
75
|
+
loadNotificationsRef.current?.(false, true);
|
|
76
|
+
}, POLL_INTERVAL);
|
|
77
|
+
return () => {
|
|
78
|
+
if (pollRef.current) {
|
|
79
|
+
clearInterval(pollRef.current);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
useEffect(() => { setSkip(0); }, [activeTab]);
|
|
85
|
+
|
|
86
|
+
const onNotificationClick = useCallback(async (notification: Notification) => {
|
|
87
|
+
if (!organizationId || !workspaceId) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const shouldMarkRead = !notification.is_read;
|
|
91
|
+
const shouldAutoAcknowledge = !notification.is_acknowledged && !hasAcknowledgeCTA(notification);
|
|
92
|
+
if (!shouldMarkRead && !shouldAutoAcknowledge) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const markedRead = shouldMarkRead ? await handleMarkRead(notification.uuid) : false;
|
|
97
|
+
const acknowledged = shouldAutoAcknowledge ? await handleAcknowledge(notification.uuid) : false;
|
|
98
|
+
if (!markedRead && !acknowledged) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setNotifications((prev) => prev.map((n) =>
|
|
103
|
+
n.uuid === notification.uuid ? { ...n, ...(markedRead ? { is_read: true } : {}), ...(acknowledged ? { is_acknowledged: true } : {}) } : n
|
|
104
|
+
));
|
|
105
|
+
setSummary((prev) => ({
|
|
106
|
+
...prev,
|
|
107
|
+
unread: Math.max(0, prev.unread - (markedRead ? 1 : 0)),
|
|
108
|
+
acknowledged: prev.acknowledged + (acknowledged && !notification.is_acknowledged ? 1 : 0),
|
|
109
|
+
}));
|
|
110
|
+
}, [organizationId, workspaceId, hasAcknowledgeCTA, handleMarkRead, handleAcknowledge]);
|
|
111
|
+
|
|
112
|
+
const onCTAClick = useCallback(async (notification: Notification, cta: NotificationCTA) => {
|
|
113
|
+
const result = await handleCTAClick(notification, cta);
|
|
114
|
+
if (result === 'acknowledged') {
|
|
115
|
+
setNotifications((prev) => prev.map((n) =>
|
|
116
|
+
n.uuid === notification.uuid ? { ...n, is_read: true, is_acknowledged: true } : n
|
|
117
|
+
));
|
|
118
|
+
setSummary((prev) => ({
|
|
119
|
+
...prev,
|
|
120
|
+
unread: Math.max(0, prev.unread - (notification.is_read ? 0 : 1)),
|
|
121
|
+
acknowledged: prev.acknowledged + (notification.is_acknowledged ? 0 : 1),
|
|
122
|
+
}));
|
|
123
|
+
} else if (result) {
|
|
124
|
+
onNotificationClick(notification);
|
|
125
|
+
}
|
|
126
|
+
}, [handleCTAClick, onNotificationClick]);
|
|
127
|
+
|
|
128
|
+
const onMarkAllRead = useCallback(async () => {
|
|
129
|
+
const ok = await handleMarkAllRead();
|
|
130
|
+
if (!ok) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
|
|
134
|
+
setSummary((prev) => ({ ...prev, unread: 0 }));
|
|
135
|
+
}, [handleMarkAllRead]);
|
|
136
|
+
|
|
137
|
+
const onRefresh = useCallback(() => {
|
|
138
|
+
setIsRefreshing(true);
|
|
139
|
+
loadNotifications(true);
|
|
140
|
+
}, [loadNotifications]);
|
|
141
|
+
|
|
142
|
+
const onBack = useCallback(() => navigateTo('consent-manager'), [navigateTo]);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="pc-notification-page">
|
|
146
|
+
<button type="button" onClick={onBack} className="pc-notification-back-btn">
|
|
147
|
+
<IoChevronBackOutline size={14} />
|
|
148
|
+
{t('backToManageConsent')}
|
|
149
|
+
</button>
|
|
150
|
+
|
|
151
|
+
<div className="pc-notification-stats-grid">
|
|
152
|
+
<StatCard value={summary.total} label={statLabels.total} />
|
|
153
|
+
<StatCard value={summary.unread} label={statLabels.unread} />
|
|
154
|
+
<StatCard value={summary.action_needed} label={statLabels.action_needed} />
|
|
155
|
+
<StatCard value={summary.acknowledged} label={statLabels.acknowledged} />
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<NotificationListPanel
|
|
159
|
+
notifications={notifications}
|
|
160
|
+
totalCount={totalCount}
|
|
161
|
+
activeTab={activeTab}
|
|
162
|
+
onTabChange={setActiveTab}
|
|
163
|
+
skip={skip}
|
|
164
|
+
onSkipChange={setSkip}
|
|
165
|
+
isLoading={isLoading}
|
|
166
|
+
isRefreshing={isRefreshing}
|
|
167
|
+
onRefresh={onRefresh}
|
|
168
|
+
onMarkAllRead={onMarkAllRead}
|
|
169
|
+
onNotificationClick={onNotificationClick}
|
|
170
|
+
onCTAClick={onCTAClick}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
@@ -5,6 +5,7 @@ import { PrivacyCenterConsentManagerPage } from './PrivacyCenterConsentManagerPa
|
|
|
5
5
|
import { PrivacyCenterFormPage } from './PrivacyCenterFormPage';
|
|
6
6
|
import { PrivacyCenterActivityPage } from './PrivacyCenterActivityPage';
|
|
7
7
|
import { PrivacyCenterCaseDetailsPage } from './PrivacyCenterCaseDetailsPage';
|
|
8
|
+
import { NotificationsPage } from './NotificationsPage';
|
|
8
9
|
|
|
9
10
|
export function PrivacyCenterHomePage() {
|
|
10
11
|
const { currentPage } = useInternalNavigation();
|
|
@@ -24,6 +25,8 @@ export function PrivacyCenterHomePage() {
|
|
|
24
25
|
return <PrivacyCenterActivityPage />;
|
|
25
26
|
case 'case-details':
|
|
26
27
|
return <PrivacyCenterCaseDetailsPage />;
|
|
28
|
+
case 'notifications':
|
|
29
|
+
return <NotificationsPage />;
|
|
27
30
|
default:
|
|
28
31
|
return <PrivacyCenterConsentManagerPage />;
|
|
29
32
|
}
|