@redacto.io/consent-sdk-react 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useState, useRef } from "react";
2
2
  import { fetchConsentContent, submitConsentEvent } from "./api";
3
3
  import type { ConsentContent } from "./api/types";
4
4
  import type { Props, TranslationObject } from "./types";
@@ -10,18 +10,21 @@ export const RedactoNoticeConsent = ({
10
10
  noticeId,
11
11
  accessToken,
12
12
  refreshToken,
13
+ baseUrl,
13
14
  language = "en",
14
- blockUI = false,
15
+ blockUI = true,
15
16
  onAccept,
16
17
  onDecline,
17
18
  onError,
18
19
  settings,
20
+ applicationId,
19
21
  }: Props) => {
20
22
  const [content, setContent] = useState<
21
23
  ConsentContent["detail"]["active_config"] | null
22
24
  >(null);
23
25
  const [isLoading, setIsLoading] = useState(true);
24
26
  const [isSubmitting, setIsSubmitting] = useState(false);
27
+ const [isRefreshingToken, setIsRefreshingToken] = useState(false);
25
28
  const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
26
29
  const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
27
30
  const [collapsedPurposes, setCollapsedPurposes] = useState<
@@ -34,6 +37,13 @@ export const RedactoNoticeConsent = ({
34
37
  Record<string, boolean>
35
38
  >({});
36
39
  const [hasAlreadyConsented, setHasAlreadyConsented] = useState(false);
40
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
41
+ const [fetchError, setFetchError] = useState<Error | null>(null);
42
+ const modalRef = useRef<HTMLDivElement>(null);
43
+ const firstFocusableRef = useRef<HTMLButtonElement>(null);
44
+ const lastFocusableRef = useRef<HTMLButtonElement>(null);
45
+ const MAX_REFRESH_ATTEMPTS = 1;
46
+ const [refreshAttempts, setRefreshAttempts] = useState(0);
37
47
 
38
48
  // Add media query hooks
39
49
  const isMobile = useMediaQuery("(max-width: 768px)");
@@ -86,6 +96,28 @@ export const RedactoNoticeConsent = ({
86
96
  middleSection: {
87
97
  paddingRight: isMobile ? "10px" : "15px",
88
98
  },
99
+ errorDialog: {
100
+ padding: isMobile ? "12px" : "16px",
101
+ maxWidth: isMobile ? "100%" : "400px",
102
+ marginBottom: isMobile ? "16px" : "20px",
103
+ },
104
+ errorTitle: {
105
+ fontSize: isMobile ? "16px" : "18px",
106
+ margin: isMobile ? "0 0 8px 0" : "0 0 12px 0",
107
+ },
108
+ errorMessage: {
109
+ fontSize: isMobile ? "12px" : "14px",
110
+ margin: isMobile ? "0 0 12px 0" : "0 0 16px 0",
111
+ },
112
+ errorButton: {
113
+ padding: isMobile ? "6px 12px" : "8px 16px",
114
+ fontSize: isMobile ? "12px" : "14px",
115
+ },
116
+ closeButton: {
117
+ top: isMobile ? "4px" : "8px",
118
+ right: isMobile ? "4px" : "8px",
119
+ padding: isMobile ? "2px" : "4px",
120
+ },
89
121
  }),
90
122
  [isMobile]
91
123
  );
@@ -157,71 +189,89 @@ export const RedactoNoticeConsent = ({
157
189
  return defaultText;
158
190
  };
159
191
 
160
- useEffect(() => {
161
- const fetchNotice = async () => {
162
- setIsLoading(true);
163
- try {
164
- const consentContentData = await fetchConsentContent({
165
- noticeId,
166
- accessToken,
167
- refreshToken,
168
- language,
169
- });
170
- setContent(consentContentData.detail.active_config);
171
- if (consentContentData.detail.active_config.default_language) {
172
- setSelectedLanguage(
173
- consentContentData.detail.active_config.default_language
174
- );
175
- }
176
- const initialCollapsedState =
177
- consentContentData.detail.active_config.purposes.reduce(
178
- (acc, purpose) => ({
192
+ const fetchNotice = async () => {
193
+ setIsLoading(true);
194
+ setFetchError(null);
195
+ try {
196
+ const consentContentData = await fetchConsentContent({
197
+ noticeId,
198
+ accessToken,
199
+ refreshToken,
200
+ baseUrl,
201
+ language,
202
+ specific_uuid: applicationId,
203
+ });
204
+ setContent(consentContentData.detail.active_config);
205
+ if (consentContentData.detail.active_config.default_language) {
206
+ setSelectedLanguage(
207
+ consentContentData.detail.active_config.default_language
208
+ );
209
+ }
210
+ const initialCollapsedState =
211
+ consentContentData.detail.active_config.purposes.reduce(
212
+ (acc, purpose) => ({
213
+ ...acc,
214
+ [purpose.uuid]: true,
215
+ }),
216
+ {}
217
+ );
218
+ setCollapsedPurposes(initialCollapsedState);
219
+
220
+ // Initialize selected states
221
+ const initialPurposeState: Record<string, boolean> =
222
+ consentContentData.detail.active_config.purposes.reduce(
223
+ (acc: Record<string, boolean>, purpose) => {
224
+ return {
179
225
  ...acc,
180
- [purpose.uuid]: true,
181
- }),
182
- {}
183
- );
184
- setCollapsedPurposes(initialCollapsedState);
185
-
186
- // Initialize selected states
187
- const initialPurposeState: Record<string, boolean> =
188
- consentContentData.detail.active_config.purposes.reduce(
189
- (acc: Record<string, boolean>, purpose) => {
190
- return {
191
- ...acc,
192
- [purpose.uuid]: false, // Initialize all purposes as unchecked
193
- };
194
- },
195
- {}
196
- );
197
- setSelectedPurposes(initialPurposeState);
198
-
199
- const initialDataElementState: Record<string, boolean> =
200
- consentContentData.detail.active_config.purposes.reduce(
201
- (acc: Record<string, boolean>, purpose) => {
202
- purpose.data_elements.forEach((element) => {
203
- const combinedId = `${purpose.uuid}-${element.uuid}`;
204
- acc[combinedId] = false; // Initialize all data elements as unchecked
205
- });
206
- return acc;
207
- },
208
- {}
209
- );
210
- setSelectedDataElements(initialDataElementState);
211
- } catch (err: unknown) {
212
- console.error(err);
213
- if ((err as Error & { status?: number }).status === 409) {
214
- setHasAlreadyConsented(true);
215
- } else {
216
- onError?.(err as Error);
217
- }
218
- } finally {
219
- setIsLoading(false);
226
+ [purpose.uuid]: false,
227
+ };
228
+ },
229
+ {}
230
+ );
231
+ setSelectedPurposes(initialPurposeState);
232
+
233
+ const initialDataElementState: Record<string, boolean> =
234
+ consentContentData.detail.active_config.purposes.reduce(
235
+ (acc: Record<string, boolean>, purpose) => {
236
+ purpose.data_elements.forEach((element) => {
237
+ const combinedId = `${purpose.uuid}-${element.uuid}`;
238
+ acc[combinedId] = false;
239
+ });
240
+ return acc;
241
+ },
242
+ {}
243
+ );
244
+ setSelectedDataElements(initialDataElementState);
245
+ } catch (err: unknown) {
246
+ console.error(err);
247
+ const error = err as Error & { status?: number };
248
+ setFetchError(error);
249
+ if (error.status === 401 && refreshAttempts < MAX_REFRESH_ATTEMPTS) {
250
+ setRefreshAttempts((c) => c + 1);
251
+ setIsRefreshingToken(true);
252
+ onError?.(error);
253
+ } else if (error.status === 409) {
254
+ setHasAlreadyConsented(true);
255
+ } else {
256
+ onError?.(error);
220
257
  }
221
- };
258
+ } finally {
259
+ setIsLoading(false);
260
+ setIsRefreshingToken(false);
261
+ }
262
+ };
222
263
 
264
+ useEffect(() => {
223
265
  fetchNotice();
224
- }, [noticeId, accessToken, refreshToken, language, onError]);
266
+ }, [
267
+ noticeId,
268
+ accessToken,
269
+ refreshToken,
270
+ language,
271
+ onError,
272
+ applicationId,
273
+ isRefreshingToken,
274
+ ]);
225
275
 
226
276
  const togglePurposeCollapse = (purposeUuid: string) => {
227
277
  setCollapsedPurposes((prev) => ({
@@ -301,9 +351,11 @@ export const RedactoNoticeConsent = ({
301
351
  const handleAccept = async () => {
302
352
  try {
303
353
  setIsSubmitting(true);
354
+ setErrorMessage(null);
304
355
  if (content) {
305
356
  await submitConsentEvent({
306
357
  accessToken,
358
+ baseUrl,
307
359
  noticeUuid: content.notice_uuid,
308
360
  purposes: content.purposes.map((purpose) => ({
309
361
  ...purpose,
@@ -317,11 +369,20 @@ export const RedactoNoticeConsent = ({
317
369
  })),
318
370
  })),
319
371
  declined: false,
372
+ meta_data: applicationId
373
+ ? { specific_uuid: applicationId }
374
+ : undefined,
320
375
  });
321
376
  }
322
377
  onAccept?.();
323
378
  } catch (error) {
324
379
  console.error("Error submitting consent:", error);
380
+ const err = error as Error & { status?: number };
381
+ if (err.status === 500) {
382
+ setErrorMessage(
383
+ "An error occurred while submitting your consent. Please try again later."
384
+ );
385
+ }
325
386
  onError?.(error as Error);
326
387
  } finally {
327
388
  setIsSubmitting(false);
@@ -378,7 +439,10 @@ export const RedactoNoticeConsent = ({
378
439
 
379
440
  const acceptButtonStyle = {
380
441
  ...buttonStyle,
381
- backgroundColor: settings?.button?.accept?.backgroundColor || "#4f87ff",
442
+ backgroundColor:
443
+ settings?.button?.accept?.backgroundColor ||
444
+ content?.primary_color ||
445
+ "#4f87ff",
382
446
  color: settings?.button?.accept?.textColor || "#ffffff",
383
447
  };
384
448
 
@@ -390,7 +454,7 @@ export const RedactoNoticeConsent = ({
390
454
  };
391
455
 
392
456
  const linkStyle = {
393
- color: settings?.link?.color || "#4f87ff",
457
+ color: settings?.link || content?.secondary_color || "#4f87ff",
394
458
  };
395
459
 
396
460
  const languageSelectorStyle = {
@@ -420,12 +484,74 @@ export const RedactoNoticeConsent = ({
420
484
  backgroundColor: settings?.backgroundColor || "#ffffff",
421
485
  };
422
486
 
487
+ const handleCloseError = () => {
488
+ setErrorMessage(null);
489
+ };
490
+
491
+ // Add keyboard event handler for the modal
492
+ useEffect(() => {
493
+ const handleKeyDown = (event: KeyboardEvent) => {
494
+ if (!modalRef.current) return;
495
+
496
+ const focusableElements = modalRef.current.querySelectorAll(
497
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
498
+ );
499
+ if (!focusableElements.length) return;
500
+
501
+ if (event.key === "Escape") {
502
+ handleDecline();
503
+ }
504
+
505
+ if (event.key === "Tab") {
506
+ const firstElement = focusableElements[0] as HTMLElement;
507
+ const lastElement = focusableElements[
508
+ focusableElements.length - 1
509
+ ] as HTMLElement;
510
+
511
+ if (event.shiftKey) {
512
+ if (document.activeElement === firstElement) {
513
+ event.preventDefault();
514
+ lastElement.focus();
515
+ }
516
+ } else {
517
+ if (document.activeElement === lastElement) {
518
+ event.preventDefault();
519
+ firstElement.focus();
520
+ }
521
+ }
522
+ }
523
+ };
524
+
525
+ const modal = modalRef.current;
526
+ modal?.addEventListener("keydown", handleKeyDown);
527
+
528
+ // Focus the first focusable element when modal opens
529
+ if (modal && !isLoading) {
530
+ const focusableElements = modal.querySelectorAll(
531
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
532
+ );
533
+ const firstElement = focusableElements[0] as HTMLElement;
534
+ firstElement?.focus();
535
+ }
536
+
537
+ return () => {
538
+ modal?.removeEventListener("keydown", handleKeyDown);
539
+ };
540
+ }, [isLoading, handleDecline]);
541
+
423
542
  return (
424
543
  <>
425
544
  {hasAlreadyConsented ? null : (
426
545
  <>
427
- {blockUI && <div style={styles.overlay} aria-hidden="true" />}
546
+ {blockUI && (
547
+ <div
548
+ style={styles.overlay}
549
+ aria-hidden="true"
550
+ role="presentation"
551
+ />
552
+ )}
428
553
  <div
554
+ ref={modalRef}
429
555
  style={{
430
556
  ...styles.modal,
431
557
  ...responsiveStyles.modal,
@@ -434,22 +560,178 @@ export const RedactoNoticeConsent = ({
434
560
  role="dialog"
435
561
  aria-modal="true"
436
562
  aria-labelledby="privacy-notice-title"
563
+ aria-describedby="privacy-notice-description"
564
+ tabIndex={-1}
437
565
  >
438
- {isLoading ? (
566
+ {isLoading || isRefreshingToken ? (
439
567
  <div style={styles.loadingContainer}>
440
568
  <div style={styles.loadingSpinner}></div>
441
569
  <p style={{ ...styles.privacyText, ...textStyle }}>
442
- Loading...
570
+ {isRefreshingToken ? "Refreshing session..." : "Loading..."}
443
571
  </p>
444
572
  </div>
573
+ ) : fetchError ? (
574
+ <div
575
+ style={{
576
+ ...styles.content,
577
+ ...responsiveStyles.content,
578
+ display: "flex",
579
+ flexDirection: "column",
580
+ alignItems: "center",
581
+ justifyContent: "center",
582
+ textAlign: "center",
583
+ padding: "40px 20px",
584
+ }}
585
+ >
586
+ <div
587
+ style={{
588
+ color: "#DC2626",
589
+ padding: responsiveStyles.errorDialog.padding,
590
+ borderRadius: "8px",
591
+ marginBottom: responsiveStyles.errorDialog.marginBottom,
592
+ width: "100%",
593
+ maxWidth: responsiveStyles.errorDialog.maxWidth,
594
+ position: "relative",
595
+ }}
596
+ >
597
+ <button
598
+ onClick={handleDecline}
599
+ style={{
600
+ position: "absolute",
601
+ top: responsiveStyles.closeButton.top,
602
+ right: responsiveStyles.closeButton.right,
603
+ background: "none",
604
+ border: "none",
605
+ color: "#DC2626",
606
+ cursor: "pointer",
607
+ padding: responsiveStyles.closeButton.padding,
608
+ display: "flex",
609
+ alignItems: "center",
610
+ justifyContent: "center",
611
+ }}
612
+ aria-label="Close error message"
613
+ >
614
+ <svg
615
+ width={isMobile ? "14" : "16"}
616
+ height={isMobile ? "14" : "16"}
617
+ viewBox="0 0 16 16"
618
+ fill="none"
619
+ xmlns="http://www.w3.org/2000/svg"
620
+ aria-hidden="true"
621
+ >
622
+ <path
623
+ d="M12 4L4 12M4 4L12 12"
624
+ stroke="currentColor"
625
+ strokeWidth="2"
626
+ strokeLinecap="round"
627
+ strokeLinejoin="round"
628
+ />
629
+ </svg>
630
+ </button>
631
+ <h3
632
+ style={{
633
+ margin: responsiveStyles.errorTitle.margin,
634
+ fontSize: responsiveStyles.errorTitle.fontSize,
635
+ fontWeight: "600",
636
+ }}
637
+ >
638
+ Error Loading Consent Notice
639
+ </h3>
640
+ <p
641
+ style={{
642
+ margin: responsiveStyles.errorMessage.margin,
643
+ fontSize: responsiveStyles.errorMessage.fontSize,
644
+ lineHeight: "1.5",
645
+ }}
646
+ >
647
+ {fetchError.message ||
648
+ "Failed to load consent notice. Please try again."}
649
+ </p>
650
+ <button
651
+ onClick={fetchNotice}
652
+ style={{
653
+ backgroundColor: "#DC2626",
654
+ color: "#FFFFFF",
655
+ border: "none",
656
+ padding: responsiveStyles.errorButton.padding,
657
+ borderRadius: "6px",
658
+ cursor: "pointer",
659
+ fontSize: responsiveStyles.errorButton.fontSize,
660
+ fontWeight: "500",
661
+ transition: "background-color 0.2s",
662
+ }}
663
+ onMouseOver={(e) =>
664
+ (e.currentTarget.style.backgroundColor = "#B91C1C")
665
+ }
666
+ onMouseOut={(e) =>
667
+ (e.currentTarget.style.backgroundColor = "#DC2626")
668
+ }
669
+ >
670
+ Refresh
671
+ </button>
672
+ </div>
673
+ </div>
445
674
  ) : (
446
675
  <div style={{ ...styles.content, ...responsiveStyles.content }}>
676
+ {errorMessage && (
677
+ <div
678
+ role="alert"
679
+ style={{
680
+ backgroundColor: "#FEE2E2",
681
+ color: "#DC2626",
682
+ padding: "12px",
683
+ borderRadius: "6px",
684
+ marginBottom: "16px",
685
+ fontSize: "14px",
686
+ border: "1px solid #FCA5A5",
687
+ position: "relative",
688
+ display: "flex",
689
+ alignItems: "center",
690
+ justifyContent: "space-between",
691
+ }}
692
+ >
693
+ <span>{errorMessage}</span>
694
+ <button
695
+ type="button"
696
+ onClick={handleCloseError}
697
+ style={{
698
+ background: "none",
699
+ border: "none",
700
+ color: "#DC2626",
701
+ cursor: "pointer",
702
+ padding: "4px",
703
+ marginLeft: "8px",
704
+ display: "flex",
705
+ alignItems: "center",
706
+ justifyContent: "center",
707
+ }}
708
+ aria-label="Close error message"
709
+ >
710
+ <svg
711
+ width="16"
712
+ height="16"
713
+ viewBox="0 0 16 16"
714
+ fill="none"
715
+ xmlns="http://www.w3.org/2000/svg"
716
+ aria-hidden="true"
717
+ >
718
+ <path
719
+ d="M12 4L4 12M4 4L12 12"
720
+ stroke="currentColor"
721
+ strokeWidth="2"
722
+ strokeLinecap="round"
723
+ strokeLinejoin="round"
724
+ />
725
+ </svg>
726
+ </button>
727
+ </div>
728
+ )}
447
729
  <div style={{ ...styles.topSection, ...sectionStyle }}>
448
730
  <div style={styles.topLeft}>
449
731
  <img
450
732
  style={styles.logo}
451
733
  src={content?.logo_url || logo}
452
- alt="logo"
734
+ alt="Redacto Logo"
453
735
  />
454
736
  <h2
455
737
  id="privacy-notice-title"
@@ -464,6 +746,7 @@ export const RedactoNoticeConsent = ({
464
746
  </div>
465
747
  <div style={styles.languageSelectorContainer}>
466
748
  <button
749
+ ref={firstFocusableRef}
467
750
  style={{
468
751
  ...styles.topRight,
469
752
  ...responsiveStyles.topRight,
@@ -472,6 +755,7 @@ export const RedactoNoticeConsent = ({
472
755
  onClick={toggleLanguageDropdown}
473
756
  aria-expanded={isLanguageDropdownOpen}
474
757
  aria-haspopup="listbox"
758
+ aria-controls="language-listbox"
475
759
  >
476
760
  {selectedLanguage}
477
761
  <svg
@@ -488,12 +772,14 @@ export const RedactoNoticeConsent = ({
488
772
  strokeLinecap="round"
489
773
  strokeLinejoin="round"
490
774
  style={{ flexShrink: 0, marginLeft: "4px" }}
775
+ aria-hidden="true"
491
776
  >
492
777
  <path d="M4 6l4 4 4-4" />
493
778
  </svg>
494
779
  </button>
495
780
  {isLanguageDropdownOpen && (
496
781
  <ul
782
+ id="language-listbox"
497
783
  style={{
498
784
  ...styles.languageDropdown,
499
785
  ...languageSelectorStyle,
@@ -525,6 +811,7 @@ export const RedactoNoticeConsent = ({
525
811
  </div>
526
812
  </div>
527
813
  <div
814
+ id="privacy-notice-description"
528
815
  style={{
529
816
  ...styles.middleSection,
530
817
  ...responsiveStyles.middleSection,
@@ -548,6 +835,7 @@ export const RedactoNoticeConsent = ({
548
835
  href={content?.privacy_policy_url || "#"}
549
836
  target="_blank"
550
837
  rel="noopener noreferrer"
838
+ aria-label="Privacy Policy (opens in new tab)"
551
839
  >
552
840
  [
553
841
  {getTranslatedText(
@@ -560,7 +848,9 @@ export const RedactoNoticeConsent = ({
560
848
  <a
561
849
  style={{ ...styles.link, ...linkStyle }}
562
850
  href={content?.sub_processors_url || "#"}
563
- target=""
851
+ target="_blank"
852
+ rel="noopener noreferrer"
853
+ aria-label="Vendors List (opens in new tab)"
564
854
  >
565
855
  [
566
856
  {getTranslatedText(
@@ -590,6 +880,16 @@ export const RedactoNoticeConsent = ({
590
880
  <div
591
881
  style={styles.optionLeft}
592
882
  onClick={() => togglePurposeCollapse(purpose.uuid)}
883
+ role="button"
884
+ tabIndex={0}
885
+ aria-expanded={!collapsedPurposes[purpose.uuid]}
886
+ aria-controls={`purpose-${purpose.uuid}`}
887
+ onKeyDown={(e) => {
888
+ if (e.key === "Enter" || e.key === " ") {
889
+ e.preventDefault();
890
+ togglePurposeCollapse(purpose.uuid);
891
+ }
892
+ }}
593
893
  >
594
894
  <div
595
895
  style={{
@@ -615,6 +915,7 @@ export const RedactoNoticeConsent = ({
615
915
  : "rotate(0deg)",
616
916
  transition: "transform 0.3s ease",
617
917
  }}
918
+ aria-hidden="true"
618
919
  >
619
920
  <path d="M1 1L6 6L1 11" />
620
921
  </svg>
@@ -655,12 +956,20 @@ export const RedactoNoticeConsent = ({
655
956
  onChange={() =>
656
957
  handlePurposeCheckboxChange(purpose.uuid)
657
958
  }
959
+ aria-label={`Select all data elements for ${getTranslatedText(
960
+ `purposes.name`,
961
+ purpose.name,
962
+ purpose.uuid
963
+ )}`}
658
964
  />
659
965
  </div>
660
966
  {!collapsedPurposes[purpose.uuid] &&
661
967
  purpose.data_elements &&
662
968
  purpose.data_elements.length > 0 && (
663
- <div style={styles.dataElementsContainer}>
969
+ <div
970
+ id={`purpose-${purpose.uuid}`}
971
+ style={styles.dataElementsContainer}
972
+ >
664
973
  {purpose.data_elements.map((dataElement) => (
665
974
  <div
666
975
  key={dataElement.uuid}
@@ -684,6 +993,7 @@ export const RedactoNoticeConsent = ({
684
993
  color: "red",
685
994
  marginLeft: "4px",
686
995
  }}
996
+ aria-label="required"
687
997
  >
688
998
  *
689
999
  </span>
@@ -703,6 +1013,11 @@ export const RedactoNoticeConsent = ({
703
1013
  purpose.uuid
704
1014
  )
705
1015
  }
1016
+ aria-label={`Select ${getTranslatedText(
1017
+ `data_elements.name`,
1018
+ dataElement.name,
1019
+ dataElement.uuid
1020
+ )}${dataElement.required ? " (required)" : ""}`}
706
1021
  />
707
1022
  </div>
708
1023
  ))}
@@ -720,6 +1035,7 @@ export const RedactoNoticeConsent = ({
720
1035
  }}
721
1036
  >
722
1037
  <button
1038
+ ref={lastFocusableRef}
723
1039
  style={{
724
1040
  ...styles.button,
725
1041
  ...responsiveStyles.button,
@@ -729,6 +1045,10 @@ export const RedactoNoticeConsent = ({
729
1045
  }}
730
1046
  onClick={handleAccept}
731
1047
  disabled={acceptDisabled}
1048
+ aria-label={getTranslatedText(
1049
+ "confirm_button_text",
1050
+ content?.confirm_button_text || "Accept"
1051
+ )}
732
1052
  >
733
1053
  {isSubmitting
734
1054
  ? "Processing..."
@@ -746,6 +1066,10 @@ export const RedactoNoticeConsent = ({
746
1066
  }}
747
1067
  onClick={handleDecline}
748
1068
  disabled={isSubmitting}
1069
+ aria-label={getTranslatedText(
1070
+ "decline_button_text",
1071
+ content?.decline_button_text || "Decline"
1072
+ )}
749
1073
  >
750
1074
  {isSubmitting
751
1075
  ? "Processing..."