@redacto.io/consent-sdk-react 1.0.0 → 1.2.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 +8 -8
- package/CHANGELOG.md +12 -0
- package/README.md +705 -20
- package/dist/index.d.mts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +1004 -534
- package/dist/index.mjs +995 -532
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +141 -54
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +323 -78
- package/src/RedactoNoticeConsent/api/index.ts +7 -7
- package/src/RedactoNoticeConsent/api/types.ts +1 -1
- package/src/RedactoNoticeConsent/injectStyles.ts +16 -0
- package/src/RedactoNoticeConsent/types.ts +24 -1
- package/src/RedactoNoticeConsent/useTextToSpeech.ts +378 -0
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +489 -477
- package/src/RedactoNoticeConsentInline/api/index.ts +1 -1
|
@@ -32,6 +32,8 @@ import logo from "./assets/redacto-logo.png";
|
|
|
32
32
|
import { injectCheckboxStyles } from "./injectStyles";
|
|
33
33
|
import { styles } from "./styles";
|
|
34
34
|
import { useMediaQuery } from "./useMediaQuery";
|
|
35
|
+
import { useTextToSpeech } from "./useTextToSpeech";
|
|
36
|
+
import type { TextSegment } from "./types";
|
|
35
37
|
|
|
36
38
|
// =============================================================================
|
|
37
39
|
// SUB-COMPONENTS
|
|
@@ -121,6 +123,7 @@ const PurposeItem: React.FC<PurposeItemProps> = ({
|
|
|
121
123
|
</div>
|
|
122
124
|
<div style={styles.optionTextContainer}>
|
|
123
125
|
<h2
|
|
126
|
+
id={`purpose-name-${purpose.uuid}`}
|
|
124
127
|
style={{
|
|
125
128
|
...styles.optionTitle,
|
|
126
129
|
...responsiveStyles.optionTitle,
|
|
@@ -130,6 +133,7 @@ const PurposeItem: React.FC<PurposeItemProps> = ({
|
|
|
130
133
|
{getTranslatedText(`purposes.name`, purpose.name, purpose.uuid)}
|
|
131
134
|
</h2>
|
|
132
135
|
<h3
|
|
136
|
+
id={`purpose-description-${purpose.uuid}`}
|
|
133
137
|
style={{
|
|
134
138
|
...styles.optionDescription,
|
|
135
139
|
...responsiveStyles.optionDescription,
|
|
@@ -198,6 +202,7 @@ const PurposeItem: React.FC<PurposeItemProps> = ({
|
|
|
198
202
|
{purpose.data_elements.map((dataElement: any) => (
|
|
199
203
|
<div key={dataElement.uuid} style={styles.dataElementItem}>
|
|
200
204
|
<span
|
|
205
|
+
id={`data-element-${purpose.uuid}-${dataElement.uuid}`}
|
|
201
206
|
style={{
|
|
202
207
|
...styles.dataElementText,
|
|
203
208
|
...responsiveStyles.dataElementText,
|
|
@@ -694,7 +699,7 @@ export const RedactoNoticeConsent = ({
|
|
|
694
699
|
onError,
|
|
695
700
|
settings,
|
|
696
701
|
applicationId,
|
|
697
|
-
|
|
702
|
+
validateAgainst = "all",
|
|
698
703
|
}: Props) => {
|
|
699
704
|
// =============================================================================
|
|
700
705
|
// PROP VALIDATION
|
|
@@ -969,7 +974,8 @@ export const RedactoNoticeConsent = ({
|
|
|
969
974
|
): string => {
|
|
970
975
|
if (!content) return defaultText;
|
|
971
976
|
|
|
972
|
-
|
|
977
|
+
// If English is selected, use the base content from active_config
|
|
978
|
+
if (selectedLanguage === "English" || selectedLanguage === "en") {
|
|
973
979
|
if (
|
|
974
980
|
key === "privacy_policy_anchor_text" &&
|
|
975
981
|
content.privacy_policy_anchor_text
|
|
@@ -979,6 +985,7 @@ export const RedactoNoticeConsent = ({
|
|
|
979
985
|
return defaultText;
|
|
980
986
|
}
|
|
981
987
|
|
|
988
|
+
// For other languages, check supported_languages_and_translations
|
|
982
989
|
if (!content.supported_languages_and_translations) {
|
|
983
990
|
return defaultText;
|
|
984
991
|
}
|
|
@@ -1043,13 +1050,16 @@ export const RedactoNoticeConsent = ({
|
|
|
1043
1050
|
baseUrl,
|
|
1044
1051
|
language,
|
|
1045
1052
|
specific_uuid: applicationId,
|
|
1046
|
-
|
|
1053
|
+
validate_against: validateAgainst,
|
|
1047
1054
|
signal: abortControllerRef.current?.signal,
|
|
1048
1055
|
});
|
|
1049
1056
|
setContent(consentContentData.detail.active_config);
|
|
1050
1057
|
if (consentContentData.detail.active_config.default_language) {
|
|
1058
|
+
// Normalize "en" to "English" for consistency
|
|
1059
|
+
const defaultLang =
|
|
1060
|
+
consentContentData.detail.active_config.default_language;
|
|
1051
1061
|
setSelectedLanguage(
|
|
1052
|
-
|
|
1062
|
+
defaultLang === "en" || defaultLang === "EN" ? "English" : defaultLang
|
|
1053
1063
|
);
|
|
1054
1064
|
}
|
|
1055
1065
|
const initialCollapsedState =
|
|
@@ -1178,7 +1188,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1178
1188
|
refreshToken,
|
|
1179
1189
|
language,
|
|
1180
1190
|
applicationId,
|
|
1181
|
-
|
|
1191
|
+
validateAgainst,
|
|
1182
1192
|
// Removed onError and isRefreshingToken from dependencies to prevent duplicate API calls
|
|
1183
1193
|
// onError is only called on errors, not needed for initial data fetching
|
|
1184
1194
|
]);
|
|
@@ -1437,16 +1447,217 @@ export const RedactoNoticeConsent = ({
|
|
|
1437
1447
|
// User is under 18, show guardian form
|
|
1438
1448
|
}, []);
|
|
1439
1449
|
|
|
1440
|
-
const availableLanguages = useMemo(
|
|
1441
|
-
()
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
+
const availableLanguages = useMemo(() => {
|
|
1451
|
+
if (!content) return [language];
|
|
1452
|
+
|
|
1453
|
+
// English is always available since it's in active_config
|
|
1454
|
+
const languages = ["English"];
|
|
1455
|
+
|
|
1456
|
+
// Add default language if it's not English
|
|
1457
|
+
if (
|
|
1458
|
+
content.default_language &&
|
|
1459
|
+
content.default_language !== "English" &&
|
|
1460
|
+
content.default_language !== "en"
|
|
1461
|
+
) {
|
|
1462
|
+
languages.push(content.default_language);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Add all supported languages from translations
|
|
1466
|
+
if (content.supported_languages_and_translations) {
|
|
1467
|
+
Object.keys(content.supported_languages_and_translations).forEach(
|
|
1468
|
+
(lang) => {
|
|
1469
|
+
if (!languages.includes(lang)) {
|
|
1470
|
+
languages.push(lang);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return languages;
|
|
1477
|
+
}, [content, language]);
|
|
1478
|
+
|
|
1479
|
+
// =============================================================================
|
|
1480
|
+
// TEXT-TO-SPEECH FUNCTIONALITY
|
|
1481
|
+
// =============================================================================
|
|
1482
|
+
|
|
1483
|
+
// Create text segments for TTS in reading order
|
|
1484
|
+
const textSegments = useMemo<TextSegment[]>(() => {
|
|
1485
|
+
if (!content) return [];
|
|
1486
|
+
|
|
1487
|
+
const segments: TextSegment[] = [];
|
|
1488
|
+
|
|
1489
|
+
// 1. Notice banner heading
|
|
1490
|
+
const noticeHeading = getTranslatedText(
|
|
1491
|
+
"notice_banner_heading",
|
|
1492
|
+
content.notice_banner_heading || "Privacy Notice"
|
|
1493
|
+
);
|
|
1494
|
+
if (noticeHeading) {
|
|
1495
|
+
segments.push({
|
|
1496
|
+
text: noticeHeading,
|
|
1497
|
+
elementId: "privacy-notice-title",
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// 2. Notice text
|
|
1502
|
+
const noticeText = getTranslatedText("notice_text", content.notice_text);
|
|
1503
|
+
if (noticeText) {
|
|
1504
|
+
segments.push({
|
|
1505
|
+
text: noticeText,
|
|
1506
|
+
elementId: "privacy-notice-text",
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// 3. Privacy policy text
|
|
1511
|
+
const privacyPrefix = getTranslatedText(
|
|
1512
|
+
"privacy_policy_prefix_text",
|
|
1513
|
+
content.privacy_policy_prefix_text || ""
|
|
1514
|
+
);
|
|
1515
|
+
const privacyAnchor = getTranslatedText(
|
|
1516
|
+
"privacy_policy_anchor_text",
|
|
1517
|
+
content.privacy_policy_anchor_text || "Privacy Policy"
|
|
1518
|
+
);
|
|
1519
|
+
if (privacyPrefix || privacyAnchor) {
|
|
1520
|
+
const privacyText = `${privacyPrefix} ${privacyAnchor}`.trim();
|
|
1521
|
+
if (privacyText) {
|
|
1522
|
+
segments.push({
|
|
1523
|
+
text: privacyText,
|
|
1524
|
+
elementId: "privacy-policy-text",
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// 4. Purpose section heading
|
|
1530
|
+
const purposeHeading = getTranslatedText(
|
|
1531
|
+
"purpose_section_heading",
|
|
1532
|
+
"Manage What You Share"
|
|
1533
|
+
);
|
|
1534
|
+
if (purposeHeading) {
|
|
1535
|
+
segments.push({
|
|
1536
|
+
text: purposeHeading,
|
|
1537
|
+
elementId: "purpose-section-heading",
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// 5. Purposes and data elements
|
|
1542
|
+
const purposesToRead = categorizedPurposes
|
|
1543
|
+
? [
|
|
1544
|
+
...categorizedPurposes.alreadyConsented,
|
|
1545
|
+
...categorizedPurposes.needsConsent,
|
|
1546
|
+
]
|
|
1547
|
+
: content.purposes || [];
|
|
1548
|
+
|
|
1549
|
+
purposesToRead.forEach((purpose) => {
|
|
1550
|
+
// Purpose name
|
|
1551
|
+
const purposeName = getTranslatedText(
|
|
1552
|
+
`purposes.name`,
|
|
1553
|
+
purpose.name,
|
|
1554
|
+
purpose.uuid
|
|
1555
|
+
);
|
|
1556
|
+
if (purposeName) {
|
|
1557
|
+
segments.push({
|
|
1558
|
+
text: purposeName,
|
|
1559
|
+
elementId: `purpose-name-${purpose.uuid}`,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Purpose description
|
|
1564
|
+
const purposeDescription = getTranslatedText(
|
|
1565
|
+
`purposes.description`,
|
|
1566
|
+
purpose.description,
|
|
1567
|
+
purpose.uuid
|
|
1568
|
+
);
|
|
1569
|
+
if (purposeDescription) {
|
|
1570
|
+
segments.push({
|
|
1571
|
+
text: purposeDescription,
|
|
1572
|
+
elementId: `purpose-description-${purpose.uuid}`,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Data elements
|
|
1577
|
+
if (purpose.data_elements && purpose.data_elements.length > 0) {
|
|
1578
|
+
purpose.data_elements.forEach((dataElement) => {
|
|
1579
|
+
const elementName = getTranslatedText(
|
|
1580
|
+
`data_elements.name`,
|
|
1581
|
+
dataElement.name,
|
|
1582
|
+
dataElement.uuid
|
|
1583
|
+
);
|
|
1584
|
+
if (elementName) {
|
|
1585
|
+
const requiredText = dataElement.required ? " (required)" : "";
|
|
1586
|
+
segments.push({
|
|
1587
|
+
text: `${elementName}${requiredText}`,
|
|
1588
|
+
elementId: `data-element-${purpose.uuid}-${dataElement.uuid}`,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
return segments;
|
|
1596
|
+
}, [content, categorizedPurposes, getTranslatedText]);
|
|
1597
|
+
|
|
1598
|
+
// Initialize TTS hook
|
|
1599
|
+
const tts = useTextToSpeech(textSegments, selectedLanguage);
|
|
1600
|
+
|
|
1601
|
+
// Auto-expand collapsed purposes when TTS is reading their content
|
|
1602
|
+
// This ensures accessibility - users can see what's being read even if sections were collapsed
|
|
1603
|
+
useEffect(() => {
|
|
1604
|
+
if (!tts.isPlaying || !content || textSegments.length === 0) return;
|
|
1605
|
+
|
|
1606
|
+
const currentSegment = textSegments[tts.currentIndex];
|
|
1607
|
+
if (!currentSegment) return;
|
|
1608
|
+
|
|
1609
|
+
// Extract purpose UUID from element ID
|
|
1610
|
+
let purposeUuid: string | null = null;
|
|
1611
|
+
|
|
1612
|
+
// Check if current segment is a data element (format: data-element-{purposeUuid}-{elementUuid})
|
|
1613
|
+
const dataElementMatch = currentSegment.elementId.match(
|
|
1614
|
+
/^data-element-(.+?)-(.+)$/
|
|
1615
|
+
);
|
|
1616
|
+
if (dataElementMatch) {
|
|
1617
|
+
purposeUuid = dataElementMatch[1];
|
|
1618
|
+
} else {
|
|
1619
|
+
// Check if current segment is a purpose name or description (format: purpose-name-{purposeUuid} or purpose-description-{purposeUuid})
|
|
1620
|
+
const purposeNameMatch =
|
|
1621
|
+
currentSegment.elementId.match(/^purpose-name-(.+)$/);
|
|
1622
|
+
const purposeDescMatch = currentSegment.elementId.match(
|
|
1623
|
+
/^purpose-description-(.+)$/
|
|
1624
|
+
);
|
|
1625
|
+
if (purposeNameMatch) {
|
|
1626
|
+
purposeUuid = purposeNameMatch[1];
|
|
1627
|
+
} else if (purposeDescMatch) {
|
|
1628
|
+
purposeUuid = purposeDescMatch[1];
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Expand the purpose if it's collapsed
|
|
1633
|
+
if (purposeUuid && collapsedPurposes[purposeUuid]) {
|
|
1634
|
+
setCollapsedPurposes((prev) => ({
|
|
1635
|
+
...prev,
|
|
1636
|
+
[purposeUuid!]: false,
|
|
1637
|
+
}));
|
|
1638
|
+
}
|
|
1639
|
+
}, [
|
|
1640
|
+
tts.isPlaying,
|
|
1641
|
+
tts.currentIndex,
|
|
1642
|
+
textSegments,
|
|
1643
|
+
content,
|
|
1644
|
+
collapsedPurposes,
|
|
1645
|
+
]);
|
|
1646
|
+
|
|
1647
|
+
// Stop TTS when language changes
|
|
1648
|
+
useEffect(() => {
|
|
1649
|
+
tts.stop();
|
|
1650
|
+
}, [selectedLanguage]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1651
|
+
|
|
1652
|
+
// Stop TTS when content changes
|
|
1653
|
+
useEffect(() => {
|
|
1654
|
+
tts.stop();
|
|
1655
|
+
}, [content]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1656
|
+
|
|
1657
|
+
// Handle TTS button click
|
|
1658
|
+
const handleTTSButtonClick = useCallback(() => {
|
|
1659
|
+
tts.toggle();
|
|
1660
|
+
}, [tts]);
|
|
1450
1661
|
|
|
1451
1662
|
const modalStyle = {
|
|
1452
1663
|
borderRadius: settings?.borderRadius || "8px",
|
|
@@ -1806,7 +2017,10 @@ export const RedactoNoticeConsent = ({
|
|
|
1806
2017
|
...componentHeadingStyle,
|
|
1807
2018
|
}}
|
|
1808
2019
|
>
|
|
1809
|
-
{
|
|
2020
|
+
{getTranslatedText(
|
|
2021
|
+
"notice_banner_heading",
|
|
2022
|
+
content?.notice_banner_heading || "Privacy Notice"
|
|
2023
|
+
)}
|
|
1810
2024
|
</h2>
|
|
1811
2025
|
</div>
|
|
1812
2026
|
<div style={styles.languageSelectorContainer}>
|
|
@@ -1851,37 +2065,76 @@ export const RedactoNoticeConsent = ({
|
|
|
1851
2065
|
</svg>
|
|
1852
2066
|
</button>
|
|
1853
2067
|
</div>
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
settings?.button?.language?.textColor ||
|
|
1872
|
-
"currentColor"
|
|
2068
|
+
{(selectedLanguage === "English" ||
|
|
2069
|
+
selectedLanguage === "en") && (
|
|
2070
|
+
<button
|
|
2071
|
+
onClick={handleTTSButtonClick}
|
|
2072
|
+
style={{
|
|
2073
|
+
...styles.topRight,
|
|
2074
|
+
...responsiveStyles.topRight,
|
|
2075
|
+
...languageSelectorStyle,
|
|
2076
|
+
padding: "3px 9px",
|
|
2077
|
+
opacity: textSegments.length === 0 ? 0.5 : 1,
|
|
2078
|
+
}}
|
|
2079
|
+
aria-label={
|
|
2080
|
+
tts.isPlaying && !tts.isPaused
|
|
2081
|
+
? "Pause text-to-speech"
|
|
2082
|
+
: tts.isPaused
|
|
2083
|
+
? "Resume text-to-speech"
|
|
2084
|
+
: "Start text-to-speech"
|
|
1873
2085
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2086
|
+
title={
|
|
2087
|
+
tts.isPlaying && !tts.isPaused
|
|
2088
|
+
? "Pause Talkback"
|
|
2089
|
+
: tts.isPaused
|
|
2090
|
+
? "Resume Talkback"
|
|
2091
|
+
: "Start Talkback"
|
|
2092
|
+
}
|
|
2093
|
+
disabled={textSegments.length === 0}
|
|
1878
2094
|
>
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2095
|
+
{tts.isPlaying && !tts.isPaused ? (
|
|
2096
|
+
// Pause icon
|
|
2097
|
+
<svg
|
|
2098
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2099
|
+
width="16"
|
|
2100
|
+
height="16"
|
|
2101
|
+
viewBox="0 0 24 24"
|
|
2102
|
+
fill="none"
|
|
2103
|
+
stroke={
|
|
2104
|
+
settings?.button?.language?.textColor ||
|
|
2105
|
+
"currentColor"
|
|
2106
|
+
}
|
|
2107
|
+
strokeWidth="2"
|
|
2108
|
+
strokeLinecap="round"
|
|
2109
|
+
strokeLinejoin="round"
|
|
2110
|
+
aria-hidden="true"
|
|
2111
|
+
>
|
|
2112
|
+
<rect x="6" y="4" width="4" height="16" />
|
|
2113
|
+
<rect x="14" y="4" width="4" height="16" />
|
|
2114
|
+
</svg>
|
|
2115
|
+
) : (
|
|
2116
|
+
// Play/Sound icon
|
|
2117
|
+
<svg
|
|
2118
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2119
|
+
width="16"
|
|
2120
|
+
height="16"
|
|
2121
|
+
viewBox="0 0 24 24"
|
|
2122
|
+
fill="none"
|
|
2123
|
+
stroke={
|
|
2124
|
+
settings?.button?.language?.textColor ||
|
|
2125
|
+
"currentColor"
|
|
2126
|
+
}
|
|
2127
|
+
strokeWidth="2"
|
|
2128
|
+
strokeLinecap="round"
|
|
2129
|
+
strokeLinejoin="round"
|
|
2130
|
+
aria-hidden="true"
|
|
2131
|
+
>
|
|
2132
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
2133
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
2134
|
+
</svg>
|
|
2135
|
+
)}
|
|
2136
|
+
</button>
|
|
2137
|
+
)}
|
|
1885
2138
|
</div>
|
|
1886
2139
|
{isLanguageDropdownOpen && (
|
|
1887
2140
|
<ul
|
|
@@ -1931,43 +2184,35 @@ export const RedactoNoticeConsent = ({
|
|
|
1931
2184
|
...componentTextStyle,
|
|
1932
2185
|
}}
|
|
1933
2186
|
>
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2187
|
+
<span id="privacy-notice-text">
|
|
2188
|
+
{content
|
|
2189
|
+
? getTranslatedText("notice_text", content.notice_text)
|
|
2190
|
+
: ""}
|
|
2191
|
+
</span>
|
|
1937
2192
|
<br />
|
|
1938
|
-
|
|
1939
|
-
<a
|
|
1940
|
-
style={{ ...styles.link, ...linkStyle }}
|
|
1941
|
-
href={content?.privacy_policy_url || "#"}
|
|
1942
|
-
target="_blank"
|
|
1943
|
-
rel="noopener noreferrer"
|
|
1944
|
-
aria-label="Privacy Policy (opens in new tab)"
|
|
1945
|
-
>
|
|
1946
|
-
[
|
|
2193
|
+
<span id="privacy-policy-text">
|
|
1947
2194
|
{getTranslatedText(
|
|
1948
|
-
"
|
|
1949
|
-
content?.
|
|
1950
|
-
)}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
]
|
|
1967
|
-
</a>
|
|
1968
|
-
.
|
|
2195
|
+
"privacy_policy_prefix_text",
|
|
2196
|
+
content?.privacy_policy_prefix_text || ""
|
|
2197
|
+
)}{" "}
|
|
2198
|
+
<a
|
|
2199
|
+
id="privacy-policy-link"
|
|
2200
|
+
style={{ ...styles.link, ...linkStyle }}
|
|
2201
|
+
href={content?.privacy_policy_url || "#"}
|
|
2202
|
+
target="_blank"
|
|
2203
|
+
rel="noopener noreferrer"
|
|
2204
|
+
aria-label="Privacy Policy (opens in new tab)"
|
|
2205
|
+
>
|
|
2206
|
+
{getTranslatedText(
|
|
2207
|
+
"privacy_policy_anchor_text",
|
|
2208
|
+
content?.privacy_policy_anchor_text ||
|
|
2209
|
+
"Privacy Policy"
|
|
2210
|
+
)}
|
|
2211
|
+
</a>
|
|
2212
|
+
</span>
|
|
1969
2213
|
</p>
|
|
1970
2214
|
<h2
|
|
2215
|
+
id="purpose-section-heading"
|
|
1971
2216
|
style={{
|
|
1972
2217
|
...styles.subTitle,
|
|
1973
2218
|
...responsiveStyles.subTitle,
|
|
@@ -64,12 +64,12 @@ export const fetchConsentContent = async ({
|
|
|
64
64
|
baseUrl,
|
|
65
65
|
language = "en",
|
|
66
66
|
specific_uuid,
|
|
67
|
-
|
|
67
|
+
validate_against = "all",
|
|
68
68
|
signal, // AbortSignal for request cancellation
|
|
69
69
|
}: FetchConsentContentParams & { signal?: AbortSignal }): Promise<ConsentContent> => {
|
|
70
70
|
try {
|
|
71
71
|
// Validate required parameters
|
|
72
|
-
if (!noticeId || !accessToken) {
|
|
72
|
+
if (!noticeId || !accessToken || !validate_against) {
|
|
73
73
|
throw new Error("noticeId and accessToken are required");
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -86,10 +86,10 @@ export const fetchConsentContent = async ({
|
|
|
86
86
|
const apiBaseUrl = baseUrl || BASE_URL;
|
|
87
87
|
|
|
88
88
|
// Create cache key for this request - include accessToken to ensure different users get different cache
|
|
89
|
-
const cacheKey = `${accessToken}-${noticeId}-${
|
|
89
|
+
const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}`;
|
|
90
90
|
|
|
91
91
|
// Check cache first (skip for reconsent requests as they need fresh data)
|
|
92
|
-
if (
|
|
92
|
+
if (validate_against === "all" && !specific_uuid) {
|
|
93
93
|
const cachedData = getCachedData(cacheKey);
|
|
94
94
|
if (cachedData) {
|
|
95
95
|
return cachedData;
|
|
@@ -103,7 +103,7 @@ export const fetchConsentContent = async ({
|
|
|
103
103
|
if (specific_uuid) {
|
|
104
104
|
url.searchParams.append("specific_uuid", specific_uuid);
|
|
105
105
|
}
|
|
106
|
-
url.searchParams.append("
|
|
106
|
+
url.searchParams.append("validate_against", validate_against);
|
|
107
107
|
|
|
108
108
|
const response = await fetch(url.toString(), {
|
|
109
109
|
method: "GET",
|
|
@@ -138,7 +138,7 @@ export const fetchConsentContent = async ({
|
|
|
138
138
|
const data: ConsentContent = await response.json();
|
|
139
139
|
|
|
140
140
|
// Cache successful responses (skip for reconsent as data may change)
|
|
141
|
-
if (
|
|
141
|
+
if (validate_against === "all" && !specific_uuid) {
|
|
142
142
|
setCachedData(cacheKey, data);
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -219,7 +219,7 @@ export const submitConsentEvent = async ({
|
|
|
219
219
|
const apiBaseUrl = baseUrl || BASE_URL;
|
|
220
220
|
|
|
221
221
|
const response = await fetch(
|
|
222
|
-
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/
|
|
222
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
|
|
223
223
|
{
|
|
224
224
|
method: "POST",
|
|
225
225
|
headers: {
|
|
@@ -95,6 +95,22 @@ export const injectCheckboxStyles = () => {
|
|
|
95
95
|
outline: 2px solid #4f87ff !important;
|
|
96
96
|
outline-offset: 2px !important;
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/* Text-to-Speech Highlight Styles */
|
|
100
|
+
.redacto-tts-highlight {
|
|
101
|
+
background-color: #FFF9C4 !important;
|
|
102
|
+
transition: background-color 0.3s ease !important;
|
|
103
|
+
border-radius: 2px !important;
|
|
104
|
+
padding: 2px 4px !important;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Word-by-word highlighting */
|
|
108
|
+
.redacto-tts-word-highlight {
|
|
109
|
+
background-color: #FFF9C4 !important;
|
|
110
|
+
transition: background-color 0.2s ease !important;
|
|
111
|
+
border-radius: 2px !important;
|
|
112
|
+
padding: 0 1px !important;
|
|
113
|
+
}
|
|
98
114
|
`;
|
|
99
115
|
|
|
100
116
|
document.head.appendChild(style);
|
|
@@ -12,7 +12,7 @@ export type Props = Readonly<{
|
|
|
12
12
|
onDecline: () => void;
|
|
13
13
|
onError?: (error: Error) => void;
|
|
14
14
|
applicationId?: string;
|
|
15
|
-
|
|
15
|
+
validateAgainst?: "all" | "required";
|
|
16
16
|
}>;
|
|
17
17
|
|
|
18
18
|
export type PurposeTranslations = {
|
|
@@ -43,3 +43,26 @@ export type TranslationObject = {
|
|
|
43
43
|
| Record<string, string>
|
|
44
44
|
| undefined;
|
|
45
45
|
};
|
|
46
|
+
|
|
47
|
+
export type TextSegment = {
|
|
48
|
+
text: string;
|
|
49
|
+
elementId: string;
|
|
50
|
+
elementRef?: React.RefObject<HTMLElement>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type UseTextToSpeechReturn = {
|
|
54
|
+
isPlaying: boolean;
|
|
55
|
+
isPaused: boolean;
|
|
56
|
+
currentIndex: number;
|
|
57
|
+
play: () => void;
|
|
58
|
+
pause: () => void;
|
|
59
|
+
stop: () => void;
|
|
60
|
+
toggle: () => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type WordSpan = {
|
|
64
|
+
word: string;
|
|
65
|
+
span: HTMLSpanElement;
|
|
66
|
+
startIndex: number;
|
|
67
|
+
endIndex: number;
|
|
68
|
+
};
|