@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.mjs CHANGED
@@ -152,17 +152,6 @@ function createMediaQuery(query) {
152
152
  };
153
153
  }
154
154
 
155
- // src/core/jwt-decode.ts
156
- import { jwtDecode } from "jwt-decode";
157
- function decodeTokenSafely(token) {
158
- try {
159
- return jwtDecode(token);
160
- } catch (error) {
161
- console.warn("Failed to decode JWT token:", error);
162
- return null;
163
- }
164
- }
165
-
166
155
  // src/services/cache.ts
167
156
  var apiCache = /* @__PURE__ */ new Map();
168
157
  var CACHE_TTL = 5 * 60 * 1e3;
@@ -183,6 +172,141 @@ function clearApiCache() {
183
172
  apiCache.clear();
184
173
  }
185
174
 
175
+ // src/core/jwt-decode.ts
176
+ import { jwtDecode } from "jwt-decode";
177
+ function decodeTokenSafely(token) {
178
+ try {
179
+ return jwtDecode(token);
180
+ } catch (error) {
181
+ console.warn("Failed to decode JWT token:", error);
182
+ return null;
183
+ }
184
+ }
185
+
186
+ // src/api/sandbox.ts
187
+ var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
188
+ var resolveSandboxAuth = (options) => {
189
+ var _a, _b, _c, _d, _e, _f;
190
+ const token = (_a = options.token) == null ? void 0 : _a.trim();
191
+ if (!token) {
192
+ return null;
193
+ }
194
+ const organisationUuid = (_b = options.organisationUuid) == null ? void 0 : _b.trim();
195
+ const workspaceUuid = (_c = options.workspaceUuid) == null ? void 0 : _c.trim();
196
+ const ucic = (_d = options.sandboxUcic) == null ? void 0 : _d.trim();
197
+ const email = (_e = options.sandboxEmail) == null ? void 0 : _e.trim();
198
+ const mobile = (_f = options.sandboxMobile) == null ? void 0 : _f.trim();
199
+ if (!organisationUuid || !workspaceUuid) {
200
+ throw new Error(
201
+ "Sandbox mode requires organisationUuid and workspaceUuid"
202
+ );
203
+ }
204
+ if (!ucic && !email && !mobile) {
205
+ throw new Error("Sandbox mode requires a UCIC, email, or mobile");
206
+ }
207
+ return { token, ucic, email, mobile, organisationUuid, workspaceUuid };
208
+ };
209
+ var resolveRequestAuth = (accessToken, options) => {
210
+ const sandbox = resolveSandboxAuth(options);
211
+ if (sandbox) {
212
+ return {
213
+ organisationUuid: sandbox.organisationUuid,
214
+ workspaceUuid: sandbox.workspaceUuid,
215
+ sandbox
216
+ };
217
+ }
218
+ const decodedToken = accessToken ? decodeTokenSafely(accessToken) : null;
219
+ if (!decodedToken) {
220
+ throw new Error("Invalid access token");
221
+ }
222
+ const { organisation_uuid, workspace_uuid } = decodedToken;
223
+ if (!organisation_uuid || !workspace_uuid) {
224
+ throw new Error("Invalid token: missing organization or workspace UUID");
225
+ }
226
+ return {
227
+ organisationUuid: organisation_uuid,
228
+ workspaceUuid: workspace_uuid,
229
+ sandbox: null
230
+ };
231
+ };
232
+ var buildAuthHeaders = (accessToken, sandbox) => {
233
+ if (sandbox) {
234
+ return { [SANDBOX_TOKEN_HEADER]: sandbox.token };
235
+ }
236
+ return { Authorization: `Bearer ${accessToken}` };
237
+ };
238
+ var sandboxBodyIdentity = (sandbox) => {
239
+ const payload = {};
240
+ if (sandbox.ucic) {
241
+ payload.org_user_id = sandbox.ucic;
242
+ }
243
+ if (sandbox.email) {
244
+ payload.primary_email = sandbox.email;
245
+ }
246
+ if (sandbox.mobile) {
247
+ payload.primary_mobile = sandbox.mobile;
248
+ }
249
+ return payload;
250
+ };
251
+ var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
252
+
253
+ // src/api/constants.ts
254
+ var CODE_BY_NAME = /* @__PURE__ */ new Map([
255
+ ["english", "en"],
256
+ ["assamese", "as"],
257
+ ["bengali", "bn"],
258
+ ["bodo", "brx"],
259
+ ["dogri", "doi"],
260
+ ["goan konkani", "gom"],
261
+ ["gujarati", "gu"],
262
+ ["hindi", "hi"],
263
+ ["kannada", "kn"],
264
+ ["kashmiri", "ks"],
265
+ ["maithili", "mai"],
266
+ ["malayalam", "ml"],
267
+ ["manipuri", "mni-Mtei"],
268
+ ["marathi", "mr"],
269
+ ["nepali", "ne"],
270
+ ["odia", "or"],
271
+ ["punjabi", "pa"],
272
+ ["sanskrit", "sa"],
273
+ ["santali", "sat"],
274
+ ["sindhi", "sd"],
275
+ ["tamil", "ta"],
276
+ ["telugu", "te"],
277
+ ["urdu", "ur"]
278
+ ]);
279
+ var CODE_BY_LOWER = new Map(
280
+ [...CODE_BY_NAME.values()].map((c) => [c.toLowerCase(), c])
281
+ );
282
+
283
+ // src/api/language.ts
284
+ function toLanguageCode(lang) {
285
+ var _a, _b;
286
+ if (!lang) {
287
+ return void 0;
288
+ }
289
+ const trimmed = lang.trim();
290
+ if (!trimmed) {
291
+ return void 0;
292
+ }
293
+ const lower = trimmed.toLowerCase();
294
+ return (_b = (_a = CODE_BY_LOWER.get(lower)) != null ? _a : CODE_BY_NAME.get(lower)) != null ? _b : trimmed;
295
+ }
296
+ function resolveDisplayLanguage(requested, defaultLanguage, translations) {
297
+ const fallback = defaultLanguage || "en";
298
+ const requestedCode = toLanguageCode(requested);
299
+ if (!requestedCode) {
300
+ return fallback;
301
+ }
302
+ for (const key of Object.keys(translations || {})) {
303
+ if (toLanguageCode(key) === requestedCode) {
304
+ return key;
305
+ }
306
+ }
307
+ return fallback;
308
+ }
309
+
186
310
  // src/api/index.ts
187
311
  var BASE_URL = "https://api.redacto.io/consent";
188
312
  var BASE_URL_PUBLIC = "https://api.redacto.tech/consent";
@@ -194,17 +318,22 @@ var fetchConsentContent = async ({
194
318
  specific_uuid,
195
319
  validate_against = "all",
196
320
  include_fully_consented_data = false,
197
- signal
321
+ signal,
322
+ ...sandboxParams
198
323
  }) => {
199
- if (!noticeId || !accessToken || !validate_against) {
200
- throw new Error("noticeId and accessToken are required");
324
+ if (!noticeId || !validate_against) {
325
+ throw new Error("noticeId is required");
201
326
  }
202
- const decodedToken = decodeTokenSafely(accessToken);
203
- if (!decodedToken) throw new Error("Invalid access token");
204
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
205
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
327
+ if (!accessToken && !sandboxParams.token) {
328
+ throw new Error("accessToken or token is required");
329
+ }
330
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
331
+ accessToken,
332
+ sandboxParams
333
+ );
206
334
  const apiBaseUrl = baseUrl || BASE_URL;
207
- const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
335
+ const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.email || sandbox.mobile}` : accessToken;
336
+ const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
208
337
  if (validate_against === "all" && !specific_uuid) {
209
338
  const cached = getCachedData(cacheKey);
210
339
  if (cached) return cached;
@@ -213,10 +342,15 @@ var fetchConsentContent = async ({
213
342
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
214
343
  url.searchParams.append("validate_against", validate_against);
215
344
  if (include_fully_consented_data) url.searchParams.append("include_fully_consented_data", "true");
345
+ if (sandbox) {
346
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
347
+ if (value) url.searchParams.append(key, value);
348
+ }
349
+ }
216
350
  const response = await fetch(url.toString(), {
217
351
  method: "GET",
218
352
  headers: {
219
- Authorization: `Bearer ${accessToken}`,
353
+ ...buildAuthHeaders(accessToken, sandbox),
220
354
  "Accept-Language": language
221
355
  },
222
356
  signal
@@ -242,18 +376,24 @@ var fetchConsentContentPublic = async ({
242
376
  org_uuid,
243
377
  workspace_uuid,
244
378
  notice_uuid,
379
+ accessToken,
245
380
  baseUrl,
246
381
  language = "en",
247
382
  specific_uuid
248
383
  }) => {
249
384
  const apiBaseUrl = baseUrl || BASE_URL_PUBLIC;
385
+ const noticesUrl = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices`;
250
386
  const url = new URL(
251
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices/get-notice/${notice_uuid}`
387
+ accessToken ? `${noticesUrl}/${notice_uuid}` : `${noticesUrl}/get-notice/${notice_uuid}`
252
388
  );
253
389
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
254
390
  const response = await fetch(url.toString(), {
255
391
  method: "GET",
256
- headers: { "Content-Type": "application/json" }
392
+ headers: {
393
+ "Content-Type": "application/json",
394
+ "Accept-Language": language,
395
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
396
+ }
257
397
  });
258
398
  if (response.status === 401) {
259
399
  const error = new Error("Unauthorized");
@@ -280,16 +420,21 @@ var submitConsentEvent = async ({
280
420
  meta_data,
281
421
  guardianVerificationReference,
282
422
  selfDeclaredAdult,
283
- signal
423
+ language,
424
+ signal,
425
+ ...sandboxParams
284
426
  }) => {
285
- if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
286
- throw new Error("noticeUuid, accessToken, and purposes array are required");
427
+ if (!noticeUuid || !Array.isArray(purposes)) {
428
+ throw new Error("noticeUuid and purposes array are required");
287
429
  }
288
- const decodedToken = decodeTokenSafely(accessToken);
289
- if (!decodedToken) throw new Error("Invalid access token");
290
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
291
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
292
- const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
430
+ if (!accessToken && !sandboxParams.token) {
431
+ throw new Error("accessToken or token is required");
432
+ }
433
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
434
+ accessToken,
435
+ sandboxParams
436
+ );
437
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
293
438
  const validatedPurposes = purposes.map((purpose) => {
294
439
  if (!purpose.uuid) throw new Error("Invalid purpose data: missing uuid");
295
440
  const dataElements = (purpose.data_elements || []).filter((element) => element.enabled).map((element) => ({
@@ -310,17 +455,19 @@ var submitConsentEvent = async ({
310
455
  declined: Boolean(declined),
311
456
  meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid, minor_age: meta_data.minor_age } : void 0,
312
457
  guardian_verification_reference: guardianVerificationReference,
313
- self_declared_adult: selfDeclaredAdult
458
+ self_declared_adult: selfDeclaredAdult,
459
+ language: toLanguageCode(language)
314
460
  };
461
+ const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
315
462
  const response = await fetch(
316
463
  `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/submit-consent`,
317
464
  {
318
465
  method: "POST",
319
466
  headers: {
320
- Authorization: `Bearer ${accessToken}`,
467
+ ...buildAuthHeaders(accessToken, sandbox),
321
468
  "Content-Type": "application/json"
322
469
  },
323
- body: JSON.stringify(payload),
470
+ body: JSON.stringify(requestBody),
324
471
  signal
325
472
  }
326
473
  );
@@ -352,7 +499,8 @@ var submitConsentEventPublic = async ({
352
499
  noticeUuid,
353
500
  purposes,
354
501
  declined,
355
- meta_data
502
+ meta_data,
503
+ language
356
504
  }) => {
357
505
  var _a, _b;
358
506
  const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL_PUBLIC;
@@ -373,7 +521,8 @@ var submitConsentEventPublic = async ({
373
521
  select_all_mandatory: false,
374
522
  source: "WEB",
375
523
  declined,
376
- meta_data
524
+ meta_data,
525
+ language: toLanguageCode(language)
377
526
  };
378
527
  const response = await fetch(
379
528
  `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
@@ -412,23 +561,32 @@ var submitConsentEventPublic = async ({
412
561
  var fetchTTSAudioUrls = async ({
413
562
  accessToken,
414
563
  baseUrl,
564
+ ledgerBaseUrl,
415
565
  noticeUuid,
416
566
  language,
417
- signal
567
+ signal,
568
+ ...sandboxParams
418
569
  }) => {
419
- if (!accessToken || !noticeUuid || !language) {
420
- throw new Error("accessToken, noticeUuid, and language are required");
570
+ if (!accessToken && !sandboxParams.token || !noticeUuid || !language) {
571
+ throw new Error("accessToken (or token), noticeUuid, and language are required");
421
572
  }
422
- const decodedToken = decodeTokenSafely(accessToken);
423
- if (!decodedToken) throw new Error("Invalid access token");
424
- const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
425
- if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
426
- const apiBaseUrl = baseUrl || BASE_URL;
427
- const url = `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/notices/${noticeUuid}/audio/${language}`;
428
- const response = await fetch(url, {
573
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
574
+ accessToken,
575
+ sandboxParams
576
+ );
577
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
578
+ const url = new URL(
579
+ `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/notices/${noticeUuid}/audio/${language}`
580
+ );
581
+ if (sandbox) {
582
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
583
+ if (value) url.searchParams.append(key, value);
584
+ }
585
+ }
586
+ const response = await fetch(url.toString(), {
429
587
  method: "GET",
430
588
  headers: {
431
- Authorization: `Bearer ${accessToken}`,
589
+ ...buildAuthHeaders(accessToken, sandbox),
432
590
  Accept: "application/json"
433
591
  },
434
592
  signal
@@ -895,6 +1053,42 @@ var styles = {
895
1053
  padding: 0,
896
1054
  boxSizing: "border-box"
897
1055
  },
1056
+ // The main notice header is two stacked rows: the logo alone on row 1,
1057
+ // aligned per active_config.logo_position, then the title and the
1058
+ // TTS/language controls on row 2. `topSection` and `topLeft` above keep their
1059
+ // single-row shape for the age-verification, guardian and verification
1060
+ // screens, which have no controls to place and share those styles.
1061
+ // Mirrors consent-sdk-react/src/RedactoNoticeConsent/styles.ts.
1062
+ headerStack: {
1063
+ display: "flex",
1064
+ flexDirection: "column",
1065
+ alignItems: "stretch",
1066
+ gap: "10px",
1067
+ paddingBottom: "15px",
1068
+ borderBottom: "none",
1069
+ backgroundColor: "#ffffff",
1070
+ margin: 0,
1071
+ boxSizing: "border-box",
1072
+ textAlign: "left"
1073
+ },
1074
+ logoRow: {
1075
+ display: "flex",
1076
+ alignItems: "center",
1077
+ width: "100%",
1078
+ margin: 0,
1079
+ padding: 0,
1080
+ boxSizing: "border-box"
1081
+ },
1082
+ headerRow: {
1083
+ display: "flex",
1084
+ alignItems: "center",
1085
+ justifyContent: "space-between",
1086
+ gap: "10px",
1087
+ width: "100%",
1088
+ margin: 0,
1089
+ padding: 0,
1090
+ boxSizing: "border-box"
1091
+ },
898
1092
  logo: {
899
1093
  height: "32px",
900
1094
  width: "auto",
@@ -1101,6 +1295,32 @@ var styles = {
1101
1295
  margin: 0,
1102
1296
  boxSizing: "border-box"
1103
1297
  },
1298
+ /**
1299
+ * The main notice footer. Wide: one row with decline pushed left and the
1300
+ * accepts grouped right. Narrow: a full-width stack. Review mode keeps
1301
+ * `bottomSection` — it collapses to a single button.
1302
+ */
1303
+ actionButtonRow: {
1304
+ display: "flex",
1305
+ flexDirection: "row",
1306
+ alignItems: "center",
1307
+ gap: "12px",
1308
+ paddingTop: "15px",
1309
+ borderTop: "none",
1310
+ backgroundColor: "#ffffff",
1311
+ margin: 0,
1312
+ boxSizing: "border-box"
1313
+ },
1314
+ actionButtonStack: {
1315
+ display: "flex",
1316
+ flexDirection: "column",
1317
+ gap: "12px",
1318
+ paddingTop: "15px",
1319
+ borderTop: "none",
1320
+ backgroundColor: "#ffffff",
1321
+ margin: 0,
1322
+ boxSizing: "border-box"
1323
+ },
1104
1324
  button: {
1105
1325
  width: "100%",
1106
1326
  borderRadius: "8px",
@@ -1126,6 +1346,9 @@ var styles = {
1126
1346
  },
1127
1347
  languageSelectorContainer: {
1128
1348
  position: "relative",
1349
+ // Shares `headerRow` with the title, which grows; without this a long
1350
+ // heading squeezes the language button instead of wrapping itself.
1351
+ flexShrink: 0,
1129
1352
  margin: 0,
1130
1353
  padding: 0,
1131
1354
  boxSizing: "border-box"
@@ -1184,6 +1407,51 @@ var styles = {
1184
1407
  }
1185
1408
  };
1186
1409
 
1410
+ // src/constants.ts
1411
+ var DEFAULT_LOGO_POSITION = "left";
1412
+ var DEFAULT_ACCEPT_ALL_BUTTON_TEXT = "Accept All";
1413
+ var DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT = "Accept Selected";
1414
+ var DEFAULT_DECLINE_BUTTON_TEXT = "Decline";
1415
+ var ACCEPT_ALL_BUTTON_ID = "accept-all-button-text";
1416
+ var ACCEPT_SELECTED_BUTTON_ID = "confirm-button-text";
1417
+ var DECLINE_BUTTON_ID = "decline-button-text";
1418
+ var SUBMITTING_BUTTON_TEXT = "Submitting...";
1419
+ var LOGO_POSITION_JUSTIFY = {
1420
+ left: "flex-start",
1421
+ center: "center",
1422
+ right: "flex-end"
1423
+ };
1424
+ var REQUIRED_MARKER_GLYPH = "*";
1425
+ var REQUIRED_MARKER_ARIA_LABEL = "required";
1426
+ var REQUIRED_MARKER_COLOR = "red";
1427
+ var REQUIRED_MARKER_GAP = "4px";
1428
+ var REQUIRED_MARKER_STYLE = {
1429
+ color: REQUIRED_MARKER_COLOR,
1430
+ marginLeft: REQUIRED_MARKER_GAP
1431
+ };
1432
+ var DPO_AUDIO_ELEMENT_MAP = [
1433
+ { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1434
+ { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1435
+ { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1436
+ { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1437
+ { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1438
+ { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1439
+ { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1440
+ { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1441
+ ];
1442
+
1443
+ // src/core/required-marker.ts
1444
+ function createRequiredMarker(label = REQUIRED_MARKER_ARIA_LABEL) {
1445
+ return el(
1446
+ "span",
1447
+ { style: REQUIRED_MARKER_STYLE, "aria-label": label },
1448
+ REQUIRED_MARKER_GLYPH
1449
+ );
1450
+ }
1451
+ function hasRequiredDataElement(purpose) {
1452
+ return purpose.data_elements.some((element) => element.required);
1453
+ }
1454
+
1187
1455
  // src/modal/purposes.ts
1188
1456
  function renderModalPurposes(purposes, options) {
1189
1457
  var _a, _b;
@@ -1249,6 +1517,9 @@ function renderModalPurposes(purposes, options) {
1249
1517
  }
1250
1518
  });
1251
1519
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
1520
+ if (hasRequiredDataElement(purpose)) {
1521
+ titleEl.appendChild(createRequiredMarker());
1522
+ }
1252
1523
  titleElements.set(purpose.uuid, titleEl);
1253
1524
  const descEl = el("h3", {
1254
1525
  id: `purpose-description-${purpose.uuid}`,
@@ -1316,9 +1587,7 @@ function renderModalPurposes(purposes, options) {
1316
1587
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
1317
1588
  dataElementLabels.set(combinedId, labelSpan);
1318
1589
  if (dataElement.required) {
1319
- const asterisk = el("span", { style: { color: "red", marginLeft: "4px" }, "aria-label": "required" });
1320
- asterisk.textContent = "*";
1321
- labelSpan.appendChild(asterisk);
1590
+ labelSpan.appendChild(createRequiredMarker());
1322
1591
  }
1323
1592
  let deRightElement;
1324
1593
  if (!isAlreadyConsented) {
@@ -1687,18 +1956,6 @@ var updateConsentState = (updates) => {
1687
1956
  }
1688
1957
  };
1689
1958
 
1690
- // src/constants.ts
1691
- var DPO_AUDIO_ELEMENT_MAP = [
1692
- { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1693
- { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1694
- { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1695
- { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1696
- { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1697
- { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1698
- { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1699
- { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1700
- ];
1701
-
1702
1959
  // src/modal/tts.ts
1703
1960
  var TTSManager = class {
1704
1961
  /**
@@ -1746,9 +2003,16 @@ var TTSManager = class {
1746
2003
  const response = await fetchTTSAudioUrls({
1747
2004
  accessToken: params.accessToken,
1748
2005
  baseUrl: params.baseUrl,
2006
+ ledgerBaseUrl: params.ledgerBaseUrl,
1749
2007
  noticeUuid: params.noticeUuid,
1750
2008
  language: params.language,
1751
- signal: this.abortController.signal
2009
+ signal: this.abortController.signal,
2010
+ token: params.token,
2011
+ sandboxUcic: params.sandboxUcic,
2012
+ sandboxEmail: params.sandboxEmail,
2013
+ sandboxMobile: params.sandboxMobile,
2014
+ organisationUuid: params.organisationUuid,
2015
+ workspaceUuid: params.workspaceUuid
1752
2016
  });
1753
2017
  this.audioUrls = response.detail;
1754
2018
  this.buildSequences(params.content);
@@ -1802,8 +2066,9 @@ var TTSManager = class {
1802
2066
  push(audioUrl, mapping.elementId);
1803
2067
  }
1804
2068
  }
1805
- push(urls.confirm_button_text_audio_url, "confirm-button-text");
1806
- push(urls.decline_button_text_audio_url, "decline-button-text");
2069
+ push(urls.accept_all_button_text_audio_url, ACCEPT_ALL_BUTTON_ID);
2070
+ push(urls.confirm_button_text_audio_url, ACCEPT_SELECTED_BUTTON_ID);
2071
+ push(urls.decline_button_text_audio_url, DECLINE_BUTTON_ID);
1807
2072
  this.audioSequence = audioSeq;
1808
2073
  this.elementIdSequence = elementSeq;
1809
2074
  }
@@ -2694,14 +2959,17 @@ function renderVerificationScreen(options) {
2694
2959
 
2695
2960
  // src/modal/colors.ts
2696
2961
  function resolveColors(settings, activeConfig) {
2697
- var _a, _b, _c, _d, _e, _f, _g, _h;
2962
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
2698
2963
  const primaryColor = (activeConfig == null ? void 0 : activeConfig.primary_color) || "#4f87ff";
2699
2964
  const secondaryColor = (activeConfig == null ? void 0 : activeConfig.secondary_color) || "#4f87ff";
2700
2965
  return {
2701
2966
  acceptBg: ((_b = (_a = settings == null ? void 0 : settings.button) == null ? void 0 : _a.accept) == null ? void 0 : _b.backgroundColor) || primaryColor,
2702
2967
  acceptText: ((_d = (_c = settings == null ? void 0 : settings.button) == null ? void 0 : _c.accept) == null ? void 0 : _d.textColor) || "#ffffff",
2703
- declineBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.decline) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2704
- declineText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.decline) == null ? void 0 : _h.textColor) || "#000000",
2968
+ acceptShortcutBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.acceptAll) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2969
+ acceptShortcutText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.acceptAll) == null ? void 0 : _h.textColor) || primaryColor,
2970
+ acceptShortcutBorder: primaryColor,
2971
+ declineBg: ((_j = (_i = settings == null ? void 0 : settings.button) == null ? void 0 : _i.decline) == null ? void 0 : _j.backgroundColor) || "#ffffff",
2972
+ declineText: ((_l = (_k = settings == null ? void 0 : settings.button) == null ? void 0 : _k.decline) == null ? void 0 : _l.textColor) || "#000000",
2705
2973
  declineBorder: (settings == null ? void 0 : settings.borderColor) || "#d0d5dd",
2706
2974
  link: (settings == null ? void 0 : settings.link) || secondaryColor,
2707
2975
  heading: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
@@ -2713,6 +2981,31 @@ function resolveColors(settings, activeConfig) {
2713
2981
  };
2714
2982
  }
2715
2983
 
2984
+ // src/modal/consentSelection.ts
2985
+ var dataElementKey = (purposeUuid, elementUuid) => `${purposeUuid}-${elementUuid}`;
2986
+ var buildSelectionForMode = (purposes, mode, current) => {
2987
+ if (mode === "selected") {
2988
+ return current;
2989
+ }
2990
+ const selectedPurposes = {};
2991
+ const selectedDataElements = {};
2992
+ purposes.forEach((purpose) => {
2993
+ selectedPurposes[purpose.uuid] = true;
2994
+ purpose.data_elements.forEach((element) => {
2995
+ selectedDataElements[dataElementKey(purpose.uuid, element.uuid)] = element.enabled;
2996
+ });
2997
+ });
2998
+ return { selectedPurposes, selectedDataElements };
2999
+ };
3000
+
3001
+ // src/modal/layout.ts
3002
+ function resolveLogoPosition(raw) {
3003
+ return raw === "center" || raw === "right" ? raw : DEFAULT_LOGO_POSITION;
3004
+ }
3005
+ function logoRowJustify(position) {
3006
+ return LOGO_POSITION_JUSTIFY[position];
3007
+ }
3008
+
2716
3009
  // src/modal/index.ts
2717
3010
  var ModalController = class {
2718
3011
  constructor(options) {
@@ -2737,6 +3030,7 @@ var ModalController = class {
2737
3030
  this.visibilityChangeHandler = null;
2738
3031
  // DOM refs for targeted updates
2739
3032
  this.acceptBtn = null;
3033
+ this.acceptAllBtn = null;
2740
3034
  this.declineBtn = null;
2741
3035
  this.middleSection = null;
2742
3036
  this.errorEl = null;
@@ -2817,21 +3111,47 @@ var ModalController = class {
2817
3111
  this.langSelectorRefs = null;
2818
3112
  }
2819
3113
  // ─── Private ───
3114
+ /**
3115
+ * Sandbox auth options to forward to every API call. When `token` is set the
3116
+ * API layer sends the single `X-Consent-Token` header, carries every supplied
3117
+ * identifier in each request's payload, and reads org/workspace from these
3118
+ * fields instead of decoding the JWT. Empty in the normal JWT flow.
3119
+ */
3120
+ sandboxParams() {
3121
+ var _a;
3122
+ const legacySubject = (_a = this.options.sandboxSubject) == null ? void 0 : _a.trim();
3123
+ const legacyEmail = (legacySubject == null ? void 0 : legacySubject.includes("@")) ? legacySubject : void 0;
3124
+ const legacyMobile = legacySubject && !legacySubject.includes("@") ? legacySubject : void 0;
3125
+ return {
3126
+ token: this.options.token,
3127
+ // The static token carries no identity; carry every supplied identifier
3128
+ // (ucic/email/mobile) so the server can pick the principal by precedence
3129
+ // (UCIC > email > mobile) and keep the rest as the sandbox contact.
3130
+ // sandboxUcic / sandboxEmail / sandboxMobile stay as fallbacks.
3131
+ sandboxUcic: this.options.ucic || this.options.sandboxUcic,
3132
+ sandboxEmail: this.options.email || this.options.sandboxEmail || legacyEmail,
3133
+ sandboxMobile: this.options.mobile || this.options.sandboxMobile || legacyMobile,
3134
+ organisationUuid: this.options.organisationUuid,
3135
+ workspaceUuid: this.options.workspaceUuid
3136
+ };
3137
+ }
2820
3138
  async fetchNotice() {
2821
3139
  var _a, _b, _c, _d;
2822
3140
  this.abortController = new AbortController();
2823
3141
  this.store.setState({ isLoading: true, fetchError: null });
2824
3142
  try {
3143
+ const noticeBaseUrl = this.options.ledgerBaseUrl && !this.options.applicationId && !this.options.token ? this.options.ledgerBaseUrl : this.options.baseUrl;
2825
3144
  const data = await fetchConsentContent({
2826
3145
  noticeId: this.options.noticeId,
2827
3146
  accessToken: this.options.accessToken,
2828
3147
  refreshToken: this.options.refreshToken,
2829
- baseUrl: this.options.baseUrl,
3148
+ baseUrl: noticeBaseUrl,
2830
3149
  language: this.options.language,
2831
3150
  specific_uuid: this.options.applicationId,
2832
3151
  validate_against: this.options.validateAgainst || "all",
2833
3152
  include_fully_consented_data: this.options.includeFullyConsentedData,
2834
- signal: this.abortController.signal
3153
+ signal: this.abortController.signal,
3154
+ ...this.sandboxParams()
2835
3155
  });
2836
3156
  const activeConfig = data.detail.active_config;
2837
3157
  let purposeSelections = data.detail.purpose_selections;
@@ -2893,10 +3213,11 @@ var ModalController = class {
2893
3213
  if (this.options.includeFullyConsentedData && needsConsentPurposes.length === 0 && alreadyConsentedPurposes.length > 0) {
2894
3214
  isFullyConsentedReviewMode = true;
2895
3215
  }
2896
- let selectedLanguage = this.options.language || "en";
2897
- if (activeConfig.default_language) {
2898
- selectedLanguage = activeConfig.default_language;
2899
- }
3216
+ const selectedLanguage = resolveDisplayLanguage(
3217
+ this.options.language,
3218
+ activeConfig.default_language,
3219
+ activeConfig.supported_languages_and_translations
3220
+ );
2900
3221
  let isMinorFlow = false;
2901
3222
  let showAgeVerification = false;
2902
3223
  let restoreState = {};
@@ -3194,49 +3515,32 @@ var ModalController = class {
3194
3515
  (_g = this.ttsManager) == null ? void 0 : _g.fetchAudioUrls({
3195
3516
  accessToken: this.options.accessToken,
3196
3517
  baseUrl: this.options.baseUrl,
3518
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3197
3519
  noticeUuid: activeConfig.notice_uuid,
3198
3520
  language: this.getLanguageCode(state.selectedLanguage),
3199
- content: activeConfig
3521
+ content: activeConfig,
3522
+ ...this.sandboxParams()
3200
3523
  });
3201
3524
  const topRight = el("div", {
3202
3525
  style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }
3203
3526
  }, this.ttsButton, this.langSelectorRefs.container);
3204
- let topSection;
3205
- if (isMobile) {
3206
- const headerRow = el(
3207
- "div",
3208
- {
3209
- style: {
3210
- display: "flex",
3211
- alignItems: "center",
3212
- gap: "10px",
3213
- width: "100%"
3214
- }
3215
- },
3216
- ...logoImg ? [logoImg] : [],
3527
+ const logoPosition = resolveLogoPosition(activeConfig.logo_position);
3528
+ const headerRows = [];
3529
+ if (logoImg) {
3530
+ headerRows.push(
3217
3531
  el(
3218
3532
  "div",
3219
- { style: { marginLeft: "auto", display: "flex", alignItems: "center" } },
3220
- topRight
3533
+ { style: { ...styles.logoRow, justifyContent: logoRowJustify(logoPosition) } },
3534
+ logoImg
3221
3535
  )
3222
3536
  );
3223
- topSection = el(
3224
- "div",
3225
- {
3226
- style: {
3227
- ...styles.topSection,
3228
- flexDirection: "column",
3229
- alignItems: "stretch",
3230
- flexWrap: "nowrap"
3231
- }
3232
- },
3233
- headerRow,
3234
- this.noticeTitleEl
3235
- );
3236
- } else {
3237
- const topLeft = el("div", { style: styles.topLeft }, logoImg, this.noticeTitleEl);
3238
- topSection = el("div", { style: styles.topSection }, topLeft, topRight);
3239
3537
  }
3538
+ headerRows.push(el("div", { style: styles.headerRow }, this.noticeTitleEl, topRight));
3539
+ const topSection = el(
3540
+ "div",
3541
+ { style: { ...styles.headerStack, gap: isMobile ? "12px" : "10px" } },
3542
+ ...headerRows
3543
+ );
3240
3544
  this.middleSection = el("div", { style: styles.middleSection });
3241
3545
  const baseTextStyle = { ...styles.privacyText, color: textColor, fontFamily: font, fontSize: isMobile ? "14px" : "16px" };
3242
3546
  const noticeTextSpan = el("span", { id: "privacy-notice-text" });
@@ -3427,24 +3731,57 @@ var ModalController = class {
3427
3731
  display: "none"
3428
3732
  }
3429
3733
  });
3430
- const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", activeConfig.confirm_button_text);
3734
+ const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
3735
+ "confirm_button_text",
3736
+ activeConfig.confirm_button_text || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
3737
+ );
3738
+ const gridButtonBox = {
3739
+ fontSize: isMobile ? "14px" : "16px",
3740
+ padding: state.isFullyConsentedReviewMode ? isMobile ? "10px 20px" : "9px 45px" : "9px 20px",
3741
+ width: state.isFullyConsentedReviewMode || isMobile ? "100%" : "auto",
3742
+ whiteSpace: "normal",
3743
+ wordBreak: "break-word"
3744
+ };
3431
3745
  const acceptStyle = {
3432
3746
  ...styles.button,
3433
- ...styles.acceptButton,
3434
- backgroundColor: colors.acceptBg,
3435
- color: colors.acceptText,
3747
+ ...state.isFullyConsentedReviewMode ? { ...styles.acceptButton, backgroundColor: colors.acceptBg, color: colors.acceptText } : {
3748
+ ...styles.cancelButton,
3749
+ backgroundColor: "#ffffff",
3750
+ color: colors.acceptShortcutText,
3751
+ borderColor: colors.acceptShortcutBorder
3752
+ },
3436
3753
  borderRadius: colors.borderRadius,
3437
- fontSize: isMobile ? "14px" : "16px",
3438
- padding: isMobile ? "10px 20px" : "9px 45px"
3754
+ ...gridButtonBox
3439
3755
  };
3440
3756
  this.acceptBtn = el("button", {
3441
- id: "confirm-button-text",
3757
+ id: ACCEPT_SELECTED_BUTTON_ID,
3442
3758
  style: acceptStyle,
3443
3759
  type: "button",
3444
- onClick: () => this.handleAccept()
3760
+ onClick: () => this.handleAccept("selected")
3445
3761
  });
3446
3762
  this.acceptBtn.textContent = acceptBtnText;
3447
- const declineBtnText = this.getTranslatedText("decline_button_text", activeConfig.decline_button_text);
3763
+ const shortcutStyle = {
3764
+ ...styles.button,
3765
+ ...styles.acceptButton,
3766
+ backgroundColor: colors.acceptBg,
3767
+ color: colors.acceptText,
3768
+ borderRadius: colors.borderRadius,
3769
+ ...gridButtonBox
3770
+ };
3771
+ this.acceptAllBtn = el("button", {
3772
+ id: ACCEPT_ALL_BUTTON_ID,
3773
+ style: shortcutStyle,
3774
+ type: "button",
3775
+ onClick: () => this.handleAccept("all")
3776
+ });
3777
+ this.acceptAllBtn.textContent = this.getTranslatedText(
3778
+ "accept_all_button_text",
3779
+ activeConfig.accept_all_button_text || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
3780
+ );
3781
+ const declineBtnText = this.getTranslatedText(
3782
+ "decline_button_text",
3783
+ activeConfig.decline_button_text || DEFAULT_DECLINE_BUTTON_TEXT
3784
+ );
3448
3785
  const declineStyle = {
3449
3786
  ...styles.button,
3450
3787
  ...styles.cancelButton,
@@ -3452,11 +3789,13 @@ var ModalController = class {
3452
3789
  color: colors.declineText,
3453
3790
  borderColor: colors.declineBorder,
3454
3791
  borderRadius: colors.borderRadius,
3455
- fontSize: isMobile ? "14px" : "16px",
3456
- padding: isMobile ? "10px 20px" : "9px 45px"
3792
+ ...gridButtonBox,
3793
+ // Pushes everything after decline to the right edge, which is the whole
3794
+ // of the wide layout. Inert in the stack and in review mode.
3795
+ marginRight: state.isFullyConsentedReviewMode || isMobile ? "0" : "auto"
3457
3796
  };
3458
3797
  this.declineBtn = el("button", {
3459
- id: "decline-button-text",
3798
+ id: DECLINE_BUTTON_ID,
3460
3799
  style: declineStyle,
3461
3800
  type: "button",
3462
3801
  onClick: () => this.handleDecline()
@@ -3464,12 +3803,19 @@ var ModalController = class {
3464
3803
  if (!state.isFullyConsentedReviewMode) {
3465
3804
  this.declineBtn.textContent = declineBtnText;
3466
3805
  }
3467
- const bottomSection = el("div", { style: styles.bottomSection });
3468
- if (!state.isFullyConsentedReviewMode) {
3806
+ const bottomSection = el("div", {
3807
+ style: state.isFullyConsentedReviewMode ? styles.bottomSection : isMobile ? styles.actionButtonStack : styles.actionButtonRow
3808
+ });
3809
+ if (state.isFullyConsentedReviewMode) {
3810
+ bottomSection.appendChild(this.acceptBtn);
3811
+ } else if (isMobile) {
3812
+ bottomSection.appendChild(this.acceptAllBtn);
3469
3813
  bottomSection.appendChild(this.acceptBtn);
3470
3814
  bottomSection.appendChild(this.declineBtn);
3471
3815
  } else {
3816
+ bottomSection.appendChild(this.declineBtn);
3472
3817
  bottomSection.appendChild(this.acceptBtn);
3818
+ bottomSection.appendChild(this.acceptAllBtn);
3473
3819
  }
3474
3820
  const contentWrapper = el(
3475
3821
  "div",
@@ -3625,7 +3971,12 @@ var ModalController = class {
3625
3971
  selectedPurposes: { ...state.selectedPurposes, [purposeUuid]: shouldCheckPurpose }
3626
3972
  });
3627
3973
  }
3628
- async handleAccept() {
3974
+ /**
3975
+ * Shared by all three accept buttons. The mode only decides which
3976
+ * purposes/data elements are submitted — everything downstream, including
3977
+ * the host's `onAccept` callback, is identical.
3978
+ */
3979
+ async handleAccept(mode = "selected") {
3629
3980
  var _a, _b, _c;
3630
3981
  const state = this.store.getState();
3631
3982
  if (state.isSubmitting) return;
@@ -3644,6 +3995,16 @@ var ModalController = class {
3644
3995
  if (!content) return;
3645
3996
  this.abortController = new AbortController();
3646
3997
  this.store.setState({ isSubmitting: true, isSubmittingAccept: true, errorMessage: null });
3998
+ const selection = buildSelectionForMode(content.purposes, mode, {
3999
+ selectedPurposes: state.selectedPurposes,
4000
+ selectedDataElements: state.selectedDataElements
4001
+ });
4002
+ if (mode !== "selected") {
4003
+ this.store.setState({
4004
+ selectedPurposes: selection.selectedPurposes,
4005
+ selectedDataElements: selection.selectedDataElements
4006
+ });
4007
+ }
3647
4008
  try {
3648
4009
  const purposes = content.purposes.map((purpose) => {
3649
4010
  const dataElements = purpose.data_elements.filter((de) => de.enabled).map((de) => ({
@@ -3651,13 +4012,13 @@ var ModalController = class {
3651
4012
  name: de.name,
3652
4013
  enabled: de.enabled,
3653
4014
  required: de.required,
3654
- selected: state.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
4015
+ selected: selection.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
3655
4016
  }));
3656
4017
  return {
3657
4018
  uuid: purpose.uuid,
3658
4019
  name: purpose.name,
3659
4020
  description: purpose.description,
3660
- selected: state.selectedPurposes[purpose.uuid] || false,
4021
+ selected: selection.selectedPurposes[purpose.uuid] || false,
3661
4022
  data_elements: dataElements
3662
4023
  };
3663
4024
  });
@@ -3671,7 +4032,9 @@ var ModalController = class {
3671
4032
  meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
3672
4033
  guardianVerificationReference: state.verificationReference || void 0,
3673
4034
  selfDeclaredAdult: state.selfDeclaredAdult || void 0,
3674
- signal: this.abortController.signal
4035
+ language: state.selectedLanguage,
4036
+ signal: this.abortController.signal,
4037
+ ...this.sandboxParams()
3675
4038
  });
3676
4039
  this.options.onAccept();
3677
4040
  this.destroy();
@@ -3717,11 +4080,16 @@ var ModalController = class {
3717
4080
  }
3718
4081
  updateAcceptButtonState(state) {
3719
4082
  if (!this.acceptBtn || state.isFullyConsentedReviewMode) return;
4083
+ const submitting = state.isSubmittingAccept || state.isSubmittingDecline;
3720
4084
  const allRequiredChecked = this.areAllRequiredChecked(state);
3721
- const disabled = !allRequiredChecked || state.isSubmittingAccept || state.isSubmittingDecline;
3722
- this.acceptBtn.disabled = disabled;
3723
- this.acceptBtn.style.opacity = disabled ? "0.5" : "1";
3724
- this.acceptBtn.style.cursor = disabled ? "not-allowed" : "pointer";
4085
+ const setDisabled = (btn, disabled) => {
4086
+ if (!btn) return;
4087
+ btn.disabled = disabled;
4088
+ btn.style.opacity = disabled ? "0.5" : "1";
4089
+ btn.style.cursor = disabled ? "not-allowed" : "pointer";
4090
+ };
4091
+ setDisabled(this.acceptBtn, !allRequiredChecked || submitting);
4092
+ setDisabled(this.acceptAllBtn, submitting);
3725
4093
  }
3726
4094
  updateCollapseState(state) {
3727
4095
  const updateRefs = (refs) => {
@@ -4094,25 +4462,34 @@ var ModalController = class {
4094
4462
  }
4095
4463
  updateButtonStates(state) {
4096
4464
  var _a;
4097
- if (this.acceptBtn) {
4098
- if (state.isSubmittingAccept) {
4099
- this.acceptBtn.textContent = "Submitting...";
4100
- this.acceptBtn.disabled = true;
4101
- this.acceptBtn.style.opacity = "0.7";
4102
- this.acceptBtn.style.cursor = "not-allowed";
4103
- } else {
4104
- const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4105
- this.acceptBtn.textContent = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", (content == null ? void 0 : content.confirm_button_text) || "Accept");
4106
- this.acceptBtn.disabled = false;
4107
- this.acceptBtn.style.opacity = "1";
4108
- this.acceptBtn.style.cursor = "pointer";
4109
- }
4110
- }
4465
+ const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4466
+ const applySubmittingState = (btn, idleLabel) => {
4467
+ if (!btn) return;
4468
+ btn.textContent = state.isSubmittingAccept ? SUBMITTING_BUTTON_TEXT : idleLabel;
4469
+ btn.disabled = state.isSubmittingAccept;
4470
+ btn.style.opacity = state.isSubmittingAccept ? "0.7" : "1";
4471
+ btn.style.cursor = state.isSubmittingAccept ? "not-allowed" : "pointer";
4472
+ };
4473
+ applySubmittingState(
4474
+ this.acceptBtn,
4475
+ state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
4476
+ "confirm_button_text",
4477
+ (content == null ? void 0 : content.confirm_button_text) || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
4478
+ )
4479
+ );
4480
+ applySubmittingState(
4481
+ this.acceptAllBtn,
4482
+ this.getTranslatedText(
4483
+ "accept_all_button_text",
4484
+ (content == null ? void 0 : content.accept_all_button_text) || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
4485
+ )
4486
+ );
4111
4487
  if (this.declineBtn) {
4112
4488
  this.declineBtn.disabled = state.isSubmitting;
4113
4489
  this.declineBtn.style.opacity = state.isSubmitting ? "0.7" : "1";
4114
4490
  this.declineBtn.style.cursor = state.isSubmitting ? "not-allowed" : "pointer";
4115
4491
  }
4492
+ this.updateAcceptButtonState(state);
4116
4493
  }
4117
4494
  };
4118
4495
 
@@ -4313,6 +4690,9 @@ function renderInlinePurposes(purposes, options) {
4313
4690
  }
4314
4691
  });
4315
4692
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
4693
+ if (hasRequiredDataElement(purpose)) {
4694
+ titleEl.appendChild(createRequiredMarker());
4695
+ }
4316
4696
  titleElements.set(purpose.uuid, titleEl);
4317
4697
  const descEl = el("h3", {
4318
4698
  style: {
@@ -4365,12 +4745,7 @@ function renderInlinePurposes(purposes, options) {
4365
4745
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
4366
4746
  dataElementLabels.set(combinedId, labelSpan);
4367
4747
  if (dataElement.required) {
4368
- const asterisk = el("span", {
4369
- style: { color: "red", marginLeft: "4px" },
4370
- "aria-label": "required"
4371
- });
4372
- asterisk.textContent = "*";
4373
- labelSpan.appendChild(asterisk);
4748
+ labelSpan.appendChild(createRequiredMarker());
4374
4749
  }
4375
4750
  const deCheckbox = el("input", {
4376
4751
  type: "checkbox",
@@ -4416,6 +4791,8 @@ var InlineController = class {
4416
4791
  this.errorEl = null;
4417
4792
  this.hasSubmitted = false;
4418
4793
  this.hasCheckedLedgerConsent = false;
4794
+ this.fetchedWithToken = false;
4795
+ this.fetchGeneration = 0;
4419
4796
  this.unsubscribe = null;
4420
4797
  this.abortController = null;
4421
4798
  this.options = options;
@@ -4473,6 +4850,10 @@ var InlineController = class {
4473
4850
  }
4474
4851
  updateToken(accessToken) {
4475
4852
  this.options = { ...this.options, accessToken };
4853
+ if (accessToken && !this.fetchedWithToken) {
4854
+ void this.fetchNotice();
4855
+ return;
4856
+ }
4476
4857
  if (this.shouldCheckLedgerConsent()) {
4477
4858
  void this.checkLedgerConsent();
4478
4859
  return;
@@ -4488,17 +4869,20 @@ var InlineController = class {
4488
4869
  }
4489
4870
  async fetchNotice() {
4490
4871
  var _a, _b, _c, _d;
4491
- this.store.setState({ isLoading: true, fetchError: null });
4872
+ const generation = ++this.fetchGeneration;
4873
+ this.fetchedWithToken = Boolean(this.options.accessToken);
4874
+ this.store.setState({ isLoading: true, fetchError: null, hasAlreadyConsented: false });
4492
4875
  try {
4493
4876
  const data = await fetchConsentContentPublic({
4494
4877
  org_uuid: this.options.org_uuid,
4495
4878
  workspace_uuid: this.options.workspace_uuid,
4496
4879
  notice_uuid: this.options.notice_uuid,
4880
+ accessToken: this.options.accessToken,
4497
4881
  baseUrl: this.options.baseUrl,
4498
4882
  language: this.options.language,
4499
4883
  specific_uuid: this.options.applicationId
4500
4884
  });
4501
- if (this.isDestroyed()) {
4885
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4502
4886
  return;
4503
4887
  }
4504
4888
  const activeConfig = data.detail.active_config;
@@ -4516,13 +4900,17 @@ var InlineController = class {
4516
4900
  this.store.setState({
4517
4901
  content: activeConfig,
4518
4902
  isLoading: false,
4519
- selectedLanguage: activeConfig.default_language || this.options.language || "en",
4903
+ selectedLanguage: resolveDisplayLanguage(
4904
+ this.options.language,
4905
+ activeConfig.default_language,
4906
+ activeConfig.supported_languages_and_translations
4907
+ ),
4520
4908
  collapsedPurposes: initialCollapsed,
4521
4909
  selectedPurposes: initialPurposes,
4522
4910
  selectedDataElements: initialElements
4523
4911
  });
4524
4912
  } catch (err) {
4525
- if (this.isDestroyed()) {
4913
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4526
4914
  return;
4527
4915
  }
4528
4916
  const error = err;
@@ -4839,7 +5227,8 @@ var InlineController = class {
4839
5227
  };
4840
5228
  }),
4841
5229
  declined: false,
4842
- meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0
5230
+ meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
5231
+ language: state.selectedLanguage
4843
5232
  });
4844
5233
  (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4845
5234
  this.hasSubmitted = false;