@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.js CHANGED
@@ -219,6 +219,64 @@ var sandboxBodyIdentity = (sandbox) => {
219
219
  };
220
220
  var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
221
221
 
222
+ // src/shared/idempotency.ts
223
+ var HEADER = "Idempotency-Key";
224
+ var pending = /* @__PURE__ */ new Map();
225
+ var MAX_PENDING = 16;
226
+ var mintKey = () => {
227
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
228
+ return crypto.randomUUID();
229
+ }
230
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}-${Math.random().toString(36).slice(2, 14)}`;
231
+ };
232
+ var statusOf = (outcome) => {
233
+ if (typeof Response !== "undefined" && outcome instanceof Response) {
234
+ return outcome.status;
235
+ }
236
+ if (outcome && typeof outcome === "object" && "status" in outcome) {
237
+ const status = outcome.status;
238
+ if (typeof status === "number") return status;
239
+ }
240
+ return void 0;
241
+ };
242
+ var settledOnResolve = (result) => {
243
+ const status = statusOf(result);
244
+ return status === void 0 || status < 500;
245
+ };
246
+ var settledOnReject = (error) => {
247
+ const status = statusOf(error);
248
+ return status !== void 0 && status < 500;
249
+ };
250
+ var withIdempotencyKey = async (url, body, send) => {
251
+ const signature = `${url}
252
+ ${body}`;
253
+ let entry = pending.get(signature);
254
+ if (!entry) {
255
+ entry = { key: mintKey(), inFlight: 0, keep: false };
256
+ if (pending.size >= MAX_PENDING) {
257
+ const oldest = pending.keys().next().value;
258
+ if (oldest !== void 0) pending.delete(oldest);
259
+ }
260
+ pending.set(signature, entry);
261
+ } else if (entry.inFlight === 0) {
262
+ entry.keep = false;
263
+ }
264
+ entry.inFlight += 1;
265
+ const settle = (settled) => {
266
+ entry.inFlight -= 1;
267
+ if (!settled) entry.keep = true;
268
+ if (entry.inFlight === 0 && !entry.keep) pending.delete(signature);
269
+ };
270
+ try {
271
+ const result = await send({ [HEADER]: entry.key });
272
+ settle(settledOnResolve(result));
273
+ return result;
274
+ } catch (error) {
275
+ settle(settledOnReject(error));
276
+ throw error;
277
+ }
278
+ };
279
+
222
280
  // src/shared/notice-consent-status.ts
223
281
  var import_jwt_decode = require("jwt-decode");
224
282
  var decodeNoticeConsentToken = (token) => {
@@ -420,6 +478,9 @@ var submitConsentEvent = async ({
420
478
  }));
421
479
  return {
422
480
  purpose_uuid: purpose.uuid,
481
+ // Omitted on a single-product notice, where the ledger applies the
482
+ // selection to every product the purpose covers.
483
+ product_uuid: purpose.product_uuid || void 0,
423
484
  selected: Boolean(purpose.selected),
424
485
  data_elements: dataElements.length > 0 ? dataElements : void 0
425
486
  };
@@ -436,17 +497,21 @@ var submitConsentEvent = async ({
436
497
  self_declared_adult: selfDeclaredAdult
437
498
  };
438
499
  const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
439
- const response = await fetch(
440
- `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
441
- {
500
+ const url = `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`;
501
+ const serialisedBody = JSON.stringify(requestBody);
502
+ const response = await withIdempotencyKey(
503
+ url,
504
+ serialisedBody,
505
+ (idempotencyHeaders) => fetch(url, {
442
506
  method: "POST",
443
507
  headers: {
444
508
  ...buildAuthHeaders(accessToken, sandbox),
445
- "Content-Type": "application/json"
509
+ "Content-Type": "application/json",
510
+ ...idempotencyHeaders
446
511
  },
447
- body: JSON.stringify(requestBody),
512
+ body: serialisedBody,
448
513
  signal
449
- }
514
+ })
450
515
  );
451
516
  if (!response.ok) {
452
517
  throw await parseApiError(
@@ -679,6 +744,9 @@ var DPO_AUDIO_ELEMENT_MAP = [
679
744
  { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
680
745
  { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
681
746
  ];
747
+ var PRODUCT_SECTION_COPY = {
748
+ toggleSelection: (productName, purposeCount) => `Toggle all ${purposeCount} purposes for ${productName}`
749
+ };
682
750
 
683
751
  // src/RedactoNoticeConsent/styles.ts
684
752
  var styles = {
@@ -895,6 +963,37 @@ var styles = {
895
963
  padding: 0,
896
964
  boxSizing: "border-box"
897
965
  },
966
+ // A product section's heading. Its own tokens rather than the row's: sharing
967
+ // optionTitle rendered the group name at the same size, weight and indent as
968
+ // the purposes inside it, leaving nothing to mark it as a heading.
969
+ sectionHeader: {
970
+ display: "flex",
971
+ justifyContent: "space-between",
972
+ alignItems: "center",
973
+ margin: "0 0 8px",
974
+ padding: 0,
975
+ boxSizing: "border-box"
976
+ },
977
+ // The purposes belonging to one product. Indented so the product chevron and
978
+ // the purpose chevrons below it do not sit on the same vertical line, which
979
+ // read as one flat list rather than a heading and its contents. The trailing
980
+ // margin separates a section from the next product heading.
981
+ sectionBody: {
982
+ paddingLeft: "18px",
983
+ margin: "0 0 14px",
984
+ boxSizing: "border-box"
985
+ },
986
+ // No colour token: the header always overrides it with the notice's
987
+ // headingColor, so one here would imply a default that never applies.
988
+ sectionTitle: {
989
+ fontWeight: 600,
990
+ fontSize: "17px",
991
+ lineHeight: "150%",
992
+ letterSpacing: "0.2px",
993
+ margin: 0,
994
+ padding: 0,
995
+ textAlign: "left"
996
+ },
898
997
  optionTextContainer: {
899
998
  display: "flex",
900
999
  flexDirection: "column",
@@ -1039,10 +1138,10 @@ var styles = {
1039
1138
  color: "#ffffff"
1040
1139
  },
1041
1140
  /**
1042
- * The accept-all shortcut. Tinted rather than filled so the primary
1043
- * "accept selected" action stays visually dominant, and outlined-but-coloured
1044
- * so it still reads as an accept next to the neutral decline button.
1045
- */
1141
+ * The accept-all shortcut. Tinted rather than filled so the primary
1142
+ * "accept selected" action stays visually dominant, and outlined-but-coloured
1143
+ * so it still reads as an accept next to the neutral decline button.
1144
+ */
1046
1145
  acceptSecondaryButton: {
1047
1146
  backgroundColor: "#ffffff",
1048
1147
  border: "1px solid #4f87ff",
@@ -1236,18 +1335,76 @@ var NoticeFooter = ({
1236
1335
  ] });
1237
1336
  };
1238
1337
 
1338
+ // src/shared/productMatrix.ts
1339
+ var productPurposeKey = (productUuid, purposeUuid) => productUuid ? `${productUuid}:${purposeUuid}` : purposeUuid;
1340
+ var purposeCoversProduct = (purpose, productUuid) => {
1341
+ const scope = purpose.product_uuids;
1342
+ if (!scope || scope.length === 0) return true;
1343
+ return scope.includes(productUuid);
1344
+ };
1345
+ var isMultiProduct = (products) => Boolean(products && products.length > 1);
1346
+ var groupPurposesByProduct = (products, purposes, order) => {
1347
+ if (!products || products.length === 0) return [];
1348
+ return products.map((product) => {
1349
+ const covered = purposes.filter(
1350
+ (purpose) => purposeCoversProduct(purpose, product.uuid)
1351
+ );
1352
+ const saved = order == null ? void 0 : order[product.uuid];
1353
+ if (!saved || saved.length === 0) return { product, purposes: covered };
1354
+ const byUuid = new Map(covered.map((p) => [p.uuid, p]));
1355
+ const ordered = [];
1356
+ for (const uuid of saved) {
1357
+ const found = byUuid.get(uuid);
1358
+ if (found) ordered.push(found);
1359
+ }
1360
+ const rest = covered.filter((p) => !saved.includes(p.uuid));
1361
+ return { product, purposes: [...ordered, ...rest] };
1362
+ }).filter((group) => group.purposes.length > 0);
1363
+ };
1364
+ var expandPurposesByProduct = (products, purposes, order) => {
1365
+ if (!isMultiProduct(products)) return [];
1366
+ const rows = [];
1367
+ for (const group of groupPurposesByProduct(products, purposes, order)) {
1368
+ for (const purpose of group.purposes) {
1369
+ rows.push({ product: group.product, purpose });
1370
+ }
1371
+ }
1372
+ return rows;
1373
+ };
1374
+ var noticePurposeRows = (products, purposes, order) => {
1375
+ if (!isMultiProduct(products)) {
1376
+ return purposes.map((purpose) => ({ purpose, productUuid: void 0 }));
1377
+ }
1378
+ const rows = expandPurposesByProduct(products, purposes, order).map(
1379
+ (row) => ({
1380
+ purpose: row.purpose,
1381
+ productUuid: row.product.uuid
1382
+ })
1383
+ );
1384
+ const rendered = new Set(rows.map((row) => row.purpose.uuid));
1385
+ const orphaned = purposes.filter((purpose) => !rendered.has(purpose.uuid));
1386
+ if (orphaned.length > 0) {
1387
+ console.warn(
1388
+ "[redacto] purposes cover no product on this notice and will not be shown or submitted:",
1389
+ orphaned.map((purpose) => purpose.uuid)
1390
+ );
1391
+ }
1392
+ return rows;
1393
+ };
1394
+
1239
1395
  // src/RedactoNoticeConsent/utils/consentSelection.ts
1240
- var dataElementKey = (purposeUuid, elementUuid) => `${purposeUuid}-${elementUuid}`;
1241
- var buildSelectionForMode = (purposes, mode, current) => {
1396
+ var dataElementKey = (purposeUuid, elementUuid, productUuid) => `${productPurposeKey(productUuid, purposeUuid)}-${elementUuid}`;
1397
+ var buildSelectionForMode = (rows, mode, current) => {
1242
1398
  if (mode === "selected") {
1243
1399
  return current;
1244
1400
  }
1245
1401
  const selectedPurposes = {};
1246
1402
  const selectedDataElements = {};
1247
- purposes.forEach((purpose) => {
1248
- selectedPurposes[purpose.uuid] = true;
1403
+ rows.forEach(({ purpose, productUuid }) => {
1404
+ const key = productPurposeKey(productUuid, purpose.uuid);
1405
+ selectedPurposes[key] = true;
1249
1406
  purpose.data_elements.forEach((element) => {
1250
- selectedDataElements[dataElementKey(purpose.uuid, element.uuid)] = element.enabled;
1407
+ selectedDataElements[dataElementKey(purpose.uuid, element.uuid, productUuid)] = element.enabled;
1251
1408
  });
1252
1409
  });
1253
1410
  return { selectedPurposes, selectedDataElements };
@@ -1267,10 +1424,11 @@ var buildPreselection = (purposes, mode) => {
1267
1424
  const selectedPurposes = {};
1268
1425
  const selectedDataElements = {};
1269
1426
  if (mode === "ALL") {
1270
- return buildSelectionForMode(purposes, "all", {
1271
- selectedPurposes,
1272
- selectedDataElements
1273
- });
1427
+ return buildSelectionForMode(
1428
+ purposes.map((purpose) => ({ purpose, productUuid: void 0 })),
1429
+ "all",
1430
+ { selectedPurposes, selectedDataElements }
1431
+ );
1274
1432
  }
1275
1433
  purposes.forEach((purpose) => {
1276
1434
  let hasPreselectedElement = false;
@@ -1391,6 +1549,7 @@ var injectCheckboxStyles = () => {
1391
1549
  cursor: pointer !important;
1392
1550
  position: relative !important;
1393
1551
  box-sizing: border-box !important;
1552
+ flex-shrink: 0 !important;
1394
1553
  margin: 0 !important;
1395
1554
  padding: 4px !important;
1396
1555
  }
@@ -1407,6 +1566,7 @@ var injectCheckboxStyles = () => {
1407
1566
  cursor: pointer !important;
1408
1567
  position: relative !important;
1409
1568
  box-sizing: border-box !important;
1569
+ flex-shrink: 0 !important;
1410
1570
  margin: 0 !important;
1411
1571
  padding: 4px !important;
1412
1572
  }
@@ -1735,9 +1895,9 @@ var toFontFaceCss = (entry, sources) => sources.map(
1735
1895
  ).join("\n\n");
1736
1896
  var fontSourceLoads = /* @__PURE__ */ new Map();
1737
1897
  var loadFontSources = (entry, importSources) => {
1738
- const pending = fontSourceLoads.get(entry.id);
1739
- if (pending) {
1740
- return pending;
1898
+ const pending2 = fontSourceLoads.get(entry.id);
1899
+ if (pending2) {
1900
+ return pending2;
1741
1901
  }
1742
1902
  const load = importSources().then((module2) => module2.NOTICE_FONT_SOURCES).catch((error) => {
1743
1903
  fontSourceLoads.delete(entry.id);
@@ -1899,8 +2059,113 @@ var updateConsentState = (updates) => {
1899
2059
 
1900
2060
  // src/RedactoNoticeConsent/RedactoNoticeConsent.tsx
1901
2061
  var import_jsx_runtime3 = require("react/jsx-runtime");
2062
+ var DEFAULT_HEADING_COLOR = "#323B4B";
2063
+ var CHEVRON_BUTTON_STYLE = {
2064
+ background: "none",
2065
+ border: "none",
2066
+ padding: 0,
2067
+ margin: 0,
2068
+ cursor: "pointer",
2069
+ display: "flex",
2070
+ alignItems: "center"
2071
+ };
2072
+ var CHEVRON_WRAPPER_STYLE = {
2073
+ display: "flex",
2074
+ alignItems: "center",
2075
+ justifyContent: "center",
2076
+ marginLeft: "5px"
2077
+ };
2078
+ var CollapseChevron = ({ open, color }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: CHEVRON_WRAPPER_STYLE, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2079
+ "svg",
2080
+ {
2081
+ width: "7",
2082
+ height: "12",
2083
+ viewBox: "0 0 7 12",
2084
+ fill: "none",
2085
+ xmlns: "http://www.w3.org/2000/svg",
2086
+ stroke: color,
2087
+ strokeWidth: "2",
2088
+ strokeLinecap: "round",
2089
+ strokeLinejoin: "round",
2090
+ style: {
2091
+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
2092
+ transition: "transform 0.3s ease"
2093
+ },
2094
+ "aria-hidden": "true",
2095
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M1 1L6 6L1 11" })
2096
+ }
2097
+ ) });
2098
+ var ProductSectionHeader = ({
2099
+ product,
2100
+ purposeCount,
2101
+ allSelected,
2102
+ someSelected,
2103
+ collapsed,
2104
+ locked,
2105
+ settings,
2106
+ styles: styles4,
2107
+ responsiveStyles,
2108
+ onToggleCollapse,
2109
+ onToggleSelect,
2110
+ getTranslatedText
2111
+ }) => {
2112
+ const checkboxRef = (0, import_react3.useRef)(null);
2113
+ const name = getTranslatedText("products.name", product.name, product.uuid);
2114
+ const headingColor = (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR;
2115
+ (0, import_react3.useEffect)(() => {
2116
+ if (checkboxRef.current) {
2117
+ checkboxRef.current.indeterminate = someSelected && !allSelected;
2118
+ }
2119
+ }, [someSelected, allSelected]);
2120
+ const handleToggleCollapse = () => {
2121
+ onToggleCollapse(product.uuid);
2122
+ };
2123
+ const handleToggleSelect = () => {
2124
+ onToggleSelect(product.uuid);
2125
+ };
2126
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles4.sectionHeader, children: [
2127
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles4.optionLeft, children: [
2128
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2129
+ "button",
2130
+ {
2131
+ type: "button",
2132
+ onClick: handleToggleCollapse,
2133
+ "aria-expanded": !collapsed,
2134
+ "aria-controls": `product-${product.uuid}`,
2135
+ "aria-labelledby": `product-name-${product.uuid}`,
2136
+ style: CHEVRON_BUTTON_STYLE,
2137
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CollapseChevron, { open: !collapsed, color: headingColor })
2138
+ }
2139
+ ),
2140
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles4.optionTextContainer, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2141
+ "h2",
2142
+ {
2143
+ id: `product-name-${product.uuid}`,
2144
+ style: {
2145
+ ...styles4.sectionTitle,
2146
+ ...responsiveStyles.optionTitle,
2147
+ color: headingColor
2148
+ },
2149
+ children: name
2150
+ }
2151
+ ) })
2152
+ ] }),
2153
+ !locked && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2154
+ "input",
2155
+ {
2156
+ ref: checkboxRef,
2157
+ className: "redacto-checkbox-large",
2158
+ type: "checkbox",
2159
+ checked: allSelected,
2160
+ onChange: handleToggleSelect,
2161
+ "aria-label": PRODUCT_SECTION_COPY.toggleSelection(name, purposeCount)
2162
+ }
2163
+ )
2164
+ ] });
2165
+ };
1902
2166
  var PurposeItem = ({
1903
2167
  purpose,
2168
+ productUuid,
1904
2169
  selectedPurposes,
1905
2170
  collapsedPurposes,
1906
2171
  selectedDataElements,
@@ -1915,9 +2180,10 @@ var PurposeItem = ({
1915
2180
  selectedLanguage,
1916
2181
  initialDataElementSelections = {}
1917
2182
  }) => {
2183
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
1918
2184
  const headingStyle = (0, import_react3.useMemo)(
1919
2185
  () => ({
1920
- color: (settings == null ? void 0 : settings.headingColor) || "#323B4B"
2186
+ color: (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR
1921
2187
  }),
1922
2188
  [settings == null ? void 0 : settings.headingColor]
1923
2189
  );
@@ -1933,12 +2199,12 @@ var PurposeItem = ({
1933
2199
  "div",
1934
2200
  {
1935
2201
  style: styles4.optionLeft,
1936
- onClick: () => onPurposeCollapse(purpose.uuid),
2202
+ onClick: () => onPurposeCollapse(purpose.uuid, productUuid),
1937
2203
  role: "button",
1938
2204
  tabIndex: 0,
1939
- "aria-expanded": !collapsedPurposes[purpose.uuid],
2205
+ "aria-expanded": !collapsedPurposes[stateKey],
1940
2206
  "aria-controls": `purpose-${purpose.uuid}`,
1941
- "aria-label": `${collapsedPurposes[purpose.uuid] ? "Expand" : "Collapse"} ${getTranslatedText(
2207
+ "aria-label": `${collapsedPurposes[stateKey] ? "Expand" : "Collapse"} ${getTranslatedText(
1942
2208
  `purposes.name`,
1943
2209
  purpose.name,
1944
2210
  purpose.uuid
@@ -1946,46 +2212,22 @@ var PurposeItem = ({
1946
2212
  onKeyDown: (e) => {
1947
2213
  if (e.key === "Enter" || e.key === " ") {
1948
2214
  e.preventDefault();
1949
- onPurposeCollapse(purpose.uuid);
2215
+ onPurposeCollapse(purpose.uuid, productUuid);
1950
2216
  }
1951
2217
  },
1952
2218
  children: [
1953
2219
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1954
- "div",
2220
+ CollapseChevron,
1955
2221
  {
1956
- style: {
1957
- display: "flex",
1958
- alignItems: "center",
1959
- justifyContent: "center",
1960
- marginLeft: "5px"
1961
- },
1962
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1963
- "svg",
1964
- {
1965
- width: "7",
1966
- height: "12",
1967
- viewBox: "0 0 7 12",
1968
- fill: "none",
1969
- xmlns: "http://www.w3.org/2000/svg",
1970
- stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
1971
- strokeWidth: "2",
1972
- strokeLinecap: "round",
1973
- strokeLinejoin: "round",
1974
- style: {
1975
- transform: !collapsedPurposes[purpose.uuid] ? "rotate(90deg)" : "rotate(0deg)",
1976
- transition: "transform 0.3s ease"
1977
- },
1978
- "aria-hidden": "true",
1979
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M1 1L6 6L1 11" })
1980
- }
1981
- )
2222
+ open: !collapsedPurposes[stateKey],
2223
+ color: (settings == null ? void 0 : settings.headingColor) || DEFAULT_HEADING_COLOR
1982
2224
  }
1983
2225
  ),
1984
2226
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles4.optionTextContainer, children: [
1985
2227
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1986
2228
  "h2",
1987
2229
  {
1988
- id: `purpose-name-${purpose.uuid}`,
2230
+ id: `purpose-name-${stateKey}`,
1989
2231
  style: {
1990
2232
  ...styles4.optionTitle,
1991
2233
  ...responsiveStyles.optionTitle,
@@ -2001,7 +2243,7 @@ var PurposeItem = ({
2001
2243
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2002
2244
  "h3",
2003
2245
  {
2004
- id: `purpose-description-${purpose.uuid}`,
2246
+ id: `purpose-description-${stateKey}`,
2005
2247
  style: {
2006
2248
  ...styles4.optionDescription,
2007
2249
  ...responsiveStyles.optionDescription,
@@ -2024,8 +2266,8 @@ var PurposeItem = ({
2024
2266
  {
2025
2267
  className: "redacto-checkbox-large",
2026
2268
  type: "checkbox",
2027
- checked: selectedPurposes[purpose.uuid] || false,
2028
- onChange: () => onPurposeToggle(purpose.uuid),
2269
+ checked: selectedPurposes[stateKey] || false,
2270
+ onChange: () => onPurposeToggle(purpose.uuid, productUuid),
2029
2271
  "aria-label": `Select all data elements for ${getTranslatedText(
2030
2272
  `purposes.name`,
2031
2273
  purpose.name,
@@ -2075,7 +2317,7 @@ var PurposeItem = ({
2075
2317
  }
2076
2318
  )
2077
2319
  ] }),
2078
- !collapsedPurposes[purpose.uuid] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2320
+ !collapsedPurposes[stateKey] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2079
2321
  "div",
2080
2322
  {
2081
2323
  id: `purpose-${purpose.uuid}`,
@@ -2088,7 +2330,7 @@ var PurposeItem = ({
2088
2330
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2089
2331
  "span",
2090
2332
  {
2091
- id: `data-element-${purpose.uuid}-${dataElement.uuid}`,
2333
+ id: `data-element-${stateKey}-${dataElement.uuid}`,
2092
2334
  style: {
2093
2335
  ...styles4.dataElementText,
2094
2336
  ...responsiveStyles.dataElementText,
@@ -2110,8 +2352,12 @@ var PurposeItem = ({
2110
2352
  {
2111
2353
  className: "redacto-checkbox-small",
2112
2354
  type: "checkbox",
2113
- checked: selectedDataElements[`${purpose.uuid}-${dataElement.uuid}`] || false,
2114
- onChange: () => onDataElementToggle(dataElement.uuid, purpose.uuid),
2355
+ checked: selectedDataElements[`${stateKey}-${dataElement.uuid}`] || false,
2356
+ onChange: () => onDataElementToggle(
2357
+ dataElement.uuid,
2358
+ purpose.uuid,
2359
+ productUuid
2360
+ ),
2115
2361
  "aria-label": `Select ${getTranslatedText(
2116
2362
  `data_elements.name`,
2117
2363
  dataElement.name,
@@ -2120,7 +2366,7 @@ var PurposeItem = ({
2120
2366
  }
2121
2367
  ),
2122
2368
  isAlreadyConsented && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: (() => {
2123
- const combinedId = `${purpose.uuid}-${dataElement.uuid}`;
2369
+ const combinedId = `${stateKey}-${dataElement.uuid}`;
2124
2370
  const wasOriginallySelected = initialDataElementSelections[combinedId] || false;
2125
2371
  if (wasOriginallySelected) {
2126
2372
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2258,7 +2504,9 @@ var GuardianForm = ({
2258
2504
  switch (e.key) {
2259
2505
  case "ArrowDown":
2260
2506
  e.preventDefault();
2261
- setFocusedIndex((prev) => Math.min(prev + 1, RELATIONSHIP_OPTIONS.length - 1));
2507
+ setFocusedIndex(
2508
+ (prev) => Math.min(prev + 1, RELATIONSHIP_OPTIONS.length - 1)
2509
+ );
2262
2510
  break;
2263
2511
  case "ArrowUp":
2264
2512
  e.preventDefault();
@@ -2267,7 +2515,8 @@ var GuardianForm = ({
2267
2515
  case "Enter":
2268
2516
  case " ":
2269
2517
  e.preventDefault();
2270
- if (focusedIndex >= 0) handleSelect(RELATIONSHIP_OPTIONS[focusedIndex].value);
2518
+ if (focusedIndex >= 0)
2519
+ handleSelect(RELATIONSHIP_OPTIONS[focusedIndex].value);
2271
2520
  break;
2272
2521
  case "Escape":
2273
2522
  case "Tab":
@@ -2332,7 +2581,9 @@ var GuardianForm = ({
2332
2581
  lineHeight: "1.5",
2333
2582
  color: "#1E40AF"
2334
2583
  };
2335
- const selectedLabel = (_a = RELATIONSHIP_OPTIONS.find((o) => o.value === formData.guardianRelationship)) == null ? void 0 : _a.label;
2584
+ const selectedLabel = (_a = RELATIONSHIP_OPTIONS.find(
2585
+ (o) => o.value === formData.guardianRelationship
2586
+ )) == null ? void 0 : _a.label;
2336
2587
  const hasError = Boolean(errors.guardianRelationship);
2337
2588
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2338
2589
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...styles4.topSection, ...componentTextStyle }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles4.topLeft, children: [
@@ -2432,7 +2683,8 @@ var GuardianForm = ({
2432
2683
  ] }),
2433
2684
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
2434
2685
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { htmlFor: "guardianContact", style: labelStyle, children: [
2435
- "Guardian's Phone Number ",
2686
+ "Guardian's Phone Number",
2687
+ " ",
2436
2688
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#DC2626" }, children: "*" })
2437
2689
  ] }),
2438
2690
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2488,7 +2740,17 @@ var GuardianForm = ({
2488
2740
  transition: "border-color 0.15s, box-shadow 0.15s"
2489
2741
  },
2490
2742
  children: [
2491
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: selectedLabel || "Select relationship" }),
2743
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2744
+ "span",
2745
+ {
2746
+ style: {
2747
+ overflow: "hidden",
2748
+ textOverflow: "ellipsis",
2749
+ whiteSpace: "nowrap"
2750
+ },
2751
+ children: selectedLabel || "Select relationship"
2752
+ }
2753
+ ),
2492
2754
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2493
2755
  "svg",
2494
2756
  {
@@ -3156,6 +3418,7 @@ var RedactoNoticeConsent = ({
3156
3418
  const [selectedPurposes, setSelectedPurposes] = (0, import_react3.useState)({});
3157
3419
  const [selectedDataElements, setSelectedDataElements] = (0, import_react3.useState)({});
3158
3420
  const [initialDataElementSelections, setInitialDataElementSelections] = (0, import_react3.useState)({});
3421
+ const [collapsedProducts, setCollapsedProducts] = (0, import_react3.useState)({});
3159
3422
  const [collapsedPurposes, setCollapsedPurposes] = (0, import_react3.useState)({});
3160
3423
  const [hasAlreadyConsented, setHasAlreadyConsented] = (0, import_react3.useState)(false);
3161
3424
  const [isFullyConsentedReviewMode, setIsFullyConsentedReviewMode] = (0, import_react3.useState)(false);
@@ -3201,7 +3464,9 @@ var RedactoNoticeConsent = ({
3201
3464
  const statusPollIntervalRef = (0, import_react3.useRef)(null);
3202
3465
  const verificationPopupRef = (0, import_react3.useRef)(null);
3203
3466
  const pollingCleanupRef = (0, import_react3.useRef)(null);
3204
- const autoTransitionTimerRef = (0, import_react3.useRef)(null);
3467
+ const autoTransitionTimerRef = (0, import_react3.useRef)(
3468
+ null
3469
+ );
3205
3470
  const digilockerRedirectTimeoutRef = (0, import_react3.useRef)(null);
3206
3471
  const isInitialMountRef = (0, import_react3.useRef)(true);
3207
3472
  const isPollingStatusRef = (0, import_react3.useRef)(false);
@@ -3311,21 +3576,70 @@ var RedactoNoticeConsent = ({
3311
3576
  const logoRowStyle = (0, import_react3.useMemo)(
3312
3577
  () => ({
3313
3578
  ...styles.logoRow,
3314
- justifyContent: logoRowJustify(resolveLogoPosition(content == null ? void 0 : content.logo_position))
3579
+ justifyContent: logoRowJustify(
3580
+ resolveLogoPosition(content == null ? void 0 : content.logo_position)
3581
+ )
3315
3582
  }),
3316
3583
  [content == null ? void 0 : content.logo_position]
3317
3584
  );
3585
+ const purposeRows = (0, import_react3.useMemo)(
3586
+ () => {
3587
+ var _a2;
3588
+ return noticePurposeRows(
3589
+ content == null ? void 0 : content.products,
3590
+ (_a2 = content == null ? void 0 : content.purposes) != null ? _a2 : [],
3591
+ content == null ? void 0 : content.product_purpose_order
3592
+ );
3593
+ },
3594
+ [content]
3595
+ );
3596
+ const productGroups = (0, import_react3.useMemo)(
3597
+ () => {
3598
+ var _a2;
3599
+ return groupPurposesByProduct(
3600
+ content == null ? void 0 : content.products,
3601
+ (_a2 = content == null ? void 0 : content.purposes) != null ? _a2 : [],
3602
+ content == null ? void 0 : content.product_purpose_order
3603
+ );
3604
+ },
3605
+ [content]
3606
+ );
3318
3607
  const areAllRequiredElementsChecked = (0, import_react3.useMemo)(() => {
3608
+ var _a2;
3319
3609
  if (!content) return false;
3320
- return content.purposes.every((purpose) => {
3610
+ if (purposeRows.length === 0) return true;
3611
+ const isSatisfied = ({
3612
+ purpose,
3613
+ productUuid
3614
+ }) => {
3615
+ const key = productPurposeKey(productUuid, purpose.uuid);
3321
3616
  const requiredElements = purpose.data_elements.filter(
3322
3617
  (element) => element.required && element.enabled
3323
3618
  );
3324
3619
  return requiredElements.every(
3325
- (element) => selectedDataElements[`${purpose.uuid}-${element.uuid}`]
3620
+ (element) => selectedDataElements[`${key}-${element.uuid}`]
3326
3621
  );
3327
- });
3328
- }, [content, selectedDataElements]);
3622
+ };
3623
+ const rowsByProduct = /* @__PURE__ */ new Map();
3624
+ for (const row of purposeRows) {
3625
+ const groupKey = (_a2 = row.productUuid) != null ? _a2 : "";
3626
+ const group = rowsByProduct.get(groupKey);
3627
+ if (group) {
3628
+ group.push(row);
3629
+ } else {
3630
+ rowsByProduct.set(groupKey, [row]);
3631
+ }
3632
+ }
3633
+ const groups = [...rowsByProduct.values()];
3634
+ const everyProductHasMandatory = groups.every(
3635
+ (rows) => rows.some(
3636
+ ({ purpose }) => purpose.data_elements.some(
3637
+ (element) => element.required && element.enabled
3638
+ )
3639
+ )
3640
+ );
3641
+ return everyProductHasMandatory ? groups.some((rows) => rows.every(isSatisfied)) : purposeRows.every(isSatisfied);
3642
+ }, [content, purposeRows, selectedDataElements]);
3329
3643
  const acceptDisabled = isSubmitting || !areAllRequiredElementsChecked;
3330
3644
  (0, import_react3.useEffect)(() => {
3331
3645
  var _a2, _b2;
@@ -3337,6 +3651,7 @@ var RedactoNoticeConsent = ({
3337
3651
  setSelectedDataElements({});
3338
3652
  setInitialDataElementSelections({});
3339
3653
  setCollapsedPurposes({});
3654
+ setCollapsedProducts({});
3340
3655
  setHasAlreadyConsented(false);
3341
3656
  setErrorMessage(null);
3342
3657
  setFetchError(null);
@@ -3401,7 +3716,7 @@ var RedactoNoticeConsent = ({
3401
3716
  return false;
3402
3717
  };
3403
3718
  const getTranslatedText = (key, defaultText, itemId) => {
3404
- var _a2, _b2;
3719
+ var _a2, _b2, _c2;
3405
3720
  if (!content) return defaultText;
3406
3721
  if (isDefaultLanguage(selectedLanguage)) {
3407
3722
  if (key === "privacy_policy_anchor_text" && content.privacy_policy_anchor_text) {
@@ -3439,8 +3754,18 @@ var RedactoNoticeConsent = ({
3439
3754
  }
3440
3755
  return defaultText;
3441
3756
  }
3757
+ if (key === "products.name" || key === "products.description") {
3758
+ const productData = (_b2 = translationMap.products) == null ? void 0 : _b2[itemId];
3759
+ if (productData) {
3760
+ if (typeof productData === "string") {
3761
+ return key === "products.name" ? productData : defaultText;
3762
+ }
3763
+ return key === "products.name" ? productData.name || defaultText : productData.description || defaultText;
3764
+ }
3765
+ return defaultText;
3766
+ }
3442
3767
  if (key.startsWith("data_elements.")) {
3443
- return ((_b2 = translationMap.data_elements) == null ? void 0 : _b2[itemId]) || defaultText;
3768
+ return ((_c2 = translationMap.data_elements) == null ? void 0 : _c2[itemId]) || defaultText;
3444
3769
  }
3445
3770
  }
3446
3771
  const value = translationMap[key];
@@ -3498,63 +3823,88 @@ var RedactoNoticeConsent = ({
3498
3823
  defaultLang === "en" || defaultLang === "EN" ? "English" : defaultLang
3499
3824
  );
3500
3825
  }
3501
- const initialCollapsedState = activeConfig.purposes.reduce(
3502
- (acc, purpose) => ({
3826
+ const initialCollapsedState = noticePurposeRows(
3827
+ activeConfig.products,
3828
+ activeConfig.purposes,
3829
+ activeConfig.product_purpose_order
3830
+ ).reduce(
3831
+ (acc, { purpose, productUuid }) => ({
3503
3832
  ...acc,
3504
- [purpose.uuid]: true
3833
+ [productPurposeKey(productUuid, purpose.uuid)]: true
3505
3834
  }),
3506
3835
  {}
3507
3836
  );
3508
3837
  setCollapsedPurposes(initialCollapsedState);
3838
+ setCollapsedProducts(
3839
+ groupPurposesByProduct(
3840
+ activeConfig.products,
3841
+ activeConfig.purposes,
3842
+ activeConfig.product_purpose_order
3843
+ ).reduce(
3844
+ (acc, group, index) => ({
3845
+ ...acc,
3846
+ [group.product.uuid]: index > 0
3847
+ }),
3848
+ {}
3849
+ )
3850
+ );
3509
3851
  const isReconsentRequired = consentContentData.detail.reconsent_required;
3510
3852
  const purposeSelections = consentContentData.detail.purpose_selections;
3511
3853
  const newCategorizedPurposes = isReconsentRequired ? {
3512
- alreadyConsented: activeConfig.purposes.filter(
3513
- (purpose) => {
3514
- const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3515
- return selection && (selection.status === "ACTIVE" || selection.status === "EXPIRED") && !selection.needs_reconsent;
3516
- }
3517
- ),
3518
- needsConsent: activeConfig.purposes.filter(
3519
- (purpose) => {
3520
- const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3521
- return !selection || selection.status !== "ACTIVE" || selection.needs_reconsent;
3522
- }
3523
- )
3854
+ alreadyConsented: activeConfig.purposes.filter((purpose) => {
3855
+ const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3856
+ return selection && (selection.status === "ACTIVE" || selection.status === "EXPIRED") && !selection.needs_reconsent;
3857
+ }),
3858
+ needsConsent: activeConfig.purposes.filter((purpose) => {
3859
+ const selection = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3860
+ return !selection || selection.status !== "ACTIVE" || selection.needs_reconsent;
3861
+ })
3524
3862
  } : null;
3525
3863
  setCategorizedPurposes(newCategorizedPurposes);
3526
3864
  const preselection = buildPreselection(
3527
3865
  activeConfig.purposes,
3528
3866
  resolvePurposePreselection(activeConfig.purpose_preselection)
3529
3867
  );
3530
- const initialPurposeState = activeConfig.purposes.reduce(
3531
- (acc, purpose) => {
3868
+ const seedRows = noticePurposeRows(
3869
+ activeConfig.products,
3870
+ activeConfig.purposes,
3871
+ activeConfig.product_purpose_order
3872
+ );
3873
+ const recordedFor = (purposeUuid, productUuid) => {
3874
+ var _a3, _b2;
3875
+ const perProduct = productUuid ? (_b2 = (_a3 = activeConfig.product_purpose_selections) == null ? void 0 : _a3[productUuid]) == null ? void 0 : _b2[purposeUuid] : void 0;
3876
+ return perProduct != null ? perProduct : purposeSelections == null ? void 0 : purposeSelections[purposeUuid];
3877
+ };
3878
+ const initialPurposeState = seedRows.reduce(
3879
+ (acc, { purpose, productUuid }) => {
3532
3880
  var _a3;
3533
- const recorded = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid];
3881
+ const key = productPurposeKey(productUuid, purpose.uuid);
3882
+ const recorded = recordedFor(purpose.uuid, productUuid);
3534
3883
  if (hasRecordedPurposeState(recorded)) {
3535
- acc[purpose.uuid] = recorded.selected;
3884
+ acc[key] = recorded.selected;
3536
3885
  } else {
3537
- acc[purpose.uuid] = (_a3 = preselection.selectedPurposes[purpose.uuid]) != null ? _a3 : false;
3886
+ acc[key] = (_a3 = preselection.selectedPurposes[purpose.uuid]) != null ? _a3 : false;
3538
3887
  }
3539
3888
  return acc;
3540
3889
  },
3541
3890
  {}
3542
3891
  );
3543
3892
  setSelectedPurposes(initialPurposeState);
3544
- const initialDataElementState = activeConfig.purposes.reduce(
3545
- (acc, purpose) => {
3546
- const hasRecordedSelection = hasRecordedPurposeState(
3547
- purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]
3548
- );
3893
+ const initialDataElementState = seedRows.reduce(
3894
+ (acc, { purpose, productUuid }) => {
3895
+ const key = productPurposeKey(productUuid, purpose.uuid);
3896
+ const recorded = recordedFor(purpose.uuid, productUuid);
3897
+ const hasRecordedSelection = hasRecordedPurposeState(recorded);
3549
3898
  purpose.data_elements.forEach((element) => {
3550
- var _a3, _b2, _c2;
3551
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3552
- if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3553
- acc[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3899
+ var _a3, _b2;
3900
+ const combinedId = `${key}-${element.uuid}`;
3901
+ const recordedElement = (_a3 = recorded == null ? void 0 : recorded.data_elements) == null ? void 0 : _a3[element.uuid];
3902
+ if (recordedElement) {
3903
+ acc[combinedId] = recordedElement.selected;
3554
3904
  } else if (hasRecordedSelection) {
3555
3905
  acc[combinedId] = false;
3556
3906
  } else {
3557
- acc[combinedId] = (_c2 = preselection.selectedDataElements[combinedId]) != null ? _c2 : false;
3907
+ acc[combinedId] = (_b2 = preselection.selectedDataElements[`${purpose.uuid}-${element.uuid}`]) != null ? _b2 : false;
3558
3908
  }
3559
3909
  });
3560
3910
  return acc;
@@ -3565,10 +3915,14 @@ var RedactoNoticeConsent = ({
3565
3915
  if (isReconsentRequired && newCategorizedPurposes) {
3566
3916
  setIsFullyConsentedReviewMode(false);
3567
3917
  const initialSelections = {};
3568
- newCategorizedPurposes.alreadyConsented.forEach((purpose) => {
3918
+ const alreadyConsentedUuids = new Set(
3919
+ newCategorizedPurposes.alreadyConsented.map((p) => p.uuid)
3920
+ );
3921
+ seedRows.filter(({ purpose }) => alreadyConsentedUuids.has(purpose.uuid)).forEach(({ purpose, productUuid }) => {
3922
+ const rowKey = productPurposeKey(productUuid, purpose.uuid);
3569
3923
  purpose.data_elements.forEach((element) => {
3570
3924
  var _a3, _b2;
3571
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3925
+ const combinedId = `${rowKey}-${element.uuid}`;
3572
3926
  if ((_b2 = (_a3 = purposeSelections == null ? void 0 : purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3573
3927
  initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3574
3928
  } else {
@@ -3578,28 +3932,27 @@ var RedactoNoticeConsent = ({
3578
3932
  });
3579
3933
  setInitialDataElementSelections(initialSelections);
3580
3934
  } else if (includeFullyConsentedData && purposeSelections) {
3581
- const allPurposesSelected = activeConfig.purposes.length > 0 && activeConfig.purposes.every(
3582
- (p) => {
3935
+ const allPurposesSelected = seedRows.length > 0 && seedRows.every(
3936
+ ({ purpose, productUuid }) => {
3583
3937
  var _a3;
3584
- return ((_a3 = purposeSelections[p.uuid]) == null ? void 0 : _a3.selected) === true;
3938
+ return ((_a3 = recordedFor(purpose.uuid, productUuid)) == null ? void 0 : _a3.selected) === true;
3585
3939
  }
3586
3940
  );
3587
3941
  setIsFullyConsentedReviewMode(allPurposesSelected);
3588
3942
  if (allPurposesSelected) {
3589
3943
  const initialSelections = {};
3590
- activeConfig.purposes.forEach(
3591
- (purpose) => {
3592
- purpose.data_elements.forEach((element) => {
3593
- var _a3, _b2;
3594
- const combinedId = `${purpose.uuid}-${element.uuid}`;
3595
- if ((_b2 = (_a3 = purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3596
- initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3597
- } else {
3598
- initialSelections[combinedId] = false;
3599
- }
3600
- });
3601
- }
3602
- );
3944
+ seedRows.forEach(({ purpose, productUuid }) => {
3945
+ const rowKey = productPurposeKey(productUuid, purpose.uuid);
3946
+ purpose.data_elements.forEach((element) => {
3947
+ var _a3, _b2;
3948
+ const combinedId = `${rowKey}-${element.uuid}`;
3949
+ if ((_b2 = (_a3 = purposeSelections[purpose.uuid]) == null ? void 0 : _a3.data_elements) == null ? void 0 : _b2[element.uuid]) {
3950
+ initialSelections[combinedId] = purposeSelections[purpose.uuid].data_elements[element.uuid].selected;
3951
+ } else {
3952
+ initialSelections[combinedId] = false;
3953
+ }
3954
+ });
3955
+ });
3603
3956
  setInitialDataElementSelections(initialSelections);
3604
3957
  } else {
3605
3958
  setIsFullyConsentedReviewMode(false);
@@ -3649,10 +4002,11 @@ var RedactoNoticeConsent = ({
3649
4002
  // Removed onError and isRefreshingToken from dependencies to prevent duplicate API calls
3650
4003
  // onError is only called on errors, not needed for initial data fetching
3651
4004
  ]);
3652
- const togglePurposeCollapse = (purposeUuid) => {
4005
+ const togglePurposeCollapse = (purposeUuid, productUuid) => {
4006
+ const key = productPurposeKey(productUuid, purposeUuid);
3653
4007
  setCollapsedPurposes((prev) => ({
3654
4008
  ...prev,
3655
- [purposeUuid]: !prev[purposeUuid]
4009
+ [key]: !prev[key]
3656
4010
  }));
3657
4011
  };
3658
4012
  const toggleLanguageDropdown = () => {
@@ -3662,28 +4016,70 @@ var RedactoNoticeConsent = ({
3662
4016
  setSelectedLanguage(lang);
3663
4017
  setIsLanguageDropdownOpen(false);
3664
4018
  };
3665
- const handlePurposeCheckboxChange = (purposeUuid) => {
4019
+ const handlePurposeCheckboxChange = (purposeUuid, productUuid) => {
3666
4020
  if (content) {
3667
4021
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
3668
4022
  if (purpose) {
3669
- const newPurposeState = !selectedPurposes[purposeUuid];
4023
+ const key = productPurposeKey(productUuid, purposeUuid);
4024
+ const newPurposeState = !selectedPurposes[key];
3670
4025
  setSelectedPurposes((prev) => ({
3671
4026
  ...prev,
3672
- [purposeUuid]: newPurposeState
4027
+ [key]: newPurposeState
3673
4028
  }));
3674
4029
  setSelectedDataElements((prev) => {
3675
4030
  const updated = { ...prev };
3676
4031
  purpose.data_elements.forEach((el) => {
3677
- updated[`${purposeUuid}-${el.uuid}`] = newPurposeState;
4032
+ updated[`${key}-${el.uuid}`] = newPurposeState;
3678
4033
  });
3679
4034
  return updated;
3680
4035
  });
3681
4036
  }
3682
4037
  }
3683
4038
  };
3684
- const handleDataElementCheckboxChange = (elementUuid, purposeUuid) => {
4039
+ const toggleProductCollapse = (productUuid) => {
4040
+ setCollapsedProducts((prev) => ({
4041
+ ...prev,
4042
+ [productUuid]: !prev[productUuid]
4043
+ }));
4044
+ };
4045
+ const handleProductCheckboxChange = (productUuid) => {
4046
+ if (!content) {
4047
+ return;
4048
+ }
4049
+ const group = productGroups.find(
4050
+ (candidate) => candidate.product.uuid === productUuid
4051
+ );
4052
+ if (!group) {
4053
+ return;
4054
+ }
4055
+ const nextState = !group.purposes.every(
4056
+ (purpose) => selectedPurposes[productPurposeKey(productUuid, purpose.uuid)]
4057
+ );
4058
+ setSelectedPurposes((prev) => {
4059
+ const updated = { ...prev };
4060
+ group.purposes.forEach((purpose) => {
4061
+ updated[productPurposeKey(productUuid, purpose.uuid)] = nextState;
4062
+ });
4063
+ return updated;
4064
+ });
4065
+ setSelectedDataElements((prev) => {
4066
+ const updated = { ...prev };
4067
+ group.purposes.forEach((purpose) => {
4068
+ const key = productPurposeKey(productUuid, purpose.uuid);
4069
+ purpose.data_elements.forEach((element) => {
4070
+ updated[`${key}-${element.uuid}`] = nextState;
4071
+ });
4072
+ });
4073
+ return updated;
4074
+ });
4075
+ if (!nextState) {
4076
+ setCollapsedProducts((prev) => ({ ...prev, [productUuid]: false }));
4077
+ }
4078
+ };
4079
+ const handleDataElementCheckboxChange = (elementUuid, purposeUuid, productUuid) => {
3685
4080
  if (!content) return;
3686
- const combinedId = `${purposeUuid}-${elementUuid}`;
4081
+ const key = productPurposeKey(productUuid, purposeUuid);
4082
+ const combinedId = `${key}-${elementUuid}`;
3687
4083
  setSelectedDataElements((prev) => {
3688
4084
  const newState = {
3689
4085
  ...prev,
@@ -3697,16 +4093,16 @@ var RedactoNoticeConsent = ({
3697
4093
  let shouldCheckPurpose;
3698
4094
  if (requiredElements.length > 0) {
3699
4095
  shouldCheckPurpose = requiredElements.every(
3700
- (el) => newState[`${purposeUuid}-${el.uuid}`]
4096
+ (el) => newState[`${key}-${el.uuid}`]
3701
4097
  );
3702
4098
  } else {
3703
4099
  shouldCheckPurpose = purpose.data_elements.some(
3704
- (el) => newState[`${purposeUuid}-${el.uuid}`]
4100
+ (el) => newState[`${key}-${el.uuid}`]
3705
4101
  );
3706
4102
  }
3707
4103
  setSelectedPurposes((prevPurposes) => ({
3708
4104
  ...prevPurposes,
3709
- [purposeUuid]: shouldCheckPurpose
4105
+ [key]: shouldCheckPurpose
3710
4106
  }));
3711
4107
  }
3712
4108
  return newState;
@@ -3716,14 +4112,16 @@ var RedactoNoticeConsent = ({
3716
4112
  const submitController = new AbortController();
3717
4113
  try {
3718
4114
  if (isMinorFlow && !verificationReference) {
3719
- setErrorMessage("Guardian verification is required before consent can be submitted.");
4115
+ setErrorMessage(
4116
+ "Guardian verification is required before consent can be submitted."
4117
+ );
3720
4118
  return;
3721
4119
  }
3722
4120
  setIsSubmittingAccept(true);
3723
4121
  setIsSubmitting(true);
3724
4122
  setErrorMessage(null);
3725
4123
  if (content) {
3726
- const selection = buildSelectionForMode(content.purposes, mode, {
4124
+ const selection = buildSelectionForMode(purposeRows, mode, {
3727
4125
  selectedPurposes,
3728
4126
  selectedDataElements
3729
4127
  });
@@ -3736,14 +4134,20 @@ var RedactoNoticeConsent = ({
3736
4134
  baseUrl,
3737
4135
  ledgerBaseUrl,
3738
4136
  noticeUuid: content.notice_uuid,
3739
- purposes: content.purposes.map((purpose) => {
4137
+ // One entry per (product, purpose) once a notice covers several. The
4138
+ // ledger keys consent on the pair, and repeating a purpose under two
4139
+ // products is how one submission accepts it for one and declines it
4140
+ // for the other. Collapses to content.purposes on a single-product
4141
+ // notice, so that payload is byte-identical to before.
4142
+ purposes: purposeRows.map(({ purpose, productUuid }) => {
4143
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
3740
4144
  const isAlreadyConsented = (categorizedPurposes == null ? void 0 : categorizedPurposes.alreadyConsented.some(
3741
4145
  (p) => p.uuid === purpose.uuid
3742
4146
  )) || false;
3743
- let purposeSelected = selection.selectedPurposes[purpose.uuid] || false;
4147
+ let purposeSelected = selection.selectedPurposes[stateKey] || false;
3744
4148
  if (mode === "selected" && isAlreadyConsented) {
3745
4149
  const wasModified = purpose.data_elements.some((element) => {
3746
- const combinedId = `${purpose.uuid}-${element.uuid}`;
4150
+ const combinedId = `${stateKey}-${element.uuid}`;
3747
4151
  const currentSelection = selection.selectedDataElements[combinedId] || false;
3748
4152
  const initialSelection = initialDataElementSelections[combinedId] || false;
3749
4153
  return currentSelection !== initialSelection;
@@ -3754,10 +4158,11 @@ var RedactoNoticeConsent = ({
3754
4158
  }
3755
4159
  return {
3756
4160
  ...purpose,
4161
+ ...productUuid ? { product_uuid: productUuid } : {},
3757
4162
  selected: purposeSelected,
3758
4163
  data_elements: purpose.data_elements.map((element) => ({
3759
4164
  ...element,
3760
- selected: element.required ? true : selection.selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
4165
+ selected: element.required ? true : selection.selectedDataElements[`${stateKey}-${element.uuid}`] || false
3761
4166
  }))
3762
4167
  };
3763
4168
  }),
@@ -3860,7 +4265,9 @@ var RedactoNoticeConsent = ({
3860
4265
  return;
3861
4266
  }
3862
4267
  if (!response.session_token || !response.digilocker_redirect_url) {
3863
- throw new Error("Invalid response from server: missing required fields");
4268
+ throw new Error(
4269
+ "Invalid response from server: missing required fields"
4270
+ );
3864
4271
  }
3865
4272
  setShowGuardianForm(false);
3866
4273
  setShowVerificationScreen(true);
@@ -3950,7 +4357,18 @@ var RedactoNoticeConsent = ({
3950
4357
  } finally {
3951
4358
  setIsSubmittingGuardian(false);
3952
4359
  }
3953
- }, [accessToken, sandboxParams, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
4360
+ }, [
4361
+ accessToken,
4362
+ sandboxParams,
4363
+ baseUrl,
4364
+ guardianFormData,
4365
+ digilockerMode,
4366
+ digilockerCallbackUrl,
4367
+ selectedPurposes,
4368
+ selectedDataElements,
4369
+ noticeId,
4370
+ onError
4371
+ ]);
3954
4372
  const handleAgeVerificationYes = (0, import_react3.useCallback)(() => {
3955
4373
  setShowAgeVerification(false);
3956
4374
  setIsMinorFlow(false);
@@ -4001,20 +4419,21 @@ var RedactoNoticeConsent = ({
4001
4419
  } catch {
4002
4420
  }
4003
4421
  }
4004
- setVerificationError(
4005
- "Verification window was closed. Please try again."
4006
- );
4422
+ setVerificationError("Verification window was closed. Please try again.");
4007
4423
  setIsPollingStatus(false);
4008
4424
  isPollingStatusRef.current = false;
4009
4425
  setIsInitiatingVerification(false);
4010
4426
  }, [accessToken, sandboxParams, baseUrl]);
4011
- const startPopupMonitoring = (0, import_react3.useCallback)((popup) => {
4012
- popupCheckIntervalRef.current = window.setInterval(() => {
4013
- if (popup.closed) {
4014
- handlePopupClosed();
4015
- }
4016
- }, 500);
4017
- }, [handlePopupClosed]);
4427
+ const startPopupMonitoring = (0, import_react3.useCallback)(
4428
+ (popup) => {
4429
+ popupCheckIntervalRef.current = window.setInterval(() => {
4430
+ if (popup.closed) {
4431
+ handlePopupClosed();
4432
+ }
4433
+ }, 500);
4434
+ },
4435
+ [handlePopupClosed]
4436
+ );
4018
4437
  const getVerificationErrorMessage = (0, import_react3.useCallback)(
4019
4438
  (errorCode, fallbackError) => {
4020
4439
  switch (errorCode) {
@@ -4116,7 +4535,10 @@ var RedactoNoticeConsent = ({
4116
4535
  return;
4117
4536
  }
4118
4537
  if (!paused) {
4119
- statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
4538
+ statusPollIntervalRef.current = window.setTimeout(
4539
+ poll,
4540
+ POLL_INTERVAL
4541
+ );
4120
4542
  }
4121
4543
  } catch (error) {
4122
4544
  const err = error;
@@ -4137,7 +4559,10 @@ var RedactoNoticeConsent = ({
4137
4559
  );
4138
4560
  return;
4139
4561
  }
4140
- statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
4562
+ statusPollIntervalRef.current = window.setTimeout(
4563
+ poll,
4564
+ POLL_INTERVAL
4565
+ );
4141
4566
  }
4142
4567
  };
4143
4568
  const onVisibilityChange = () => {
@@ -4438,12 +4863,19 @@ var RedactoNoticeConsent = ({
4438
4863
  const dataElementsAudioCount = ((_a2 = purpose.data_elements) == null ? void 0 : _a2.length) || 0;
4439
4864
  const totalPurposeAudioCount = purposeAudioCount + dataElementsAudioCount;
4440
4865
  if (currentIndex >= audioOffset && currentIndex < audioOffset + totalPurposeAudioCount) {
4441
- if (collapsedPurposes[purpose.uuid]) {
4442
- setCollapsedPurposes((prev) => ({
4443
- ...prev,
4444
- [purpose.uuid]: false
4445
- }));
4446
- }
4866
+ setCollapsedPurposes((prev) => {
4867
+ const next = { ...prev };
4868
+ let changed = false;
4869
+ for (const row of purposeRows) {
4870
+ if (row.purpose.uuid !== purpose.uuid) continue;
4871
+ const rowKey = productPurposeKey(row.productUuid, purpose.uuid);
4872
+ if (next[rowKey]) {
4873
+ next[rowKey] = false;
4874
+ changed = true;
4875
+ }
4876
+ }
4877
+ return changed ? next : prev;
4878
+ });
4447
4879
  break;
4448
4880
  }
4449
4881
  audioOffset += totalPurposeAudioCount;
@@ -4521,7 +4953,15 @@ var RedactoNoticeConsent = ({
4521
4953
  return () => {
4522
4954
  ttsAbortController.abort();
4523
4955
  };
4524
- }, [content, selectedLanguage, accessToken, sandboxParams, baseUrl, ledgerBaseUrl, getLanguageCode]);
4956
+ }, [
4957
+ content,
4958
+ selectedLanguage,
4959
+ accessToken,
4960
+ sandboxParams,
4961
+ baseUrl,
4962
+ ledgerBaseUrl,
4963
+ getLanguageCode
4964
+ ]);
4525
4965
  const modalStyle = {
4526
4966
  borderRadius: (settings == null ? void 0 : settings.borderRadius) || "8px",
4527
4967
  backgroundColor: (settings == null ? void 0 : settings.backgroundColor) || "#ffffff",
@@ -5220,7 +5660,7 @@ var RedactoNoticeConsent = ({
5220
5660
  },
5221
5661
  `purpose-section-heading-${selectedLanguage}`
5222
5662
  ),
5223
- categorizedPurposes ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.optionsContainer, children: [
5663
+ categorizedPurposes && !isMultiProduct(content == null ? void 0 : content.products) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.optionsContainer, children: [
5224
5664
  categorizedPurposes.alreadyConsented.map((purpose) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5225
5665
  PurposeItem,
5226
5666
  {
@@ -5262,7 +5702,62 @@ var RedactoNoticeConsent = ({
5262
5702
  ))
5263
5703
  ] }) : (
5264
5704
  /* Regular consent UI */
5265
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles.optionsContainer, children: (_y = content == null ? void 0 : content.purposes) == null ? void 0 : _y.map((purpose) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles.optionsContainer, children: isMultiProduct(content == null ? void 0 : content.products) ? productGroups.map((group) => {
5706
+ const selectedInGroup = group.purposes.filter(
5707
+ (purpose) => selectedPurposes[productPurposeKey(
5708
+ group.product.uuid,
5709
+ purpose.uuid
5710
+ )]
5711
+ ).length;
5712
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
5713
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5714
+ ProductSectionHeader,
5715
+ {
5716
+ product: group.product,
5717
+ purposeCount: group.purposes.length,
5718
+ allSelected: selectedInGroup === group.purposes.length,
5719
+ someSelected: selectedInGroup > 0,
5720
+ collapsed: collapsedProducts[group.product.uuid] || false,
5721
+ locked: isFullyConsentedReviewMode,
5722
+ settings,
5723
+ styles,
5724
+ responsiveStyles,
5725
+ onToggleCollapse: toggleProductCollapse,
5726
+ onToggleSelect: handleProductCheckboxChange,
5727
+ getTranslatedText
5728
+ }
5729
+ ),
5730
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5731
+ "div",
5732
+ {
5733
+ id: `product-${group.product.uuid}`,
5734
+ style: styles.sectionBody,
5735
+ hidden: collapsedProducts[group.product.uuid] || false,
5736
+ children: group.purposes.map((purpose) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5737
+ PurposeItem,
5738
+ {
5739
+ purpose,
5740
+ productUuid: group.product.uuid,
5741
+ selectedPurposes,
5742
+ collapsedPurposes,
5743
+ selectedDataElements,
5744
+ settings,
5745
+ styles,
5746
+ responsiveStyles,
5747
+ onPurposeToggle: handlePurposeCheckboxChange,
5748
+ onPurposeCollapse: togglePurposeCollapse,
5749
+ onDataElementToggle: handleDataElementCheckboxChange,
5750
+ getTranslatedText,
5751
+ isAlreadyConsented: isFullyConsentedReviewMode,
5752
+ selectedLanguage,
5753
+ initialDataElementSelections
5754
+ },
5755
+ `${group.product.uuid}-${purpose.uuid}-${selectedLanguage}`
5756
+ ))
5757
+ }
5758
+ )
5759
+ ] }, group.product.uuid);
5760
+ }) : (_y = content == null ? void 0 : content.purposes) == null ? void 0 : _y.map((purpose) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5266
5761
  PurposeItem,
5267
5762
  {
5268
5763
  purpose,
@@ -5527,16 +6022,20 @@ var submitConsentEvent2 = async ({
5527
6022
  declined,
5528
6023
  meta_data
5529
6024
  };
5530
- const response = await fetch(
5531
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
5532
- {
6025
+ const url = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`;
6026
+ const serialisedBody = JSON.stringify(payload);
6027
+ const response = await withIdempotencyKey(
6028
+ url,
6029
+ serialisedBody,
6030
+ (idempotencyHeaders) => fetch(url, {
5533
6031
  method: "POST",
5534
6032
  headers: {
5535
6033
  Authorization: `Bearer ${accessToken}`,
5536
- "Content-Type": "application/json"
6034
+ "Content-Type": "application/json",
6035
+ ...idempotencyHeaders
5537
6036
  },
5538
- body: JSON.stringify(payload)
5539
- }
6037
+ body: serialisedBody
6038
+ })
5540
6039
  );
5541
6040
  if (!response.ok) {
5542
6041
  throw await parseApiError(
@@ -5973,7 +6472,6 @@ var RedactoConsentInlineComponent = ({
5973
6472
  settings,
5974
6473
  applicationId
5975
6474
  }) => {
5976
- var _a;
5977
6475
  const [content, setContent] = (0, import_react5.useState)(null);
5978
6476
  const [isLoading, setIsLoading] = (0, import_react5.useState)(true);
5979
6477
  const [isSubmitting, setIsSubmitting] = (0, import_react5.useState)(false);
@@ -6030,7 +6528,7 @@ var RedactoConsentInlineComponent = ({
6030
6528
  }
6031
6529
  }, [hasAlreadyConsented, onAccept]);
6032
6530
  const getTranslatedText = (key, defaultText, itemId) => {
6033
- var _a2, _b;
6531
+ var _a, _b, _c;
6034
6532
  if (!content) return defaultText;
6035
6533
  if (selectedLanguage === "English" || selectedLanguage === "en" || selectedLanguage === "EN") {
6036
6534
  if (key === "privacy_policy_anchor_text" && content.privacy_policy_anchor_text) {
@@ -6047,7 +6545,7 @@ var RedactoConsentInlineComponent = ({
6047
6545
  }
6048
6546
  if (itemId) {
6049
6547
  if (key === "purposes.name" || key === "purposes.description") {
6050
- const purposeData = (_a2 = translationMap.purposes) == null ? void 0 : _a2[itemId];
6548
+ const purposeData = (_a = translationMap.purposes) == null ? void 0 : _a[itemId];
6051
6549
  if (purposeData) {
6052
6550
  if (typeof purposeData === "string") {
6053
6551
  return key === "purposes.name" ? purposeData : defaultText;
@@ -6056,8 +6554,18 @@ var RedactoConsentInlineComponent = ({
6056
6554
  }
6057
6555
  return defaultText;
6058
6556
  }
6557
+ if (key === "products.name" || key === "products.description") {
6558
+ const productData = (_b = translationMap.products) == null ? void 0 : _b[itemId];
6559
+ if (productData) {
6560
+ if (typeof productData === "string") {
6561
+ return key === "products.name" ? productData : defaultText;
6562
+ }
6563
+ return key === "products.name" ? productData.name || defaultText : productData.description || defaultText;
6564
+ }
6565
+ return defaultText;
6566
+ }
6059
6567
  if (key.startsWith("data_elements.")) {
6060
- return ((_b = translationMap.data_elements) == null ? void 0 : _b[itemId]) || defaultText;
6568
+ return ((_c = translationMap.data_elements) == null ? void 0 : _c[itemId]) || defaultText;
6061
6569
  }
6062
6570
  }
6063
6571
  const value = translationMap[key];
@@ -6166,25 +6674,27 @@ var RedactoConsentInlineComponent = ({
6166
6674
  controller.abort();
6167
6675
  };
6168
6676
  }, [ledgerBaseUrl, applicationId, accessToken, content, notice_uuid]);
6169
- const togglePurposeCollapse = (purposeUuid) => {
6677
+ const togglePurposeCollapse = (purposeUuid, productUuid) => {
6678
+ const key = productPurposeKey(productUuid, purposeUuid);
6170
6679
  setCollapsedPurposes((prev) => ({
6171
6680
  ...prev,
6172
- [purposeUuid]: !prev[purposeUuid]
6681
+ [key]: !prev[key]
6173
6682
  }));
6174
6683
  };
6175
- const handlePurposeCheckboxChange = (purposeUuid) => {
6684
+ const handlePurposeCheckboxChange = (purposeUuid, productUuid) => {
6176
6685
  if (content) {
6177
6686
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
6178
6687
  if (purpose) {
6179
- const newPurposeState = !selectedPurposes[purposeUuid];
6688
+ const key = productPurposeKey(productUuid, purposeUuid);
6689
+ const newPurposeState = !selectedPurposes[key];
6180
6690
  setSelectedPurposes((prev) => ({
6181
6691
  ...prev,
6182
- [purposeUuid]: newPurposeState
6692
+ [key]: newPurposeState
6183
6693
  }));
6184
6694
  setSelectedDataElements((prev) => {
6185
6695
  const updated = { ...prev };
6186
6696
  purpose.data_elements.forEach((el) => {
6187
- const combinedId = `${purposeUuid}-${el.uuid}`;
6697
+ const combinedId = `${key}-${el.uuid}`;
6188
6698
  updated[combinedId] = newPurposeState;
6189
6699
  });
6190
6700
  return updated;
@@ -6192,7 +6702,7 @@ var RedactoConsentInlineComponent = ({
6192
6702
  }
6193
6703
  }
6194
6704
  };
6195
- const handleDataElementCheckboxChange = (elementUuid, purposeUuid) => {
6705
+ const handleDataElementCheckboxChange = (elementUuid, purposeUuid, productUuid) => {
6196
6706
  if (!content) return;
6197
6707
  const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
6198
6708
  if (!purpose) return;
@@ -6200,7 +6710,8 @@ var RedactoConsentInlineComponent = ({
6200
6710
  (el) => el.uuid === elementUuid
6201
6711
  );
6202
6712
  if (!dataElement) return;
6203
- const combinedId = `${purposeUuid}-${elementUuid}`;
6713
+ const key = productPurposeKey(productUuid, purposeUuid);
6714
+ const combinedId = `${key}-${elementUuid}`;
6204
6715
  const currentState = selectedDataElements[combinedId] || false;
6205
6716
  const newElementState = !currentState;
6206
6717
  setSelectedDataElements((prev) => {
@@ -6214,16 +6725,16 @@ var RedactoConsentInlineComponent = ({
6214
6725
  let shouldCheckPurpose;
6215
6726
  if (requiredElements.length > 0) {
6216
6727
  shouldCheckPurpose = requiredElements.every(
6217
- (el) => newState[`${purposeUuid}-${el.uuid}`]
6728
+ (el) => newState[`${key}-${el.uuid}`]
6218
6729
  );
6219
6730
  } else {
6220
6731
  shouldCheckPurpose = purpose.data_elements.some(
6221
- (el) => newState[`${purposeUuid}-${el.uuid}`]
6732
+ (el) => newState[`${key}-${el.uuid}`]
6222
6733
  );
6223
6734
  }
6224
6735
  setSelectedPurposes((prevPurposes) => ({
6225
6736
  ...prevPurposes,
6226
- [purposeUuid]: shouldCheckPurpose
6737
+ [key]: shouldCheckPurpose
6227
6738
  }));
6228
6739
  return newState;
6229
6740
  });
@@ -6244,20 +6755,36 @@ var RedactoConsentInlineComponent = ({
6244
6755
  baseUrl,
6245
6756
  ledgerBaseUrl,
6246
6757
  noticeUuid: content.notice_uuid,
6247
- purposes: content.purposes.map((purpose) => {
6758
+ // One entry per (product, purpose) once a notice covers several, so a
6759
+ // purpose can be accepted for one product and declined for another.
6760
+ // Collapses to content.purposes below two products, leaving that
6761
+ // payload byte-identical to before.
6762
+ purposes: (isMultiProduct(content.products) ? expandPurposesByProduct(
6763
+ content.products,
6764
+ content.purposes,
6765
+ content.product_purpose_order
6766
+ ).map((row) => ({
6767
+ purpose: row.purpose,
6768
+ productUuid: row.product.uuid
6769
+ })) : content.purposes.map((purpose) => ({
6770
+ purpose,
6771
+ productUuid: void 0
6772
+ }))).map(({ purpose, productUuid }) => {
6773
+ const key = productPurposeKey(productUuid, purpose.uuid);
6248
6774
  const hasSelectedRequiredElement = purpose.data_elements.some(
6249
- (element) => element.required && (selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false)
6775
+ (element) => element.required && (selectedDataElements[`${key}-${element.uuid}`] || false)
6250
6776
  );
6251
6777
  const hasSelectedDataElement = purpose.data_elements.some(
6252
- (element) => selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
6778
+ (element) => selectedDataElements[`${key}-${element.uuid}`] || false
6253
6779
  );
6254
- const purposeSelected = selectedPurposes[purpose.uuid] || hasSelectedRequiredElement || hasSelectedDataElement;
6780
+ const purposeSelected = selectedPurposes[key] || hasSelectedRequiredElement || hasSelectedDataElement;
6255
6781
  return {
6256
6782
  ...purpose,
6783
+ ...productUuid ? { product_uuid: productUuid } : {},
6257
6784
  selected: purposeSelected,
6258
6785
  data_elements: purpose.data_elements.map((element) => ({
6259
6786
  ...element,
6260
- selected: selectedDataElements[`${purpose.uuid}-${element.uuid}`] || false
6787
+ selected: selectedDataElements[`${key}-${element.uuid}`] || false
6261
6788
  }))
6262
6789
  };
6263
6790
  }),
@@ -6358,133 +6885,99 @@ var RedactoConsentInlineComponent = ({
6358
6885
  children: errorMessage
6359
6886
  }
6360
6887
  ),
6361
- (_a = content == null ? void 0 : content.purposes) == null ? void 0 : _a.map((purpose) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
6362
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles2.optionItem, children: [
6363
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6364
- "div",
6365
- {
6366
- style: styles2.optionLeft,
6367
- onClick: () => togglePurposeCollapse(purpose.uuid),
6368
- role: "button",
6369
- tabIndex: 0,
6370
- "aria-expanded": !collapsedPurposes[purpose.uuid],
6371
- "aria-controls": `purpose-${purpose.uuid}`,
6372
- "aria-label": `${collapsedPurposes[purpose.uuid] ? "Expand" : "Collapse"} ${getTranslatedText(
6373
- `purposes.name`,
6374
- purpose.name,
6375
- purpose.uuid
6376
- )} details`,
6377
- onKeyDown: (e) => {
6378
- if (e.key === "Enter" || e.key === " ") {
6379
- e.preventDefault();
6380
- togglePurposeCollapse(purpose.uuid);
6381
- }
6382
- },
6383
- children: [
6384
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6385
- "div",
6386
- {
6387
- style: {
6388
- display: "flex",
6389
- alignItems: "center",
6390
- justifyContent: "center",
6391
- marginLeft: "5px"
6392
- },
6393
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6394
- "svg",
6395
- {
6396
- width: "7",
6397
- height: "12",
6398
- viewBox: "0 0 7 12",
6399
- fill: "none",
6400
- xmlns: "http://www.w3.org/2000/svg",
6401
- stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
6402
- strokeWidth: "2",
6403
- strokeLinecap: "round",
6404
- strokeLinejoin: "round",
6405
- style: {
6406
- transform: !collapsedPurposes[purpose.uuid] ? "rotate(90deg)" : "rotate(0deg)",
6407
- transition: "transform 0.3s ease"
6408
- },
6409
- "aria-hidden": "true",
6410
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M1 1L6 6L1 11" })
6411
- }
6412
- )
6413
- }
6414
- ),
6415
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles2.optionTextContainer, children: [
6416
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6417
- "h2",
6418
- {
6419
- style: {
6420
- ...styles2.optionTitle,
6421
- ...responsiveStyles.optionTitle,
6422
- ...headingStyle
6423
- },
6424
- children: [
6425
- getTranslatedText(
6426
- `purposes.name`,
6427
- purpose.name,
6428
- purpose.uuid
6429
- ),
6430
- hasRequiredDataElement(purpose) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RequiredMarker, {})
6431
- ]
6432
- }
6433
- ),
6434
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6435
- "h3",
6436
- {
6437
- style: {
6438
- ...styles2.optionDescription,
6439
- ...responsiveStyles.optionDescription,
6440
- ...textStyle
6441
- },
6442
- children: getTranslatedText(
6443
- `purposes.description`,
6444
- purpose.description,
6445
- purpose.uuid
6446
- )
6447
- }
6448
- )
6449
- ] })
6450
- ]
6451
- }
6452
- ),
6453
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6454
- "input",
6455
- {
6456
- type: "checkbox",
6457
- checked: selectedPurposes[purpose.uuid] || false,
6458
- onChange: () => handlePurposeCheckboxChange(purpose.uuid),
6459
- "aria-label": `Select all data elements for ${getTranslatedText(
6460
- `purposes.name`,
6461
- purpose.name,
6462
- purpose.uuid
6463
- )}`
6464
- }
6465
- )
6466
- ] }),
6467
- !collapsedPurposes[purpose.uuid] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6468
- "div",
6469
- {
6470
- id: `purpose-${purpose.uuid}`,
6471
- style: styles2.dataElementsContainer,
6472
- children: purpose.data_elements.map((dataElement) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles2.dataElementItem, children: [
6888
+ (() => {
6889
+ var _a, _b;
6890
+ const renderPurpose = (purpose, productUuid) => {
6891
+ const stateKey = productPurposeKey(productUuid, purpose.uuid);
6892
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
6893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles2.optionItem, children: [
6473
6894
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6474
- "span",
6895
+ "div",
6475
6896
  {
6476
- style: {
6477
- ...styles2.dataElementText,
6478
- ...responsiveStyles.dataElementText,
6479
- ...textStyle
6897
+ style: styles2.optionLeft,
6898
+ onClick: () => togglePurposeCollapse(purpose.uuid, productUuid),
6899
+ role: "button",
6900
+ tabIndex: 0,
6901
+ "aria-expanded": !collapsedPurposes[stateKey],
6902
+ "aria-controls": `purpose-${stateKey}`,
6903
+ "aria-label": `${collapsedPurposes[stateKey] ? "Expand" : "Collapse"} ${getTranslatedText(
6904
+ `purposes.name`,
6905
+ purpose.name,
6906
+ purpose.uuid
6907
+ )} details`,
6908
+ onKeyDown: (e) => {
6909
+ if (e.key === "Enter" || e.key === " ") {
6910
+ e.preventDefault();
6911
+ togglePurposeCollapse(purpose.uuid, productUuid);
6912
+ }
6480
6913
  },
6481
6914
  children: [
6482
- getTranslatedText(
6483
- `data_elements.name`,
6484
- dataElement.name,
6485
- dataElement.uuid
6915
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6916
+ "div",
6917
+ {
6918
+ style: {
6919
+ display: "flex",
6920
+ alignItems: "center",
6921
+ justifyContent: "center",
6922
+ marginLeft: "5px"
6923
+ },
6924
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6925
+ "svg",
6926
+ {
6927
+ width: "7",
6928
+ height: "12",
6929
+ viewBox: "0 0 7 12",
6930
+ fill: "none",
6931
+ xmlns: "http://www.w3.org/2000/svg",
6932
+ stroke: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
6933
+ strokeWidth: "2",
6934
+ strokeLinecap: "round",
6935
+ strokeLinejoin: "round",
6936
+ style: {
6937
+ transform: !collapsedPurposes[stateKey] ? "rotate(90deg)" : "rotate(0deg)",
6938
+ transition: "transform 0.3s ease"
6939
+ },
6940
+ "aria-hidden": "true",
6941
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M1 1L6 6L1 11" })
6942
+ }
6943
+ )
6944
+ }
6486
6945
  ),
6487
- dataElement.required && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RequiredMarker, {})
6946
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles2.optionTextContainer, children: [
6947
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6948
+ "h2",
6949
+ {
6950
+ style: {
6951
+ ...styles2.optionTitle,
6952
+ ...responsiveStyles.optionTitle,
6953
+ ...headingStyle
6954
+ },
6955
+ children: [
6956
+ getTranslatedText(
6957
+ `purposes.name`,
6958
+ purpose.name,
6959
+ purpose.uuid
6960
+ ),
6961
+ hasRequiredDataElement(purpose) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RequiredMarker, {})
6962
+ ]
6963
+ }
6964
+ ),
6965
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6966
+ "h3",
6967
+ {
6968
+ style: {
6969
+ ...styles2.optionDescription,
6970
+ ...responsiveStyles.optionDescription,
6971
+ ...textStyle
6972
+ },
6973
+ children: getTranslatedText(
6974
+ `purposes.description`,
6975
+ purpose.description,
6976
+ purpose.uuid
6977
+ )
6978
+ }
6979
+ )
6980
+ ] })
6488
6981
  ]
6489
6982
  }
6490
6983
  ),
@@ -6492,22 +6985,87 @@ var RedactoConsentInlineComponent = ({
6492
6985
  "input",
6493
6986
  {
6494
6987
  type: "checkbox",
6495
- checked: selectedDataElements[`${purpose.uuid}-${dataElement.uuid}`] || false,
6496
- onChange: () => handleDataElementCheckboxChange(
6497
- dataElement.uuid,
6988
+ checked: selectedPurposes[stateKey] || false,
6989
+ onChange: () => handlePurposeCheckboxChange(purpose.uuid, productUuid),
6990
+ "aria-label": `Select all data elements for ${getTranslatedText(
6991
+ `purposes.name`,
6992
+ purpose.name,
6498
6993
  purpose.uuid
6499
- ),
6500
- "aria-label": `Select ${getTranslatedText(
6501
- `data_elements.name`,
6502
- dataElement.name,
6503
- dataElement.uuid
6504
- )}${dataElement.required ? " (required)" : ""}`
6994
+ )}`
6505
6995
  }
6506
6996
  )
6507
- ] }, dataElement.uuid))
6508
- }
6509
- )
6510
- ] }, purpose.uuid))
6997
+ ] }),
6998
+ !collapsedPurposes[stateKey] && purpose.data_elements && purpose.data_elements.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6999
+ "div",
7000
+ {
7001
+ id: `purpose-${stateKey}`,
7002
+ style: styles2.dataElementsContainer,
7003
+ children: purpose.data_elements.map((dataElement) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
7004
+ "div",
7005
+ {
7006
+ style: styles2.dataElementItem,
7007
+ children: [
7008
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
7009
+ "span",
7010
+ {
7011
+ style: {
7012
+ ...styles2.dataElementText,
7013
+ ...responsiveStyles.dataElementText,
7014
+ ...textStyle
7015
+ },
7016
+ children: [
7017
+ getTranslatedText(
7018
+ `data_elements.name`,
7019
+ dataElement.name,
7020
+ dataElement.uuid
7021
+ ),
7022
+ dataElement.required && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RequiredMarker, {})
7023
+ ]
7024
+ }
7025
+ ),
7026
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
7027
+ "input",
7028
+ {
7029
+ type: "checkbox",
7030
+ checked: selectedDataElements[`${stateKey}-${dataElement.uuid}`] || false,
7031
+ onChange: () => handleDataElementCheckboxChange(
7032
+ dataElement.uuid,
7033
+ purpose.uuid,
7034
+ productUuid
7035
+ ),
7036
+ "aria-label": `Select ${getTranslatedText(
7037
+ `data_elements.name`,
7038
+ dataElement.name,
7039
+ dataElement.uuid
7040
+ )}${dataElement.required ? " (required)" : ""}`
7041
+ }
7042
+ )
7043
+ ]
7044
+ },
7045
+ dataElement.uuid
7046
+ ))
7047
+ }
7048
+ )
7049
+ ] }, stateKey);
7050
+ };
7051
+ if (!isMultiProduct(content == null ? void 0 : content.products)) {
7052
+ return (_a = content == null ? void 0 : content.purposes) == null ? void 0 : _a.map((purpose) => renderPurpose(purpose));
7053
+ }
7054
+ return groupPurposesByProduct(
7055
+ content == null ? void 0 : content.products,
7056
+ (_b = content == null ? void 0 : content.purposes) != null ? _b : [],
7057
+ content == null ? void 0 : content.product_purpose_order
7058
+ ).map((group) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
7059
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...styles2.optionTitle, margin: "0 0 8px" }, children: getTranslatedText(
7060
+ "products.name",
7061
+ group.product.name,
7062
+ group.product.uuid
7063
+ ) }),
7064
+ group.purposes.map(
7065
+ (purpose) => renderPurpose(purpose, group.product.uuid)
7066
+ )
7067
+ ] }, group.product.uuid));
7068
+ })()
6511
7069
  ] });
6512
7070
  };
6513
7071
  RedactoConsentInlineComponent.displayName = "RedactoConsentInline";
@@ -7647,16 +8205,22 @@ var submitAssistedConsent = async ({
7647
8205
  language: language || void 0
7648
8206
  };
7649
8207
  const url = `${publicScope(ledgerBaseUrl || baseUrl, organisationUuid, workspaceUuid)}/submit-consent`;
7650
- const response = await fetch(url, {
7651
- method: "POST",
7652
- headers: {
7653
- accept: "application/json",
7654
- Authorization: `Bearer ${accessToken}`,
7655
- "Content-Type": "application/json"
7656
- },
7657
- body: JSON.stringify(payload),
7658
- signal
7659
- });
8208
+ const serialisedBody = JSON.stringify(payload);
8209
+ const response = await withIdempotencyKey(
8210
+ url,
8211
+ serialisedBody,
8212
+ (idempotencyHeaders) => fetch(url, {
8213
+ method: "POST",
8214
+ headers: {
8215
+ accept: "application/json",
8216
+ Authorization: `Bearer ${accessToken}`,
8217
+ "Content-Type": "application/json",
8218
+ ...idempotencyHeaders
8219
+ },
8220
+ body: serialisedBody,
8221
+ signal
8222
+ })
8223
+ );
7660
8224
  if (!response.ok) {
7661
8225
  throw await parseApiError(
7662
8226
  response,
@@ -9649,10 +10213,14 @@ var RedactoNoticeAssisted = ({
9649
10213
  const handleConfirmStep = (0, import_react7.useCallback)(
9650
10214
  (mode = "selected") => {
9651
10215
  if (mode !== "selected" && content) {
9652
- const selection = buildSelectionForMode(content.purposes, mode, {
9653
- selectedPurposes,
9654
- selectedDataElements
9655
- });
10216
+ const selection = buildSelectionForMode(
10217
+ noticePurposeRows(void 0, content.purposes),
10218
+ mode,
10219
+ {
10220
+ selectedPurposes,
10221
+ selectedDataElements
10222
+ }
10223
+ );
9656
10224
  setSelectedPurposes(selection.selectedPurposes);
9657
10225
  setSelectedDataElements(selection.selectedDataElements);
9658
10226
  }
@@ -9853,10 +10421,14 @@ var RedactoNoticeAssisted = ({
9853
10421
  return;
9854
10422
  }
9855
10423
  }
9856
- const selection = buildSelectionForMode(content.purposes, acceptMode, {
9857
- selectedPurposes,
9858
- selectedDataElements
9859
- });
10424
+ const selection = buildSelectionForMode(
10425
+ noticePurposeRows(void 0, content.purposes),
10426
+ acceptMode,
10427
+ {
10428
+ selectedPurposes,
10429
+ selectedDataElements
10430
+ }
10431
+ );
9860
10432
  const submitPurposes = content.purposes.map((purpose) => ({
9861
10433
  ...purpose,
9862
10434
  selected: selection.selectedPurposes[purpose.uuid] || false,