@redacto.io/consent-sdk-js 1.0.0 → 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
@@ -186,7 +186,7 @@ function decodeTokenSafely(token) {
186
186
  // src/api/sandbox.ts
187
187
  var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
188
188
  var resolveSandboxAuth = (options) => {
189
- var _a, _b, _c, _d, _e;
189
+ var _a, _b, _c, _d, _e, _f;
190
190
  const token = (_a = options.token) == null ? void 0 : _a.trim();
191
191
  if (!token) {
192
192
  return null;
@@ -194,16 +194,17 @@ var resolveSandboxAuth = (options) => {
194
194
  const organisationUuid = (_b = options.organisationUuid) == null ? void 0 : _b.trim();
195
195
  const workspaceUuid = (_c = options.workspaceUuid) == null ? void 0 : _c.trim();
196
196
  const ucic = (_d = options.sandboxUcic) == null ? void 0 : _d.trim();
197
- const subject = (_e = options.sandboxSubject) == null ? void 0 : _e.trim();
197
+ const email = (_e = options.sandboxEmail) == null ? void 0 : _e.trim();
198
+ const mobile = (_f = options.sandboxMobile) == null ? void 0 : _f.trim();
198
199
  if (!organisationUuid || !workspaceUuid) {
199
200
  throw new Error(
200
201
  "Sandbox mode requires organisationUuid and workspaceUuid"
201
202
  );
202
203
  }
203
- if (!ucic && !subject) {
204
+ if (!ucic && !email && !mobile) {
204
205
  throw new Error("Sandbox mode requires a UCIC, email, or mobile");
205
206
  }
206
- return { token, ucic, subject: subject != null ? subject : "", organisationUuid, workspaceUuid };
207
+ return { token, ucic, email, mobile, organisationUuid, workspaceUuid };
207
208
  };
208
209
  var resolveRequestAuth = (accessToken, options) => {
209
210
  const sandbox = resolveSandboxAuth(options);
@@ -235,10 +236,17 @@ var buildAuthHeaders = (accessToken, sandbox) => {
235
236
  return { Authorization: `Bearer ${accessToken}` };
236
237
  };
237
238
  var sandboxBodyIdentity = (sandbox) => {
239
+ const payload = {};
238
240
  if (sandbox.ucic) {
239
- return { org_user_id: sandbox.ucic };
241
+ payload.org_user_id = sandbox.ucic;
240
242
  }
241
- return sandbox.subject.includes("@") ? { primary_email: sandbox.subject } : { primary_mobile: sandbox.subject };
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;
242
250
  };
243
251
  var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
244
252
 
@@ -324,7 +332,7 @@ var fetchConsentContent = async ({
324
332
  sandboxParams
325
333
  );
326
334
  const apiBaseUrl = baseUrl || BASE_URL;
327
- const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.subject}` : accessToken;
335
+ const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.email || sandbox.mobile}` : accessToken;
328
336
  const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
329
337
  if (validate_against === "all" && !specific_uuid) {
330
338
  const cached = getCachedData(cacheKey);
@@ -336,7 +344,7 @@ var fetchConsentContent = async ({
336
344
  if (include_fully_consented_data) url.searchParams.append("include_fully_consented_data", "true");
337
345
  if (sandbox) {
338
346
  for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
339
- url.searchParams.append(key, value);
347
+ if (value) url.searchParams.append(key, value);
340
348
  }
341
349
  }
342
350
  const response = await fetch(url.toString(), {
@@ -572,7 +580,7 @@ var fetchTTSAudioUrls = async ({
572
580
  );
573
581
  if (sandbox) {
574
582
  for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
575
- url.searchParams.append(key, value);
583
+ if (value) url.searchParams.append(key, value);
576
584
  }
577
585
  }
578
586
  const response = await fetch(url.toString(), {
@@ -1287,6 +1295,32 @@ var styles = {
1287
1295
  margin: 0,
1288
1296
  boxSizing: "border-box"
1289
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
+ },
1290
1324
  button: {
1291
1325
  width: "100%",
1292
1326
  borderRadius: "8px",
@@ -1373,6 +1407,51 @@ var styles = {
1373
1407
  }
1374
1408
  };
1375
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
+
1376
1455
  // src/modal/purposes.ts
1377
1456
  function renderModalPurposes(purposes, options) {
1378
1457
  var _a, _b;
@@ -1438,6 +1517,9 @@ function renderModalPurposes(purposes, options) {
1438
1517
  }
1439
1518
  });
1440
1519
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
1520
+ if (hasRequiredDataElement(purpose)) {
1521
+ titleEl.appendChild(createRequiredMarker());
1522
+ }
1441
1523
  titleElements.set(purpose.uuid, titleEl);
1442
1524
  const descEl = el("h3", {
1443
1525
  id: `purpose-description-${purpose.uuid}`,
@@ -1505,9 +1587,7 @@ function renderModalPurposes(purposes, options) {
1505
1587
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
1506
1588
  dataElementLabels.set(combinedId, labelSpan);
1507
1589
  if (dataElement.required) {
1508
- const asterisk = el("span", { style: { color: "red", marginLeft: "4px" }, "aria-label": "required" });
1509
- asterisk.textContent = "*";
1510
- labelSpan.appendChild(asterisk);
1590
+ labelSpan.appendChild(createRequiredMarker());
1511
1591
  }
1512
1592
  let deRightElement;
1513
1593
  if (!isAlreadyConsented) {
@@ -1876,24 +1956,6 @@ var updateConsentState = (updates) => {
1876
1956
  }
1877
1957
  };
1878
1958
 
1879
- // src/constants.ts
1880
- var DEFAULT_LOGO_POSITION = "left";
1881
- var LOGO_POSITION_JUSTIFY = {
1882
- left: "flex-start",
1883
- center: "center",
1884
- right: "flex-end"
1885
- };
1886
- var DPO_AUDIO_ELEMENT_MAP = [
1887
- { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1888
- { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1889
- { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1890
- { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1891
- { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1892
- { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1893
- { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1894
- { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1895
- ];
1896
-
1897
1959
  // src/modal/tts.ts
1898
1960
  var TTSManager = class {
1899
1961
  /**
@@ -1947,7 +2009,8 @@ var TTSManager = class {
1947
2009
  signal: this.abortController.signal,
1948
2010
  token: params.token,
1949
2011
  sandboxUcic: params.sandboxUcic,
1950
- sandboxSubject: params.sandboxSubject,
2012
+ sandboxEmail: params.sandboxEmail,
2013
+ sandboxMobile: params.sandboxMobile,
1951
2014
  organisationUuid: params.organisationUuid,
1952
2015
  workspaceUuid: params.workspaceUuid
1953
2016
  });
@@ -2003,8 +2066,9 @@ var TTSManager = class {
2003
2066
  push(audioUrl, mapping.elementId);
2004
2067
  }
2005
2068
  }
2006
- push(urls.confirm_button_text_audio_url, "confirm-button-text");
2007
- 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);
2008
2072
  this.audioSequence = audioSeq;
2009
2073
  this.elementIdSequence = elementSeq;
2010
2074
  }
@@ -2895,14 +2959,17 @@ function renderVerificationScreen(options) {
2895
2959
 
2896
2960
  // src/modal/colors.ts
2897
2961
  function resolveColors(settings, activeConfig) {
2898
- var _a, _b, _c, _d, _e, _f, _g, _h;
2962
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
2899
2963
  const primaryColor = (activeConfig == null ? void 0 : activeConfig.primary_color) || "#4f87ff";
2900
2964
  const secondaryColor = (activeConfig == null ? void 0 : activeConfig.secondary_color) || "#4f87ff";
2901
2965
  return {
2902
2966
  acceptBg: ((_b = (_a = settings == null ? void 0 : settings.button) == null ? void 0 : _a.accept) == null ? void 0 : _b.backgroundColor) || primaryColor,
2903
2967
  acceptText: ((_d = (_c = settings == null ? void 0 : settings.button) == null ? void 0 : _c.accept) == null ? void 0 : _d.textColor) || "#ffffff",
2904
- declineBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.decline) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2905
- 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",
2906
2973
  declineBorder: (settings == null ? void 0 : settings.borderColor) || "#d0d5dd",
2907
2974
  link: (settings == null ? void 0 : settings.link) || secondaryColor,
2908
2975
  heading: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
@@ -2914,6 +2981,23 @@ function resolveColors(settings, activeConfig) {
2914
2981
  };
2915
2982
  }
2916
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
+
2917
3001
  // src/modal/layout.ts
2918
3002
  function resolveLogoPosition(raw) {
2919
3003
  return raw === "center" || raw === "right" ? raw : DEFAULT_LOGO_POSITION;
@@ -2946,6 +3030,7 @@ var ModalController = class {
2946
3030
  this.visibilityChangeHandler = null;
2947
3031
  // DOM refs for targeted updates
2948
3032
  this.acceptBtn = null;
3033
+ this.acceptAllBtn = null;
2949
3034
  this.declineBtn = null;
2950
3035
  this.middleSection = null;
2951
3036
  this.errorEl = null;
@@ -3028,18 +3113,24 @@ var ModalController = class {
3028
3113
  // ─── Private ───
3029
3114
  /**
3030
3115
  * Sandbox auth options to forward to every API call. When `token` is set the
3031
- * API layer sends the single `X-Consent-Token` header, injects the subject
3032
- * into each request's payload, and reads org/workspace from these fields
3033
- * instead of decoding the JWT. Empty in the normal JWT flow.
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.
3034
3119
  */
3035
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;
3036
3125
  return {
3037
3126
  token: this.options.token,
3038
- // The static token carries no identity; read the acting principal from the
3039
- // optional ucic/email/mobile. UCIC takes precedence server-side.
3040
- // sandboxUcic / sandboxSubject stay as fallbacks.
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.
3041
3131
  sandboxUcic: this.options.ucic || this.options.sandboxUcic,
3042
- sandboxSubject: this.options.email || this.options.mobile || this.options.sandboxSubject,
3132
+ sandboxEmail: this.options.email || this.options.sandboxEmail || legacyEmail,
3133
+ sandboxMobile: this.options.mobile || this.options.sandboxMobile || legacyMobile,
3043
3134
  organisationUuid: this.options.organisationUuid,
3044
3135
  workspaceUuid: this.options.workspaceUuid
3045
3136
  };
@@ -3640,24 +3731,57 @@ var ModalController = class {
3640
3731
  display: "none"
3641
3732
  }
3642
3733
  });
3643
- 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
+ };
3644
3745
  const acceptStyle = {
3645
3746
  ...styles.button,
3646
- ...styles.acceptButton,
3647
- backgroundColor: colors.acceptBg,
3648
- 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
+ },
3649
3753
  borderRadius: colors.borderRadius,
3650
- fontSize: isMobile ? "14px" : "16px",
3651
- padding: isMobile ? "10px 20px" : "9px 45px"
3754
+ ...gridButtonBox
3652
3755
  };
3653
3756
  this.acceptBtn = el("button", {
3654
- id: "confirm-button-text",
3757
+ id: ACCEPT_SELECTED_BUTTON_ID,
3655
3758
  style: acceptStyle,
3656
3759
  type: "button",
3657
- onClick: () => this.handleAccept()
3760
+ onClick: () => this.handleAccept("selected")
3658
3761
  });
3659
3762
  this.acceptBtn.textContent = acceptBtnText;
3660
- 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
+ );
3661
3785
  const declineStyle = {
3662
3786
  ...styles.button,
3663
3787
  ...styles.cancelButton,
@@ -3665,11 +3789,13 @@ var ModalController = class {
3665
3789
  color: colors.declineText,
3666
3790
  borderColor: colors.declineBorder,
3667
3791
  borderRadius: colors.borderRadius,
3668
- fontSize: isMobile ? "14px" : "16px",
3669
- 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"
3670
3796
  };
3671
3797
  this.declineBtn = el("button", {
3672
- id: "decline-button-text",
3798
+ id: DECLINE_BUTTON_ID,
3673
3799
  style: declineStyle,
3674
3800
  type: "button",
3675
3801
  onClick: () => this.handleDecline()
@@ -3677,12 +3803,19 @@ var ModalController = class {
3677
3803
  if (!state.isFullyConsentedReviewMode) {
3678
3804
  this.declineBtn.textContent = declineBtnText;
3679
3805
  }
3680
- const bottomSection = el("div", { style: styles.bottomSection });
3681
- 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);
3682
3813
  bottomSection.appendChild(this.acceptBtn);
3683
3814
  bottomSection.appendChild(this.declineBtn);
3684
3815
  } else {
3816
+ bottomSection.appendChild(this.declineBtn);
3685
3817
  bottomSection.appendChild(this.acceptBtn);
3818
+ bottomSection.appendChild(this.acceptAllBtn);
3686
3819
  }
3687
3820
  const contentWrapper = el(
3688
3821
  "div",
@@ -3838,7 +3971,12 @@ var ModalController = class {
3838
3971
  selectedPurposes: { ...state.selectedPurposes, [purposeUuid]: shouldCheckPurpose }
3839
3972
  });
3840
3973
  }
3841
- 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") {
3842
3980
  var _a, _b, _c;
3843
3981
  const state = this.store.getState();
3844
3982
  if (state.isSubmitting) return;
@@ -3857,6 +3995,16 @@ var ModalController = class {
3857
3995
  if (!content) return;
3858
3996
  this.abortController = new AbortController();
3859
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
+ }
3860
4008
  try {
3861
4009
  const purposes = content.purposes.map((purpose) => {
3862
4010
  const dataElements = purpose.data_elements.filter((de) => de.enabled).map((de) => ({
@@ -3864,13 +4012,13 @@ var ModalController = class {
3864
4012
  name: de.name,
3865
4013
  enabled: de.enabled,
3866
4014
  required: de.required,
3867
- selected: state.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
4015
+ selected: selection.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
3868
4016
  }));
3869
4017
  return {
3870
4018
  uuid: purpose.uuid,
3871
4019
  name: purpose.name,
3872
4020
  description: purpose.description,
3873
- selected: state.selectedPurposes[purpose.uuid] || false,
4021
+ selected: selection.selectedPurposes[purpose.uuid] || false,
3874
4022
  data_elements: dataElements
3875
4023
  };
3876
4024
  });
@@ -3932,11 +4080,16 @@ var ModalController = class {
3932
4080
  }
3933
4081
  updateAcceptButtonState(state) {
3934
4082
  if (!this.acceptBtn || state.isFullyConsentedReviewMode) return;
4083
+ const submitting = state.isSubmittingAccept || state.isSubmittingDecline;
3935
4084
  const allRequiredChecked = this.areAllRequiredChecked(state);
3936
- const disabled = !allRequiredChecked || state.isSubmittingAccept || state.isSubmittingDecline;
3937
- this.acceptBtn.disabled = disabled;
3938
- this.acceptBtn.style.opacity = disabled ? "0.5" : "1";
3939
- 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);
3940
4093
  }
3941
4094
  updateCollapseState(state) {
3942
4095
  const updateRefs = (refs) => {
@@ -4309,25 +4462,34 @@ var ModalController = class {
4309
4462
  }
4310
4463
  updateButtonStates(state) {
4311
4464
  var _a;
4312
- if (this.acceptBtn) {
4313
- if (state.isSubmittingAccept) {
4314
- this.acceptBtn.textContent = "Submitting...";
4315
- this.acceptBtn.disabled = true;
4316
- this.acceptBtn.style.opacity = "0.7";
4317
- this.acceptBtn.style.cursor = "not-allowed";
4318
- } else {
4319
- const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4320
- this.acceptBtn.textContent = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", (content == null ? void 0 : content.confirm_button_text) || "Accept");
4321
- this.acceptBtn.disabled = false;
4322
- this.acceptBtn.style.opacity = "1";
4323
- this.acceptBtn.style.cursor = "pointer";
4324
- }
4325
- }
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
+ );
4326
4487
  if (this.declineBtn) {
4327
4488
  this.declineBtn.disabled = state.isSubmitting;
4328
4489
  this.declineBtn.style.opacity = state.isSubmitting ? "0.7" : "1";
4329
4490
  this.declineBtn.style.cursor = state.isSubmitting ? "not-allowed" : "pointer";
4330
4491
  }
4492
+ this.updateAcceptButtonState(state);
4331
4493
  }
4332
4494
  };
4333
4495
 
@@ -4528,6 +4690,9 @@ function renderInlinePurposes(purposes, options) {
4528
4690
  }
4529
4691
  });
4530
4692
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
4693
+ if (hasRequiredDataElement(purpose)) {
4694
+ titleEl.appendChild(createRequiredMarker());
4695
+ }
4531
4696
  titleElements.set(purpose.uuid, titleEl);
4532
4697
  const descEl = el("h3", {
4533
4698
  style: {
@@ -4580,12 +4745,7 @@ function renderInlinePurposes(purposes, options) {
4580
4745
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
4581
4746
  dataElementLabels.set(combinedId, labelSpan);
4582
4747
  if (dataElement.required) {
4583
- const asterisk = el("span", {
4584
- style: { color: "red", marginLeft: "4px" },
4585
- "aria-label": "required"
4586
- });
4587
- asterisk.textContent = "*";
4588
- labelSpan.appendChild(asterisk);
4748
+ labelSpan.appendChild(createRequiredMarker());
4589
4749
  }
4590
4750
  const deCheckbox = el("input", {
4591
4751
  type: "checkbox",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-js",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "dist/redacto.js",
5
5
  "module": "dist/redacto.mjs",
6
6
  "types": "dist/redacto.d.ts",