@redacto.io/consent-sdk-react 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +267 -153
- package/dist/index.mjs +267 -153
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +191 -97
- package/src/RedactoNoticeConsent/api/index.ts +62 -52
- package/src/RedactoNoticeConsent/api/types.ts +20 -5
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +18 -23
- package/src/RedactoNoticeConsentInline/api/index.ts +65 -27
- package/src/RedactoNoticeConsentInline/api/types.ts +20 -5
- package/src/utils/isLegacyApi.ts +12 -0
|
@@ -45,7 +45,10 @@ import { useMediaQuery } from "./useMediaQuery";
|
|
|
45
45
|
* Supports both regular consent and reconsent UI modes.
|
|
46
46
|
*/
|
|
47
47
|
const PurposeItem: React.FC<
|
|
48
|
-
PurposeItemProps & {
|
|
48
|
+
PurposeItemProps & {
|
|
49
|
+
selectedLanguage: string;
|
|
50
|
+
initialDataElementSelections?: Record<string, boolean>;
|
|
51
|
+
}
|
|
49
52
|
> = ({
|
|
50
53
|
purpose,
|
|
51
54
|
selectedPurposes,
|
|
@@ -60,6 +63,7 @@ const PurposeItem: React.FC<
|
|
|
60
63
|
getTranslatedText,
|
|
61
64
|
isAlreadyConsented,
|
|
62
65
|
selectedLanguage,
|
|
66
|
+
initialDataElementSelections = {},
|
|
63
67
|
}) => {
|
|
64
68
|
const headingStyle = useMemo(
|
|
65
69
|
() => ({
|
|
@@ -266,39 +270,71 @@ const PurposeItem: React.FC<
|
|
|
266
270
|
/>
|
|
267
271
|
)}
|
|
268
272
|
{isAlreadyConsented && (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
273
|
+
<>
|
|
274
|
+
{(() => {
|
|
275
|
+
const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
|
|
276
|
+
const isCurrentlySelected =
|
|
277
|
+
selectedDataElements[combinedId] || false;
|
|
278
|
+
const wasOriginallySelected =
|
|
279
|
+
initialDataElementSelections[combinedId] || false;
|
|
280
|
+
|
|
281
|
+
// Show green tick only if it was originally selected
|
|
282
|
+
// Show checkbox if it's newly selected or unselected
|
|
283
|
+
if (isCurrentlySelected && wasOriginallySelected) {
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
style={{
|
|
287
|
+
display: "flex",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
justifyContent: "center",
|
|
290
|
+
width: "16px",
|
|
291
|
+
height: "16px",
|
|
292
|
+
borderRadius: "50%",
|
|
293
|
+
backgroundColor: "#10B981",
|
|
294
|
+
}}
|
|
295
|
+
aria-label={`Already consented to ${getTranslatedText(
|
|
296
|
+
`data_elements.name`,
|
|
297
|
+
dataElement.name,
|
|
298
|
+
dataElement.uuid
|
|
299
|
+
)}`}
|
|
300
|
+
>
|
|
301
|
+
<svg
|
|
302
|
+
width="10"
|
|
303
|
+
height="10"
|
|
304
|
+
viewBox="0 0 10 10"
|
|
305
|
+
fill="none"
|
|
306
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
307
|
+
aria-hidden="true"
|
|
308
|
+
>
|
|
309
|
+
<path
|
|
310
|
+
d="M3 5L4.5 6.5L7.5 3.5"
|
|
311
|
+
stroke="white"
|
|
312
|
+
strokeWidth="1.5"
|
|
313
|
+
strokeLinecap="round"
|
|
314
|
+
strokeLinejoin="round"
|
|
315
|
+
/>
|
|
316
|
+
</svg>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
return (
|
|
321
|
+
<input
|
|
322
|
+
className="redacto-checkbox-small"
|
|
323
|
+
type="checkbox"
|
|
324
|
+
checked={isCurrentlySelected}
|
|
325
|
+
onChange={() =>
|
|
326
|
+
onDataElementToggle(dataElement.uuid, purpose.uuid)
|
|
327
|
+
}
|
|
328
|
+
aria-label={`Select ${getTranslatedText(
|
|
329
|
+
`data_elements.name`,
|
|
330
|
+
dataElement.name,
|
|
331
|
+
dataElement.uuid
|
|
332
|
+
)}${dataElement.required ? " (required)" : ""}`}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
})()}
|
|
337
|
+
</>
|
|
302
338
|
)}
|
|
303
339
|
</div>
|
|
304
340
|
))}
|
|
@@ -776,6 +812,8 @@ export const RedactoNoticeConsent = ({
|
|
|
776
812
|
// Loading and submission states
|
|
777
813
|
const [isLoading, setIsLoading] = useState(true);
|
|
778
814
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
815
|
+
const [isSubmittingAccept, setIsSubmittingAccept] = useState(false);
|
|
816
|
+
const [isSubmittingDecline, setIsSubmittingDecline] = useState(false);
|
|
779
817
|
|
|
780
818
|
// Content and consent data
|
|
781
819
|
const [content, setContent] = useState<
|
|
@@ -793,6 +831,9 @@ export const RedactoNoticeConsent = ({
|
|
|
793
831
|
const [selectedDataElements, setSelectedDataElements] = useState<
|
|
794
832
|
Record<string, boolean>
|
|
795
833
|
>({});
|
|
834
|
+
// Track initial data element selections for already-consented purposes
|
|
835
|
+
const [initialDataElementSelections, setInitialDataElementSelections] =
|
|
836
|
+
useState<Record<string, boolean>>({});
|
|
796
837
|
const [collapsedPurposes, setCollapsedPurposes] = useState<
|
|
797
838
|
Record<string, boolean>
|
|
798
839
|
>({});
|
|
@@ -987,11 +1028,14 @@ export const RedactoNoticeConsent = ({
|
|
|
987
1028
|
setCategorizedPurposes(null);
|
|
988
1029
|
setSelectedPurposes({});
|
|
989
1030
|
setSelectedDataElements({});
|
|
1031
|
+
setInitialDataElementSelections({});
|
|
990
1032
|
setCollapsedPurposes({});
|
|
991
1033
|
setHasAlreadyConsented(false);
|
|
992
1034
|
setErrorMessage(null);
|
|
993
1035
|
setFetchError(null);
|
|
994
1036
|
setIsLoading(true);
|
|
1037
|
+
setIsSubmittingAccept(false);
|
|
1038
|
+
setIsSubmittingDecline(false);
|
|
995
1039
|
|
|
996
1040
|
// Clear age verification and guardian form state
|
|
997
1041
|
setShowAgeVerification(false);
|
|
@@ -1119,17 +1163,16 @@ export const RedactoNoticeConsent = ({
|
|
|
1119
1163
|
validate_against: validateAgainst,
|
|
1120
1164
|
signal: abortControllerRef.current?.signal,
|
|
1121
1165
|
});
|
|
1122
|
-
|
|
1123
|
-
|
|
1166
|
+
const activeConfig = consentContentData.detail.active_config;
|
|
1167
|
+
setContent(activeConfig);
|
|
1168
|
+
if (activeConfig.default_language) {
|
|
1124
1169
|
// Normalize "en" to "English" for consistency
|
|
1125
|
-
const defaultLang =
|
|
1126
|
-
consentContentData.detail.active_config.default_language;
|
|
1170
|
+
const defaultLang = activeConfig.default_language;
|
|
1127
1171
|
setSelectedLanguage(
|
|
1128
1172
|
defaultLang === "en" || defaultLang === "EN" ? "English" : defaultLang
|
|
1129
1173
|
);
|
|
1130
1174
|
}
|
|
1131
|
-
const initialCollapsedState =
|
|
1132
|
-
consentContentData.detail.active_config.purposes.reduce(
|
|
1175
|
+
const initialCollapsedState = activeConfig.purposes.reduce(
|
|
1133
1176
|
(acc, purpose) => ({
|
|
1134
1177
|
...acc,
|
|
1135
1178
|
[purpose.uuid]: true,
|
|
@@ -1145,8 +1188,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1145
1188
|
// Categorize purposes for reconsent UI
|
|
1146
1189
|
const newCategorizedPurposes = isReconsentRequired
|
|
1147
1190
|
? {
|
|
1148
|
-
alreadyConsented:
|
|
1149
|
-
consentContentData.detail.active_config.purposes.filter(
|
|
1191
|
+
alreadyConsented: activeConfig.purposes.filter(
|
|
1150
1192
|
(purpose) => {
|
|
1151
1193
|
const selection = purposeSelections?.[purpose.uuid];
|
|
1152
1194
|
return (
|
|
@@ -1157,8 +1199,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1157
1199
|
);
|
|
1158
1200
|
}
|
|
1159
1201
|
),
|
|
1160
|
-
needsConsent:
|
|
1161
|
-
consentContentData.detail.active_config.purposes.filter(
|
|
1202
|
+
needsConsent: activeConfig.purposes.filter(
|
|
1162
1203
|
(purpose) => {
|
|
1163
1204
|
const selection = purposeSelections?.[purpose.uuid];
|
|
1164
1205
|
return (
|
|
@@ -1174,7 +1215,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1174
1215
|
setCategorizedPurposes(newCategorizedPurposes);
|
|
1175
1216
|
|
|
1176
1217
|
const initialPurposeState: Record<string, boolean> =
|
|
1177
|
-
|
|
1218
|
+
activeConfig.purposes.reduce(
|
|
1178
1219
|
(acc: Record<string, boolean>, purpose) => {
|
|
1179
1220
|
if (isReconsentRequired && purposeSelections?.[purpose.uuid]) {
|
|
1180
1221
|
// Pre-populate with existing selection for reconsent
|
|
@@ -1190,7 +1231,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1190
1231
|
setSelectedPurposes(initialPurposeState);
|
|
1191
1232
|
|
|
1192
1233
|
const initialDataElementState: Record<string, boolean> =
|
|
1193
|
-
|
|
1234
|
+
activeConfig.purposes.reduce(
|
|
1194
1235
|
(acc: Record<string, boolean>, purpose) => {
|
|
1195
1236
|
purpose.data_elements.forEach((element) => {
|
|
1196
1237
|
const combinedId = `${purpose.uuid}-${element.uuid}`;
|
|
@@ -1213,6 +1254,27 @@ export const RedactoNoticeConsent = ({
|
|
|
1213
1254
|
{}
|
|
1214
1255
|
);
|
|
1215
1256
|
setSelectedDataElements(initialDataElementState);
|
|
1257
|
+
// Store initial selections for already-consented purposes to track modifications
|
|
1258
|
+
if (isReconsentRequired && newCategorizedPurposes) {
|
|
1259
|
+
const initialSelections: Record<string, boolean> = {};
|
|
1260
|
+
newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
|
|
1261
|
+
purpose.data_elements.forEach((element) => {
|
|
1262
|
+
const combinedId = `${purpose.uuid}-${element.uuid}`;
|
|
1263
|
+
if (
|
|
1264
|
+
purposeSelections?.[purpose.uuid]?.data_elements?.[element.uuid]
|
|
1265
|
+
) {
|
|
1266
|
+
initialSelections[combinedId] =
|
|
1267
|
+
purposeSelections[purpose.uuid].data_elements[element.uuid]
|
|
1268
|
+
.selected;
|
|
1269
|
+
} else {
|
|
1270
|
+
initialSelections[combinedId] = false;
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
setInitialDataElementSelections(initialSelections);
|
|
1275
|
+
} else {
|
|
1276
|
+
setInitialDataElementSelections({});
|
|
1277
|
+
}
|
|
1216
1278
|
|
|
1217
1279
|
// Check if age verification is required - set this after all content is loaded
|
|
1218
1280
|
// but before setIsLoading(false) to ensure proper render order
|
|
@@ -1228,7 +1290,6 @@ export const RedactoNoticeConsent = ({
|
|
|
1228
1290
|
return; // Don't update state for cancelled requests
|
|
1229
1291
|
}
|
|
1230
1292
|
|
|
1231
|
-
console.error(err);
|
|
1232
1293
|
const error = err as Error & { status?: number };
|
|
1233
1294
|
setFetchError(error);
|
|
1234
1295
|
if (error.status === 409) {
|
|
@@ -1350,24 +1411,52 @@ export const RedactoNoticeConsent = ({
|
|
|
1350
1411
|
const submitController = new AbortController();
|
|
1351
1412
|
|
|
1352
1413
|
try {
|
|
1353
|
-
|
|
1414
|
+
setIsSubmittingAccept(true);
|
|
1415
|
+
setIsSubmitting(true); // Keep for backward compatibility with acceptDisabled
|
|
1354
1416
|
setErrorMessage(null);
|
|
1355
1417
|
if (content) {
|
|
1356
1418
|
await submitConsentEvent({
|
|
1357
1419
|
accessToken,
|
|
1358
1420
|
baseUrl,
|
|
1359
1421
|
noticeUuid: content.notice_uuid,
|
|
1360
|
-
purposes: content.purposes.map((purpose) =>
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1422
|
+
purposes: content.purposes.map((purpose) => {
|
|
1423
|
+
// Check if this is an already-consented purpose that was modified
|
|
1424
|
+
const isAlreadyConsented =
|
|
1425
|
+
categorizedPurposes?.alreadyConsented.some(
|
|
1426
|
+
(p) => p.uuid === purpose.uuid
|
|
1427
|
+
) || false;
|
|
1428
|
+
|
|
1429
|
+
let purposeSelected = selectedPurposes[purpose.uuid] || false;
|
|
1430
|
+
|
|
1431
|
+
// If it's an already-consented purpose, check if any data elements were modified
|
|
1432
|
+
if (isAlreadyConsented) {
|
|
1433
|
+
const wasModified = purpose.data_elements.some((element) => {
|
|
1434
|
+
const combinedId = `${purpose.uuid}-${element.uuid}`;
|
|
1435
|
+
const currentSelection =
|
|
1436
|
+
selectedDataElements[combinedId] || false;
|
|
1437
|
+
const initialSelection =
|
|
1438
|
+
initialDataElementSelections[combinedId] || false;
|
|
1439
|
+
return currentSelection !== initialSelection;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// If modified, treat as new selection (mark purpose as selected)
|
|
1443
|
+
if (wasModified) {
|
|
1444
|
+
purposeSelected = true;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return {
|
|
1449
|
+
...purpose,
|
|
1450
|
+
selected: purposeSelected,
|
|
1451
|
+
data_elements: purpose.data_elements.map((element) => ({
|
|
1452
|
+
...element,
|
|
1453
|
+
selected: element.required
|
|
1454
|
+
? true
|
|
1455
|
+
: selectedDataElements[`${purpose.uuid}-${element.uuid}`] ||
|
|
1456
|
+
false,
|
|
1457
|
+
})),
|
|
1458
|
+
};
|
|
1459
|
+
}),
|
|
1371
1460
|
declined: false,
|
|
1372
1461
|
meta_data: applicationId
|
|
1373
1462
|
? { specific_uuid: applicationId }
|
|
@@ -1382,45 +1471,50 @@ export const RedactoNoticeConsent = ({
|
|
|
1382
1471
|
return;
|
|
1383
1472
|
}
|
|
1384
1473
|
|
|
1385
|
-
console.error("Error submitting consent:", error);
|
|
1386
1474
|
const err = error as Error & { status?: number };
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
"An error occurred while submitting your consent. Please try again later."
|
|
1390
|
-
|
|
1391
|
-
|
|
1475
|
+
const message =
|
|
1476
|
+
err.status === 500
|
|
1477
|
+
? "An error occurred while submitting your consent. Please try again later."
|
|
1478
|
+
: err.message || "Failed to submit consent. Please try again.";
|
|
1479
|
+
setErrorMessage(message);
|
|
1392
1480
|
onError?.(error as Error);
|
|
1393
1481
|
} finally {
|
|
1394
|
-
|
|
1482
|
+
setIsSubmittingAccept(false);
|
|
1483
|
+
setIsSubmitting(false); // Keep for backward compatibility
|
|
1395
1484
|
}
|
|
1396
1485
|
};
|
|
1397
1486
|
|
|
1398
1487
|
const handleDecline = useCallback(async () => {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1488
|
+
try {
|
|
1489
|
+
setIsSubmittingDecline(true);
|
|
1490
|
+
setIsSubmitting(true); // Keep for backward compatibility
|
|
1491
|
+
// try {
|
|
1492
|
+
// if (content) {
|
|
1493
|
+
// await submitConsentEvent(
|
|
1494
|
+
// accessToken,
|
|
1495
|
+
// content.notice_uuid,
|
|
1496
|
+
// content.purposes.map((purpose) => ({
|
|
1497
|
+
// ...purpose,
|
|
1498
|
+
// selected: false,
|
|
1499
|
+
// data_elements: purpose.data_elements.map((element) => ({
|
|
1500
|
+
// ...element,
|
|
1501
|
+
// selected: element.required ? true : false,
|
|
1502
|
+
// })),
|
|
1503
|
+
// })),
|
|
1504
|
+
// true
|
|
1505
|
+
// );
|
|
1506
|
+
// }
|
|
1507
|
+
// } catch (error) {
|
|
1508
|
+
// console.error("Error submitting consent:", error);
|
|
1509
|
+
// onError?.(error as Error);
|
|
1510
|
+
// } finally {
|
|
1511
|
+
// setIsSubmitting(false);
|
|
1512
|
+
// }
|
|
1513
|
+
onDecline?.();
|
|
1514
|
+
} finally {
|
|
1515
|
+
setIsSubmittingDecline(false);
|
|
1516
|
+
setIsSubmitting(false); // Keep for backward compatibility
|
|
1517
|
+
}
|
|
1424
1518
|
}, [onDecline]);
|
|
1425
1519
|
|
|
1426
1520
|
// Guardian form handlers
|
|
@@ -1981,9 +2075,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1981
2075
|
})
|
|
1982
2076
|
.catch((error) => {
|
|
1983
2077
|
// Handle errors gracefully - don't break the UI if TTS API fails
|
|
1984
|
-
// Only log if it's not an AbortError (which is expected when component unmounts or request is cancelled)
|
|
1985
2078
|
if (error instanceof Error && error.name !== "AbortError") {
|
|
1986
|
-
console.warn("Failed to fetch TTS audio URLs:", error.message);
|
|
1987
2079
|
setIsTTSAvailable(false);
|
|
1988
2080
|
setTtsAudioUrls(null);
|
|
1989
2081
|
}
|
|
@@ -2606,6 +2698,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2606
2698
|
getTranslatedText={getTranslatedText}
|
|
2607
2699
|
isAlreadyConsented={true}
|
|
2608
2700
|
selectedLanguage={selectedLanguage}
|
|
2701
|
+
initialDataElementSelections={initialDataElementSelections}
|
|
2609
2702
|
/>
|
|
2610
2703
|
))}
|
|
2611
2704
|
|
|
@@ -2671,13 +2764,13 @@ export const RedactoNoticeConsent = ({
|
|
|
2671
2764
|
opacity: acceptDisabled ? 0.5 : 1,
|
|
2672
2765
|
}}
|
|
2673
2766
|
onClick={handleAccept}
|
|
2674
|
-
disabled={acceptDisabled}
|
|
2767
|
+
disabled={acceptDisabled || isSubmittingDecline}
|
|
2675
2768
|
aria-label={getTranslatedText(
|
|
2676
2769
|
"confirm_button_text",
|
|
2677
2770
|
content?.confirm_button_text || "Accept"
|
|
2678
2771
|
)}
|
|
2679
2772
|
>
|
|
2680
|
-
{
|
|
2773
|
+
{isSubmittingAccept
|
|
2681
2774
|
? "Processing..."
|
|
2682
2775
|
: getTranslatedText(
|
|
2683
2776
|
"confirm_button_text",
|
|
@@ -2691,15 +2784,16 @@ export const RedactoNoticeConsent = ({
|
|
|
2691
2784
|
...responsiveStyles.button,
|
|
2692
2785
|
...styles.cancelButton,
|
|
2693
2786
|
...declineButtonStyle,
|
|
2787
|
+
opacity: isSubmittingAccept ? 0.5 : 1,
|
|
2694
2788
|
}}
|
|
2695
2789
|
onClick={handleDecline}
|
|
2696
|
-
disabled={
|
|
2790
|
+
disabled={isSubmittingDecline || isSubmittingAccept}
|
|
2697
2791
|
aria-label={getTranslatedText(
|
|
2698
2792
|
"decline_button_text",
|
|
2699
2793
|
content?.decline_button_text || "Decline"
|
|
2700
2794
|
)}
|
|
2701
2795
|
>
|
|
2702
|
-
{
|
|
2796
|
+
{isSubmittingDecline
|
|
2703
2797
|
? "Processing..."
|
|
2704
2798
|
: getTranslatedText(
|
|
2705
2799
|
"decline_button_text",
|
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
FetchConsentContentParams,
|
|
6
6
|
FetchTTSAudioUrlsParams,
|
|
7
7
|
GuardianInfoPayload,
|
|
8
|
+
LegacyConsentEventPayload,
|
|
8
9
|
RedactoJwtPayload,
|
|
9
10
|
SubmitConsentEventParams,
|
|
10
11
|
SubmitGuardianInfoParams,
|
|
11
12
|
TTSAudioUrlsResponse,
|
|
12
13
|
} from "./types";
|
|
14
|
+
import { isLegacyApi } from "../../utils/isLegacyApi";
|
|
13
15
|
|
|
14
16
|
const BASE_URL = "https://api.redacto.io/consent";
|
|
15
17
|
|
|
@@ -45,16 +47,6 @@ function decodeTokenSafely(token: string): RedactoJwtPayload | null {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
// Abstract localStorage access for better testability
|
|
49
|
-
const storage = {
|
|
50
|
-
getItem: (key: string): string | null => {
|
|
51
|
-
if (typeof window !== 'undefined') {
|
|
52
|
-
return localStorage.getItem(key);
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
50
|
// Function to clear API cache (useful for testing or when switching users)
|
|
59
51
|
export const clearApiCache = (): void => {
|
|
60
52
|
apiCache.clear();
|
|
@@ -146,8 +138,6 @@ export const fetchConsentContent = async ({
|
|
|
146
138
|
|
|
147
139
|
return data;
|
|
148
140
|
} catch (error) {
|
|
149
|
-
// Log error for debugging but don't expose sensitive information
|
|
150
|
-
console.error("Error fetching consent content:", error instanceof Error ? error.message : error);
|
|
151
141
|
throw error;
|
|
152
142
|
}
|
|
153
143
|
};
|
|
@@ -177,48 +167,72 @@ export const submitConsentEvent = async ({
|
|
|
177
167
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
178
168
|
}
|
|
179
169
|
|
|
180
|
-
const
|
|
170
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
181
171
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
172
|
+
let payload: ConsentEventPayload | LegacyConsentEventPayload;
|
|
173
|
+
|
|
174
|
+
if (isLegacyApi(apiBaseUrl)) {
|
|
175
|
+
// @deprecated Legacy payload for production (.io) backend
|
|
176
|
+
const primaryEmail = typeof window !== "undefined" ? localStorage.getItem("userEmail") : null;
|
|
177
|
+
payload = {
|
|
178
|
+
primary_email: primaryEmail || undefined,
|
|
179
|
+
notice_uuid: noticeUuid,
|
|
180
|
+
source: "WEB",
|
|
181
|
+
declined: Boolean(declined),
|
|
182
|
+
consents: {
|
|
183
|
+
purposes: purposes.map((purpose) => {
|
|
184
|
+
if (!purpose.uuid) {
|
|
185
|
+
throw new Error(`Invalid purpose data: missing uuid`);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
uuid: purpose.uuid,
|
|
189
|
+
name: purpose.name,
|
|
190
|
+
description: purpose.description || "",
|
|
191
|
+
industries: purpose.industries || "",
|
|
192
|
+
selected: Boolean(purpose.selected),
|
|
193
|
+
data_elements: (purpose.data_elements || []).map((element) => ({
|
|
194
|
+
uuid: element.uuid,
|
|
195
|
+
name: element.name,
|
|
196
|
+
enabled: Boolean(element.enabled),
|
|
197
|
+
required: Boolean(element.required),
|
|
198
|
+
selected: element.required ? true : Boolean(element.selected),
|
|
199
|
+
})),
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
|
|
204
|
+
};
|
|
205
|
+
} else {
|
|
206
|
+
// New payload for staging (.tech) / localhost
|
|
207
|
+
const validatedPurposes = purposes.map((purpose) => {
|
|
208
|
+
if (!purpose.uuid) {
|
|
209
|
+
throw new Error(`Invalid purpose data: missing uuid`);
|
|
210
|
+
}
|
|
187
211
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
industries: purpose.industries || "",
|
|
193
|
-
selected: Boolean(purpose.selected),
|
|
194
|
-
data_elements: (purpose.data_elements || []).map((element) => {
|
|
195
|
-
if (!element.uuid || !element.name) {
|
|
196
|
-
throw new Error(`Invalid data element: missing uuid or name`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return {
|
|
212
|
+
// Only include enabled data elements (disabled elements are not selectable)
|
|
213
|
+
const dataElements = (purpose.data_elements || [])
|
|
214
|
+
.filter((element) => element.enabled)
|
|
215
|
+
.map((element) => ({
|
|
200
216
|
uuid: element.uuid,
|
|
201
|
-
name: element.name,
|
|
202
|
-
enabled: Boolean(element.enabled),
|
|
203
|
-
required: Boolean(element.required),
|
|
204
217
|
selected: element.required ? true : Boolean(element.selected),
|
|
205
|
-
};
|
|
206
|
-
}),
|
|
207
|
-
};
|
|
208
|
-
});
|
|
218
|
+
}));
|
|
209
219
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
purposes: validatedPurposes,
|
|
217
|
-
},
|
|
218
|
-
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
|
|
219
|
-
};
|
|
220
|
+
return {
|
|
221
|
+
purpose_uuid: purpose.uuid,
|
|
222
|
+
selected: Boolean(purpose.selected),
|
|
223
|
+
data_elements: dataElements.length > 0 ? dataElements : undefined,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
220
226
|
|
|
221
|
-
|
|
227
|
+
payload = {
|
|
228
|
+
notice_uuid: noticeUuid,
|
|
229
|
+
purposes: validatedPurposes,
|
|
230
|
+
select_all_mandatory: false,
|
|
231
|
+
source: "WEB",
|
|
232
|
+
declined: Boolean(declined),
|
|
233
|
+
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
222
236
|
|
|
223
237
|
const response = await fetch(
|
|
224
238
|
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
|
|
@@ -257,8 +271,6 @@ export const submitConsentEvent = async ({
|
|
|
257
271
|
apiCache.clear();
|
|
258
272
|
|
|
259
273
|
} catch (error) {
|
|
260
|
-
// Log error for debugging but don't expose sensitive information
|
|
261
|
-
console.error("Error submitting consent event:", error instanceof Error ? error.message : error);
|
|
262
274
|
throw error;
|
|
263
275
|
}
|
|
264
276
|
};
|
|
@@ -414,8 +426,6 @@ export const fetchTTSAudioUrls = async ({
|
|
|
414
426
|
|
|
415
427
|
return data;
|
|
416
428
|
} catch (error) {
|
|
417
|
-
// Log error for debugging but don't expose sensitive information
|
|
418
|
-
console.error("Error fetching TTS audio URLs:", error instanceof Error ? error.message : error);
|
|
419
429
|
throw error;
|
|
420
430
|
}
|
|
421
431
|
};
|
|
@@ -157,9 +157,26 @@ export type FetchConsentContentParams = {
|
|
|
157
157
|
};
|
|
158
158
|
|
|
159
159
|
export type ConsentEventPayload = {
|
|
160
|
-
|
|
160
|
+
notice_uuid: string;
|
|
161
|
+
purposes: Array<{
|
|
162
|
+
purpose_uuid: string;
|
|
163
|
+
selected: boolean;
|
|
164
|
+
data_elements?: Array<{
|
|
165
|
+
uuid: string;
|
|
166
|
+
selected: boolean;
|
|
167
|
+
}>;
|
|
168
|
+
}>;
|
|
169
|
+
select_all_mandatory: boolean;
|
|
170
|
+
source?: string;
|
|
171
|
+
declined?: boolean;
|
|
172
|
+
meta_data?: {
|
|
173
|
+
specific_uuid: string;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// @deprecated Remove when production backend supports the new payload format.
|
|
178
|
+
export type LegacyConsentEventPayload = {
|
|
161
179
|
primary_email?: string;
|
|
162
|
-
primary_mobile?: string;
|
|
163
180
|
notice_uuid: string;
|
|
164
181
|
source: string;
|
|
165
182
|
declined: boolean;
|
|
@@ -179,9 +196,7 @@ export type ConsentEventPayload = {
|
|
|
179
196
|
}>;
|
|
180
197
|
}>;
|
|
181
198
|
};
|
|
182
|
-
meta_data?: {
|
|
183
|
-
specific_uuid: string;
|
|
184
|
-
};
|
|
199
|
+
meta_data?: { specific_uuid: string };
|
|
185
200
|
};
|
|
186
201
|
|
|
187
202
|
export type DataElement = {
|