@redacto.io/consent-sdk-react 10.1.0 → 10.2.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/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  SANDBOX_TOKEN_HEADER,
3
- useMediaQuery
4
- } from "./chunk-OGL6OC3A.mjs";
3
+ useMediaQuery,
4
+ withIdempotencyKey
5
+ } from "./chunk-6EET4YOY.mjs";
5
6
  import {
6
7
  FONT_FAMILY_SEPARATOR,
7
8
  FONT_NAME_QUOTE_CHARACTERS,
@@ -17,7 +18,13 @@ import {
17
18
  } from "./chunk-FT5G2PXK.mjs";
18
19
 
19
20
  // src/RedactoNoticeConsent/RedactoNoticeConsent.tsx
20
- import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef, useState } from "react";
21
+ import {
22
+ useCallback,
23
+ useEffect as useEffect2,
24
+ useMemo as useMemo2,
25
+ useRef,
26
+ useState
27
+ } from "react";
21
28
 
22
29
  // src/RedactoNoticeConsent/api/index.ts
23
30
  import { jwtDecode as jwtDecode2 } from "jwt-decode";
@@ -347,6 +354,9 @@ var submitConsentEvent = async ({
347
354
  }));
348
355
  return {
349
356
  purpose_uuid: purpose.uuid,
357
+ // Omitted on a single-product notice, where the ledger applies the
358
+ // selection to every product the purpose covers.
359
+ product_uuid: purpose.product_uuid || void 0,
350
360
  selected: Boolean(purpose.selected),
351
361
  data_elements: dataElements.length > 0 ? dataElements : void 0
352
362
  };
@@ -363,17 +373,21 @@ var submitConsentEvent = async ({
363
373
  self_declared_adult: selfDeclaredAdult
364
374
  };
365
375
  const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
366
- const response = await fetch(
367
- `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
368
- {
376
+ const url = `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`;
377
+ const serialisedBody = JSON.stringify(requestBody);
378
+ const response = await withIdempotencyKey(
379
+ url,
380
+ serialisedBody,
381
+ (idempotencyHeaders) => fetch(url, {
369
382
  method: "POST",
370
383
  headers: {
371
384
  ...buildAuthHeaders(accessToken, sandbox),
372
- "Content-Type": "application/json"
385
+ "Content-Type": "application/json",
386
+ ...idempotencyHeaders
373
387
  },
374
- body: JSON.stringify(requestBody),
388
+ body: serialisedBody,
375
389
  signal
376
- }
390
+ })
377
391
  );
378
392
  if (!response.ok) {
379
393
  throw await parseApiError(
@@ -606,6 +620,9 @@ var DPO_AUDIO_ELEMENT_MAP = [
606
620
  { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
607
621
  { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
608
622
  ];
623
+ var PRODUCT_SECTION_COPY = {
624
+ toggleSelection: (productName, purposeCount) => `Toggle all ${purposeCount} purposes for ${productName}`
625
+ };
609
626
 
610
627
  // src/RedactoNoticeConsent/styles.ts
611
628
  var styles = {
@@ -822,6 +839,37 @@ var styles = {
822
839
  padding: 0,
823
840
  boxSizing: "border-box"
824
841
  },
842
+ // A product section's heading. Its own tokens rather than the row's: sharing
843
+ // optionTitle rendered the group name at the same size, weight and indent as
844
+ // the purposes inside it, leaving nothing to mark it as a heading.
845
+ sectionHeader: {
846
+ display: "flex",
847
+ justifyContent: "space-between",
848
+ alignItems: "center",
849
+ margin: "0 0 8px",
850
+ padding: 0,
851
+ boxSizing: "border-box"
852
+ },
853
+ // The purposes belonging to one product. Indented so the product chevron and
854
+ // the purpose chevrons below it do not sit on the same vertical line, which
855
+ // read as one flat list rather than a heading and its contents. The trailing
856
+ // margin separates a section from the next product heading.
857
+ sectionBody: {
858
+ paddingLeft: "18px",
859
+ margin: "0 0 14px",
860
+ boxSizing: "border-box"
861
+ },
862
+ // No colour token: the header always overrides it with the notice's
863
+ // headingColor, so one here would imply a default that never applies.
864
+ sectionTitle: {
865
+ fontWeight: 600,
866
+ fontSize: "17px",
867
+ lineHeight: "150%",
868
+ letterSpacing: "0.2px",
869
+ margin: 0,
870
+ padding: 0,
871
+ textAlign: "left"
872
+ },
825
873
  optionTextContainer: {
826
874
  display: "flex",
827
875
  flexDirection: "column",
@@ -966,10 +1014,10 @@ var styles = {
966
1014
  color: "#ffffff"
967
1015
  },
968
1016
  /**
969
- * The accept-all shortcut. Tinted rather than filled so the primary
970
- * "accept selected" action stays visually dominant, and outlined-but-coloured
971
- * so it still reads as an accept next to the neutral decline button.
972
- */
1017
+ * The accept-all shortcut. Tinted rather than filled so the primary
1018
+ * "accept selected" action stays visually dominant, and outlined-but-coloured
1019
+ * so it still reads as an accept next to the neutral decline button.
1020
+ */
973
1021
  acceptSecondaryButton: {
974
1022
  backgroundColor: "#ffffff",
975
1023
  border: "1px solid #4f87ff",
@@ -1163,18 +1211,76 @@ var NoticeFooter = ({
1163
1211
  ] });
1164
1212
  };
1165
1213
 
1214
+ // src/shared/productMatrix.ts
1215
+ var productPurposeKey = (productUuid, purposeUuid) => productUuid ? `${productUuid}:${purposeUuid}` : purposeUuid;
1216
+ var purposeCoversProduct = (purpose, productUuid) => {
1217
+ const scope = purpose.product_uuids;
1218
+ if (!scope || scope.length === 0) return true;
1219
+ return scope.includes(productUuid);
1220
+ };
1221
+ var isMultiProduct = (products) => Boolean(products && products.length > 1);
1222
+ var groupPurposesByProduct = (products, purposes, order) => {
1223
+ if (!products || products.length === 0) return [];
1224
+ return products.map((product) => {
1225
+ const covered = purposes.filter(
1226
+ (purpose) => purposeCoversProduct(purpose, product.uuid)
1227
+ );
1228
+ const saved = order == null ? void 0 : order[product.uuid];
1229
+ if (!saved || saved.length === 0) return { product, purposes: covered };
1230
+ const byUuid = new Map(covered.map((p) => [p.uuid, p]));
1231
+ const ordered = [];
1232
+ for (const uuid of saved) {
1233
+ const found = byUuid.get(uuid);
1234
+ if (found) ordered.push(found);
1235
+ }
1236
+ const rest = covered.filter((p) => !saved.includes(p.uuid));
1237
+ return { product, purposes: [...ordered, ...rest] };
1238
+ }).filter((group) => group.purposes.length > 0);
1239
+ };
1240
+ var expandPurposesByProduct = (products, purposes, order) => {
1241
+ if (!isMultiProduct(products)) return [];
1242
+ const rows = [];
1243
+ for (const group of groupPurposesByProduct(products, purposes, order)) {
1244
+ for (const purpose of group.purposes) {
1245
+ rows.push({ product: group.product, purpose });
1246
+ }
1247
+ }
1248
+ return rows;
1249
+ };
1250
+ var noticePurposeRows = (products, purposes, order) => {
1251
+ if (!isMultiProduct(products)) {
1252
+ return purposes.map((purpose) => ({ purpose, productUuid: void 0 }));
1253
+ }
1254
+ const rows = expandPurposesByProduct(products, purposes, order).map(
1255
+ (row) => ({
1256
+ purpose: row.purpose,
1257
+ productUuid: row.product.uuid
1258
+ })
1259
+ );
1260
+ const rendered = new Set(rows.map((row) => row.purpose.uuid));
1261
+ const orphaned = purposes.filter((purpose) => !rendered.has(purpose.uuid));
1262
+ if (orphaned.length > 0) {
1263
+ console.warn(
1264
+ "[redacto] purposes cover no product on this notice and will not be shown or submitted:",
1265
+ orphaned.map((purpose) => purpose.uuid)
1266
+ );
1267
+ }
1268
+ return rows;
1269
+ };
1270
+
1166
1271
  // src/RedactoNoticeConsent/utils/consentSelection.ts
1167
- var dataElementKey = (purposeUuid, elementUuid) => `${purposeUuid}-${elementUuid}`;
1168
- var buildSelectionForMode = (purposes, mode, current) => {
1272
+ var dataElementKey = (purposeUuid, elementUuid, productUuid) => `${productPurposeKey(productUuid, purposeUuid)}-${elementUuid}`;
1273
+ var buildSelectionForMode = (rows, mode, current) => {
1169
1274
  if (mode === "selected") {
1170
1275
  return current;
1171
1276
  }
1172
1277
  const selectedPurposes = {};
1173
1278
  const selectedDataElements = {};
1174
- purposes.forEach((purpose) => {
1175
- selectedPurposes[purpose.uuid] = true;
1279
+ rows.forEach(({ purpose, productUuid }) => {
1280
+ const key = productPurposeKey(productUuid, purpose.uuid);
1281
+ selectedPurposes[key] = true;
1176
1282
  purpose.data_elements.forEach((element) => {
1177
- selectedDataElements[dataElementKey(purpose.uuid, element.uuid)] = element.enabled;
1283
+ selectedDataElements[dataElementKey(purpose.uuid, element.uuid, productUuid)] = element.enabled;
1178
1284
  });
1179
1285
  });
1180
1286
  return { selectedPurposes, selectedDataElements };
@@ -1194,10 +1300,11 @@ var buildPreselection = (purposes, mode) => {
1194
1300
  const selectedPurposes = {};
1195
1301
  const selectedDataElements = {};
1196
1302
  if (mode === "ALL") {
1197
- return buildSelectionForMode(purposes, "all", {
1198
- selectedPurposes,
1199
- selectedDataElements
1200
- });
1303
+ return buildSelectionForMode(
1304
+ purposes.map((purpose) => ({ purpose, productUuid: void 0 })),
1305
+ "all",
1306
+ { selectedPurposes, selectedDataElements }
1307
+ );
1201
1308
  }
1202
1309
  purposes.forEach((purpose) => {
1203
1310
  let hasPreselectedElement = false;
@@ -1318,6 +1425,7 @@ var injectCheckboxStyles = () => {
1318
1425
  cursor: pointer !important;
1319
1426
  position: relative !important;
1320
1427
  box-sizing: border-box !important;
1428
+ flex-shrink: 0 !important;
1321
1429
  margin: 0 !important;
1322
1430
  padding: 4px !important;
1323
1431
  }
@@ -1334,6 +1442,7 @@ var injectCheckboxStyles = () => {
1334
1442
  cursor: pointer !important;
1335
1443
  position: relative !important;
1336
1444
  box-sizing: border-box !important;
1445
+ flex-shrink: 0 !important;
1337
1446
  margin: 0 !important;
1338
1447
  padding: 4px !important;
1339
1448
  }
@@ -1625,8 +1734,113 @@ var updateConsentState = (updates) => {
1625
1734
 
1626
1735
  // src/RedactoNoticeConsent/RedactoNoticeConsent.tsx
1627
1736
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1737
+ var DEFAULT_HEADING_COLOR = "#323B4B";
1738
+ var CHEVRON_BUTTON_STYLE = {
1739
+ background: "none",
1740
+ border: "none",
1741
+ padding: 0,
1742
+ margin: 0,
1743
+ cursor: "pointer",
1744
+ display: "flex",
1745
+ alignItems: "center"
1746
+ };
1747
+ var CHEVRON_WRAPPER_STYLE = {
1748
+ display: "flex",
1749
+ alignItems: "center",
1750
+ justifyContent: "center",
1751
+ marginLeft: "5px"
1752
+ };
1753
+ var CollapseChevron = ({ open, color }) => /* @__PURE__ */ jsx3("div", { style: CHEVRON_WRAPPER_STYLE, children: /* @__PURE__ */ jsx3(
1754
+ "svg",
1755
+ {
1756
+ width: "7",
1757
+ height: "12",
1758
+ viewBox: "0 0 7 12",
1759
+ fill: "none",
1760
+ xmlns: "http://www.w3.org/2000/svg",
1761
+ stroke: color,
1762
+ strokeWidth: "2",
1763
+ strokeLinecap: "round",
1764
+ strokeLinejoin: "round",
1765
+ style: {
1766
+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
1767
+ transition: "transform 0.3s ease"
1768
+ },
1769
+ "aria-hidden": "true",
1770
+ children: /* @__PURE__ */ jsx3("path", { d: "M1 1L6 6L1 11" })
1771
+ }
1772
+ ) });
1773
+ var ProductSectionHeader = ({
1774
+ product,
1775
+ purposeCount,
1776
+ allSelected,
1777
+ someSelected,
1778
+ collapsed,
1779
+ locked,
1780
+ settings,
1781
+ styles: styles4,
1782
+ responsiveStyles,
1783
+ onToggleCollapse,
1784
+ onToggleSelect,
1785
+ getTranslatedText
1786
+ }) => {
1787
+ const checkboxRef = useRef(null);
1788
+ const name = getTranslatedText("products.name", product.name, product.uuid);
1789
+ const headingColor = (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR;
1790
+ useEffect2(() => {
1791
+ if (checkboxRef.current) {
1792
+ checkboxRef.current.indeterminate = someSelected && !allSelected;
1793
+ }
1794
+ }, [someSelected, allSelected]);
1795
+ const handleToggleCollapse = () => {
1796
+ onToggleCollapse(product.uuid);
1797
+ };
1798
+ const handleToggleSelect = () => {
1799
+ onToggleSelect(product.uuid);
1800
+ };
1801
+ return /* @__PURE__ */ jsxs2("div", { style: styles4.sectionHeader, children: [
1802
+ /* @__PURE__ */ jsxs2("div", { style: styles4.optionLeft, children: [
1803
+ /* @__PURE__ */ jsx3(
1804
+ "button",
1805
+ {
1806
+ type: "button",
1807
+ onClick: handleToggleCollapse,
1808
+ "aria-expanded": !collapsed,
1809
+ "aria-controls": `product-${product.uuid}`,
1810
+ "aria-labelledby": `product-name-${product.uuid}`,
1811
+ style: CHEVRON_BUTTON_STYLE,
1812
+ children: /* @__PURE__ */ jsx3(CollapseChevron, { open: !collapsed, color: headingColor })
1813
+ }
1814
+ ),
1815
+ /* @__PURE__ */ jsx3("div", { style: styles4.optionTextContainer, children: /* @__PURE__ */ jsx3(
1816
+ "h2",
1817
+ {
1818
+ id: `product-name-${product.uuid}`,
1819
+ style: {
1820
+ ...styles4.sectionTitle,
1821
+ ...responsiveStyles.optionTitle,
1822
+ color: headingColor
1823
+ },
1824
+ children: name
1825
+ }
1826
+ ) })
1827
+ ] }),
1828
+ !locked && /* @__PURE__ */ jsx3(
1829
+ "input",
1830
+ {
1831
+ ref: checkboxRef,
1832
+ className: "redacto-checkbox-large",
1833
+ type: "checkbox",
1834
+ checked: allSelected,
1835
+ onChange: handleToggleSelect,
1836
+ "aria-label": PRODUCT_SECTION_COPY.toggleSelection(name, purposeCount)
1837
+ }
1838
+ )
1839
+ ] });
1840
+ };
1628
1841
  var PurposeItem = ({
1629
1842
  purpose,
1843
+ productUuid,
1630
1844
  selectedPurposes,
1631
1845
  collapsedPurposes,
1632
1846
  selectedDataElements,
@@ -1641,9 +1855,10 @@ var PurposeItem = ({
1641
1855
  selectedLanguage,
1642
1856
  initialDataElementSelections = {}
1643
1857
  }) => {
1858
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
1644
1859
  const headingStyle = useMemo2(
1645
1860
  () => ({
1646
- color: (settings == null ? void 0 : settings.headingColor) || "#323B4B"
1861
+ color: (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR
1647
1862
  }),
1648
1863
  [settings == null ? void 0 : settings.headingColor]
1649
1864
  );
@@ -1659,12 +1874,12 @@ var PurposeItem = ({
1659
1874
  "div",
1660
1875
  {
1661
1876
  style: styles4.optionLeft,
1662
- onClick: () => onPurposeCollapse(purpose.uuid),
1877
+ onClick: () => onPurposeCollapse(purpose.uuid, productUuid),
1663
1878
  role: "button",
1664
1879
  tabIndex: 0,
1665
- "aria-expanded": !collapsedPurposes[purpose.uuid],
1880
+ "aria-expanded": !collapsedPurposes[stateKey],
1666
1881
  "aria-controls": `purpose-${purpose.uuid}`,
1667
- "aria-label": `${collapsedPurposes[purpose.uuid] ? "Expand" : "Collapse"} ${getTranslatedText(
1882
+ "aria-label": `${collapsedPurposes[stateKey] ? "Expand" : "Collapse"} ${getTranslatedText(
1668
1883
  `purposes.name`,
1669
1884
  purpose.name,
1670
1885
  purpose.uuid
@@ -1672,46 +1887,22 @@ var PurposeItem = ({
1672
1887
  onKeyDown: (e) => {
1673
1888
  if (e.key === "Enter" || e.key === " ") {
1674
1889
  e.preventDefault();
1675
- onPurposeCollapse(purpose.uuid);
1890
+ onPurposeCollapse(purpose.uuid, productUuid);
1676
1891
  }
1677
1892
  },
1678
1893
  children: [
1679
1894
  /* @__PURE__ */ jsx3(
1680
- "div",
1895
+ CollapseChevron,
1681
1896
  {
1682
- style: {
1683
- display: "flex",
1684
- alignItems: "center",
1685
- justifyContent: "center",
1686
- marginLeft: "5px"
1687
- },
1688
- children: /* @__PURE__ */ jsx3(
1689
- "svg",
1690
- {
1691
- width: "7",
1692
- height: "12",
1693
- viewBox: "0 0 7 12",
1694
- fill: "none",
1695
- xmlns: "http://www.w3.org/2000/svg",
1696
- stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
1697
- strokeWidth: "2",
1698
- strokeLinecap: "round",
1699
- strokeLinejoin: "round",
1700
- style: {
1701
- transform: !collapsedPurposes[purpose.uuid] ? "rotate(90deg)" : "rotate(0deg)",
1702
- transition: "transform 0.3s ease"
1703
- },
1704
- "aria-hidden": "true",
1705
- children: /* @__PURE__ */ jsx3("path", { d: "M1 1L6 6L1 11" })
1706
- }
1707
- )
1897
+ open: !collapsedPurposes[stateKey],
1898
+ color: (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR
1708
1899
  }
1709
1900
  ),
1710
1901
  /* @__PURE__ */ jsxs2("div", { style: styles4.optionTextContainer, children: [
1711
1902
  /* @__PURE__ */ jsxs2(
1712
1903
  "h2",
1713
1904
  {
1714
- id: `purpose-name-${purpose.uuid}`,
1905
+ id: `purpose-name-${stateKey}`,
1715
1906
  style: {
1716
1907
  ...styles4.optionTitle,
1717
1908
  ...responsiveStyles.optionTitle,
@@ -1727,7 +1918,7 @@ var PurposeItem = ({
1727
1918
  /* @__PURE__ */ jsx3(
1728
1919
  "h3",
1729
1920
  {
1730
- id: `purpose-description-${purpose.uuid}`,
1921
+ id: `purpose-description-${stateKey}`,
1731
1922
  style: {
1732
1923
  ...styles4.optionDescription,
1733
1924
  ...responsiveStyles.optionDescription,
@@ -1750,8 +1941,8 @@ var PurposeItem = ({
1750
1941
  {
1751
1942
  className: "redacto-checkbox-large",
1752
1943
  type: "checkbox",
1753
- checked: selectedPurposes[purpose.uuid] || false,
1754
- onChange: () => onPurposeToggle(purpose.uuid),
1944
+ checked: selectedPurposes[stateKey] || false,
1945
+ onChange: () => onPurposeToggle(purpose.uuid, productUuid),
1755
1946
  "aria-label": `Select all data elements for ${getTranslatedText(
1756
1947
  `purposes.name`,
1757
1948
  purpose.name,
@@ -1801,7 +1992,7 @@ var PurposeItem = ({
1801
1992
  }
1802
1993
  )
1803
1994
  ] }),
1804
- !collapsedPurposes[purpose.uuid] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ jsx3(
1995
+ !collapsedPurposes[stateKey] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ jsx3(
1805
1996
  "div",
1806
1997
  {
1807
1998
  id: `purpose-${purpose.uuid}`,
@@ -1814,7 +2005,7 @@ var PurposeItem = ({
1814
2005
  /* @__PURE__ */ jsxs2(
1815
2006
  "span",
1816
2007
  {
1817
- id: `data-element-${purpose.uuid}-${dataElement.uuid}`,
2008
+ id: `data-element-${stateKey}-${dataElement.uuid}`,
1818
2009
  style: {
1819
2010
  ...styles4.dataElementText,
1820
2011
  ...responsiveStyles.dataElementText,
@@ -1836,8 +2027,12 @@ var PurposeItem = ({
1836
2027
  {
1837
2028
  className: "redacto-checkbox-small",
1838
2029
  type: "checkbox",
1839
- checked: selectedDataElements[`${purpose.uuid}-${dataElement.uuid}`] || false,
1840
- onChange: () => onDataElementToggle(dataElement.uuid, purpose.uuid),
2030
+ checked: selectedDataElements[`${stateKey}-${dataElement.uuid}`] || false,
2031
+ onChange: () => onDataElementToggle(
2032
+ dataElement.uuid,
2033
+ purpose.uuid,
2034
+ productUuid
2035
+ ),
1841
2036
  "aria-label": `Select ${getTranslatedText(
1842
2037
  `data_elements.name`,
1843
2038
  dataElement.name,
@@ -1846,7 +2041,7 @@ var PurposeItem = ({
1846
2041
  }
1847
2042
  ),
1848
2043
  isAlreadyConsented && /* @__PURE__ */ jsx3(Fragment2, { children: (() => {
1849
- const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
2044
+ const combinedId = `${stateKey}-${dataElement.uuid}`;
1850
2045
  const wasOriginallySelected = initialDataElementSelections[combinedId] || false;
1851
2046
  if (wasOriginallySelected) {
1852
2047
  return /* @__PURE__ */ jsx3(
@@ -1984,7 +2179,9 @@ var GuardianForm = ({
1984
2179
  switch (e.key) {
1985
2180
  case "ArrowDown":
1986
2181
  e.preventDefault();
1987
- setFocusedIndex((prev) => Math.min(prev + 1, RELATIONSHIP_OPTIONS.length - 1));
2182
+ setFocusedIndex(
2183
+ (prev) => Math.min(prev + 1, RELATIONSHIP_OPTIONS.length - 1)
2184
+ );
1988
2185
  break;
1989
2186
  case "ArrowUp":
1990
2187
  e.preventDefault();
@@ -1993,7 +2190,8 @@ var GuardianForm = ({
1993
2190
  case "Enter":
1994
2191
  case " ":
1995
2192
  e.preventDefault();
1996
- if (focusedIndex >= 0) handleSelect(RELATIONSHIP_OPTIONS[focusedIndex].value);
2193
+ if (focusedIndex >= 0)
2194
+ handleSelect(RELATIONSHIP_OPTIONS[focusedIndex].value);
1997
2195
  break;
1998
2196
  case "Escape":
1999
2197
  case "Tab":
@@ -2058,7 +2256,9 @@ var GuardianForm = ({
2058
2256
  lineHeight: "1.5",
2059
2257
  color: "#1E40AF"
2060
2258
  };
2061
- const selectedLabel = (_a = RELATIONSHIP_OPTIONS.find((o) => o.value === formData.guardianRelationship)) == null ? void 0 : _a.label;
2259
+ const selectedLabel = (_a = RELATIONSHIP_OPTIONS.find(
2260
+ (o) => o.value === formData.guardianRelationship
2261
+ )) == null ? void 0 : _a.label;
2062
2262
  const hasError = Boolean(errors.guardianRelationship);
2063
2263
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2064
2264
  /* @__PURE__ */ jsx3("div", { style: { ...styles4.topSection, ...componentTextStyle }, children: /* @__PURE__ */ jsxs2("div", { style: styles4.topLeft, children: [
@@ -2158,7 +2358,8 @@ var GuardianForm = ({
2158
2358
  ] }),
2159
2359
  /* @__PURE__ */ jsxs2("div", { children: [
2160
2360
  /* @__PURE__ */ jsxs2("label", { htmlFor: "guardianContact", style: labelStyle, children: [
2161
- "Guardian's Phone Number ",
2361
+ "Guardian's Phone Number",
2362
+ " ",
2162
2363
  /* @__PURE__ */ jsx3("span", { style: { color: "#DC2626" }, children: "*" })
2163
2364
  ] }),
2164
2365
  /* @__PURE__ */ jsx3(
@@ -2214,7 +2415,17 @@ var GuardianForm = ({
2214
2415
  transition: "border-color 0.15s, box-shadow 0.15s"
2215
2416
  },
2216
2417
  children: [
2217
- /* @__PURE__ */ jsx3("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: selectedLabel || "Select relationship" }),
2418
+ /* @__PURE__ */ jsx3(
2419
+ "span",
2420
+ {
2421
+ style: {
2422
+ overflow: "hidden",
2423
+ textOverflow: "ellipsis",
2424
+ whiteSpace: "nowrap"
2425
+ },
2426
+ children: selectedLabel || "Select relationship"
2427
+ }
2428
+ ),
2218
2429
  /* @__PURE__ */ jsx3(
2219
2430
  "svg",
2220
2431
  {
@@ -2882,6 +3093,7 @@ var RedactoNoticeConsent = ({
2882
3093
  const [selectedPurposes, setSelectedPurposes] = useState({});
2883
3094
  const [selectedDataElements, setSelectedDataElements] = useState({});
2884
3095
  const [initialDataElementSelections, setInitialDataElementSelections] = useState({});
3096
+ const [collapsedProducts, setCollapsedProducts] = useState({});
2885
3097
  const [collapsedPurposes, setCollapsedPurposes] = useState({});
2886
3098
  const [hasAlreadyConsented, setHasAlreadyConsented] = useState(false);
2887
3099
  const [isFullyConsentedReviewMode, setIsFullyConsentedReviewMode] = useState(false);
@@ -2927,7 +3139,9 @@ var RedactoNoticeConsent = ({
2927
3139
  const statusPollIntervalRef = useRef(null);
2928
3140
  const verificationPopupRef = useRef(null);
2929
3141
  const pollingCleanupRef = useRef(null);
2930
- const autoTransitionTimerRef = useRef(null);
3142
+ const autoTransitionTimerRef = useRef(
3143
+ null
3144
+ );
2931
3145
  const digilockerRedirectTimeoutRef = useRef(null);
2932
3146
  const isInitialMountRef = useRef(true);
2933
3147
  const isPollingStatusRef = useRef(false);
@@ -3037,21 +3251,70 @@ var RedactoNoticeConsent = ({
3037
3251
  const logoRowStyle = useMemo2(
3038
3252
  () => ({
3039
3253
  ...styles.logoRow,
3040
- justifyContent: logoRowJustify(resolveLogoPosition(content == null ? void 0 : content.logo_position))
3254
+ justifyContent: logoRowJustify(
3255
+ resolveLogoPosition(content == null ? void 0 : content.logo_position)
3256
+ )
3041
3257
  }),
3042
3258
  [content == null ? void 0 : content.logo_position]
3043
3259
  );
3260
+ const purposeRows = useMemo2(
3261
+ () => {
3262
+ var _a2;
3263
+ return noticePurposeRows(
3264
+ content == null ? void 0 : content.products,
3265
+ (_a2 = content == null ? void 0 : content.purposes) != null ? _a2 : [],
3266
+ content == null ? void 0 : content.product_purpose_order
3267
+ );
3268
+ },
3269
+ [content]
3270
+ );
3271
+ const productGroups = useMemo2(
3272
+ () => {
3273
+ var _a2;
3274
+ return groupPurposesByProduct(
3275
+ content == null ? void 0 : content.products,
3276
+ (_a2 = content == null ? void 0 : content.purposes) != null ? _a2 : [],
3277
+ content == null ? void 0 : content.product_purpose_order
3278
+ );
3279
+ },
3280
+ [content]
3281
+ );
3044
3282
  const areAllRequiredElementsChecked = useMemo2(() => {
3283
+ var _a2;
3045
3284
  if (!content) return false;
3046
- return content.purposes.every((purpose) => {
3285
+ if (purposeRows.length === 0) return true;
3286
+ const isSatisfied = ({
3287
+ purpose,
3288
+ productUuid
3289
+ }) => {
3290
+ const key = productPurposeKey(productUuid, purpose.uuid);
3047
3291
  const requiredElements = purpose.data_elements.filter(
3048
3292
  (element) => element.required && element.enabled
3049
3293
  );
3050
3294
  return requiredElements.every(
3051
- (element) => selectedDataElements[`${purpose.uuid}-${element.uuid}`]
3295
+ (element) => selectedDataElements[`${key}-${element.uuid}`]
3052
3296
  );
3053
- });
3054
- }, [content, selectedDataElements]);
3297
+ };
3298
+ const rowsByProduct = /* @__PURE__ */ new Map();
3299
+ for (const row of purposeRows) {
3300
+ const groupKey = (_a2 = row.productUuid) != null ? _a2 : "";
3301
+ const group = rowsByProduct.get(groupKey);
3302
+ if (group) {
3303
+ group.push(row);
3304
+ } else {
3305
+ rowsByProduct.set(groupKey, [row]);
3306
+ }
3307
+ }
3308
+ const groups = [...rowsByProduct.values()];
3309
+ const everyProductHasMandatory = groups.every(
3310
+ (rows) => rows.some(
3311
+ ({ purpose }) => purpose.data_elements.some(
3312
+ (element) => element.required && element.enabled
3313
+ )
3314
+ )
3315
+ );
3316
+ return everyProductHasMandatory ? groups.some((rows) => rows.every(isSatisfied)) : purposeRows.every(isSatisfied);
3317
+ }, [content, purposeRows, selectedDataElements]);
3055
3318
  const acceptDisabled = isSubmitting || !areAllRequiredElementsChecked;
3056
3319
  useEffect2(() => {
3057
3320
  var _a2, _b2;
@@ -3063,6 +3326,7 @@ var RedactoNoticeConsent = ({
3063
3326
  setSelectedDataElements({});
3064
3327
  setInitialDataElementSelections({});
3065
3328
  setCollapsedPurposes({});
3329
+ setCollapsedProducts({});
3066
3330
  setHasAlreadyConsented(false);
3067
3331
  setErrorMessage(null);
3068
3332
  setFetchError(null);
@@ -3127,7 +3391,7 @@ var RedactoNoticeConsent = ({
3127
3391
  return false;
3128
3392
  };
3129
3393
  const getTranslatedText = (key, defaultText, itemId) => {
3130
- var _a2, _b2;
3394
+ var _a2, _b2, _c2;
3131
3395
  if (!content) return defaultText;
3132
3396
  if (isDefaultLanguage(selectedLanguage)) {
3133
3397
  if (key === "privacy_policy_anchor_text" && content.privacy_policy_anchor_text) {
@@ -3165,8 +3429,18 @@ var RedactoNoticeConsent = ({
3165
3429
  }
3166
3430
  return defaultText;
3167
3431
  }
3432
+ if (key === "products.name" || key === "products.description") {
3433
+ const productData = (_b2 = translationMap.products) == null ? void 0 : _b2[itemId];
3434
+ if (productData) {
3435
+ if (typeof productData === "string") {
3436
+ return key === "products.name" ? productData : defaultText;
3437
+ }
3438
+ return key === "products.name" ? productData.name || defaultText : productData.description || defaultText;
3439
+ }
3440
+ return defaultText;
3441
+ }
3168
3442
  if (key.startsWith("data_elements.")) {
3169
- return ((_b2 = translationMap.data_elements) == null ? void 0 : _b2[itemId]) || defaultText;
3443
+ return ((_c2 = translationMap.data_elements) == null ? void 0 : _c2[itemId]) || defaultText;
3170
3444
  }
3171
3445
  }
3172
3446
  const value = translationMap[key];
@@ -3224,63 +3498,88 @@ var RedactoNoticeConsent = ({
3224
3498
  defaultLang === "en" || defaultLang === "EN" ? "English" : defaultLang
3225
3499
  );
3226
3500
  }
3227
- const initialCollapsedState = activeConfig.purposes.reduce(
3228
- (acc, purpose) => ({
3501
+ const initialCollapsedState = noticePurposeRows(
3502
+ activeConfig.products,
3503
+ activeConfig.purposes,
3504
+ activeConfig.product_purpose_order
3505
+ ).reduce(
3506
+ (acc, { purpose, productUuid }) => ({
3229
3507
  ...acc,
3230
- [purpose.uuid]: true
3508
+ [productPurposeKey(productUuid, purpose.uuid)]: true
3231
3509
  }),
3232
3510
  {}
3233
3511
  );
3234
3512
  setCollapsedPurposes(initialCollapsedState);
3513
+ setCollapsedProducts(
3514
+ groupPurposesByProduct(
3515
+ activeConfig.products,
3516
+ activeConfig.purposes,
3517
+ activeConfig.product_purpose_order
3518
+ ).reduce(
3519
+ (acc, group, index) => ({
3520
+ ...acc,
3521
+ [group.product.uuid]: index > 0
3522
+ }),
3523
+ {}
3524
+ )
3525
+ );
3235
3526
  const isReconsentRequired = consentContentData.detail.reconsent_required;
3236
3527
  const purposeSelections = consentContentData.detail.purpose_selections;
3237
3528
  const newCategorizedPurposes = isReconsentRequired ? {
3238
- alreadyConsented: activeConfig.purposes.filter(
3239
- (purpose) => {
3240
- const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3241
- return selection && (selection.status === "ACTIVE" || selection.status === "EXPIRED") && !selection.needs_reconsent;
3242
- }
3243
- ),
3244
- needsConsent: activeConfig.purposes.filter(
3245
- (purpose) => {
3246
- const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3247
- return !selection || selection.status !== "ACTIVE" || selection.needs_reconsent;
3248
- }
3249
- )
3529
+ alreadyConsented: activeConfig.purposes.filter((purpose) => {
3530
+ const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3531
+ return selection && (selection.status === "ACTIVE" || selection.status === "EXPIRED") && !selection.needs_reconsent;
3532
+ }),
3533
+ needsConsent: activeConfig.purposes.filter((purpose) => {
3534
+ const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3535
+ return !selection || selection.status !== "ACTIVE" || selection.needs_reconsent;
3536
+ })
3250
3537
  } : null;
3251
3538
  setCategorizedPurposes(newCategorizedPurposes);
3252
3539
  const preselection = buildPreselection(
3253
3540
  activeConfig.purposes,
3254
3541
  resolvePurposePreselection(activeConfig.purpose_preselection)
3255
3542
  );
3256
- const initialPurposeState = activeConfig.purposes.reduce(
3257
- (acc, purpose) => {
3543
+ const seedRows = noticePurposeRows(
3544
+ activeConfig.products,
3545
+ activeConfig.purposes,
3546
+ activeConfig.product_purpose_order
3547
+ );
3548
+ const recordedFor = (purposeUuid, productUuid) => {
3549
+ var _a3, _b2;
3550
+ const perProduct = productUuid ? (_b2 = (_a3 = activeConfig.product_purpose_selections) == null ? void 0 : _a3[productUuid]) == null ? void 0 : _b2[purposeUuid] : void 0;
3551
+ return perProduct != null ? perProduct : purposeSelections == null ? void 0 : purposeSelections[purposeUuid];
3552
+ };
3553
+ const initialPurposeState = seedRows.reduce(
3554
+ (acc, { purpose, productUuid }) => {
3258
3555
  var _a3;
3259
- const recorded = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3556
+ const key = productPurposeKey(productUuid, purpose.uuid);
3557
+ const recorded = recordedFor(purpose.uuid, productUuid);
3260
3558
  if (hasRecordedPurposeState(recorded)) {
3261
- acc[purpose.uuid] = recorded.selected;
3559
+ acc[key] = recorded.selected;
3262
3560
  } else {
3263
- acc[purpose.uuid] = (_a3 = preselection.selectedPurposes[purpose.uuid]) != null ? _a3 : false;
3561
+ acc[key] = (_a3 = preselection.selectedPurposes[purpose.uuid]) != null ? _a3 : false;
3264
3562
  }
3265
3563
  return acc;
3266
3564
  },
3267
3565
  {}
3268
3566
  );
3269
3567
  setSelectedPurposes(initialPurposeState);
3270
- const initialDataElementState = activeConfig.purposes.reduce(
3271
- (acc, purpose) => {
3272
- const hasRecordedSelection = hasRecordedPurposeState(
3273
- purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]
3274
- );
3568
+ const initialDataElementState = seedRows.reduce(
3569
+ (acc, { purpose, productUuid }) => {
3570
+ const key = productPurposeKey(productUuid, purpose.uuid);
3571
+ const recorded = recordedFor(purpose.uuid, productUuid);
3572
+ const hasRecordedSelection = hasRecordedPurposeState(recorded);
3275
3573
  purpose.data_elements.forEach((element) => {
3276
- var _a3, _b2, _c2;
3277
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3278
- if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3279
- acc[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3574
+ var _a3, _b2;
3575
+ const combinedId = `${key}-${element.uuid}`;
3576
+ const recordedElement = (_a3 = recorded == null ? void 0 : recorded.data_elements) == null ? void 0 : _a3[element.uuid];
3577
+ if (recordedElement) {
3578
+ acc[combinedId] = recordedElement.selected;
3280
3579
  } else if (hasRecordedSelection) {
3281
3580
  acc[combinedId] = false;
3282
3581
  } else {
3283
- acc[combinedId] = (_c2 = preselection.selectedDataElements[combinedId]) != null ? _c2 : false;
3582
+ acc[combinedId] = (_b2 = preselection.selectedDataElements[`${purpose.uuid}-${element.uuid}`]) != null ? _b2 : false;
3284
3583
  }
3285
3584
  });
3286
3585
  return acc;
@@ -3291,10 +3590,14 @@ var RedactoNoticeConsent = ({
3291
3590
  if (isReconsentRequired && newCategorizedPurposes) {
3292
3591
  setIsFullyConsentedReviewMode(false);
3293
3592
  const initialSelections = {};
3294
- newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
3593
+ const alreadyConsentedUuids = new Set(
3594
+ newCategorizedPurposes.alreadyConsented.map((p) => p.uuid)
3595
+ );
3596
+ seedRows.filter(({ purpose }) => alreadyConsentedUuids.has(purpose.uuid)).forEach(({ purpose, productUuid }) => {
3597
+ const rowKey = productPurposeKey(productUuid, purpose.uuid);
3295
3598
  purpose.data_elements.forEach((element) => {
3296
3599
  var _a3, _b2;
3297
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3600
+ const combinedId = `${rowKey}-${element.uuid}`;
3298
3601
  if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3299
3602
  initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3300
3603
  } else {
@@ -3304,28 +3607,27 @@ var RedactoNoticeConsent = ({
3304
3607
  });
3305
3608
  setInitialDataElementSelections(initialSelections);
3306
3609
  } else if (includeFullyConsentedData && purposeSelections) {
3307
- const allPurposesSelected = activeConfig.purposes.length > 0 && activeConfig.purposes.every(
3308
- (p) => {
3610
+ const allPurposesSelected = seedRows.length > 0 && seedRows.every(
3611
+ ({ purpose, productUuid }) => {
3309
3612
  var _a3;
3310
- return ((_a3 = purposeSelections[p.uuid]) == null ? void 0 : _a3.selected) === true;
3613
+ return ((_a3 = recordedFor(purpose.uuid, productUuid)) == null ? void 0 : _a3.selected) === true;
3311
3614
  }
3312
3615
  );
3313
3616
  setIsFullyConsentedReviewMode(allPurposesSelected);
3314
3617
  if (allPurposesSelected) {
3315
3618
  const initialSelections = {};
3316
- activeConfig.purposes.forEach(
3317
- (purpose) => {
3318
- purpose.data_elements.forEach((element) => {
3319
- var _a3, _b2;
3320
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3321
- if ((_b2 = (_a3 = purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3322
- initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3323
- } else {
3324
- initialSelections[combinedId] = false;
3325
- }
3326
- });
3327
- }
3328
- );
3619
+ seedRows.forEach(({ purpose, productUuid }) => {
3620
+ const rowKey = productPurposeKey(productUuid, purpose.uuid);
3621
+ purpose.data_elements.forEach((element) => {
3622
+ var _a3, _b2;
3623
+ const combinedId = `${rowKey}-${element.uuid}`;
3624
+ if ((_b2 = (_a3 = purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3625
+ initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3626
+ } else {
3627
+ initialSelections[combinedId] = false;
3628
+ }
3629
+ });
3630
+ });
3329
3631
  setInitialDataElementSelections(initialSelections);
3330
3632
  } else {
3331
3633
  setIsFullyConsentedReviewMode(false);
@@ -3375,10 +3677,11 @@ var RedactoNoticeConsent = ({
3375
3677
  // Removed onError and isRefreshingToken from dependencies to prevent duplicate API calls
3376
3678
  // onError is only called on errors, not needed for initial data fetching
3377
3679
  ]);
3378
- const togglePurposeCollapse = (purposeUuid) => {
3680
+ const togglePurposeCollapse = (purposeUuid, productUuid) => {
3681
+ const key = productPurposeKey(productUuid, purposeUuid);
3379
3682
  setCollapsedPurposes((prev) => ({
3380
3683
  ...prev,
3381
- [purposeUuid]: !prev[purposeUuid]
3684
+ [key]: !prev[key]
3382
3685
  }));
3383
3686
  };
3384
3687
  const toggleLanguageDropdown = () => {
@@ -3388,28 +3691,70 @@ var RedactoNoticeConsent = ({
3388
3691
  setSelectedLanguage(lang);
3389
3692
  setIsLanguageDropdownOpen(false);
3390
3693
  };
3391
- const handlePurposeCheckboxChange = (purposeUuid) => {
3694
+ const handlePurposeCheckboxChange = (purposeUuid, productUuid) => {
3392
3695
  if (content) {
3393
3696
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
3394
3697
  if (purpose) {
3395
- const newPurposeState = !selectedPurposes[purposeUuid];
3698
+ const key = productPurposeKey(productUuid, purposeUuid);
3699
+ const newPurposeState = !selectedPurposes[key];
3396
3700
  setSelectedPurposes((prev) => ({
3397
3701
  ...prev,
3398
- [purposeUuid]: newPurposeState
3702
+ [key]: newPurposeState
3399
3703
  }));
3400
3704
  setSelectedDataElements((prev) => {
3401
3705
  const updated = { ...prev };
3402
3706
  purpose.data_elements.forEach((el) => {
3403
- updated[`${purposeUuid}-${el.uuid}`] = newPurposeState;
3707
+ updated[`${key}-${el.uuid}`] = newPurposeState;
3404
3708
  });
3405
3709
  return updated;
3406
3710
  });
3407
3711
  }
3408
3712
  }
3409
3713
  };
3410
- const handleDataElementCheckboxChange = (elementUuid, purposeUuid) => {
3714
+ const toggleProductCollapse = (productUuid) => {
3715
+ setCollapsedProducts((prev) => ({
3716
+ ...prev,
3717
+ [productUuid]: !prev[productUuid]
3718
+ }));
3719
+ };
3720
+ const handleProductCheckboxChange = (productUuid) => {
3721
+ if (!content) {
3722
+ return;
3723
+ }
3724
+ const group = productGroups.find(
3725
+ (candidate) => candidate.product.uuid === productUuid
3726
+ );
3727
+ if (!group) {
3728
+ return;
3729
+ }
3730
+ const nextState = !group.purposes.every(
3731
+ (purpose) => selectedPurposes[productPurposeKey(productUuid, purpose.uuid)]
3732
+ );
3733
+ setSelectedPurposes((prev) => {
3734
+ const updated = { ...prev };
3735
+ group.purposes.forEach((purpose) => {
3736
+ updated[productPurposeKey(productUuid, purpose.uuid)] = nextState;
3737
+ });
3738
+ return updated;
3739
+ });
3740
+ setSelectedDataElements((prev) => {
3741
+ const updated = { ...prev };
3742
+ group.purposes.forEach((purpose) => {
3743
+ const key = productPurposeKey(productUuid, purpose.uuid);
3744
+ purpose.data_elements.forEach((element) => {
3745
+ updated[`${key}-${element.uuid}`] = nextState;
3746
+ });
3747
+ });
3748
+ return updated;
3749
+ });
3750
+ if (!nextState) {
3751
+ setCollapsedProducts((prev) => ({ ...prev, [productUuid]: false }));
3752
+ }
3753
+ };
3754
+ const handleDataElementCheckboxChange = (elementUuid, purposeUuid, productUuid) => {
3411
3755
  if (!content) return;
3412
- const combinedId = `${purposeUuid}-${elementUuid}`;
3756
+ const key = productPurposeKey(productUuid, purposeUuid);
3757
+ const combinedId = `${key}-${elementUuid}`;
3413
3758
  setSelectedDataElements((prev) => {
3414
3759
  const newState = {
3415
3760
  ...prev,
@@ -3423,16 +3768,16 @@ var RedactoNoticeConsent = ({
3423
3768
  let shouldCheckPurpose;
3424
3769
  if (requiredElements.length > 0) {
3425
3770
  shouldCheckPurpose = requiredElements.every(
3426
- (el) => newState[`${purposeUuid}-${el.uuid}`]
3771
+ (el) => newState[`${key}-${el.uuid}`]
3427
3772
  );
3428
3773
  } else {
3429
3774
  shouldCheckPurpose = purpose.data_elements.some(
3430
- (el) => newState[`${purposeUuid}-${el.uuid}`]
3775
+ (el) => newState[`${key}-${el.uuid}`]
3431
3776
  );
3432
3777
  }
3433
3778
  setSelectedPurposes((prevPurposes) => ({
3434
3779
  ...prevPurposes,
3435
- [purposeUuid]: shouldCheckPurpose
3780
+ [key]: shouldCheckPurpose
3436
3781
  }));
3437
3782
  }
3438
3783
  return newState;
@@ -3442,14 +3787,16 @@ var RedactoNoticeConsent = ({
3442
3787
  const submitController = new AbortController();
3443
3788
  try {
3444
3789
  if (isMinorFlow && !verificationReference) {
3445
- setErrorMessage("Guardian verification is required before consent can be submitted.");
3790
+ setErrorMessage(
3791
+ "Guardian verification is required before consent can be submitted."
3792
+ );
3446
3793
  return;
3447
3794
  }
3448
3795
  setIsSubmittingAccept(true);
3449
3796
  setIsSubmitting(true);
3450
3797
  setErrorMessage(null);
3451
3798
  if (content) {
3452
- const selection = buildSelectionForMode(content.purposes, mode, {
3799
+ const selection = buildSelectionForMode(purposeRows, mode, {
3453
3800
  selectedPurposes,
3454
3801
  selectedDataElements
3455
3802
  });
@@ -3462,14 +3809,20 @@ var RedactoNoticeConsent = ({
3462
3809
  baseUrl,
3463
3810
  ledgerBaseUrl,
3464
3811
  noticeUuid: content.notice_uuid,
3465
- purposes: content.purposes.map((purpose) => {
3812
+ // One entry per (product, purpose) once a notice covers several. The
3813
+ // ledger keys consent on the pair, and repeating a purpose under two
3814
+ // products is how one submission accepts it for one and declines it
3815
+ // for the other. Collapses to content.purposes on a single-product
3816
+ // notice, so that payload is byte-identical to before.
3817
+ purposes: purposeRows.map(({ purpose, productUuid }) => {
3818
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
3466
3819
  const isAlreadyConsented = (categorizedPurposes == null ? void 0 : categorizedPurposes.alreadyConsented.some(
3467
3820
  (p) => p.uuid === purpose.uuid
3468
3821
  )) || false;
3469
- let purposeSelected = selection.selectedPurposes[purpose.uuid] || false;
3822
+ let purposeSelected = selection.selectedPurposes[stateKey] || false;
3470
3823
  if (mode === "selected" && isAlreadyConsented) {
3471
3824
  const wasModified = purpose.data_elements.some((element) => {
3472
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3825
+ const combinedId = `${stateKey}-${element.uuid}`;
3473
3826
  const currentSelection = selection.selectedDataElements[combinedId] || false;
3474
3827
  const initialSelection = initialDataElementSelections[combinedId] || false;
3475
3828
  return currentSelection !== initialSelection;
@@ -3480,10 +3833,11 @@ var RedactoNoticeConsent = ({
3480
3833
  }
3481
3834
  return {
3482
3835
  ...purpose,
3836
+ ...productUuid ? { product_uuid: productUuid } : {},
3483
3837
  selected: purposeSelected,
3484
3838
  data_elements: purpose.data_elements.map((element) => ({
3485
3839
  ...element,
3486
- selected: element.required ? true : selection.selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
3840
+ selected: element.required ? true : selection.selectedDataElements[`${stateKey}-${element.uuid}`] || false
3487
3841
  }))
3488
3842
  };
3489
3843
  }),
@@ -3586,7 +3940,9 @@ var RedactoNoticeConsent = ({
3586
3940
  return;
3587
3941
  }
3588
3942
  if (!response.session_token || !response.digilocker_redirect_url) {
3589
- throw new Error("Invalid response from server: missing required fields");
3943
+ throw new Error(
3944
+ "Invalid response from server: missing required fields"
3945
+ );
3590
3946
  }
3591
3947
  setShowGuardianForm(false);
3592
3948
  setShowVerificationScreen(true);
@@ -3676,7 +4032,18 @@ var RedactoNoticeConsent = ({
3676
4032
  } finally {
3677
4033
  setIsSubmittingGuardian(false);
3678
4034
  }
3679
- }, [accessToken, sandboxParams, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
4035
+ }, [
4036
+ accessToken,
4037
+ sandboxParams,
4038
+ baseUrl,
4039
+ guardianFormData,
4040
+ digilockerMode,
4041
+ digilockerCallbackUrl,
4042
+ selectedPurposes,
4043
+ selectedDataElements,
4044
+ noticeId,
4045
+ onError
4046
+ ]);
3680
4047
  const handleAgeVerificationYes = useCallback(() => {
3681
4048
  setShowAgeVerification(false);
3682
4049
  setIsMinorFlow(false);
@@ -3727,20 +4094,21 @@ var RedactoNoticeConsent = ({
3727
4094
  } catch {
3728
4095
  }
3729
4096
  }
3730
- setVerificationError(
3731
- "Verification window was closed. Please try again."
3732
- );
4097
+ setVerificationError("Verification window was closed. Please try again.");
3733
4098
  setIsPollingStatus(false);
3734
4099
  isPollingStatusRef.current = false;
3735
4100
  setIsInitiatingVerification(false);
3736
4101
  }, [accessToken, sandboxParams, baseUrl]);
3737
- const startPopupMonitoring = useCallback((popup) => {
3738
- popupCheckIntervalRef.current = window.setInterval(() => {
3739
- if (popup.closed) {
3740
- handlePopupClosed();
3741
- }
3742
- }, 500);
3743
- }, [handlePopupClosed]);
4102
+ const startPopupMonitoring = useCallback(
4103
+ (popup) => {
4104
+ popupCheckIntervalRef.current = window.setInterval(() => {
4105
+ if (popup.closed) {
4106
+ handlePopupClosed();
4107
+ }
4108
+ }, 500);
4109
+ },
4110
+ [handlePopupClosed]
4111
+ );
3744
4112
  const getVerificationErrorMessage = useCallback(
3745
4113
  (errorCode, fallbackError) => {
3746
4114
  switch (errorCode) {
@@ -3842,7 +4210,10 @@ var RedactoNoticeConsent = ({
3842
4210
  return;
3843
4211
  }
3844
4212
  if (!paused) {
3845
- statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
4213
+ statusPollIntervalRef.current = window.setTimeout(
4214
+ poll,
4215
+ POLL_INTERVAL
4216
+ );
3846
4217
  }
3847
4218
  } catch (error) {
3848
4219
  const err = error;
@@ -3863,7 +4234,10 @@ var RedactoNoticeConsent = ({
3863
4234
  );
3864
4235
  return;
3865
4236
  }
3866
- statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
4237
+ statusPollIntervalRef.current = window.setTimeout(
4238
+ poll,
4239
+ POLL_INTERVAL
4240
+ );
3867
4241
  }
3868
4242
  };
3869
4243
  const onVisibilityChange = () => {
@@ -4164,12 +4538,19 @@ var RedactoNoticeConsent = ({
4164
4538
  const dataElementsAudioCount = ((_a2 = purpose.data_elements) == null ? void 0 : _a2.length) || 0;
4165
4539
  const totalPurposeAudioCount = purposeAudioCount + dataElementsAudioCount;
4166
4540
  if (currentIndex >= audioOffset && currentIndex < audioOffset + totalPurposeAudioCount) {
4167
- if (collapsedPurposes[purpose.uuid]) {
4168
- setCollapsedPurposes((prev) => ({
4169
- ...prev,
4170
- [purpose.uuid]: false
4171
- }));
4172
- }
4541
+ setCollapsedPurposes((prev) => {
4542
+ const next = { ...prev };
4543
+ let changed = false;
4544
+ for (const row of purposeRows) {
4545
+ if (row.purpose.uuid !== purpose.uuid) continue;
4546
+ const rowKey = productPurposeKey(row.productUuid, purpose.uuid);
4547
+ if (next[rowKey]) {
4548
+ next[rowKey] = false;
4549
+ changed = true;
4550
+ }
4551
+ }
4552
+ return changed ? next : prev;
4553
+ });
4173
4554
  break;
4174
4555
  }
4175
4556
  audioOffset += totalPurposeAudioCount;
@@ -4247,7 +4628,15 @@ var RedactoNoticeConsent = ({
4247
4628
  return () => {
4248
4629
  ttsAbortController.abort();
4249
4630
  };
4250
- }, [content, selectedLanguage, accessToken, sandboxParams, baseUrl, ledgerBaseUrl, getLanguageCode]);
4631
+ }, [
4632
+ content,
4633
+ selectedLanguage,
4634
+ accessToken,
4635
+ sandboxParams,
4636
+ baseUrl,
4637
+ ledgerBaseUrl,
4638
+ getLanguageCode
4639
+ ]);
4251
4640
  const modalStyle = {
4252
4641
  borderRadius: (settings == null ? void 0 : settings.borderRadius) || "8px",
4253
4642
  backgroundColor: (settings == null ? void 0 : settings.backgroundColor) || "#ffffff",
@@ -4946,7 +5335,7 @@ var RedactoNoticeConsent = ({
4946
5335
  },
4947
5336
  `purpose-section-heading-${selectedLanguage}`
4948
5337
  ),
4949
- categorizedPurposes ? /* @__PURE__ */ jsxs2("div", { style: styles.optionsContainer, children: [
5338
+ categorizedPurposes && !isMultiProduct(content == null ? void 0 : content.products) ? /* @__PURE__ */ jsxs2("div", { style: styles.optionsContainer, children: [
4950
5339
  categorizedPurposes.alreadyConsented.map((purpose) => /* @__PURE__ */ jsx3(
4951
5340
  PurposeItem,
4952
5341
  {
@@ -4988,7 +5377,62 @@ var RedactoNoticeConsent = ({
4988
5377
  ))
4989
5378
  ] }) : (
4990
5379
  /* Regular consent UI */
4991
- /* @__PURE__ */ jsx3("div", { style: styles.optionsContainer, children: (_y = content == null ? void 0 : content.purposes) == null ? void 0 : _y.map((purpose) => /* @__PURE__ */ jsx3(
5380
+ /* @__PURE__ */ jsx3("div", { style: styles.optionsContainer, children: isMultiProduct(content == null ? void 0 : content.products) ? productGroups.map((group) => {
5381
+ const selectedInGroup = group.purposes.filter(
5382
+ (purpose) => selectedPurposes[productPurposeKey(
5383
+ group.product.uuid,
5384
+ purpose.uuid
5385
+ )]
5386
+ ).length;
5387
+ return /* @__PURE__ */ jsxs2("div", { children: [
5388
+ /* @__PURE__ */ jsx3(
5389
+ ProductSectionHeader,
5390
+ {
5391
+ product: group.product,
5392
+ purposeCount: group.purposes.length,
5393
+ allSelected: selectedInGroup === group.purposes.length,
5394
+ someSelected: selectedInGroup > 0,
5395
+ collapsed: collapsedProducts[group.product.uuid] || false,
5396
+ locked: isFullyConsentedReviewMode,
5397
+ settings,
5398
+ styles,
5399
+ responsiveStyles,
5400
+ onToggleCollapse: toggleProductCollapse,
5401
+ onToggleSelect: handleProductCheckboxChange,
5402
+ getTranslatedText
5403
+ }
5404
+ ),
5405
+ /* @__PURE__ */ jsx3(
5406
+ "div",
5407
+ {
5408
+ id: `product-${group.product.uuid}`,
5409
+ style: styles.sectionBody,
5410
+ hidden: collapsedProducts[group.product.uuid] || false,
5411
+ children: group.purposes.map((purpose) => /* @__PURE__ */ jsx3(
5412
+ PurposeItem,
5413
+ {
5414
+ purpose,
5415
+ productUuid: group.product.uuid,
5416
+ selectedPurposes,
5417
+ collapsedPurposes,
5418
+ selectedDataElements,
5419
+ settings,
5420
+ styles,
5421
+ responsiveStyles,
5422
+ onPurposeToggle: handlePurposeCheckboxChange,
5423
+ onPurposeCollapse: togglePurposeCollapse,
5424
+ onDataElementToggle: handleDataElementCheckboxChange,
5425
+ getTranslatedText,
5426
+ isAlreadyConsented: isFullyConsentedReviewMode,
5427
+ selectedLanguage,
5428
+ initialDataElementSelections
5429
+ },
5430
+ `${group.product.uuid}-${purpose.uuid}-${selectedLanguage}`
5431
+ ))
5432
+ }
5433
+ )
5434
+ ] }, group.product.uuid);
5435
+ }) : (_y = content == null ? void 0 : content.purposes) == null ? void 0 : _y.map((purpose) => /* @__PURE__ */ jsx3(
4992
5436
  PurposeItem,
4993
5437
  {
4994
5438
  purpose,
@@ -5253,16 +5697,20 @@ var submitConsentEvent2 = async ({
5253
5697
  declined,
5254
5698
  meta_data
5255
5699
  };
5256
- const response = await fetch(
5257
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
5258
- {
5700
+ const url = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`;
5701
+ const serialisedBody = JSON.stringify(payload);
5702
+ const response = await withIdempotencyKey(
5703
+ url,
5704
+ serialisedBody,
5705
+ (idempotencyHeaders) => fetch(url, {
5259
5706
  method: "POST",
5260
5707
  headers: {
5261
5708
  Authorization: `Bearer ${accessToken}`,
5262
- "Content-Type": "application/json"
5709
+ "Content-Type": "application/json",
5710
+ ...idempotencyHeaders
5263
5711
  },
5264
- body: JSON.stringify(payload)
5265
- }
5712
+ body: serialisedBody
5713
+ })
5266
5714
  );
5267
5715
  if (!response.ok) {
5268
5716
  throw await parseApiError(
@@ -5699,7 +6147,6 @@ var RedactoConsentInlineComponent = ({
5699
6147
  settings,
5700
6148
  applicationId
5701
6149
  }) => {
5702
- var _a;
5703
6150
  const [content, setContent] = useState3(null);
5704
6151
  const [isLoading, setIsLoading] = useState3(true);
5705
6152
  const [isSubmitting, setIsSubmitting] = useState3(false);
@@ -5756,7 +6203,7 @@ var RedactoConsentInlineComponent = ({
5756
6203
  }
5757
6204
  }, [hasAlreadyConsented, onAccept]);
5758
6205
  const getTranslatedText = (key, defaultText, itemId) => {
5759
- var _a2, _b;
6206
+ var _a, _b, _c;
5760
6207
  if (!content) return defaultText;
5761
6208
  if (selectedLanguage === "English" || selectedLanguage === "en" || selectedLanguage === "EN") {
5762
6209
  if (key === "privacy_policy_anchor_text" && content.privacy_policy_anchor_text) {
@@ -5773,7 +6220,7 @@ var RedactoConsentInlineComponent = ({
5773
6220
  }
5774
6221
  if (itemId) {
5775
6222
  if (key === "purposes.name" || key === "purposes.description") {
5776
- const purposeData = (_a2 = translationMap.purposes) == null ? void 0 : _a2[itemId];
6223
+ const purposeData = (_a = translationMap.purposes) == null ? void 0 : _a[itemId];
5777
6224
  if (purposeData) {
5778
6225
  if (typeof purposeData === "string") {
5779
6226
  return key === "purposes.name" ? purposeData : defaultText;
@@ -5782,8 +6229,18 @@ var RedactoConsentInlineComponent = ({
5782
6229
  }
5783
6230
  return defaultText;
5784
6231
  }
6232
+ if (key === "products.name" || key === "products.description") {
6233
+ const productData = (_b = translationMap.products) == null ? void 0 : _b[itemId];
6234
+ if (productData) {
6235
+ if (typeof productData === "string") {
6236
+ return key === "products.name" ? productData : defaultText;
6237
+ }
6238
+ return key === "products.name" ? productData.name || defaultText : productData.description || defaultText;
6239
+ }
6240
+ return defaultText;
6241
+ }
5785
6242
  if (key.startsWith("data_elements.")) {
5786
- return ((_b = translationMap.data_elements) == null ? void 0 : _b[itemId]) || defaultText;
6243
+ return ((_c = translationMap.data_elements) == null ? void 0 : _c[itemId]) || defaultText;
5787
6244
  }
5788
6245
  }
5789
6246
  const value = translationMap[key];
@@ -5892,25 +6349,27 @@ var RedactoConsentInlineComponent = ({
5892
6349
  controller.abort();
5893
6350
  };
5894
6351
  }, [ledgerBaseUrl, applicationId, accessToken, content, notice_uuid]);
5895
- const togglePurposeCollapse = (purposeUuid) => {
6352
+ const togglePurposeCollapse = (purposeUuid, productUuid) => {
6353
+ const key = productPurposeKey(productUuid, purposeUuid);
5896
6354
  setCollapsedPurposes((prev) => ({
5897
6355
  ...prev,
5898
- [purposeUuid]: !prev[purposeUuid]
6356
+ [key]: !prev[key]
5899
6357
  }));
5900
6358
  };
5901
- const handlePurposeCheckboxChange = (purposeUuid) => {
6359
+ const handlePurposeCheckboxChange = (purposeUuid, productUuid) => {
5902
6360
  if (content) {
5903
6361
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
5904
6362
  if (purpose) {
5905
- const newPurposeState = !selectedPurposes[purposeUuid];
6363
+ const key = productPurposeKey(productUuid, purposeUuid);
6364
+ const newPurposeState = !selectedPurposes[key];
5906
6365
  setSelectedPurposes((prev) => ({
5907
6366
  ...prev,
5908
- [purposeUuid]: newPurposeState
6367
+ [key]: newPurposeState
5909
6368
  }));
5910
6369
  setSelectedDataElements((prev) => {
5911
6370
  const updated = { ...prev };
5912
6371
  purpose.data_elements.forEach((el) => {
5913
- const combinedId = `${purposeUuid}-${el.uuid}`;
6372
+ const combinedId = `${key}-${el.uuid}`;
5914
6373
  updated[combinedId] = newPurposeState;
5915
6374
  });
5916
6375
  return updated;
@@ -5918,7 +6377,7 @@ var RedactoConsentInlineComponent = ({
5918
6377
  }
5919
6378
  }
5920
6379
  };
5921
- const handleDataElementCheckboxChange = (elementUuid, purposeUuid) => {
6380
+ const handleDataElementCheckboxChange = (elementUuid, purposeUuid, productUuid) => {
5922
6381
  if (!content) return;
5923
6382
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
5924
6383
  if (!purpose) return;
@@ -5926,7 +6385,8 @@ var RedactoConsentInlineComponent = ({
5926
6385
  (el) => el.uuid === elementUuid
5927
6386
  );
5928
6387
  if (!dataElement) return;
5929
- const combinedId = `${purposeUuid}-${elementUuid}`;
6388
+ const key = productPurposeKey(productUuid, purposeUuid);
6389
+ const combinedId = `${key}-${elementUuid}`;
5930
6390
  const currentState = selectedDataElements[combinedId] || false;
5931
6391
  const newElementState = !currentState;
5932
6392
  setSelectedDataElements((prev) => {
@@ -5940,16 +6400,16 @@ var RedactoConsentInlineComponent = ({
5940
6400
  let shouldCheckPurpose;
5941
6401
  if (requiredElements.length > 0) {
5942
6402
  shouldCheckPurpose = requiredElements.every(
5943
- (el) => newState[`${purposeUuid}-${el.uuid}`]
6403
+ (el) => newState[`${key}-${el.uuid}`]
5944
6404
  );
5945
6405
  } else {
5946
6406
  shouldCheckPurpose = purpose.data_elements.some(
5947
- (el) => newState[`${purposeUuid}-${el.uuid}`]
6407
+ (el) => newState[`${key}-${el.uuid}`]
5948
6408
  );
5949
6409
  }
5950
6410
  setSelectedPurposes((prevPurposes) => ({
5951
6411
  ...prevPurposes,
5952
- [purposeUuid]: shouldCheckPurpose
6412
+ [key]: shouldCheckPurpose
5953
6413
  }));
5954
6414
  return newState;
5955
6415
  });
@@ -5970,20 +6430,36 @@ var RedactoConsentInlineComponent = ({
5970
6430
  baseUrl,
5971
6431
  ledgerBaseUrl,
5972
6432
  noticeUuid: content.notice_uuid,
5973
- purposes: content.purposes.map((purpose) => {
6433
+ // One entry per (product, purpose) once a notice covers several, so a
6434
+ // purpose can be accepted for one product and declined for another.
6435
+ // Collapses to content.purposes below two products, leaving that
6436
+ // payload byte-identical to before.
6437
+ purposes: (isMultiProduct(content.products) ? expandPurposesByProduct(
6438
+ content.products,
6439
+ content.purposes,
6440
+ content.product_purpose_order
6441
+ ).map((row) => ({
6442
+ purpose: row.purpose,
6443
+ productUuid: row.product.uuid
6444
+ })) : content.purposes.map((purpose) => ({
6445
+ purpose,
6446
+ productUuid: void 0
6447
+ }))).map(({ purpose, productUuid }) => {
6448
+ const key = productPurposeKey(productUuid, purpose.uuid);
5974
6449
  const hasSelectedRequiredElement = purpose.data_elements.some(
5975
- (element) => element.required && (selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false)
6450
+ (element) => element.required && (selectedDataElements[`${key}-${element.uuid}`] || false)
5976
6451
  );
5977
6452
  const hasSelectedDataElement = purpose.data_elements.some(
5978
- (element) => selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
6453
+ (element) => selectedDataElements[`${key}-${element.uuid}`] || false
5979
6454
  );
5980
- const purposeSelected = selectedPurposes[purpose.uuid] || hasSelectedRequiredElement || hasSelectedDataElement;
6455
+ const purposeSelected = selectedPurposes[key] || hasSelectedRequiredElement || hasSelectedDataElement;
5981
6456
  return {
5982
6457
  ...purpose,
6458
+ ...productUuid ? { product_uuid: productUuid } : {},
5983
6459
  selected: purposeSelected,
5984
6460
  data_elements: purpose.data_elements.map((element) => ({
5985
6461
  ...element,
5986
- selected: selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
6462
+ selected: selectedDataElements[`${key}-${element.uuid}`] || false
5987
6463
  }))
5988
6464
  };
5989
6465
  }),
@@ -6084,133 +6560,99 @@ var RedactoConsentInlineComponent = ({
6084
6560
  children: errorMessage
6085
6561
  }
6086
6562
  ),
6087
- (_a = content == null ? void 0 : content.purposes) == null ? void 0 : _a.map((purpose) => /* @__PURE__ */ jsxs3("div", { children: [
6088
- /* @__PURE__ */ jsxs3("div", { style: styles2.optionItem, children: [
6089
- /* @__PURE__ */ jsxs3(
6090
- "div",
6091
- {
6092
- style: styles2.optionLeft,
6093
- onClick: () => togglePurposeCollapse(purpose.uuid),
6094
- role: "button",
6095
- tabIndex: 0,
6096
- "aria-expanded": !collapsedPurposes[purpose.uuid],
6097
- "aria-controls": `purpose-${purpose.uuid}`,
6098
- "aria-label": `${collapsedPurposes[purpose.uuid] ? "Expand" : "Collapse"} ${getTranslatedText(
6099
- `purposes.name`,
6100
- purpose.name,
6101
- purpose.uuid
6102
- )} details`,
6103
- onKeyDown: (e) => {
6104
- if (e.key === "Enter" || e.key === " ") {
6105
- e.preventDefault();
6106
- togglePurposeCollapse(purpose.uuid);
6107
- }
6108
- },
6109
- children: [
6110
- /* @__PURE__ */ jsx4(
6111
- "div",
6112
- {
6113
- style: {
6114
- display: "flex",
6115
- alignItems: "center",
6116
- justifyContent: "center",
6117
- marginLeft: "5px"
6118
- },
6119
- children: /* @__PURE__ */ jsx4(
6120
- "svg",
6121
- {
6122
- width: "7",
6123
- height: "12",
6124
- viewBox: "0 0 7 12",
6125
- fill: "none",
6126
- xmlns: "http://www.w3.org/2000/svg",
6127
- stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
6128
- strokeWidth: "2",
6129
- strokeLinecap: "round",
6130
- strokeLinejoin: "round",
6131
- style: {
6132
- transform: !collapsedPurposes[purpose.uuid] ? "rotate(90deg)" : "rotate(0deg)",
6133
- transition: "transform 0.3s ease"
6134
- },
6135
- "aria-hidden": "true",
6136
- children: /* @__PURE__ */ jsx4("path", { d: "M1 1L6 6L1 11" })
6137
- }
6138
- )
6139
- }
6140
- ),
6141
- /* @__PURE__ */ jsxs3("div", { style: styles2.optionTextContainer, children: [
6142
- /* @__PURE__ */ jsxs3(
6143
- "h2",
6144
- {
6145
- style: {
6146
- ...styles2.optionTitle,
6147
- ...responsiveStyles.optionTitle,
6148
- ...headingStyle
6149
- },
6150
- children: [
6151
- getTranslatedText(
6152
- `purposes.name`,
6153
- purpose.name,
6154
- purpose.uuid
6155
- ),
6156
- hasRequiredDataElement(purpose) && /* @__PURE__ */ jsx4(RequiredMarker, {})
6157
- ]
6158
- }
6159
- ),
6160
- /* @__PURE__ */ jsx4(
6161
- "h3",
6162
- {
6163
- style: {
6164
- ...styles2.optionDescription,
6165
- ...responsiveStyles.optionDescription,
6166
- ...textStyle
6167
- },
6168
- children: getTranslatedText(
6169
- `purposes.description`,
6170
- purpose.description,
6171
- purpose.uuid
6172
- )
6173
- }
6174
- )
6175
- ] })
6176
- ]
6177
- }
6178
- ),
6179
- /* @__PURE__ */ jsx4(
6180
- "input",
6181
- {
6182
- type: "checkbox",
6183
- checked: selectedPurposes[purpose.uuid] || false,
6184
- onChange: () => handlePurposeCheckboxChange(purpose.uuid),
6185
- "aria-label": `Select all data elements for ${getTranslatedText(
6186
- `purposes.name`,
6187
- purpose.name,
6188
- purpose.uuid
6189
- )}`
6190
- }
6191
- )
6192
- ] }),
6193
- !collapsedPurposes[purpose.uuid] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ jsx4(
6194
- "div",
6195
- {
6196
- id: `purpose-${purpose.uuid}`,
6197
- style: styles2.dataElementsContainer,
6198
- children: purpose.data_elements.map((dataElement) => /* @__PURE__ */ jsxs3("div", { style: styles2.dataElementItem, children: [
6563
+ (() => {
6564
+ var _a, _b;
6565
+ const renderPurpose = (purpose, productUuid) => {
6566
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
6567
+ return /* @__PURE__ */ jsxs3("div", { children: [
6568
+ /* @__PURE__ */ jsxs3("div", { style: styles2.optionItem, children: [
6199
6569
  /* @__PURE__ */ jsxs3(
6200
- "span",
6570
+ "div",
6201
6571
  {
6202
- style: {
6203
- ...styles2.dataElementText,
6204
- ...responsiveStyles.dataElementText,
6205
- ...textStyle
6572
+ style: styles2.optionLeft,
6573
+ onClick: () => togglePurposeCollapse(purpose.uuid, productUuid),
6574
+ role: "button",
6575
+ tabIndex: 0,
6576
+ "aria-expanded": !collapsedPurposes[stateKey],
6577
+ "aria-controls": `purpose-${stateKey}`,
6578
+ "aria-label": `${collapsedPurposes[stateKey] ? "Expand" : "Collapse"} ${getTranslatedText(
6579
+ `purposes.name`,
6580
+ purpose.name,
6581
+ purpose.uuid
6582
+ )} details`,
6583
+ onKeyDown: (e) => {
6584
+ if (e.key === "Enter" || e.key === " ") {
6585
+ e.preventDefault();
6586
+ togglePurposeCollapse(purpose.uuid, productUuid);
6587
+ }
6206
6588
  },
6207
6589
  children: [
6208
- getTranslatedText(
6209
- `data_elements.name`,
6210
- dataElement.name,
6211
- dataElement.uuid
6590
+ /* @__PURE__ */ jsx4(
6591
+ "div",
6592
+ {
6593
+ style: {
6594
+ display: "flex",
6595
+ alignItems: "center",
6596
+ justifyContent: "center",
6597
+ marginLeft: "5px"
6598
+ },
6599
+ children: /* @__PURE__ */ jsx4(
6600
+ "svg",
6601
+ {
6602
+ width: "7",
6603
+ height: "12",
6604
+ viewBox: "0 0 7 12",
6605
+ fill: "none",
6606
+ xmlns: "http://www.w3.org/2000/svg",
6607
+ stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
6608
+ strokeWidth: "2",
6609
+ strokeLinecap: "round",
6610
+ strokeLinejoin: "round",
6611
+ style: {
6612
+ transform: !collapsedPurposes[stateKey] ? "rotate(90deg)" : "rotate(0deg)",
6613
+ transition: "transform 0.3s ease"
6614
+ },
6615
+ "aria-hidden": "true",
6616
+ children: /* @__PURE__ */ jsx4("path", { d: "M1 1L6 6L1 11" })
6617
+ }
6618
+ )
6619
+ }
6212
6620
  ),
6213
- dataElement.required && /* @__PURE__ */ jsx4(RequiredMarker, {})
6621
+ /* @__PURE__ */ jsxs3("div", { style: styles2.optionTextContainer, children: [
6622
+ /* @__PURE__ */ jsxs3(
6623
+ "h2",
6624
+ {
6625
+ style: {
6626
+ ...styles2.optionTitle,
6627
+ ...responsiveStyles.optionTitle,
6628
+ ...headingStyle
6629
+ },
6630
+ children: [
6631
+ getTranslatedText(
6632
+ `purposes.name`,
6633
+ purpose.name,
6634
+ purpose.uuid
6635
+ ),
6636
+ hasRequiredDataElement(purpose) && /* @__PURE__ */ jsx4(RequiredMarker, {})
6637
+ ]
6638
+ }
6639
+ ),
6640
+ /* @__PURE__ */ jsx4(
6641
+ "h3",
6642
+ {
6643
+ style: {
6644
+ ...styles2.optionDescription,
6645
+ ...responsiveStyles.optionDescription,
6646
+ ...textStyle
6647
+ },
6648
+ children: getTranslatedText(
6649
+ `purposes.description`,
6650
+ purpose.description,
6651
+ purpose.uuid
6652
+ )
6653
+ }
6654
+ )
6655
+ ] })
6214
6656
  ]
6215
6657
  }
6216
6658
  ),
@@ -6218,22 +6660,87 @@ var RedactoConsentInlineComponent = ({
6218
6660
  "input",
6219
6661
  {
6220
6662
  type: "checkbox",
6221
- checked: selectedDataElements[`${purpose.uuid}-${dataElement.uuid}`] || false,
6222
- onChange: () => handleDataElementCheckboxChange(
6223
- dataElement.uuid,
6663
+ checked: selectedPurposes[stateKey] || false,
6664
+ onChange: () => handlePurposeCheckboxChange(purpose.uuid, productUuid),
6665
+ "aria-label": `Select all data elements for ${getTranslatedText(
6666
+ `purposes.name`,
6667
+ purpose.name,
6224
6668
  purpose.uuid
6225
- ),
6226
- "aria-label": `Select ${getTranslatedText(
6227
- `data_elements.name`,
6228
- dataElement.name,
6229
- dataElement.uuid
6230
- )}${dataElement.required ? " (required)" : ""}`
6669
+ )}`
6231
6670
  }
6232
6671
  )
6233
- ] }, dataElement.uuid))
6234
- }
6235
- )
6236
- ] }, purpose.uuid))
6672
+ ] }),
6673
+ !collapsedPurposes[stateKey] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ jsx4(
6674
+ "div",
6675
+ {
6676
+ id: `purpose-${stateKey}`,
6677
+ style: styles2.dataElementsContainer,
6678
+ children: purpose.data_elements.map((dataElement) => /* @__PURE__ */ jsxs3(
6679
+ "div",
6680
+ {
6681
+ style: styles2.dataElementItem,
6682
+ children: [
6683
+ /* @__PURE__ */ jsxs3(
6684
+ "span",
6685
+ {
6686
+ style: {
6687
+ ...styles2.dataElementText,
6688
+ ...responsiveStyles.dataElementText,
6689
+ ...textStyle
6690
+ },
6691
+ children: [
6692
+ getTranslatedText(
6693
+ `data_elements.name`,
6694
+ dataElement.name,
6695
+ dataElement.uuid
6696
+ ),
6697
+ dataElement.required && /* @__PURE__ */ jsx4(RequiredMarker, {})
6698
+ ]
6699
+ }
6700
+ ),
6701
+ /* @__PURE__ */ jsx4(
6702
+ "input",
6703
+ {
6704
+ type: "checkbox",
6705
+ checked: selectedDataElements[`${stateKey}-${dataElement.uuid}`] || false,
6706
+ onChange: () => handleDataElementCheckboxChange(
6707
+ dataElement.uuid,
6708
+ purpose.uuid,
6709
+ productUuid
6710
+ ),
6711
+ "aria-label": `Select ${getTranslatedText(
6712
+ `data_elements.name`,
6713
+ dataElement.name,
6714
+ dataElement.uuid
6715
+ )}${dataElement.required ? " (required)" : ""}`
6716
+ }
6717
+ )
6718
+ ]
6719
+ },
6720
+ dataElement.uuid
6721
+ ))
6722
+ }
6723
+ )
6724
+ ] }, stateKey);
6725
+ };
6726
+ if (!isMultiProduct(content == null ? void 0 : content.products)) {
6727
+ return (_a = content == null ? void 0 : content.purposes) == null ? void 0 : _a.map((purpose) => renderPurpose(purpose));
6728
+ }
6729
+ return groupPurposesByProduct(
6730
+ content == null ? void 0 : content.products,
6731
+ (_b = content == null ? void 0 : content.purposes) != null ? _b : [],
6732
+ content == null ? void 0 : content.product_purpose_order
6733
+ ).map((group) => /* @__PURE__ */ jsxs3("div", { children: [
6734
+ /* @__PURE__ */ jsx4("div", { style: { ...styles2.optionTitle, margin: "0 0 8px" }, children: getTranslatedText(
6735
+ "products.name",
6736
+ group.product.name,
6737
+ group.product.uuid
6738
+ ) }),
6739
+ group.purposes.map(
6740
+ (purpose) => renderPurpose(purpose, group.product.uuid)
6741
+ )
6742
+ ] }, group.product.uuid));
6743
+ })()
6237
6744
  ] });
6238
6745
  };
6239
6746
  RedactoConsentInlineComponent.displayName = "RedactoConsentInline";
@@ -7373,16 +7880,22 @@ var submitAssistedConsent = async ({
7373
7880
  language: language || void 0
7374
7881
  };
7375
7882
  const url = `${publicScope(ledgerBaseUrl || baseUrl, organisationUuid, workspaceUuid)}/submit-consent`;
7376
- const response = await fetch(url, {
7377
- method: "POST",
7378
- headers: {
7379
- accept: "application/json",
7380
- Authorization: `Bearer ${accessToken}`,
7381
- "Content-Type": "application/json"
7382
- },
7383
- body: JSON.stringify(payload),
7384
- signal
7385
- });
7883
+ const serialisedBody = JSON.stringify(payload);
7884
+ const response = await withIdempotencyKey(
7885
+ url,
7886
+ serialisedBody,
7887
+ (idempotencyHeaders) => fetch(url, {
7888
+ method: "POST",
7889
+ headers: {
7890
+ accept: "application/json",
7891
+ Authorization: `Bearer ${accessToken}`,
7892
+ "Content-Type": "application/json",
7893
+ ...idempotencyHeaders
7894
+ },
7895
+ body: serialisedBody,
7896
+ signal
7897
+ })
7898
+ );
7386
7899
  if (!response.ok) {
7387
7900
  throw await parseApiError(
7388
7901
  response,
@@ -9375,10 +9888,14 @@ var RedactoNoticeAssisted = ({
9375
9888
  const handleConfirmStep = useCallback3(
9376
9889
  (mode = "selected") => {
9377
9890
  if (mode !== "selected" && content) {
9378
- const selection = buildSelectionForMode(content.purposes, mode, {
9379
- selectedPurposes,
9380
- selectedDataElements
9381
- });
9891
+ const selection = buildSelectionForMode(
9892
+ noticePurposeRows(void 0, content.purposes),
9893
+ mode,
9894
+ {
9895
+ selectedPurposes,
9896
+ selectedDataElements
9897
+ }
9898
+ );
9382
9899
  setSelectedPurposes(selection.selectedPurposes);
9383
9900
  setSelectedDataElements(selection.selectedDataElements);
9384
9901
  }
@@ -9579,10 +10096,14 @@ var RedactoNoticeAssisted = ({
9579
10096
  return;
9580
10097
  }
9581
10098
  }
9582
- const selection = buildSelectionForMode(content.purposes, acceptMode, {
9583
- selectedPurposes,
9584
- selectedDataElements
9585
- });
10099
+ const selection = buildSelectionForMode(
10100
+ noticePurposeRows(void 0, content.purposes),
10101
+ acceptMode,
10102
+ {
10103
+ selectedPurposes,
10104
+ selectedDataElements
10105
+ }
10106
+ );
9586
10107
  const submitPurposes = content.purposes.map((purpose) => ({
9587
10108
  ...purpose,
9588
10109
  selected: selection.selectedPurposes[purpose.uuid] || false,