@redacto.io/consent-sdk-js 1.0.0-beta.0 → 1.0.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,133 @@ 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;
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 subject = (_e = options.sandboxSubject) == null ? void 0 : _e.trim();
228
+ if (!organisationUuid || !workspaceUuid) {
229
+ throw new Error(
230
+ "Sandbox mode requires organisationUuid and workspaceUuid"
231
+ );
232
+ }
233
+ if (!ucic && !subject) {
234
+ throw new Error("Sandbox mode requires a UCIC, email, or mobile");
235
+ }
236
+ return { token, ucic, subject: subject != null ? subject : "", organisationUuid, workspaceUuid };
237
+ };
238
+ var resolveRequestAuth = (accessToken, options) => {
239
+ const sandbox = resolveSandboxAuth(options);
240
+ if (sandbox) {
241
+ return {
242
+ organisationUuid: sandbox.organisationUuid,
243
+ workspaceUuid: sandbox.workspaceUuid,
244
+ sandbox
245
+ };
246
+ }
247
+ const decodedToken = accessToken ? decodeTokenSafely(accessToken) : null;
248
+ if (!decodedToken) {
249
+ throw new Error("Invalid access token");
250
+ }
251
+ const { organisation_uuid, workspace_uuid } = decodedToken;
252
+ if (!organisation_uuid || !workspace_uuid) {
253
+ throw new Error("Invalid token: missing organization or workspace UUID");
254
+ }
255
+ return {
256
+ organisationUuid: organisation_uuid,
257
+ workspaceUuid: workspace_uuid,
258
+ sandbox: null
259
+ };
260
+ };
261
+ var buildAuthHeaders = (accessToken, sandbox) => {
262
+ if (sandbox) {
263
+ return { [SANDBOX_TOKEN_HEADER]: sandbox.token };
264
+ }
265
+ return { Authorization: `Bearer ${accessToken}` };
266
+ };
267
+ var sandboxBodyIdentity = (sandbox) => {
268
+ if (sandbox.ucic) {
269
+ return { org_user_id: sandbox.ucic };
270
+ }
271
+ return sandbox.subject.includes("@") ? { primary_email: sandbox.subject } : { primary_mobile: sandbox.subject };
272
+ };
273
+ var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
274
+
275
+ // src/api/constants.ts
276
+ var CODE_BY_NAME = /* @__PURE__ */ new Map([
277
+ ["english", "en"],
278
+ ["assamese", "as"],
279
+ ["bengali", "bn"],
280
+ ["bodo", "brx"],
281
+ ["dogri", "doi"],
282
+ ["goan konkani", "gom"],
283
+ ["gujarati", "gu"],
284
+ ["hindi", "hi"],
285
+ ["kannada", "kn"],
286
+ ["kashmiri", "ks"],
287
+ ["maithili", "mai"],
288
+ ["malayalam", "ml"],
289
+ ["manipuri", "mni-Mtei"],
290
+ ["marathi", "mr"],
291
+ ["nepali", "ne"],
292
+ ["odia", "or"],
293
+ ["punjabi", "pa"],
294
+ ["sanskrit", "sa"],
295
+ ["santali", "sat"],
296
+ ["sindhi", "sd"],
297
+ ["tamil", "ta"],
298
+ ["telugu", "te"],
299
+ ["urdu", "ur"]
300
+ ]);
301
+ var CODE_BY_LOWER = new Map(
302
+ [...CODE_BY_NAME.values()].map((c) => [c.toLowerCase(), c])
303
+ );
304
+
305
+ // src/api/language.ts
306
+ function toLanguageCode(lang) {
307
+ var _a, _b;
308
+ if (!lang) {
309
+ return void 0;
310
+ }
311
+ const trimmed = lang.trim();
312
+ if (!trimmed) {
313
+ return void 0;
314
+ }
315
+ const lower = trimmed.toLowerCase();
316
+ return (_b = (_a = CODE_BY_LOWER.get(lower)) != null ? _a : CODE_BY_NAME.get(lower)) != null ? _b : trimmed;
317
+ }
318
+ function resolveDisplayLanguage(requested, defaultLanguage, translations) {
319
+ const fallback = defaultLanguage || "en";
320
+ const requestedCode = toLanguageCode(requested);
321
+ if (!requestedCode) {
322
+ return fallback;
323
+ }
324
+ for (const key of Object.keys(translations || {})) {
325
+ if (toLanguageCode(key) === requestedCode) {
326
+ return key;
327
+ }
328
+ }
329
+ return fallback;
330
+ }
331
+
216
332
  // src/api/index.ts
217
333
  var BASE_URL = "https://api.redacto.io/consent";
218
334
  var BASE_URL_PUBLIC = "https://api.redacto.tech/consent";
@@ -224,17 +340,22 @@ var fetchConsentContent = async ({
224
340
  specific_uuid,
225
341
  validate_against = "all",
226
342
  include_fully_consented_data = false,
227
- signal
343
+ signal,
344
+ ...sandboxParams
228
345
  }) => {
229
- if (!noticeId || !accessToken || !validate_against) {
230
- throw new Error("noticeId and accessToken are required");
346
+ if (!noticeId || !validate_against) {
347
+ throw new Error("noticeId is required");
231
348
  }
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");
349
+ if (!accessToken && !sandboxParams.token) {
350
+ throw new Error("accessToken or token is required");
351
+ }
352
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
353
+ accessToken,
354
+ sandboxParams
355
+ );
236
356
  const apiBaseUrl = baseUrl || BASE_URL;
237
- const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
357
+ const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.subject}` : accessToken;
358
+ const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
238
359
  if (validate_against === "all" && !specific_uuid) {
239
360
  const cached = getCachedData(cacheKey);
240
361
  if (cached) return cached;
@@ -243,10 +364,15 @@ var fetchConsentContent = async ({
243
364
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
244
365
  url.searchParams.append("validate_against", validate_against);
245
366
  if (include_fully_consented_data) url.searchParams.append("include_fully_consented_data", "true");
367
+ if (sandbox) {
368
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
369
+ url.searchParams.append(key, value);
370
+ }
371
+ }
246
372
  const response = await fetch(url.toString(), {
247
373
  method: "GET",
248
374
  headers: {
249
- Authorization: `Bearer ${accessToken}`,
375
+ ...buildAuthHeaders(accessToken, sandbox),
250
376
  "Accept-Language": language
251
377
  },
252
378
  signal
@@ -272,18 +398,24 @@ var fetchConsentContentPublic = async ({
272
398
  org_uuid,
273
399
  workspace_uuid,
274
400
  notice_uuid,
401
+ accessToken,
275
402
  baseUrl,
276
403
  language = "en",
277
404
  specific_uuid
278
405
  }) => {
279
406
  const apiBaseUrl = baseUrl || BASE_URL_PUBLIC;
407
+ const noticesUrl = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices`;
280
408
  const url = new URL(
281
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices/get-notice/${notice_uuid}`
409
+ accessToken ? `${noticesUrl}/${notice_uuid}` : `${noticesUrl}/get-notice/${notice_uuid}`
282
410
  );
283
411
  if (specific_uuid) url.searchParams.append("specific_uuid", specific_uuid);
284
412
  const response = await fetch(url.toString(), {
285
413
  method: "GET",
286
- headers: { "Content-Type": "application/json" }
414
+ headers: {
415
+ "Content-Type": "application/json",
416
+ "Accept-Language": language,
417
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
418
+ }
287
419
  });
288
420
  if (response.status === 401) {
289
421
  const error = new Error("Unauthorized");
@@ -310,16 +442,21 @@ var submitConsentEvent = async ({
310
442
  meta_data,
311
443
  guardianVerificationReference,
312
444
  selfDeclaredAdult,
313
- signal
445
+ language,
446
+ signal,
447
+ ...sandboxParams
314
448
  }) => {
315
- if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
316
- throw new Error("noticeUuid, accessToken, and purposes array are required");
449
+ if (!noticeUuid || !Array.isArray(purposes)) {
450
+ throw new Error("noticeUuid and purposes array are required");
317
451
  }
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;
452
+ if (!accessToken && !sandboxParams.token) {
453
+ throw new Error("accessToken or token is required");
454
+ }
455
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
456
+ accessToken,
457
+ sandboxParams
458
+ );
459
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
323
460
  const validatedPurposes = purposes.map((purpose) => {
324
461
  if (!purpose.uuid) throw new Error("Invalid purpose data: missing uuid");
325
462
  const dataElements = (purpose.data_elements || []).filter((element) => element.enabled).map((element) => ({
@@ -340,17 +477,19 @@ var submitConsentEvent = async ({
340
477
  declined: Boolean(declined),
341
478
  meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid, minor_age: meta_data.minor_age } : void 0,
342
479
  guardian_verification_reference: guardianVerificationReference,
343
- self_declared_adult: selfDeclaredAdult
480
+ self_declared_adult: selfDeclaredAdult,
481
+ language: toLanguageCode(language)
344
482
  };
483
+ const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
345
484
  const response = await fetch(
346
485
  `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/submit-consent`,
347
486
  {
348
487
  method: "POST",
349
488
  headers: {
350
- Authorization: `Bearer ${accessToken}`,
489
+ ...buildAuthHeaders(accessToken, sandbox),
351
490
  "Content-Type": "application/json"
352
491
  },
353
- body: JSON.stringify(payload),
492
+ body: JSON.stringify(requestBody),
354
493
  signal
355
494
  }
356
495
  );
@@ -382,7 +521,8 @@ var submitConsentEventPublic = async ({
382
521
  noticeUuid,
383
522
  purposes,
384
523
  declined,
385
- meta_data
524
+ meta_data,
525
+ language
386
526
  }) => {
387
527
  var _a, _b;
388
528
  const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL_PUBLIC;
@@ -403,7 +543,8 @@ var submitConsentEventPublic = async ({
403
543
  select_all_mandatory: false,
404
544
  source: "WEB",
405
545
  declined,
406
- meta_data
546
+ meta_data,
547
+ language: toLanguageCode(language)
407
548
  };
408
549
  const response = await fetch(
409
550
  `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
@@ -442,23 +583,32 @@ var submitConsentEventPublic = async ({
442
583
  var fetchTTSAudioUrls = async ({
443
584
  accessToken,
444
585
  baseUrl,
586
+ ledgerBaseUrl,
445
587
  noticeUuid,
446
588
  language,
447
- signal
589
+ signal,
590
+ ...sandboxParams
448
591
  }) => {
449
- if (!accessToken || !noticeUuid || !language) {
450
- throw new Error("accessToken, noticeUuid, and language are required");
592
+ if (!accessToken && !sandboxParams.token || !noticeUuid || !language) {
593
+ throw new Error("accessToken (or token), noticeUuid, and language are required");
451
594
  }
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, {
595
+ const { organisationUuid: ORG, workspaceUuid: WS, sandbox } = resolveRequestAuth(
596
+ accessToken,
597
+ sandboxParams
598
+ );
599
+ const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
600
+ const url = new URL(
601
+ `${apiBaseUrl}/public/organisations/${ORG}/workspaces/${WS}/notices/${noticeUuid}/audio/${language}`
602
+ );
603
+ if (sandbox) {
604
+ for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
605
+ url.searchParams.append(key, value);
606
+ }
607
+ }
608
+ const response = await fetch(url.toString(), {
459
609
  method: "GET",
460
610
  headers: {
461
- Authorization: `Bearer ${accessToken}`,
611
+ ...buildAuthHeaders(accessToken, sandbox),
462
612
  Accept: "application/json"
463
613
  },
464
614
  signal
@@ -925,6 +1075,42 @@ var styles = {
925
1075
  padding: 0,
926
1076
  boxSizing: "border-box"
927
1077
  },
1078
+ // The main notice header is two stacked rows: the logo alone on row 1,
1079
+ // aligned per active_config.logo_position, then the title and the
1080
+ // TTS/language controls on row 2. `topSection` and `topLeft` above keep their
1081
+ // single-row shape for the age-verification, guardian and verification
1082
+ // screens, which have no controls to place and share those styles.
1083
+ // Mirrors consent-sdk-react/src/RedactoNoticeConsent/styles.ts.
1084
+ headerStack: {
1085
+ display: "flex",
1086
+ flexDirection: "column",
1087
+ alignItems: "stretch",
1088
+ gap: "10px",
1089
+ paddingBottom: "15px",
1090
+ borderBottom: "none",
1091
+ backgroundColor: "#ffffff",
1092
+ margin: 0,
1093
+ boxSizing: "border-box",
1094
+ textAlign: "left"
1095
+ },
1096
+ logoRow: {
1097
+ display: "flex",
1098
+ alignItems: "center",
1099
+ width: "100%",
1100
+ margin: 0,
1101
+ padding: 0,
1102
+ boxSizing: "border-box"
1103
+ },
1104
+ headerRow: {
1105
+ display: "flex",
1106
+ alignItems: "center",
1107
+ justifyContent: "space-between",
1108
+ gap: "10px",
1109
+ width: "100%",
1110
+ margin: 0,
1111
+ padding: 0,
1112
+ boxSizing: "border-box"
1113
+ },
928
1114
  logo: {
929
1115
  height: "32px",
930
1116
  width: "auto",
@@ -1156,6 +1342,9 @@ var styles = {
1156
1342
  },
1157
1343
  languageSelectorContainer: {
1158
1344
  position: "relative",
1345
+ // Shares `headerRow` with the title, which grows; without this a long
1346
+ // heading squeezes the language button instead of wrapping itself.
1347
+ flexShrink: 0,
1159
1348
  margin: 0,
1160
1349
  padding: 0,
1161
1350
  boxSizing: "border-box"
@@ -1718,6 +1907,12 @@ var updateConsentState = (updates) => {
1718
1907
  };
1719
1908
 
1720
1909
  // src/constants.ts
1910
+ var DEFAULT_LOGO_POSITION = "left";
1911
+ var LOGO_POSITION_JUSTIFY = {
1912
+ left: "flex-start",
1913
+ center: "center",
1914
+ right: "flex-end"
1915
+ };
1721
1916
  var DPO_AUDIO_ELEMENT_MAP = [
1722
1917
  { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1723
1918
  { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
@@ -1776,9 +1971,15 @@ var TTSManager = class {
1776
1971
  const response = await fetchTTSAudioUrls({
1777
1972
  accessToken: params.accessToken,
1778
1973
  baseUrl: params.baseUrl,
1974
+ ledgerBaseUrl: params.ledgerBaseUrl,
1779
1975
  noticeUuid: params.noticeUuid,
1780
1976
  language: params.language,
1781
- signal: this.abortController.signal
1977
+ signal: this.abortController.signal,
1978
+ token: params.token,
1979
+ sandboxUcic: params.sandboxUcic,
1980
+ sandboxSubject: params.sandboxSubject,
1981
+ organisationUuid: params.organisationUuid,
1982
+ workspaceUuid: params.workspaceUuid
1782
1983
  });
1783
1984
  this.audioUrls = response.detail;
1784
1985
  this.buildSequences(params.content);
@@ -2743,6 +2944,14 @@ function resolveColors(settings, activeConfig) {
2743
2944
  };
2744
2945
  }
2745
2946
 
2947
+ // src/modal/layout.ts
2948
+ function resolveLogoPosition(raw) {
2949
+ return raw === "center" || raw === "right" ? raw : DEFAULT_LOGO_POSITION;
2950
+ }
2951
+ function logoRowJustify(position) {
2952
+ return LOGO_POSITION_JUSTIFY[position];
2953
+ }
2954
+
2746
2955
  // src/modal/index.ts
2747
2956
  var ModalController = class {
2748
2957
  constructor(options) {
@@ -2847,21 +3056,41 @@ var ModalController = class {
2847
3056
  this.langSelectorRefs = null;
2848
3057
  }
2849
3058
  // ─── Private ───
3059
+ /**
3060
+ * Sandbox auth options to forward to every API call. When `token` is set the
3061
+ * API layer sends the single `X-Consent-Token` header, injects the subject
3062
+ * into each request's payload, and reads org/workspace from these fields
3063
+ * instead of decoding the JWT. Empty in the normal JWT flow.
3064
+ */
3065
+ sandboxParams() {
3066
+ return {
3067
+ token: this.options.token,
3068
+ // The static token carries no identity; read the acting principal from the
3069
+ // optional ucic/email/mobile. UCIC takes precedence server-side.
3070
+ // sandboxUcic / sandboxSubject stay as fallbacks.
3071
+ sandboxUcic: this.options.ucic || this.options.sandboxUcic,
3072
+ sandboxSubject: this.options.email || this.options.mobile || this.options.sandboxSubject,
3073
+ organisationUuid: this.options.organisationUuid,
3074
+ workspaceUuid: this.options.workspaceUuid
3075
+ };
3076
+ }
2850
3077
  async fetchNotice() {
2851
3078
  var _a, _b, _c, _d;
2852
3079
  this.abortController = new AbortController();
2853
3080
  this.store.setState({ isLoading: true, fetchError: null });
2854
3081
  try {
3082
+ const noticeBaseUrl = this.options.ledgerBaseUrl && !this.options.applicationId && !this.options.token ? this.options.ledgerBaseUrl : this.options.baseUrl;
2855
3083
  const data = await fetchConsentContent({
2856
3084
  noticeId: this.options.noticeId,
2857
3085
  accessToken: this.options.accessToken,
2858
3086
  refreshToken: this.options.refreshToken,
2859
- baseUrl: this.options.baseUrl,
3087
+ baseUrl: noticeBaseUrl,
2860
3088
  language: this.options.language,
2861
3089
  specific_uuid: this.options.applicationId,
2862
3090
  validate_against: this.options.validateAgainst || "all",
2863
3091
  include_fully_consented_data: this.options.includeFullyConsentedData,
2864
- signal: this.abortController.signal
3092
+ signal: this.abortController.signal,
3093
+ ...this.sandboxParams()
2865
3094
  });
2866
3095
  const activeConfig = data.detail.active_config;
2867
3096
  let purposeSelections = data.detail.purpose_selections;
@@ -2923,10 +3152,11 @@ var ModalController = class {
2923
3152
  if (this.options.includeFullyConsentedData && needsConsentPurposes.length === 0 && alreadyConsentedPurposes.length > 0) {
2924
3153
  isFullyConsentedReviewMode = true;
2925
3154
  }
2926
- let selectedLanguage = this.options.language || "en";
2927
- if (activeConfig.default_language) {
2928
- selectedLanguage = activeConfig.default_language;
2929
- }
3155
+ const selectedLanguage = resolveDisplayLanguage(
3156
+ this.options.language,
3157
+ activeConfig.default_language,
3158
+ activeConfig.supported_languages_and_translations
3159
+ );
2930
3160
  let isMinorFlow = false;
2931
3161
  let showAgeVerification = false;
2932
3162
  let restoreState = {};
@@ -3224,49 +3454,32 @@ var ModalController = class {
3224
3454
  (_g = this.ttsManager) == null ? void 0 : _g.fetchAudioUrls({
3225
3455
  accessToken: this.options.accessToken,
3226
3456
  baseUrl: this.options.baseUrl,
3457
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3227
3458
  noticeUuid: activeConfig.notice_uuid,
3228
3459
  language: this.getLanguageCode(state.selectedLanguage),
3229
- content: activeConfig
3460
+ content: activeConfig,
3461
+ ...this.sandboxParams()
3230
3462
  });
3231
3463
  const topRight = el("div", {
3232
3464
  style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }
3233
3465
  }, 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] : [],
3466
+ const logoPosition = resolveLogoPosition(activeConfig.logo_position);
3467
+ const headerRows = [];
3468
+ if (logoImg) {
3469
+ headerRows.push(
3247
3470
  el(
3248
3471
  "div",
3249
- { style: { marginLeft: "auto", display: "flex", alignItems: "center" } },
3250
- topRight
3472
+ { style: { ...styles.logoRow, justifyContent: logoRowJustify(logoPosition) } },
3473
+ logoImg
3251
3474
  )
3252
3475
  );
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
3476
  }
3477
+ headerRows.push(el("div", { style: styles.headerRow }, this.noticeTitleEl, topRight));
3478
+ const topSection = el(
3479
+ "div",
3480
+ { style: { ...styles.headerStack, gap: isMobile ? "12px" : "10px" } },
3481
+ ...headerRows
3482
+ );
3270
3483
  this.middleSection = el("div", { style: styles.middleSection });
3271
3484
  const baseTextStyle = { ...styles.privacyText, color: textColor, fontFamily: font, fontSize: isMobile ? "14px" : "16px" };
3272
3485
  const noticeTextSpan = el("span", { id: "privacy-notice-text" });
@@ -3701,7 +3914,9 @@ var ModalController = class {
3701
3914
  meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
3702
3915
  guardianVerificationReference: state.verificationReference || void 0,
3703
3916
  selfDeclaredAdult: state.selfDeclaredAdult || void 0,
3704
- signal: this.abortController.signal
3917
+ language: state.selectedLanguage,
3918
+ signal: this.abortController.signal,
3919
+ ...this.sandboxParams()
3705
3920
  });
3706
3921
  this.options.onAccept();
3707
3922
  this.destroy();
@@ -4446,7 +4661,10 @@ var InlineController = class {
4446
4661
  this.errorEl = null;
4447
4662
  this.hasSubmitted = false;
4448
4663
  this.hasCheckedLedgerConsent = false;
4664
+ this.fetchedWithToken = false;
4665
+ this.fetchGeneration = 0;
4449
4666
  this.unsubscribe = null;
4667
+ this.abortController = null;
4450
4668
  this.options = options;
4451
4669
  this.store = new Store({
4452
4670
  content: null,
@@ -4464,6 +4682,7 @@ var InlineController = class {
4464
4682
  this.mediaQuery = createMediaQuery("(max-width: 768px)");
4465
4683
  }
4466
4684
  async init() {
4685
+ this.abortController = new AbortController();
4467
4686
  if (typeof this.options.target === "string") {
4468
4687
  this.targetEl = document.querySelector(this.options.target);
4469
4688
  if (!this.targetEl) {
@@ -4486,8 +4705,9 @@ var InlineController = class {
4486
4705
  await this.fetchNotice();
4487
4706
  }
4488
4707
  destroy() {
4489
- var _a;
4490
- (_a = this.unsubscribe) == null ? void 0 : _a.call(this);
4708
+ var _a, _b;
4709
+ (_a = this.abortController) == null ? void 0 : _a.abort();
4710
+ (_b = this.unsubscribe) == null ? void 0 : _b.call(this);
4491
4711
  this.mediaQuery.destroy();
4492
4712
  this.store.destroy();
4493
4713
  if (this.hostEl && this.hostEl.parentNode) {
@@ -4500,6 +4720,10 @@ var InlineController = class {
4500
4720
  }
4501
4721
  updateToken(accessToken) {
4502
4722
  this.options = { ...this.options, accessToken };
4723
+ if (accessToken && !this.fetchedWithToken) {
4724
+ void this.fetchNotice();
4725
+ return;
4726
+ }
4503
4727
  if (this.shouldCheckLedgerConsent()) {
4504
4728
  void this.checkLedgerConsent();
4505
4729
  return;
@@ -4507,18 +4731,30 @@ var InlineController = class {
4507
4731
  this.tryAutoSubmit();
4508
4732
  }
4509
4733
  // ─── Private ───
4734
+ // destroy() aborts the controller, so an aborted signal is the marker that
4735
+ // teardown has happened and async continuations must not touch the host.
4736
+ isDestroyed() {
4737
+ var _a;
4738
+ return Boolean((_a = this.abortController) == null ? void 0 : _a.signal.aborted);
4739
+ }
4510
4740
  async fetchNotice() {
4511
4741
  var _a, _b, _c, _d;
4512
- this.store.setState({ isLoading: true, fetchError: null });
4742
+ const generation = ++this.fetchGeneration;
4743
+ this.fetchedWithToken = Boolean(this.options.accessToken);
4744
+ this.store.setState({ isLoading: true, fetchError: null, hasAlreadyConsented: false });
4513
4745
  try {
4514
4746
  const data = await fetchConsentContentPublic({
4515
4747
  org_uuid: this.options.org_uuid,
4516
4748
  workspace_uuid: this.options.workspace_uuid,
4517
4749
  notice_uuid: this.options.notice_uuid,
4750
+ accessToken: this.options.accessToken,
4518
4751
  baseUrl: this.options.baseUrl,
4519
4752
  language: this.options.language,
4520
4753
  specific_uuid: this.options.applicationId
4521
4754
  });
4755
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4756
+ return;
4757
+ }
4522
4758
  const activeConfig = data.detail.active_config;
4523
4759
  const initialCollapsed = {};
4524
4760
  const initialPurposes = {};
@@ -4534,12 +4770,19 @@ var InlineController = class {
4534
4770
  this.store.setState({
4535
4771
  content: activeConfig,
4536
4772
  isLoading: false,
4537
- selectedLanguage: activeConfig.default_language || this.options.language || "en",
4773
+ selectedLanguage: resolveDisplayLanguage(
4774
+ this.options.language,
4775
+ activeConfig.default_language,
4776
+ activeConfig.supported_languages_and_translations
4777
+ ),
4538
4778
  collapsedPurposes: initialCollapsed,
4539
4779
  selectedPurposes: initialPurposes,
4540
4780
  selectedDataElements: initialElements
4541
4781
  });
4542
4782
  } catch (err) {
4783
+ if (this.isDestroyed() || generation !== this.fetchGeneration) {
4784
+ return;
4785
+ }
4543
4786
  const error = err;
4544
4787
  if (error.status === 409) {
4545
4788
  this.store.setState({ hasAlreadyConsented: true, isLoading: false });
@@ -4558,7 +4801,7 @@ var InlineController = class {
4558
4801
  // (the 409 path governs) and for the magic-link applicationId flow. Any
4559
4802
  // failure returns null and falls through to the normal flow.
4560
4803
  async checkLedgerConsent() {
4561
- var _a, _b;
4804
+ var _a, _b, _c;
4562
4805
  if (!this.shouldCheckLedgerConsent()) return;
4563
4806
  this.hasCheckedLedgerConsent = true;
4564
4807
  this.store.setState({ isCheckingConsent: true });
@@ -4568,15 +4811,21 @@ var InlineController = class {
4568
4811
  ledgerBaseUrl: this.options.ledgerBaseUrl,
4569
4812
  organisationUuid: this.options.org_uuid,
4570
4813
  workspaceUuid: this.options.workspace_uuid,
4571
- noticeUuid: this.options.notice_uuid
4814
+ noticeUuid: this.options.notice_uuid,
4815
+ signal: (_a = this.abortController) == null ? void 0 : _a.signal
4572
4816
  });
4817
+ if (this.isDestroyed()) {
4818
+ return;
4819
+ }
4573
4820
  if (status && isNoticeFullyConsented(status)) {
4574
4821
  this.store.setState({ hasAlreadyConsented: true });
4575
- (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4822
+ (_c = (_b = this.options).onAccept) == null ? void 0 : _c.call(_b);
4576
4823
  }
4577
4824
  } finally {
4578
- this.store.setState({ isCheckingConsent: false });
4579
- this.tryAutoSubmit();
4825
+ if (!this.isDestroyed()) {
4826
+ this.store.setState({ isCheckingConsent: false });
4827
+ this.tryAutoSubmit();
4828
+ }
4580
4829
  }
4581
4830
  }
4582
4831
  handleStateChange(state, prevState) {
@@ -4798,6 +5047,9 @@ var InlineController = class {
4798
5047
  }
4799
5048
  async tryAutoSubmit() {
4800
5049
  var _a, _b;
5050
+ if (this.isDestroyed()) {
5051
+ return;
5052
+ }
4801
5053
  const state = this.store.getState();
4802
5054
  const accessToken = this.options.accessToken;
4803
5055
  if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !state.isCheckingConsent && !state.hasAlreadyConsented && !this.hasSubmitted) {
@@ -4845,7 +5097,8 @@ var InlineController = class {
4845
5097
  };
4846
5098
  }),
4847
5099
  declined: false,
4848
- meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0
5100
+ meta_data: this.options.applicationId ? { specific_uuid: this.options.applicationId } : void 0,
5101
+ language: state.selectedLanguage
4849
5102
  });
4850
5103
  (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4851
5104
  this.hasSubmitted = false;