@redacto.io/consent-sdk-react 0.0.1

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.
@@ -0,0 +1,765 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { fetchConsentContent, submitConsentEvent } from "./api";
3
+ import type { ConsentContent } from "./api/types";
4
+ import type { Props, TranslationObject } from "./types";
5
+ import { styles } from "./styles";
6
+ import logo from "./assets/redacto-logo.png";
7
+ import { useMediaQuery } from "./useMediaQuery";
8
+
9
+ export const RedactoNoticeConsent = ({
10
+ noticeId,
11
+ accessToken,
12
+ refreshToken,
13
+ language = "en",
14
+ blockUI = false,
15
+ onAccept,
16
+ onDecline,
17
+ onError,
18
+ settings,
19
+ }: Props) => {
20
+ const [content, setContent] = useState<
21
+ ConsentContent["detail"]["active_config"] | null
22
+ >(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [isSubmitting, setIsSubmitting] = useState(false);
25
+ const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
26
+ const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
27
+ const [collapsedPurposes, setCollapsedPurposes] = useState<
28
+ Record<string, boolean>
29
+ >({});
30
+ const [selectedPurposes, setSelectedPurposes] = useState<
31
+ Record<string, boolean>
32
+ >({});
33
+ const [selectedDataElements, setSelectedDataElements] = useState<
34
+ Record<string, boolean>
35
+ >({});
36
+ const [hasAlreadyConsented, setHasAlreadyConsented] = useState(false);
37
+
38
+ // Add media query hooks
39
+ const isMobile = useMediaQuery("(max-width: 768px)");
40
+
41
+ // Responsive styles
42
+ const responsiveStyles = useMemo(
43
+ () => ({
44
+ modal: {
45
+ width: isMobile ? "90%" : "700px",
46
+ },
47
+ content: {
48
+ margin: isMobile ? "20px" : "22px",
49
+ },
50
+ title: {
51
+ fontSize: isMobile ? "16px" : "18px",
52
+ },
53
+ subTitle: {
54
+ fontSize: isMobile ? "14px" : "16px",
55
+ },
56
+ privacyText: {
57
+ fontSize: isMobile ? "14px" : "16px",
58
+ },
59
+ optionTitle: {
60
+ fontSize: isMobile ? "14px" : "16px",
61
+ },
62
+ optionDescription: {
63
+ fontSize: isMobile ? "11px" : "12px",
64
+ },
65
+ dataElementText: {
66
+ fontSize: isMobile ? "12px" : "14px",
67
+ },
68
+ topRight: {
69
+ fontSize: isMobile ? "11px" : "12px",
70
+ padding: isMobile ? "3px 6px" : "3px 9px",
71
+ height: isMobile ? "auto" : undefined,
72
+ width: isMobile ? "auto" : undefined,
73
+ },
74
+ languageItem: {
75
+ fontSize: isMobile ? "11px" : "12px",
76
+ padding: isMobile ? "6px 10px" : "8px 12px",
77
+ },
78
+ bottomSection: {
79
+ flexDirection: isMobile ? ("column" as const) : ("row" as const),
80
+ gap: isMobile ? "12px" : "29px",
81
+ },
82
+ button: {
83
+ fontSize: isMobile ? "14px" : "16px",
84
+ padding: isMobile ? "10px 20px" : "9px 45px",
85
+ },
86
+ middleSection: {
87
+ paddingRight: isMobile ? "10px" : "15px",
88
+ },
89
+ }),
90
+ [isMobile]
91
+ );
92
+
93
+ const areAllRequiredElementsChecked = useMemo(() => {
94
+ if (!content) return false;
95
+
96
+ return content.purposes.every((purpose) => {
97
+ const requiredElements = purpose.data_elements.filter(
98
+ (element) => element.required
99
+ );
100
+ return requiredElements.every(
101
+ (element) => selectedDataElements[`${purpose.uuid}-${element.uuid}`]
102
+ );
103
+ });
104
+ }, [content, selectedDataElements]);
105
+
106
+ const acceptDisabled = isSubmitting || !areAllRequiredElementsChecked;
107
+
108
+ useEffect(() => {
109
+ if (hasAlreadyConsented) {
110
+ onAccept?.();
111
+ }
112
+ }, [hasAlreadyConsented, onAccept]);
113
+
114
+ const getTranslatedText = (
115
+ key: string,
116
+ defaultText: string,
117
+ itemId?: string
118
+ ): string => {
119
+ if (!content) return defaultText;
120
+
121
+ if (selectedLanguage === content.default_language) {
122
+ if (
123
+ key === "privacy_policy_anchor_text" &&
124
+ content.privacy_policy_anchor_text
125
+ ) {
126
+ return content.privacy_policy_anchor_text;
127
+ }
128
+ return defaultText;
129
+ }
130
+
131
+ if (!content.supported_languages_and_translations) {
132
+ return defaultText;
133
+ }
134
+
135
+ const translationMap = content.supported_languages_and_translations[
136
+ selectedLanguage
137
+ ] as TranslationObject | undefined;
138
+
139
+ if (!translationMap) {
140
+ return defaultText;
141
+ }
142
+
143
+ if (itemId) {
144
+ if (key === "purposes.name" || key === "purposes.description") {
145
+ return translationMap.purposes?.[itemId] || defaultText;
146
+ }
147
+ if (key.startsWith("data_elements.")) {
148
+ return translationMap.data_elements?.[itemId] || defaultText;
149
+ }
150
+ }
151
+
152
+ const value = translationMap[key];
153
+ if (typeof value === "string") {
154
+ return value;
155
+ }
156
+
157
+ return defaultText;
158
+ };
159
+
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) => ({
179
+ ...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);
220
+ }
221
+ };
222
+
223
+ fetchNotice();
224
+ }, [noticeId, accessToken, refreshToken, language, onError]);
225
+
226
+ const togglePurposeCollapse = (purposeUuid: string) => {
227
+ setCollapsedPurposes((prev) => ({
228
+ ...prev,
229
+ [purposeUuid]: !prev[purposeUuid],
230
+ }));
231
+ };
232
+
233
+ const toggleLanguageDropdown = () => {
234
+ setIsLanguageDropdownOpen(!isLanguageDropdownOpen);
235
+ };
236
+
237
+ const handleLanguageSelect = (lang: string) => {
238
+ setSelectedLanguage(lang);
239
+ setIsLanguageDropdownOpen(false);
240
+ };
241
+
242
+ const handlePurposeCheckboxChange = (purposeUuid: string) => {
243
+ if (content) {
244
+ const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
245
+ if (purpose) {
246
+ const newPurposeState = !selectedPurposes[purposeUuid];
247
+
248
+ // Update purpose state
249
+ setSelectedPurposes((prev) => ({
250
+ ...prev,
251
+ [purposeUuid]: newPurposeState,
252
+ }));
253
+
254
+ // Update all data elements for this purpose
255
+ setSelectedDataElements((prev) => {
256
+ const updated = { ...prev };
257
+ purpose.data_elements.forEach((el) => {
258
+ updated[`${purposeUuid}-${el.uuid}`] = newPurposeState;
259
+ });
260
+ return updated;
261
+ });
262
+ }
263
+ }
264
+ };
265
+
266
+ const handleDataElementCheckboxChange = (
267
+ elementUuid: string,
268
+ purposeUuid: string
269
+ ) => {
270
+ const combinedId = `${purposeUuid}-${elementUuid}`;
271
+
272
+ // Update data element state
273
+ setSelectedDataElements((prev) => {
274
+ const newState = {
275
+ ...prev,
276
+ [combinedId]: !prev[combinedId],
277
+ };
278
+
279
+ // Check if all required elements for this purpose are checked
280
+ if (content) {
281
+ const purpose = content.purposes.find((p) => p.uuid === purposeUuid);
282
+ if (purpose) {
283
+ const requiredElements = purpose.data_elements.filter(
284
+ (el) => el.required
285
+ );
286
+ const allRequiredElementsChecked = requiredElements.every(
287
+ (el) => newState[`${purposeUuid}-${el.uuid}`]
288
+ );
289
+
290
+ setSelectedPurposes((prevPurposes) => ({
291
+ ...prevPurposes,
292
+ [purposeUuid]: allRequiredElementsChecked,
293
+ }));
294
+ }
295
+ }
296
+
297
+ return newState;
298
+ });
299
+ };
300
+
301
+ const handleAccept = async () => {
302
+ try {
303
+ setIsSubmitting(true);
304
+ if (content) {
305
+ await submitConsentEvent({
306
+ accessToken,
307
+ noticeUuid: content.notice_uuid,
308
+ purposes: content.purposes.map((purpose) => ({
309
+ ...purpose,
310
+ selected: selectedPurposes[purpose.uuid] || false,
311
+ data_elements: purpose.data_elements.map((element) => ({
312
+ ...element,
313
+ selected: element.required
314
+ ? true
315
+ : selectedDataElements[`${purpose.uuid}-${element.uuid}`] ||
316
+ false,
317
+ })),
318
+ })),
319
+ declined: false,
320
+ });
321
+ }
322
+ onAccept?.();
323
+ } catch (error) {
324
+ console.error("Error submitting consent:", error);
325
+ onError?.(error as Error);
326
+ } finally {
327
+ setIsSubmitting(false);
328
+ }
329
+ };
330
+
331
+ const handleDecline = async () => {
332
+ // try {
333
+ // setIsSubmitting(true);
334
+ // if (content) {
335
+ // await submitConsentEvent(
336
+ // accessToken,
337
+ // content.notice_uuid,
338
+ // content.purposes.map((purpose) => ({
339
+ // ...purpose,
340
+ // selected: false,
341
+ // data_elements: purpose.data_elements.map((element) => ({
342
+ // ...element,
343
+ // selected: element.required ? true : false,
344
+ // })),
345
+ // })),
346
+ // true
347
+ // );
348
+ // }
349
+ // onDecline?.();
350
+ // } catch (error) {
351
+ // console.error("Error submitting consent:", error);
352
+ // onError?.(error as Error);
353
+ // } finally {
354
+ // setIsSubmitting(false);
355
+ // }
356
+ onDecline?.();
357
+ };
358
+
359
+ const availableLanguages = useMemo(
360
+ () =>
361
+ content
362
+ ? [
363
+ content.default_language,
364
+ ...Object.keys(content.supported_languages_and_translations || {}),
365
+ ].filter((value, index, self) => self.indexOf(value) === index)
366
+ : [language],
367
+ [content, language]
368
+ );
369
+
370
+ const modalStyle = {
371
+ borderRadius: settings?.borderRadius || "8px",
372
+ backgroundColor: settings?.backgroundColor || "#ffffff",
373
+ };
374
+
375
+ const buttonStyle = {
376
+ borderRadius: settings?.borderRadius || "8px",
377
+ };
378
+
379
+ const acceptButtonStyle = {
380
+ ...buttonStyle,
381
+ backgroundColor: settings?.button?.accept?.backgroundColor || "#4f87ff",
382
+ color: settings?.button?.accept?.textColor || "#ffffff",
383
+ };
384
+
385
+ const declineButtonStyle = {
386
+ ...buttonStyle,
387
+ backgroundColor: settings?.button?.decline?.backgroundColor || "#ffffff",
388
+ color: settings?.button?.decline?.textColor || "#000000",
389
+ borderColor: settings?.borderColor || "#d0d5dd",
390
+ };
391
+
392
+ const linkStyle = {
393
+ color: settings?.link?.color || "#4f87ff",
394
+ };
395
+
396
+ const languageSelectorStyle = {
397
+ borderRadius: settings?.borderRadius || "8px",
398
+ borderColor: settings?.borderColor || "#d0d5dd",
399
+ backgroundColor: settings?.button?.language?.backgroundColor || "#ffffff",
400
+ color: settings?.button?.language?.textColor || "#344054",
401
+ };
402
+
403
+ const selectedLanguageItemStyle = {
404
+ ...languageSelectorStyle,
405
+ backgroundColor:
406
+ settings?.button?.language?.selectedBackgroundColor || "#f3f4f6",
407
+ color: settings?.button?.language?.selectedTextColor || "#344054",
408
+ fontWeight: 600,
409
+ };
410
+
411
+ const headingStyle = {
412
+ color: settings?.headingColor || "#101828",
413
+ };
414
+
415
+ const textStyle = {
416
+ color: settings?.textColor || "#344054",
417
+ };
418
+
419
+ const sectionStyle = {
420
+ backgroundColor: settings?.backgroundColor || "#ffffff",
421
+ };
422
+
423
+ return (
424
+ <>
425
+ {hasAlreadyConsented ? null : (
426
+ <>
427
+ {blockUI && <div style={styles.overlay} aria-hidden="true" />}
428
+ <div
429
+ style={{
430
+ ...styles.modal,
431
+ ...responsiveStyles.modal,
432
+ ...modalStyle,
433
+ }}
434
+ role="dialog"
435
+ aria-modal="true"
436
+ aria-labelledby="privacy-notice-title"
437
+ >
438
+ {isLoading ? (
439
+ <div style={styles.loadingContainer}>
440
+ <div style={styles.loadingSpinner}></div>
441
+ <p style={{ ...styles.privacyText, ...textStyle }}>
442
+ Loading...
443
+ </p>
444
+ </div>
445
+ ) : (
446
+ <div style={{ ...styles.content, ...responsiveStyles.content }}>
447
+ <div style={{ ...styles.topSection, ...sectionStyle }}>
448
+ <div style={styles.topLeft}>
449
+ <img
450
+ style={styles.logo}
451
+ src={content?.logo_url || logo}
452
+ alt="logo"
453
+ />
454
+ <h2
455
+ id="privacy-notice-title"
456
+ style={{
457
+ ...styles.title,
458
+ ...responsiveStyles.title,
459
+ ...headingStyle,
460
+ }}
461
+ >
462
+ Your Privacy Matters
463
+ </h2>
464
+ </div>
465
+ <div style={styles.languageSelectorContainer}>
466
+ <button
467
+ style={{
468
+ ...styles.topRight,
469
+ ...responsiveStyles.topRight,
470
+ ...languageSelectorStyle,
471
+ }}
472
+ onClick={toggleLanguageDropdown}
473
+ aria-expanded={isLanguageDropdownOpen}
474
+ aria-haspopup="listbox"
475
+ >
476
+ {selectedLanguage}
477
+ <svg
478
+ xmlns="http://www.w3.org/2000/svg"
479
+ width="16"
480
+ height="16"
481
+ viewBox="0 0 16 16"
482
+ fill="none"
483
+ stroke={
484
+ settings?.button?.language?.textColor ||
485
+ "currentColor"
486
+ }
487
+ strokeWidth="2"
488
+ strokeLinecap="round"
489
+ strokeLinejoin="round"
490
+ style={{ flexShrink: 0, marginLeft: "4px" }}
491
+ >
492
+ <path d="M4 6l4 4 4-4" />
493
+ </svg>
494
+ </button>
495
+ {isLanguageDropdownOpen && (
496
+ <ul
497
+ style={{
498
+ ...styles.languageDropdown,
499
+ ...languageSelectorStyle,
500
+ }}
501
+ role="listbox"
502
+ aria-label="Select language"
503
+ >
504
+ {availableLanguages.map((lang: string) => (
505
+ <li
506
+ key={lang}
507
+ style={{
508
+ ...styles.languageItem,
509
+ ...responsiveStyles.languageItem,
510
+ ...(selectedLanguage === lang
511
+ ? selectedLanguageItemStyle
512
+ : languageSelectorStyle),
513
+ listStyle: "none",
514
+ }}
515
+ onClick={() => handleLanguageSelect(lang)}
516
+ role="option"
517
+ aria-selected={selectedLanguage === lang}
518
+ tabIndex={0}
519
+ >
520
+ {lang}
521
+ </li>
522
+ ))}
523
+ </ul>
524
+ )}
525
+ </div>
526
+ </div>
527
+ <div
528
+ style={{
529
+ ...styles.middleSection,
530
+ ...responsiveStyles.middleSection,
531
+ ...sectionStyle,
532
+ }}
533
+ >
534
+ <p
535
+ style={{
536
+ ...styles.privacyText,
537
+ ...responsiveStyles.privacyText,
538
+ ...textStyle,
539
+ }}
540
+ >
541
+ {content
542
+ ? getTranslatedText("notice_text", content.notice_text)
543
+ : ""}
544
+ <br />
545
+ Learn more in our{" "}
546
+ <a
547
+ style={{ ...styles.link, ...linkStyle }}
548
+ href={content?.privacy_policy_url || "#"}
549
+ target="_blank"
550
+ rel="noopener noreferrer"
551
+ >
552
+ [
553
+ {getTranslatedText(
554
+ "privacy_policy_anchor_text",
555
+ content?.privacy_policy_anchor_text || "Privacy Policy"
556
+ )}
557
+ ]
558
+ </a>{" "}
559
+ and{" "}
560
+ <a
561
+ style={{ ...styles.link, ...linkStyle }}
562
+ href={content?.sub_processors_url || "#"}
563
+ target=""
564
+ >
565
+ [
566
+ {getTranslatedText(
567
+ "vendors_list_anchor_text",
568
+ content?.vendors_list_anchor_text || "Vendors List"
569
+ )}
570
+ ]
571
+ </a>
572
+ .
573
+ </p>
574
+ <h2
575
+ style={{
576
+ ...styles.subTitle,
577
+ ...responsiveStyles.subTitle,
578
+ ...headingStyle,
579
+ }}
580
+ >
581
+ {getTranslatedText(
582
+ "purpose_section_heading",
583
+ "Manage What You Share"
584
+ )}
585
+ </h2>
586
+ <div style={styles.optionsContainer}>
587
+ {content?.purposes?.map((purpose) => (
588
+ <div key={purpose.uuid}>
589
+ <div style={styles.optionItem}>
590
+ <div
591
+ style={styles.optionLeft}
592
+ onClick={() => togglePurposeCollapse(purpose.uuid)}
593
+ >
594
+ <div
595
+ style={{
596
+ display: "flex",
597
+ alignItems: "center",
598
+ justifyContent: "center",
599
+ marginLeft: "5px",
600
+ }}
601
+ >
602
+ <svg
603
+ width="7"
604
+ height="12"
605
+ viewBox="0 0 7 12"
606
+ fill="none"
607
+ xmlns="http://www.w3.org/2000/svg"
608
+ stroke={settings?.headingColor || "#323B4B"}
609
+ strokeWidth="2"
610
+ strokeLinecap="round"
611
+ strokeLinejoin="round"
612
+ style={{
613
+ transform: !collapsedPurposes[purpose.uuid]
614
+ ? "rotate(90deg)"
615
+ : "rotate(0deg)",
616
+ transition: "transform 0.3s ease",
617
+ }}
618
+ >
619
+ <path d="M1 1L6 6L1 11" />
620
+ </svg>
621
+ </div>
622
+ <div style={styles.optionTextContainer}>
623
+ <h2
624
+ style={{
625
+ ...styles.optionTitle,
626
+ ...responsiveStyles.optionTitle,
627
+ ...headingStyle,
628
+ }}
629
+ >
630
+ {getTranslatedText(
631
+ `purposes.name`,
632
+ purpose.name,
633
+ purpose.uuid
634
+ )}
635
+ </h2>
636
+ <h3
637
+ style={{
638
+ ...styles.optionDescription,
639
+ ...responsiveStyles.optionDescription,
640
+ ...textStyle,
641
+ }}
642
+ >
643
+ {getTranslatedText(
644
+ `purposes.description`,
645
+ purpose.description,
646
+ purpose.uuid
647
+ )}
648
+ </h3>
649
+ </div>
650
+ </div>
651
+ <input
652
+ style={styles.checkboxLarge}
653
+ type="checkbox"
654
+ checked={selectedPurposes[purpose.uuid] || false}
655
+ onChange={() =>
656
+ handlePurposeCheckboxChange(purpose.uuid)
657
+ }
658
+ />
659
+ </div>
660
+ {!collapsedPurposes[purpose.uuid] &&
661
+ purpose.data_elements &&
662
+ purpose.data_elements.length > 0 && (
663
+ <div style={styles.dataElementsContainer}>
664
+ {purpose.data_elements.map((dataElement) => (
665
+ <div
666
+ key={dataElement.uuid}
667
+ style={styles.dataElementItem}
668
+ >
669
+ <span
670
+ style={{
671
+ ...styles.dataElementText,
672
+ ...responsiveStyles.dataElementText,
673
+ ...textStyle,
674
+ }}
675
+ >
676
+ {getTranslatedText(
677
+ `data_elements.name`,
678
+ dataElement.name,
679
+ dataElement.uuid
680
+ )}
681
+ {dataElement.required && (
682
+ <span
683
+ style={{
684
+ color: "red",
685
+ marginLeft: "4px",
686
+ }}
687
+ >
688
+ *
689
+ </span>
690
+ )}
691
+ </span>
692
+ <input
693
+ style={styles.checkboxSmall}
694
+ type="checkbox"
695
+ checked={
696
+ selectedDataElements[
697
+ `${purpose.uuid}-${dataElement.uuid}`
698
+ ] || false
699
+ }
700
+ onChange={() =>
701
+ handleDataElementCheckboxChange(
702
+ dataElement.uuid,
703
+ purpose.uuid
704
+ )
705
+ }
706
+ />
707
+ </div>
708
+ ))}
709
+ </div>
710
+ )}
711
+ </div>
712
+ ))}
713
+ </div>
714
+ </div>
715
+ <div
716
+ style={{
717
+ ...styles.bottomSection,
718
+ ...responsiveStyles.bottomSection,
719
+ ...sectionStyle,
720
+ }}
721
+ >
722
+ <button
723
+ style={{
724
+ ...styles.button,
725
+ ...responsiveStyles.button,
726
+ ...styles.acceptButton,
727
+ ...acceptButtonStyle,
728
+ opacity: acceptDisabled ? 0.5 : 1,
729
+ }}
730
+ onClick={handleAccept}
731
+ disabled={acceptDisabled}
732
+ >
733
+ {isSubmitting
734
+ ? "Processing..."
735
+ : getTranslatedText(
736
+ "confirm_button_text",
737
+ content?.confirm_button_text || "Accept"
738
+ )}
739
+ </button>
740
+ <button
741
+ style={{
742
+ ...styles.button,
743
+ ...responsiveStyles.button,
744
+ ...styles.cancelButton,
745
+ ...declineButtonStyle,
746
+ }}
747
+ onClick={handleDecline}
748
+ disabled={isSubmitting}
749
+ >
750
+ {isSubmitting
751
+ ? "Processing..."
752
+ : getTranslatedText(
753
+ "decline_button_text",
754
+ content?.decline_button_text || "Decline"
755
+ )}
756
+ </button>
757
+ </div>
758
+ </div>
759
+ )}
760
+ </div>
761
+ </>
762
+ )}
763
+ </>
764
+ );
765
+ };