@redacto.io/consent-sdk-js 1.0.0-beta.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/redacto.js CHANGED
@@ -182,17 +182,6 @@ function createMediaQuery(query) {
182
182
  };
183
183
  }
184
184
 
185
- // src/core/jwt-decode.ts
186
- var import_jwt_decode = require("jwt-decode");
187
- function decodeTokenSafely(token) {
188
- try {
189
- return (0, import_jwt_decode.jwtDecode)(token);
190
- } catch (error) {
191
- console.warn("Failed to decode JWT token:", error);
192
- return null;
193
- }
194
- }
195
-
196
185
  // src/services/cache.ts
197
186
  var apiCache = /* @__PURE__ */ new Map();
198
187
  var CACHE_TTL = 5 * 60 * 1e3;
@@ -213,6 +202,141 @@ function clearApiCache() {
213
202
  apiCache.clear();
214
203
  }
215
204
 
205
+ // src/core/jwt-decode.ts
206
+ var import_jwt_decode = require("jwt-decode");
207
+ function decodeTokenSafely(token) {
208
+ try {
209
+ return (0, import_jwt_decode.jwtDecode)(token);
210
+ } catch (error) {
211
+ console.warn("Failed to decode JWT token:", error);
212
+ return null;
213
+ }
214
+ }
215
+
216
+ // src/api/sandbox.ts
217
+ var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
218
+ var resolveSandboxAuth = (options) => {
219
+ var _a, _b, _c, _d, _e, _f;
220
+ const token = (_a = options.token) == null ? void 0 : _a.trim();
221
+ if (!token) {
222
+ return null;
223
+ }
224
+ const organisationUuid = (_b = options.organisationUuid) == null ? void 0 : _b.trim();
225
+ const workspaceUuid = (_c = options.workspaceUuid) == null ? void 0 : _c.trim();
226
+ const ucic = (_d = options.sandboxUcic) == null ? void 0 : _d.trim();
227
+ const email = (_e = options.sandboxEmail) == null ? void 0 : _e.trim();
228
+ const mobile = (_f = options.sandboxMobile) == null ? void 0 : _f.trim();
229
+ if (!organisationUuid || !workspaceUuid) {
230
+ throw new Error(
231
+ "Sandbox mode requires organisationUuid and workspaceUuid"
232
+ );
233
+ }
234
+ if (!ucic && !email && !mobile) {
235
+ throw new Error("Sandbox mode requires a UCIC, email, or mobile");
236
+ }
237
+ return { token, ucic, email, mobile, organisationUuid, workspaceUuid };
238
+ };
239
+ var resolveRequestAuth = (accessToken, options) => {
240
+ const sandbox = resolveSandboxAuth(options);
241
+ if (sandbox) {
242
+ return {
243
+ organisationUuid: sandbox.organisationUuid,
244
+ workspaceUuid: sandbox.workspaceUuid,
245
+ sandbox
246
+ };
247
+ }
248
+ const decodedToken = accessToken ? decodeTokenSafely(accessToken) : null;
249
+ if (!decodedToken) {
250
+ throw new Error("Invalid access token");
251
+ }
252
+ const { organisation_uuid, workspace_uuid } = decodedToken;
253
+ if (!organisation_uuid || !workspace_uuid) {
254
+ throw new Error("Invalid token: missing organization or workspace UUID");
255
+ }
256
+ return {
257
+ organisationUuid: organisation_uuid,
258
+ workspaceUuid: workspace_uuid,
259
+ sandbox: null
260
+ };
261
+ };
262
+ var buildAuthHeaders = (accessToken, sandbox) => {
263
+ if (sandbox) {
264
+ return { [SANDBOX_TOKEN_HEADER]: sandbox.token };
265
+ }
266
+ return { Authorization: `Bearer ${accessToken}` };
267
+ };
268
+ var sandboxBodyIdentity = (sandbox) => {
269
+ const payload = {};
270
+ if (sandbox.ucic) {
271
+ payload.org_user_id = sandbox.ucic;
272
+ }
273
+ if (sandbox.email) {
274
+ payload.primary_email = sandbox.email;
275
+ }
276
+ if (sandbox.mobile) {
277
+ payload.primary_mobile = sandbox.mobile;
278
+ }
279
+ return payload;
280
+ };
281
+ var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
282
+
283
+ // src/api/constants.ts
284
+ var CODE_BY_NAME = /* @__PURE__ */ new Map([
285
+ ["english", "en"],
286
+ ["assamese", "as"],
287
+ ["bengali", "bn"],
288
+ ["bodo", "brx"],
289
+ ["dogri", "doi"],
290
+ ["goan konkani", "gom"],
291
+ ["gujarati", "gu"],
292
+ ["hindi", "hi"],
293
+ ["kannada", "kn"],
294
+ ["kashmiri", "ks"],
295
+ ["maithili", "mai"],
296
+ ["malayalam", "ml"],
297
+ ["manipuri", "mni-Mtei"],
298
+ ["marathi", "mr"],
299
+ ["nepali", "ne"],
300
+ ["odia", "or"],
301
+ ["punjabi", "pa"],
302
+ ["sanskrit", "sa"],
303
+ ["santali", "sat"],
304
+ ["sindhi", "sd"],
305
+ ["tamil", "ta"],
306
+ ["telugu", "te"],
307
+ ["urdu", "ur"]
308
+ ]);
309
+ var CODE_BY_LOWER = new Map(
310
+ [...CODE_BY_NAME.values()].map((c) => [c.toLowerCase(), c])
311
+ );
312
+
313
+ // src/api/language.ts
314
+ function toLanguageCode(lang) {
315
+ var _a, _b;
316
+ if (!lang) {
317
+ return void 0;
318
+ }
319
+ const trimmed = lang.trim();
320
+ if (!trimmed) {
321
+ return void 0;
322
+ }
323
+ const lower = trimmed.toLowerCase();
324
+ return (_b = (_a = CODE_BY_LOWER.get(lower)) != null ? _a : CODE_BY_NAME.get(lower)) != null ? _b : trimmed;
325
+ }
326
+ function resolveDisplayLanguage(requested, defaultLanguage, translations) {
327
+ const fallback = defaultLanguage || "en";
328
+ const requestedCode = toLanguageCode(requested);
329
+ if (!requestedCode) {
330
+ return fallback;
331
+ }
332
+ for (const key of Object.keys(translations || {})) {
333
+ if (toLanguageCode(key) === requestedCode) {
334
+ return key;
335
+ }
336
+ }
337
+ return fallback;
338
+ }
339
+
216
340
  // src/api/index.ts
217
341
  var BASE_URL = "https://api.redacto.io/consent";
218
342
  var BASE_URL_PUBLIC = "https://api.redacto.tech/consent";
@@ -224,17 +348,22 @@ var fetchConsentContent = async ({
224
348
  specific_uuid,
225
349
  validate_against = "all",
226
350
  include_fully_consented_data = false,
227
- signal
351
+ signal,
352
+ ...sandboxParams
228
353
  }) => {
229
- if (!noticeId || !accessToken || !validate_against) {
230
- throw new Error("noticeId and accessToken are required");
354
+ if (!noticeId || !validate_against) {
355
+ throw new Error("noticeId is required");
231
356
  }
232
- const decodedToken = decodeTokenSafely(accessToken);
233
- if (!decodedToken) throw new Error("Invalid access token");
234
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
235
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
357
+ if (!accessToken && !sandboxParams.token) {
358
+ throw new Error("accessToken or token is required");
359
+ }
360
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
361
+ accessToken,
362
+ sandboxParams
363
+ );
236
364
  const apiBaseUrl = baseUrl || BASE_URL;
237
- const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
365
+ const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.email || sandbox.mobile}` : accessToken;
366
+ const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
238
367
  if (validate_against === "all" && !specific_uuid) {
239
368
  const cached = getCachedData(cacheKey);
240
369
  if (cached) return cached;
@@ -243,10 +372,15 @@ var fetchConsentContent = async ({
243
372
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
244
373
  url.searchParams.append("validate_against", validate_against);
245
374
  if (include_fully_consented_data) url.searchParams.append("include_fully_consented_data", "true");
375
+ if (sandbox) {
376
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
377
+ if (value) url.searchParams.append(key, value);
378
+ }
379
+ }
246
380
  const response = await fetch(url.toString(), {
247
381
  method: "GET",
248
382
  headers: {
249
- Authorization: `Bearer ${accessToken}`,
383
+ ...buildAuthHeaders(accessToken, sandbox),
250
384
  "Accept-Language": language
251
385
  },
252
386
  signal
@@ -272,18 +406,24 @@ var fetchConsentContentPublic = async ({
272
406
  org_uuid,
273
407
  workspace_uuid,
274
408
  notice_uuid,
409
+ accessToken,
275
410
  baseUrl,
276
411
  language = "en",
277
412
  specific_uuid
278
413
  }) => {
279
414
  const apiBaseUrl = baseUrl || BASE_URL_PUBLIC;
415
+ const noticesUrl = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices`;
280
416
  const url = new URL(
281
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices/get-notice/${notice_uuid}`
417
+ accessToken ? `${noticesUrl}/${notice_uuid}` : `${noticesUrl}/get-notice/${notice_uuid}`
282
418
  );
283
419
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
284
420
  const response = await fetch(url.toString(), {
285
421
  method: "GET",
286
- headers: { "Content-Type": "application/json" }
422
+ headers: {
423
+ "Content-Type": "application/json",
424
+ "Accept-Language": language,
425
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
426
+ }
287
427
  });
288
428
  if (response.status === 401) {
289
429
  const error = new Error("Unauthorized");
@@ -310,16 +450,21 @@ var submitConsentEvent = async ({
310
450
  meta_data,
311
451
  guardianVerificationReference,
312
452
  selfDeclaredAdult,
313
- signal
453
+ language,
454
+ signal,
455
+ ...sandboxParams
314
456
  }) => {
315
- if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
316
- throw new Error("noticeUuid, accessToken, and purposes array are required");
457
+ if (!noticeUuid || !Array.isArray(purposes)) {
458
+ throw new Error("noticeUuid and purposes array are required");
317
459
  }
318
- const decodedToken = decodeTokenSafely(accessToken);
319
- if (!decodedToken) throw new Error("Invalid access token");
320
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
321
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
322
- const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
460
+ if (!accessToken && !sandboxParams.token) {
461
+ throw new Error("accessToken or token is required");
462
+ }
463
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
464
+ accessToken,
465
+ sandboxParams
466
+ );
467
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
323
468
  const validatedPurposes = purposes.map((purpose) => {
324
469
  if (!purpose.uuid) throw new Error("Invalid purpose data: missing uuid");
325
470
  const dataElements = (purpose.data_elements || []).filter((element) => element.enabled).map((element) => ({
@@ -340,17 +485,19 @@ var submitConsentEvent = async ({
340
485
  declined: Boolean(declined),
341
486
  meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid, minor_age: meta_data.minor_age } : void 0,
342
487
  guardian_verification_reference: guardianVerificationReference,
343
- self_declared_adult: selfDeclaredAdult
488
+ self_declared_adult: selfDeclaredAdult,
489
+ language: toLanguageCode(language)
344
490
  };
491
+ const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
345
492
  const response = await fetch(
346
493
  `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/submit-consent`,
347
494
  {
348
495
  method: "POST",
349
496
  headers: {
350
- Authorization: `Bearer ${accessToken}`,
497
+ ...buildAuthHeaders(accessToken, sandbox),
351
498
  "Content-Type": "application/json"
352
499
  },
353
- body: JSON.stringify(payload),
500
+ body: JSON.stringify(requestBody),
354
501
  signal
355
502
  }
356
503
  );
@@ -382,7 +529,8 @@ var submitConsentEventPublic = async ({
382
529
  noticeUuid,
383
530
  purposes,
384
531
  declined,
385
- meta_data
532
+ meta_data,
533
+ language
386
534
  }) => {
387
535
  var _a, _b;
388
536
  const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL_PUBLIC;
@@ -403,7 +551,8 @@ var submitConsentEventPublic = async ({
403
551
  select_all_mandatory: false,
404
552
  source: "WEB",
405
553
  declined,
406
- meta_data
554
+ meta_data,
555
+ language: toLanguageCode(language)
407
556
  };
408
557
  const response = await fetch(
409
558
  `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
@@ -442,23 +591,32 @@ var submitConsentEventPublic = async ({
442
591
  var fetchTTSAudioUrls = async ({
443
592
  accessToken,
444
593
  baseUrl,
594
+ ledgerBaseUrl,
445
595
  noticeUuid,
446
596
  language,
447
- signal
597
+ signal,
598
+ ...sandboxParams
448
599
  }) => {
449
- if (!accessToken || !noticeUuid || !language) {
450
- throw new Error("accessToken, noticeUuid, and language are required");
600
+ if (!accessToken && !sandboxParams.token || !noticeUuid || !language) {
601
+ throw new Error("accessToken (or token), noticeUuid, and language are required");
451
602
  }
452
- const decodedToken = decodeTokenSafely(accessToken);
453
- if (!decodedToken) throw new Error("Invalid access token");
454
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
455
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
456
- const apiBaseUrl = baseUrl || BASE_URL;
457
- const url = `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/notices/${noticeUuid}/audio/${language}`;
458
- const response = await fetch(url, {
603
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
604
+ accessToken,
605
+ sandboxParams
606
+ );
607
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
608
+ const url = new URL(
609
+ `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/notices/${noticeUuid}/audio/${language}`
610
+ );
611
+ if (sandbox) {
612
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
613
+ if (value) url.searchParams.append(key, value);
614
+ }
615
+ }
616
+ const response = await fetch(url.toString(), {
459
617
  method: "GET",
460
618
  headers: {
461
- Authorization: `Bearer ${accessToken}`,
619
+ ...buildAuthHeaders(accessToken, sandbox),
462
620
  Accept: "application/json"
463
621
  },
464
622
  signal
@@ -925,6 +1083,42 @@ var styles = {
925
1083
  padding: 0,
926
1084
  boxSizing: "border-box"
927
1085
  },
1086
+ // The main notice header is two stacked rows: the logo alone on row 1,
1087
+ // aligned per active_config.logo_position, then the title and the
1088
+ // TTS/language controls on row 2. `topSection` and `topLeft` above keep their
1089
+ // single-row shape for the age-verification, guardian and verification
1090
+ // screens, which have no controls to place and share those styles.
1091
+ // Mirrors consent-sdk-react/src/RedactoNoticeConsent/styles.ts.
1092
+ headerStack: {
1093
+ display: "flex",
1094
+ flexDirection: "column",
1095
+ alignItems: "stretch",
1096
+ gap: "10px",
1097
+ paddingBottom: "15px",
1098
+ borderBottom: "none",
1099
+ backgroundColor: "#ffffff",
1100
+ margin: 0,
1101
+ boxSizing: "border-box",
1102
+ textAlign: "left"
1103
+ },
1104
+ logoRow: {
1105
+ display: "flex",
1106
+ alignItems: "center",
1107
+ width: "100%",
1108
+ margin: 0,
1109
+ padding: 0,
1110
+ boxSizing: "border-box"
1111
+ },
1112
+ headerRow: {
1113
+ display: "flex",
1114
+ alignItems: "center",
1115
+ justifyContent: "space-between",
1116
+ gap: "10px",
1117
+ width: "100%",
1118
+ margin: 0,
1119
+ padding: 0,
1120
+ boxSizing: "border-box"
1121
+ },
928
1122
  logo: {
929
1123
  height: "32px",
930
1124
  width: "auto",
@@ -1131,6 +1325,32 @@ var styles = {
1131
1325
  margin: 0,
1132
1326
  boxSizing: "border-box"
1133
1327
  },
1328
+ /**
1329
+ * The main notice footer. Wide: one row with decline pushed left and the
1330
+ * accepts grouped right. Narrow: a full-width stack. Review mode keeps
1331
+ * `bottomSection` — it collapses to a single button.
1332
+ */
1333
+ actionButtonRow: {
1334
+ display: "flex",
1335
+ flexDirection: "row",
1336
+ alignItems: "center",
1337
+ gap: "12px",
1338
+ paddingTop: "15px",
1339
+ borderTop: "none",
1340
+ backgroundColor: "#ffffff",
1341
+ margin: 0,
1342
+ boxSizing: "border-box"
1343
+ },
1344
+ actionButtonStack: {
1345
+ display: "flex",
1346
+ flexDirection: "column",
1347
+ gap: "12px",
1348
+ paddingTop: "15px",
1349
+ borderTop: "none",
1350
+ backgroundColor: "#ffffff",
1351
+ margin: 0,
1352
+ boxSizing: "border-box"
1353
+ },
1134
1354
  button: {
1135
1355
  width: "100%",
1136
1356
  borderRadius: "8px",
@@ -1156,6 +1376,9 @@ var styles = {
1156
1376
  },
1157
1377
  languageSelectorContainer: {
1158
1378
  position: "relative",
1379
+ // Shares `headerRow` with the title, which grows; without this a long
1380
+ // heading squeezes the language button instead of wrapping itself.
1381
+ flexShrink: 0,
1159
1382
  margin: 0,
1160
1383
  padding: 0,
1161
1384
  boxSizing: "border-box"
@@ -1214,6 +1437,51 @@ var styles = {
1214
1437
  }
1215
1438
  };
1216
1439
 
1440
+ // src/constants.ts
1441
+ var DEFAULT_LOGO_POSITION = "left";
1442
+ var DEFAULT_ACCEPT_ALL_BUTTON_TEXT = "Accept All";
1443
+ var DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT = "Accept Selected";
1444
+ var DEFAULT_DECLINE_BUTTON_TEXT = "Decline";
1445
+ var ACCEPT_ALL_BUTTON_ID = "accept-all-button-text";
1446
+ var ACCEPT_SELECTED_BUTTON_ID = "confirm-button-text";
1447
+ var DECLINE_BUTTON_ID = "decline-button-text";
1448
+ var SUBMITTING_BUTTON_TEXT = "Submitting...";
1449
+ var LOGO_POSITION_JUSTIFY = {
1450
+ left: "flex-start",
1451
+ center: "center",
1452
+ right: "flex-end"
1453
+ };
1454
+ var REQUIRED_MARKER_GLYPH = "*";
1455
+ var REQUIRED_MARKER_ARIA_LABEL = "required";
1456
+ var REQUIRED_MARKER_COLOR = "red";
1457
+ var REQUIRED_MARKER_GAP = "4px";
1458
+ var REQUIRED_MARKER_STYLE = {
1459
+ color: REQUIRED_MARKER_COLOR,
1460
+ marginLeft: REQUIRED_MARKER_GAP
1461
+ };
1462
+ var DPO_AUDIO_ELEMENT_MAP = [
1463
+ { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1464
+ { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1465
+ { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1466
+ { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1467
+ { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1468
+ { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1469
+ { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1470
+ { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1471
+ ];
1472
+
1473
+ // src/core/required-marker.ts
1474
+ function createRequiredMarker(label = REQUIRED_MARKER_ARIA_LABEL) {
1475
+ return el(
1476
+ "span",
1477
+ { style: REQUIRED_MARKER_STYLE, "aria-label": label },
1478
+ REQUIRED_MARKER_GLYPH
1479
+ );
1480
+ }
1481
+ function hasRequiredDataElement(purpose) {
1482
+ return purpose.data_elements.some((element) => element.required);
1483
+ }
1484
+
1217
1485
  // src/modal/purposes.ts
1218
1486
  function renderModalPurposes(purposes, options) {
1219
1487
  var _a, _b;
@@ -1279,6 +1547,9 @@ function renderModalPurposes(purposes, options) {
1279
1547
  }
1280
1548
  });
1281
1549
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
1550
+ if (hasRequiredDataElement(purpose)) {
1551
+ titleEl.appendChild(createRequiredMarker());
1552
+ }
1282
1553
  titleElements.set(purpose.uuid, titleEl);
1283
1554
  const descEl = el("h3", {
1284
1555
  id: `purpose-description-${purpose.uuid}`,
@@ -1346,9 +1617,7 @@ function renderModalPurposes(purposes, options) {
1346
1617
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
1347
1618
  dataElementLabels.set(combinedId, labelSpan);
1348
1619
  if (dataElement.required) {
1349
- const asterisk = el("span", { style: { color: "red", marginLeft: "4px" }, "aria-label": "required" });
1350
- asterisk.textContent = "*";
1351
- labelSpan.appendChild(asterisk);
1620
+ labelSpan.appendChild(createRequiredMarker());
1352
1621
  }
1353
1622
  let deRightElement;
1354
1623
  if (!isAlreadyConsented) {
@@ -1717,18 +1986,6 @@ var updateConsentState = (updates) => {
1717
1986
  }
1718
1987
  };
1719
1988
 
1720
- // src/constants.ts
1721
- var DPO_AUDIO_ELEMENT_MAP = [
1722
- { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1723
- { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1724
- { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1725
- { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1726
- { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1727
- { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1728
- { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1729
- { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1730
- ];
1731
-
1732
1989
  // src/modal/tts.ts
1733
1990
  var TTSManager = class {
1734
1991
  /**
@@ -1776,9 +2033,16 @@ var TTSManager = class {
1776
2033
  const response = await fetchTTSAudioUrls({
1777
2034
  accessToken: params.accessToken,
1778
2035
  baseUrl: params.baseUrl,
2036
+ ledgerBaseUrl: params.ledgerBaseUrl,
1779
2037
  noticeUuid: params.noticeUuid,
1780
2038
  language: params.language,
1781
- signal: this.abortController.signal
2039
+ signal: this.abortController.signal,
2040
+ token: params.token,
2041
+ sandboxUcic: params.sandboxUcic,
2042
+ sandboxEmail: params.sandboxEmail,
2043
+ sandboxMobile: params.sandboxMobile,
2044
+ organisationUuid: params.organisationUuid,
2045
+ workspaceUuid: params.workspaceUuid
1782
2046
  });
1783
2047
  this.audioUrls = response.detail;
1784
2048
  this.buildSequences(params.content);
@@ -1832,8 +2096,9 @@ var TTSManager = class {
1832
2096
  push(audioUrl, mapping.elementId);
1833
2097
  }
1834
2098
  }
1835
- push(urls.confirm_button_text_audio_url, "confirm-button-text");
1836
- push(urls.decline_button_text_audio_url, "decline-button-text");
2099
+ push(urls.accept_all_button_text_audio_url, ACCEPT_ALL_BUTTON_ID);
2100
+ push(urls.confirm_button_text_audio_url, ACCEPT_SELECTED_BUTTON_ID);
2101
+ push(urls.decline_button_text_audio_url, DECLINE_BUTTON_ID);
1837
2102
  this.audioSequence = audioSeq;
1838
2103
  this.elementIdSequence = elementSeq;
1839
2104
  }
@@ -2724,14 +2989,17 @@ function renderVerificationScreen(options) {
2724
2989
 
2725
2990
  // src/modal/colors.ts
2726
2991
  function resolveColors(settings, activeConfig) {
2727
- var _a, _b, _c, _d, _e, _f, _g, _h;
2992
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
2728
2993
  const primaryColor = (activeConfig == null ? void 0 : activeConfig.primary_color) || "#4f87ff";
2729
2994
  const secondaryColor = (activeConfig == null ? void 0 : activeConfig.secondary_color) || "#4f87ff";
2730
2995
  return {
2731
2996
  acceptBg: ((_b = (_a = settings == null ? void 0 : settings.button) == null ? void 0 : _a.accept) == null ? void 0 : _b.backgroundColor) || primaryColor,
2732
2997
  acceptText: ((_d = (_c = settings == null ? void 0 : settings.button) == null ? void 0 : _c.accept) == null ? void 0 : _d.textColor) || "#ffffff",
2733
- declineBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.decline) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2734
- declineText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.decline) == null ? void 0 : _h.textColor) || "#000000",
2998
+ acceptShortcutBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.acceptAll) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2999
+ acceptShortcutText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.acceptAll) == null ? void 0 : _h.textColor) || primaryColor,
3000
+ acceptShortcutBorder: primaryColor,
3001
+ declineBg: ((_j = (_i = settings == null ? void 0 : settings.button) == null ? void 0 : _i.decline) == null ? void 0 : _j.backgroundColor) || "#ffffff",
3002
+ declineText: ((_l = (_k = settings == null ? void 0 : settings.button) == null ? void 0 : _k.decline) == null ? void 0 : _l.textColor) || "#000000",
2735
3003
  declineBorder: (settings == null ? void 0 : settings.borderColor) || "#d0d5dd",
2736
3004
  link: (settings == null ? void 0 : settings.link) || secondaryColor,
2737
3005
  heading: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
@@ -2743,6 +3011,31 @@ function resolveColors(settings, activeConfig) {
2743
3011
  };
2744
3012
  }
2745
3013
 
3014
+ // src/modal/consentSelection.ts
3015
+ var dataElementKey = (purposeUuid, elementUuid) => `${purposeUuid}-${elementUuid}`;
3016
+ var buildSelectionForMode = (purposes, mode, current) => {
3017
+ if (mode === "selected") {
3018
+ return current;
3019
+ }
3020
+ const selectedPurposes = {};
3021
+ const selectedDataElements = {};
3022
+ purposes.forEach((purpose) => {
3023
+ selectedPurposes[purpose.uuid] = true;
3024
+ purpose.data_elements.forEach((element) => {
3025
+ selectedDataElements[dataElementKey(purpose.uuid, element.uuid)] = element.enabled;
3026
+ });
3027
+ });
3028
+ return { selectedPurposes, selectedDataElements };
3029
+ };
3030
+
3031
+ // src/modal/layout.ts
3032
+ function resolveLogoPosition(raw) {
3033
+ return raw === "center" || raw === "right" ? raw : DEFAULT_LOGO_POSITION;
3034
+ }
3035
+ function logoRowJustify(position) {
3036
+ return LOGO_POSITION_JUSTIFY[position];
3037
+ }
3038
+
2746
3039
  // src/modal/index.ts
2747
3040
  var ModalController = class {
2748
3041
  constructor(options) {
@@ -2767,6 +3060,7 @@ var ModalController = class {
2767
3060
  this.visibilityChangeHandler = null;
2768
3061
  // DOM refs for targeted updates
2769
3062
  this.acceptBtn = null;
3063
+ this.acceptAllBtn = null;
2770
3064
  this.declineBtn = null;
2771
3065
  this.middleSection = null;
2772
3066
  this.errorEl = null;
@@ -2847,21 +3141,47 @@ var ModalController = class {
2847
3141
  this.langSelectorRefs = null;
2848
3142
  }
2849
3143
  // ─── Private ───
3144
+ /**
3145
+ * Sandbox auth options to forward to every API call. When `token` is set the
3146
+ * API layer sends the single `X-Consent-Token` header, carries every supplied
3147
+ * identifier in each request's payload, and reads org/workspace from these
3148
+ * fields instead of decoding the JWT. Empty in the normal JWT flow.
3149
+ */
3150
+ sandboxParams() {
3151
+ var _a;
3152
+ const legacySubject = (_a = this.options.sandboxSubject) == null ? void 0 : _a.trim();
3153
+ const legacyEmail = (legacySubject == null ? void 0 : legacySubject.includes("@")) ? legacySubject : void 0;
3154
+ const legacyMobile = legacySubject && !legacySubject.includes("@") ? legacySubject : void 0;
3155
+ return {
3156
+ token: this.options.token,
3157
+ // The static token carries no identity; carry every supplied identifier
3158
+ // (ucic/email/mobile) so the server can pick the principal by precedence
3159
+ // (UCIC > email > mobile) and keep the rest as the sandbox contact.
3160
+ // sandboxUcic / sandboxEmail / sandboxMobile stay as fallbacks.
3161
+ sandboxUcic: this.options.ucic || this.options.sandboxUcic,
3162
+ sandboxEmail: this.options.email || this.options.sandboxEmail || legacyEmail,
3163
+ sandboxMobile: this.options.mobile || this.options.sandboxMobile || legacyMobile,
3164
+ organisationUuid: this.options.organisationUuid,
3165
+ workspaceUuid: this.options.workspaceUuid
3166
+ };
3167
+ }
2850
3168
  async fetchNotice() {
2851
3169
  var _a, _b, _c, _d;
2852
3170
  this.abortController = new AbortController();
2853
3171
  this.store.setState({ isLoading: true, fetchError: null });
2854
3172
  try {
3173
+ const noticeBaseUrl = this.options.ledgerBaseUrl && !this.options.applicationId && !this.options.token ? this.options.ledgerBaseUrl : this.options.baseUrl;
2855
3174
  const data = await fetchConsentContent({
2856
3175
  noticeId: this.options.noticeId,
2857
3176
  accessToken: this.options.accessToken,
2858
3177
  refreshToken: this.options.refreshToken,
2859
- baseUrl: this.options.baseUrl,
3178
+ baseUrl: noticeBaseUrl,
2860
3179
  language: this.options.language,
2861
3180
  specific_uuid: this.options.applicationId,
2862
3181
  validate_against: this.options.validateAgainst || "all",
2863
3182
  include_fully_consented_data: this.options.includeFullyConsentedData,
2864
- signal: this.abortController.signal
3183
+ signal: this.abortController.signal,
3184
+ ...this.sandboxParams()
2865
3185
  });
2866
3186
  const activeConfig = data.detail.active_config;
2867
3187
  let purposeSelections = data.detail.purpose_selections;
@@ -2923,10 +3243,11 @@ var ModalController = class {
2923
3243
  if (this.options.includeFullyConsentedData && needsConsentPurposes.length === 0 && alreadyConsentedPurposes.length > 0) {
2924
3244
  isFullyConsentedReviewMode = true;
2925
3245
  }
2926
- let selectedLanguage = this.options.language || "en";
2927
- if (activeConfig.default_language) {
2928
- selectedLanguage = activeConfig.default_language;
2929
- }
3246
+ const selectedLanguage = resolveDisplayLanguage(
3247
+ this.options.language,
3248
+ activeConfig.default_language,
3249
+ activeConfig.supported_languages_and_translations
3250
+ );
2930
3251
  let isMinorFlow = false;
2931
3252
  let showAgeVerification = false;
2932
3253
  let restoreState = {};
@@ -3224,49 +3545,32 @@ var ModalController = class {
3224
3545
  (_g = this.ttsManager) == null ? void 0 : _g.fetchAudioUrls({
3225
3546
  accessToken: this.options.accessToken,
3226
3547
  baseUrl: this.options.baseUrl,
3548
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3227
3549
  noticeUuid: activeConfig.notice_uuid,
3228
3550
  language: this.getLanguageCode(state.selectedLanguage),
3229
- content: activeConfig
3551
+ content: activeConfig,
3552
+ ...this.sandboxParams()
3230
3553
  });
3231
3554
  const topRight = el("div", {
3232
3555
  style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }
3233
3556
  }, this.ttsButton, this.langSelectorRefs.container);
3234
- let topSection;
3235
- if (isMobile) {
3236
- const headerRow = el(
3237
- "div",
3238
- {
3239
- style: {
3240
- display: "flex",
3241
- alignItems: "center",
3242
- gap: "10px",
3243
- width: "100%"
3244
- }
3245
- },
3246
- ...logoImg ? [logoImg] : [],
3557
+ const logoPosition = resolveLogoPosition(activeConfig.logo_position);
3558
+ const headerRows = [];
3559
+ if (logoImg) {
3560
+ headerRows.push(
3247
3561
  el(
3248
3562
  "div",
3249
- { style: { marginLeft: "auto", display: "flex", alignItems: "center" } },
3250
- topRight
3563
+ { style: { ...styles.logoRow, justifyContent: logoRowJustify(logoPosition) } },
3564
+ logoImg
3251
3565
  )
3252
3566
  );
3253
- topSection = el(
3254
- "div",
3255
- {
3256
- style: {
3257
- ...styles.topSection,
3258
- flexDirection: "column",
3259
- alignItems: "stretch",
3260
- flexWrap: "nowrap"
3261
- }
3262
- },
3263
- headerRow,
3264
- this.noticeTitleEl
3265
- );
3266
- } else {
3267
- const topLeft = el("div", { style: styles.topLeft }, logoImg, this.noticeTitleEl);
3268
- topSection = el("div", { style: styles.topSection }, topLeft, topRight);
3269
3567
  }
3568
+ headerRows.push(el("div", { style: styles.headerRow }, this.noticeTitleEl, topRight));
3569
+ const topSection = el(
3570
+ "div",
3571
+ { style: { ...styles.headerStack, gap: isMobile ? "12px" : "10px" } },
3572
+ ...headerRows
3573
+ );
3270
3574
  this.middleSection = el("div", { style: styles.middleSection });
3271
3575
  const baseTextStyle = { ...styles.privacyText, color: textColor, fontFamily: font, fontSize: isMobile ? "14px" : "16px" };
3272
3576
  const noticeTextSpan = el("span", { id: "privacy-notice-text" });
@@ -3457,24 +3761,57 @@ var ModalController = class {
3457
3761
  display: "none"
3458
3762
  }
3459
3763
  });
3460
- const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", activeConfig.confirm_button_text);
3764
+ const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
3765
+ "confirm_button_text",
3766
+ activeConfig.confirm_button_text || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
3767
+ );
3768
+ const gridButtonBox = {
3769
+ fontSize: isMobile ? "14px" : "16px",
3770
+ padding: state.isFullyConsentedReviewMode ? isMobile ? "10px 20px" : "9px 45px" : "9px 20px",
3771
+ width: state.isFullyConsentedReviewMode || isMobile ? "100%" : "auto",
3772
+ whiteSpace: "normal",
3773
+ wordBreak: "break-word"
3774
+ };
3461
3775
  const acceptStyle = {
3462
3776
  ...styles.button,
3463
- ...styles.acceptButton,
3464
- backgroundColor: colors.acceptBg,
3465
- color: colors.acceptText,
3777
+ ...state.isFullyConsentedReviewMode ? { ...styles.acceptButton, backgroundColor: colors.acceptBg, color: colors.acceptText } : {
3778
+ ...styles.cancelButton,
3779
+ backgroundColor: "#ffffff",
3780
+ color: colors.acceptShortcutText,
3781
+ borderColor: colors.acceptShortcutBorder
3782
+ },
3466
3783
  borderRadius: colors.borderRadius,
3467
- fontSize: isMobile ? "14px" : "16px",
3468
- padding: isMobile ? "10px 20px" : "9px 45px"
3784
+ ...gridButtonBox
3469
3785
  };
3470
3786
  this.acceptBtn = el("button", {
3471
- id: "confirm-button-text",
3787
+ id: ACCEPT_SELECTED_BUTTON_ID,
3472
3788
  style: acceptStyle,
3473
3789
  type: "button",
3474
- onClick: () => this.handleAccept()
3790
+ onClick: () => this.handleAccept("selected")
3475
3791
  });
3476
3792
  this.acceptBtn.textContent = acceptBtnText;
3477
- const declineBtnText = this.getTranslatedText("decline_button_text", activeConfig.decline_button_text);
3793
+ const shortcutStyle = {
3794
+ ...styles.button,
3795
+ ...styles.acceptButton,
3796
+ backgroundColor: colors.acceptBg,
3797
+ color: colors.acceptText,
3798
+ borderRadius: colors.borderRadius,
3799
+ ...gridButtonBox
3800
+ };
3801
+ this.acceptAllBtn = el("button", {
3802
+ id: ACCEPT_ALL_BUTTON_ID,
3803
+ style: shortcutStyle,
3804
+ type: "button",
3805
+ onClick: () => this.handleAccept("all")
3806
+ });
3807
+ this.acceptAllBtn.textContent = this.getTranslatedText(
3808
+ "accept_all_button_text",
3809
+ activeConfig.accept_all_button_text || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
3810
+ );
3811
+ const declineBtnText = this.getTranslatedText(
3812
+ "decline_button_text",
3813
+ activeConfig.decline_button_text || DEFAULT_DECLINE_BUTTON_TEXT
3814
+ );
3478
3815
  const declineStyle = {
3479
3816
  ...styles.button,
3480
3817
  ...styles.cancelButton,
@@ -3482,11 +3819,13 @@ var ModalController = class {
3482
3819
  color: colors.declineText,
3483
3820
  borderColor: colors.declineBorder,
3484
3821
  borderRadius: colors.borderRadius,
3485
- fontSize: isMobile ? "14px" : "16px",
3486
- padding: isMobile ? "10px 20px" : "9px 45px"
3822
+ ...gridButtonBox,
3823
+ // Pushes everything after decline to the right edge, which is the whole
3824
+ // of the wide layout. Inert in the stack and in review mode.
3825
+ marginRight: state.isFullyConsentedReviewMode || isMobile ? "0" : "auto"
3487
3826
  };
3488
3827
  this.declineBtn = el("button", {
3489
- id: "decline-button-text",
3828
+ id: DECLINE_BUTTON_ID,
3490
3829
  style: declineStyle,
3491
3830
  type: "button",
3492
3831
  onClick: () => this.handleDecline()
@@ -3494,12 +3833,19 @@ var ModalController = class {
3494
3833
  if (!state.isFullyConsentedReviewMode) {
3495
3834
  this.declineBtn.textContent = declineBtnText;
3496
3835
  }
3497
- const bottomSection = el("div", { style: styles.bottomSection });
3498
- if (!state.isFullyConsentedReviewMode) {
3836
+ const bottomSection = el("div", {
3837
+ style: state.isFullyConsentedReviewMode ? styles.bottomSection : isMobile ? styles.actionButtonStack : styles.actionButtonRow
3838
+ });
3839
+ if (state.isFullyConsentedReviewMode) {
3840
+ bottomSection.appendChild(this.acceptBtn);
3841
+ } else if (isMobile) {
3842
+ bottomSection.appendChild(this.acceptAllBtn);
3499
3843
  bottomSection.appendChild(this.acceptBtn);
3500
3844
  bottomSection.appendChild(this.declineBtn);
3501
3845
  } else {
3846
+ bottomSection.appendChild(this.declineBtn);
3502
3847
  bottomSection.appendChild(this.acceptBtn);
3848
+ bottomSection.appendChild(this.acceptAllBtn);
3503
3849
  }
3504
3850
  const contentWrapper = el(
3505
3851
  "div",
@@ -3655,7 +4001,12 @@ var ModalController = class {
3655
4001
  selectedPurposes: { ...state.selectedPurposes, [purposeUuid]: shouldCheckPurpose }
3656
4002
  });
3657
4003
  }
3658
- async handleAccept() {
4004
+ /**
4005
+ * Shared by all three accept buttons. The mode only decides which
4006
+ * purposes/data elements are submitted — everything downstream, including
4007
+ * the host's `onAccept` callback, is identical.
4008
+ */
4009
+ async handleAccept(mode = "selected") {
3659
4010
  var _a, _b, _c;
3660
4011
  const state = this.store.getState();
3661
4012
  if (state.isSubmitting) return;
@@ -3674,6 +4025,16 @@ var ModalController = class {
3674
4025
  if (!content) return;
3675
4026
  this.abortController = new AbortController();
3676
4027
  this.store.setState({ isSubmitting: true, isSubmittingAccept: true, errorMessage: null });
4028
+ const selection = buildSelectionForMode(content.purposes, mode, {
4029
+ selectedPurposes: state.selectedPurposes,
4030
+ selectedDataElements: state.selectedDataElements
4031
+ });
4032
+ if (mode !== "selected") {
4033
+ this.store.setState({
4034
+ selectedPurposes: selection.selectedPurposes,
4035
+ selectedDataElements: selection.selectedDataElements
4036
+ });
4037
+ }
3677
4038
  try {
3678
4039
  const purposes = content.purposes.map((purpose) => {
3679
4040
  const dataElements = purpose.data_elements.filter((de) => de.enabled).map((de) => ({
@@ -3681,13 +4042,13 @@ var ModalController = class {
3681
4042
  name: de.name,
3682
4043
  enabled: de.enabled,
3683
4044
  required: de.required,
3684
- selected: state.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
4045
+ selected: selection.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
3685
4046
  }));
3686
4047
  return {
3687
4048
  uuid: purpose.uuid,
3688
4049
  name: purpose.name,
3689
4050
  description: purpose.description,
3690
- selected: state.selectedPurposes[purpose.uuid] || false,
4051
+ selected: selection.selectedPurposes[purpose.uuid] || false,
3691
4052
  data_elements: dataElements
3692
4053
  };
3693
4054
  });
@@ -3701,7 +4062,9 @@ var ModalController = class {
3701
4062
  meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
3702
4063
  guardianVerificationReference: state.verificationReference || void 0,
3703
4064
  selfDeclaredAdult: state.selfDeclaredAdult || void 0,
3704
- signal: this.abortController.signal
4065
+ language: state.selectedLanguage,
4066
+ signal: this.abortController.signal,
4067
+ ...this.sandboxParams()
3705
4068
  });
3706
4069
  this.options.onAccept();
3707
4070
  this.destroy();
@@ -3747,11 +4110,16 @@ var ModalController = class {
3747
4110
  }
3748
4111
  updateAcceptButtonState(state) {
3749
4112
  if (!this.acceptBtn || state.isFullyConsentedReviewMode) return;
4113
+ const submitting = state.isSubmittingAccept || state.isSubmittingDecline;
3750
4114
  const allRequiredChecked = this.areAllRequiredChecked(state);
3751
- const disabled = !allRequiredChecked || state.isSubmittingAccept || state.isSubmittingDecline;
3752
- this.acceptBtn.disabled = disabled;
3753
- this.acceptBtn.style.opacity = disabled ? "0.5" : "1";
3754
- this.acceptBtn.style.cursor = disabled ? "not-allowed" : "pointer";
4115
+ const setDisabled = (btn, disabled) => {
4116
+ if (!btn) return;
4117
+ btn.disabled = disabled;
4118
+ btn.style.opacity = disabled ? "0.5" : "1";
4119
+ btn.style.cursor = disabled ? "not-allowed" : "pointer";
4120
+ };
4121
+ setDisabled(this.acceptBtn, !allRequiredChecked || submitting);
4122
+ setDisabled(this.acceptAllBtn, submitting);
3755
4123
  }
3756
4124
  updateCollapseState(state) {
3757
4125
  const updateRefs = (refs) => {
@@ -4124,25 +4492,34 @@ var ModalController = class {
4124
4492
  }
4125
4493
  updateButtonStates(state) {
4126
4494
  var _a;
4127
- if (this.acceptBtn) {
4128
- if (state.isSubmittingAccept) {
4129
- this.acceptBtn.textContent = "Submitting...";
4130
- this.acceptBtn.disabled = true;
4131
- this.acceptBtn.style.opacity = "0.7";
4132
- this.acceptBtn.style.cursor = "not-allowed";
4133
- } else {
4134
- const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4135
- this.acceptBtn.textContent = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", (content == null ? void 0 : content.confirm_button_text) || "Accept");
4136
- this.acceptBtn.disabled = false;
4137
- this.acceptBtn.style.opacity = "1";
4138
- this.acceptBtn.style.cursor = "pointer";
4139
- }
4140
- }
4495
+ const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4496
+ const applySubmittingState = (btn, idleLabel) => {
4497
+ if (!btn) return;
4498
+ btn.textContent = state.isSubmittingAccept ? SUBMITTING_BUTTON_TEXT : idleLabel;
4499
+ btn.disabled = state.isSubmittingAccept;
4500
+ btn.style.opacity = state.isSubmittingAccept ? "0.7" : "1";
4501
+ btn.style.cursor = state.isSubmittingAccept ? "not-allowed" : "pointer";
4502
+ };
4503
+ applySubmittingState(
4504
+ this.acceptBtn,
4505
+ state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
4506
+ "confirm_button_text",
4507
+ (content == null ? void 0 : content.confirm_button_text) || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
4508
+ )
4509
+ );
4510
+ applySubmittingState(
4511
+ this.acceptAllBtn,
4512
+ this.getTranslatedText(
4513
+ "accept_all_button_text",
4514
+ (content == null ? void 0 : content.accept_all_button_text) || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
4515
+ )
4516
+ );
4141
4517
  if (this.declineBtn) {
4142
4518
  this.declineBtn.disabled = state.isSubmitting;
4143
4519
  this.declineBtn.style.opacity = state.isSubmitting ? "0.7" : "1";
4144
4520
  this.declineBtn.style.cursor = state.isSubmitting ? "not-allowed" : "pointer";
4145
4521
  }
4522
+ this.updateAcceptButtonState(state);
4146
4523
  }
4147
4524
  };
4148
4525
 
@@ -4343,6 +4720,9 @@ function renderInlinePurposes(purposes, options) {
4343
4720
  }
4344
4721
  });
4345
4722
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
4723
+ if (hasRequiredDataElement(purpose)) {
4724
+ titleEl.appendChild(createRequiredMarker());
4725
+ }
4346
4726
  titleElements.set(purpose.uuid, titleEl);
4347
4727
  const descEl = el("h3", {
4348
4728
  style: {
@@ -4395,12 +4775,7 @@ function renderInlinePurposes(purposes, options) {
4395
4775
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
4396
4776
  dataElementLabels.set(combinedId, labelSpan);
4397
4777
  if (dataElement.required) {
4398
- const asterisk = el("span", {
4399
- style: { color: "red", marginLeft: "4px" },
4400
- "aria-label": "required"
4401
- });
4402
- asterisk.textContent = "*";
4403
- labelSpan.appendChild(asterisk);
4778
+ labelSpan.appendChild(createRequiredMarker());
4404
4779
  }
4405
4780
  const deCheckbox = el("input", {
4406
4781
  type: "checkbox",
@@ -4446,6 +4821,8 @@ var InlineController = class {
4446
4821
  this.errorEl = null;
4447
4822
  this.hasSubmitted = false;
4448
4823
  this.hasCheckedLedgerConsent = false;
4824
+ this.fetchedWithToken = false;
4825
+ this.fetchGeneration = 0;
4449
4826
  this.unsubscribe = null;
4450
4827
  this.abortController = null;
4451
4828
  this.options = options;
@@ -4503,6 +4880,10 @@ var InlineController = class {
4503
4880
  }
4504
4881
  updateToken(accessToken) {
4505
4882
  this.options = { ...this.options, accessToken };
4883
+ if (accessToken && !this.fetchedWithToken) {
4884
+ void this.fetchNotice();
4885
+ return;
4886
+ }
4506
4887
  if (this.shouldCheckLedgerConsent()) {
4507
4888
  void this.checkLedgerConsent();
4508
4889
  return;
@@ -4518,17 +4899,20 @@ var InlineController = class {
4518
4899
  }
4519
4900
  async fetchNotice() {
4520
4901
  var _a, _b, _c, _d;
4521
- this.store.setState({ isLoading: true, fetchError: null });
4902
+ const generation = ++this.fetchGeneration;
4903
+ this.fetchedWithToken = Boolean(this.options.accessToken);
4904
+ this.store.setState({ isLoading: true, fetchError: null, hasAlreadyConsented: false });
4522
4905
  try {
4523
4906
  const data = await fetchConsentContentPublic({
4524
4907
  org_uuid: this.options.org_uuid,
4525
4908
  workspace_uuid: this.options.workspace_uuid,
4526
4909
  notice_uuid: this.options.notice_uuid,
4910
+ accessToken: this.options.accessToken,
4527
4911
  baseUrl: this.options.baseUrl,
4528
4912
  language: this.options.language,
4529
4913
  specific_uuid: this.options.applicationId
4530
4914
  });
4531
- if (this.isDestroyed()) {
4915
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4532
4916
  return;
4533
4917
  }
4534
4918
  const activeConfig = data.detail.active_config;
@@ -4546,13 +4930,17 @@ var InlineController = class {
4546
4930
  this.store.setState({
4547
4931
  content: activeConfig,
4548
4932
  isLoading: false,
4549
- selectedLanguage: activeConfig.default_language || this.options.language || "en",
4933
+ selectedLanguage: resolveDisplayLanguage(
4934
+ this.options.language,
4935
+ activeConfig.default_language,
4936
+ activeConfig.supported_languages_and_translations
4937
+ ),
4550
4938
  collapsedPurposes: initialCollapsed,
4551
4939
  selectedPurposes: initialPurposes,
4552
4940
  selectedDataElements: initialElements
4553
4941
  });
4554
4942
  } catch (err) {
4555
- if (this.isDestroyed()) {
4943
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4556
4944
  return;
4557
4945
  }
4558
4946
  const error = err;
@@ -4869,7 +5257,8 @@ var InlineController = class {
4869
5257
  };
4870
5258
  }),
4871
5259
  declined: false,
4872
- meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0
5260
+ meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
5261
+ language: state.selectedLanguage
4873
5262
  });
4874
5263
  (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4875
5264
  this.hasSubmitted = false;