@redacto.io/consent-sdk-react 4.0.0 → 5.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/LICENSE +176 -0
  3. package/README.md +98 -1
  4. package/dist/index.js +231 -96
  5. package/dist/index.mjs +231 -96
  6. package/dist/privacy-center.d.mts +649 -0
  7. package/dist/privacy-center.d.ts +649 -0
  8. package/dist/privacy-center.js +20824 -0
  9. package/dist/privacy-center.mjs +20789 -0
  10. package/package.json +47 -3
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +6 -4
  12. package/src/RedactoNoticeConsent/api/index.ts +2 -0
  13. package/src/RedactoNoticeConsent/api/types.ts +2 -0
  14. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +128 -62
  15. package/src/RedactoNoticeConsent/language-codes.ts +97 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  17. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  18. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  19. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  20. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  21. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  22. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +30 -0
  23. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +173 -0
  24. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +55 -0
  25. package/src/RedactoPrivacyCenter/api/actions.ts +476 -0
  26. package/src/RedactoPrivacyCenter/api/client.ts +524 -0
  27. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  29. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  30. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  31. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  32. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  33. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  34. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  35. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  36. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  37. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  38. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  39. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  40. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  41. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  42. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  43. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  44. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  45. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  46. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  47. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  48. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  49. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +469 -0
  50. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +426 -0
  51. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +732 -0
  52. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +238 -0
  53. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  54. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  55. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  56. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  57. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  58. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +304 -0
  59. package/src/RedactoPrivacyCenter/components/Form.tsx +1596 -0
  60. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +174 -0
  61. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +337 -0
  62. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +330 -0
  63. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  64. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +304 -0
  65. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +232 -0
  66. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  67. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +408 -0
  68. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  69. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  70. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  71. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  72. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  73. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  74. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +159 -0
  75. package/src/RedactoPrivacyCenter/components/constant.ts +16 -0
  76. package/src/RedactoPrivacyCenter/components/types.ts +76 -0
  77. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  78. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  79. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  80. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +137 -0
  81. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +41 -0
  82. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  83. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +96 -0
  84. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  85. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  86. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  87. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  88. package/src/RedactoPrivacyCenter/lib/api.ts +566 -0
  89. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  90. package/src/RedactoPrivacyCenter/lib/constants.ts +161 -0
  91. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  92. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  93. package/src/RedactoPrivacyCenter/lib/queries.ts +134 -0
  94. package/src/RedactoPrivacyCenter/lib/types.ts +531 -0
  95. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  96. package/src/RedactoPrivacyCenter/lib/utils.ts +626 -0
  97. package/src/RedactoPrivacyCenter/locales/as/translation.json +280 -0
  98. package/src/RedactoPrivacyCenter/locales/bho/translation.json +280 -0
  99. package/src/RedactoPrivacyCenter/locales/bn/translation.json +280 -0
  100. package/src/RedactoPrivacyCenter/locales/brx/translation.json +280 -0
  101. package/src/RedactoPrivacyCenter/locales/doi/translation.json +280 -0
  102. package/src/RedactoPrivacyCenter/locales/en/translation.json +255 -0
  103. package/src/RedactoPrivacyCenter/locales/gom/translation.json +280 -0
  104. package/src/RedactoPrivacyCenter/locales/gu/translation.json +280 -0
  105. package/src/RedactoPrivacyCenter/locales/hi/translation.json +280 -0
  106. package/src/RedactoPrivacyCenter/locales/kn/translation.json +280 -0
  107. package/src/RedactoPrivacyCenter/locales/ks/translation.json +280 -0
  108. package/src/RedactoPrivacyCenter/locales/mai/translation.json +280 -0
  109. package/src/RedactoPrivacyCenter/locales/ml/translation.json +280 -0
  110. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +280 -0
  111. package/src/RedactoPrivacyCenter/locales/mr/translation.json +280 -0
  112. package/src/RedactoPrivacyCenter/locales/ne/translation.json +280 -0
  113. package/src/RedactoPrivacyCenter/locales/or/translation.json +280 -0
  114. package/src/RedactoPrivacyCenter/locales/pa/translation.json +280 -0
  115. package/src/RedactoPrivacyCenter/locales/sa/translation.json +280 -0
  116. package/src/RedactoPrivacyCenter/locales/sat/translation.json +280 -0
  117. package/src/RedactoPrivacyCenter/locales/sd/translation.json +280 -0
  118. package/src/RedactoPrivacyCenter/locales/ta/translation.json +280 -0
  119. package/src/RedactoPrivacyCenter/locales/te/translation.json +280 -0
  120. package/src/RedactoPrivacyCenter/locales/ur/translation.json +280 -0
  121. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  122. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1725 -0
  123. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +813 -0
  124. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  125. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  126. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +367 -0
  127. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +476 -0
  128. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  129. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  130. package/src/RedactoPrivacyCenter/ui/Footer.tsx +75 -0
  131. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  132. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +93 -0
  133. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  134. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  135. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  136. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  137. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  138. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +107 -0
  139. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  140. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +132 -0
  141. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +31 -0
  142. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +106 -0
  143. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +173 -0
  144. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +387 -0
  145. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +149 -0
  146. package/src/RedactoPrivacyCenter/ui/PCText.tsx +41 -0
  147. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  148. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  149. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  150. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  151. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +53 -0
  152. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +72 -0
  153. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  154. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  155. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  156. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  157. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  158. package/src/privacy-center.ts +108 -0
  159. package/tsconfig.json +2 -0
  160. package/tsup.config.ts +16 -1
  161. package/.turbo/turbo-build.log +0 -21
@@ -0,0 +1,154 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { IoArrowBack } from 'react-icons/io5';
4
+ import { PCButton, PCBadge, PCLoader } from './ui';
5
+ import { useInternalNavigation } from './context/NavigationContext';
6
+ import { formatDate } from './components/utils';
7
+ import type { CaseDetailsTab, TabId } from './lib/types';
8
+ import { getCaseStatusVariant, formatCaseStatus } from './components/CaseDetails/utils';
9
+ import { MessagesTab } from './components/CaseDetails/MessagesTab';
10
+ import { RequestDetailsTab } from './components/CaseDetails/RequestDetailsTab';
11
+ import { useCaseHistory } from './lib/queries';
12
+ import { useOrganization } from './context/OrganizationContext';
13
+ import { useAuth } from './context/AuthContext';
14
+
15
+ // ─── Page shell ──────────────────────────────────────────────────────────────
16
+
17
+ export const PrivacyCenterCaseDetailsPage = () => {
18
+ const { t } = useTranslation();
19
+ const { navigateTo, selectedCase, setSelectedCase, pendingCaseUuid } = useInternalNavigation();
20
+ const [activeTab, setActiveTab] = useState<TabId>('request-details');
21
+
22
+ const { organization } = useOrganization();
23
+ const { accessToken, refreshToken, onError, updateTokens } = useAuth();
24
+ const organizationId = organization?.uuid;
25
+ const workspaceId = organization?.workspaces[0]?.uuid;
26
+ const contact = typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '';
27
+
28
+ const authContext = useMemo(() => ({
29
+ accessToken, refreshToken, onError, updateTokens,
30
+ }), [accessToken, refreshToken, onError, updateTokens]);
31
+
32
+ // The target UUID comes from the in-memory case (normal navigation) or the
33
+ // persisted UUID (page-refresh). Always fetch so language changes re-hydrate
34
+ // selectedCase with fresh server-translated data.
35
+ const targetUuid = selectedCase?.uuid ?? pendingCaseUuid;
36
+
37
+ const { data: cases, isLoading } = useCaseHistory(
38
+ organizationId,
39
+ workspaceId,
40
+ contact,
41
+ authContext,
42
+ );
43
+
44
+ // Update selectedCase from fetched list (covers language-change re-hydration and page-refresh).
45
+ useEffect(() => {
46
+ if (!cases) return;
47
+ if (cases.length === 0) {
48
+ navigateTo('form');
49
+ return;
50
+ }
51
+ if (!targetUuid) return;
52
+ const found = cases.find((c) => c.uuid === targetUuid);
53
+ if (found) {
54
+ setSelectedCase(found);
55
+ } else {
56
+ navigateTo('form');
57
+ }
58
+ }, [cases, targetUuid, setSelectedCase, navigateTo]);
59
+
60
+ // If the query is disabled (missing org/workspace/contact), cases stays undefined
61
+ // and isLoading stays false — navigate away so the component does not stay blank.
62
+ useEffect(() => {
63
+ if (!targetUuid || isLoading || cases !== undefined) return;
64
+ if (!organizationId || !workspaceId || !contact) {
65
+ navigateTo('form');
66
+ }
67
+ }, [targetUuid, organizationId, workspaceId, contact, cases, isLoading, navigateTo]);
68
+
69
+ // Navigate away when there is no case and nothing to load — kept in an effect
70
+ // to avoid calling navigateTo during render.
71
+ useEffect(() => {
72
+ if (!selectedCase && !targetUuid && !isLoading) {
73
+ navigateTo('form');
74
+ }
75
+ }, [selectedCase, targetUuid, isLoading, navigateTo]);
76
+
77
+ if (!selectedCase) {
78
+ return isLoading ? (
79
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
80
+ <PCLoader size="lg" />
81
+ </div>
82
+ ) : null;
83
+ }
84
+
85
+ const { request_details } = selectedCase;
86
+ const requestTypeLabel = selectedCase.request_type_display || selectedCase.request_type.replace('_', ' ');
87
+ const isDueDateOverdue = selectedCase.due_date && new Date(selectedCase.due_date) < new Date();
88
+
89
+ const tabs: CaseDetailsTab[] = [
90
+ { id: 'request-details', label: t('requestDetails') },
91
+ { id: 'messages', label: t('messages') },
92
+ ];
93
+
94
+ return (
95
+ <div className="pc-case-page">
96
+ {/* Back */}
97
+ <div className="pc-case-back">
98
+ <PCButton
99
+ variant="text"
100
+ size="sm"
101
+ onClick={() => navigateTo('form')}
102
+ className="pc-case-back-btn"
103
+ >
104
+ <IoArrowBack size={16} className="pc-case-back-icon" />
105
+ {t('backToHome')}
106
+ </PCButton>
107
+ </div>
108
+
109
+ {/* Case header */}
110
+ <div className="pc-case-header">
111
+ <span className="pc-case-header-id">
112
+ {selectedCase.case_id}
113
+ </span>
114
+ <span>·</span>
115
+ <span>{requestTypeLabel}</span>
116
+ <span>·</span>
117
+ <span>{t('submitted')} {formatDate(selectedCase.created_at)}</span>
118
+ {selectedCase.due_date && (
119
+ <>
120
+ <span>·</span>
121
+ <span className={isDueDateOverdue ? 'pc-case-header-overdue' : undefined}>
122
+ {t('responseDue')} {formatDate(selectedCase.due_date)}
123
+ </span>
124
+ </>
125
+ )}
126
+ <span>·</span>
127
+ <PCBadge variant={getCaseStatusVariant(selectedCase.status)} size="sm" className="pc-status-badge">
128
+ {selectedCase.status_display || formatCaseStatus(selectedCase.status)}
129
+ </PCBadge>
130
+ </div>
131
+
132
+ {/* Tabs */}
133
+ <div className="pc-case-tabs">
134
+ {tabs.map((tab) => (
135
+ <button
136
+ key={tab.id}
137
+ onClick={() => setActiveTab(tab.id)}
138
+ className={`pc-case-tab ${activeTab === tab.id ? 'pc-case-tab--active' : ''}`}
139
+ >
140
+ {tab.label}
141
+ </button>
142
+ ))}
143
+ </div>
144
+
145
+ {/* Tab content */}
146
+ <div className="pc-case-tab-content">
147
+ {activeTab === 'messages' && <MessagesTab caseUuid={selectedCase.uuid} />}
148
+ {activeTab === 'request-details' && (
149
+ <RequestDetailsTab selectedCase={selectedCase} request_details={request_details} requestTypeLabel={requestTypeLabel} />
150
+ )}
151
+ </div>
152
+ </div>
153
+ );
154
+ };
@@ -0,0 +1,17 @@
1
+ import { useEffect } from 'react';
2
+ import i18n from './lib/i18n';
3
+ import ConsentManager from './components/ConsentManager';
4
+
5
+ export function PrivacyCenterConsentManagerPage() {
6
+ useEffect(() => {
7
+ if (!i18n.language) {
8
+ i18n.changeLanguage('en');
9
+ }
10
+ }, []);
11
+
12
+ return (
13
+ <div style={{ width: '100%' }}>
14
+ <ConsentManager />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,79 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { PCButton } from './ui';
4
+ import { pcStyles } from './styles/pcStyles';
5
+ import { IoArrowBack } from 'react-icons/io5';
6
+ import i18n from './lib/i18n';
7
+ import Form from './components/Form';
8
+ import CaseHistory from './components/CaseHistory';
9
+ import { usePageChrome } from './context/PageChromeContext';
10
+
11
+ const getVar = (varName: string): string => `var(${varName})`;
12
+
13
+ export function PrivacyCenterFormPage() {
14
+ const { t } = useTranslation();
15
+ const [showForm, setShowForm] = useState(() => {
16
+ try {
17
+ return sessionStorage.getItem('pc_grievance_view') === 'form';
18
+ } catch {
19
+ return false;
20
+ }
21
+ });
22
+ const { setPageChromeOverride, clearPageChromeOverride } = usePageChrome();
23
+
24
+ useEffect(() => {
25
+ if (!i18n.language) {
26
+ i18n.changeLanguage('en');
27
+ }
28
+ }, []);
29
+
30
+ useEffect(() => {
31
+ try {
32
+ sessionStorage.setItem('pc_grievance_view', showForm ? 'form' : 'list');
33
+ } catch {
34
+ // sessionStorage unavailable
35
+ }
36
+ }, [showForm]);
37
+
38
+ useEffect(() => {
39
+ if (showForm) {
40
+ setPageChromeOverride({
41
+ titleKey: 'yourPrivacyCenter',
42
+ descriptionKey: 'yourRightsOverDataSimplified',
43
+ });
44
+ } else {
45
+ clearPageChromeOverride();
46
+ }
47
+
48
+ return () => {
49
+ clearPageChromeOverride();
50
+ };
51
+ }, [showForm, setPageChromeOverride, clearPageChromeOverride]);
52
+
53
+ return (
54
+ <div style={{ ...pcStyles.wFull, ...pcStyles.flex, ...pcStyles.flexCol, gap: pcStyles.gapLg.gap }}>
55
+ {showForm ? (
56
+ <>
57
+ <div style={pcStyles.mbMd}>
58
+ <PCButton
59
+ variant="text"
60
+ size="sm"
61
+ onClick={() => setShowForm(false)}
62
+ style={{ paddingLeft: 0, color: getVar('--pc-text-secondary') }}
63
+ >
64
+ <IoArrowBack size={16} style={{ marginRight: '0.5rem' }} />
65
+ {t('backToHome')}
66
+ </PCButton>
67
+ </div>
68
+ <div style={{ width: '100%' }}>
69
+ <Form onCreateRequest={() => setShowForm(false)} />
70
+ </div>
71
+ </>
72
+ ) : (
73
+ <div style={{ width: '100%' }}>
74
+ <CaseHistory onCreateRequest={() => setShowForm(true)} />
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,30 @@
1
+ import { useEffect } from 'react';
2
+ import i18n from './lib/i18n';
3
+ import { useInternalNavigation } from './context/NavigationContext';
4
+ import { PrivacyCenterConsentManagerPage } from './PrivacyCenterConsentManagerPage';
5
+ import { PrivacyCenterFormPage } from './PrivacyCenterFormPage';
6
+ import { PrivacyCenterActivityPage } from './PrivacyCenterActivityPage';
7
+ import { PrivacyCenterCaseDetailsPage } from './PrivacyCenterCaseDetailsPage';
8
+
9
+ export function PrivacyCenterHomePage() {
10
+ const { currentPage } = useInternalNavigation();
11
+
12
+ useEffect(() => {
13
+ if (!i18n.language) {
14
+ i18n.changeLanguage('en');
15
+ }
16
+ }, []);
17
+
18
+ switch (currentPage) {
19
+ case 'consent-manager':
20
+ return <PrivacyCenterConsentManagerPage />;
21
+ case 'form':
22
+ return <PrivacyCenterFormPage />;
23
+ case 'activity':
24
+ return <PrivacyCenterActivityPage />;
25
+ case 'case-details':
26
+ return <PrivacyCenterCaseDetailsPage />;
27
+ default:
28
+ return <PrivacyCenterConsentManagerPage />;
29
+ }
30
+ }
@@ -0,0 +1,173 @@
1
+ import { useEffect, useState, useRef, type ReactNode } from 'react';
2
+ import { OrganizationProvider } from './context/OrganizationContext';
3
+ import { ThemeProvider } from './context/theme';
4
+ import type { OrganizationDetail } from './lib/types';
5
+ import {
6
+ checkAndRefreshAuth,
7
+ decodeJwtToken,
8
+ } from './lib/utils';
9
+ import { useBaseUrl } from './context/BaseUrlContext';
10
+ import { useAuth } from './context/AuthContext';
11
+ import { PCLoader } from './ui';
12
+ import { pcStyles } from './styles/pcStyles';
13
+ import PrivacyCenterLayoutComponent from './components/PrivacyCenterLayout';
14
+
15
+ interface PrivacyCenterLayoutProps {
16
+ children: ReactNode;
17
+ defaultTheme?: 'light' | 'dark';
18
+ slug: string;
19
+ onBack?: 'back' | 'signout';
20
+ }
21
+
22
+ export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: PrivacyCenterLayoutProps) {
23
+ const { consentClient } = useBaseUrl();
24
+ // Read accessToken from AuthContext so this effect re-runs after AuthProvider
25
+ // syncs JWT data to sessionStorage (fixes race condition where child effects
26
+ // run before parent effects in React)
27
+ const { accessToken: authAccessToken } = useAuth();
28
+ const [organization, setOrganization] = useState<OrganizationDetail | null>(null);
29
+ const [isCheckingAuth, setIsCheckingAuth] = useState(true);
30
+ const [isLoadingOrg, setIsLoadingOrg] = useState(true);
31
+ const hasRedirectedRef = useRef(false);
32
+ const authCheckInProgressRef = useRef(false);
33
+
34
+ useEffect(() => {
35
+ if (typeof window === 'undefined') return;
36
+
37
+ // Try sessionStorage first
38
+ let accessToken = sessionStorage.getItem('access_token');
39
+ let orgId = sessionStorage.getItem('pc_org_id');
40
+ let workspaceId = sessionStorage.getItem('pc_workspace_id');
41
+ let orgSlug = sessionStorage.getItem('pc_org_slug');
42
+
43
+ // If sessionStorage is not yet populated (race condition: AuthProvider's
44
+ // effect hasn't run yet), fall back to decoding the JWT from AuthContext
45
+ if (authAccessToken && (!accessToken || !orgId || !workspaceId || !orgSlug)) {
46
+ const decoded = decodeJwtToken(authAccessToken);
47
+ if (decoded) {
48
+ accessToken = accessToken || authAccessToken;
49
+ orgId = orgId || (typeof decoded.organisation_uuid === 'string' ? decoded.organisation_uuid : null);
50
+ workspaceId = workspaceId || (typeof decoded.workspace_uuid === 'string' ? decoded.workspace_uuid : null);
51
+ orgSlug = orgSlug || (typeof decoded.organisation_slug === 'string' ? decoded.organisation_slug : null);
52
+
53
+ // Write to sessionStorage so child components (SideBarPC, ConsentManager)
54
+ // can read these values when they mount
55
+ if (accessToken) sessionStorage.setItem('access_token', accessToken);
56
+ if (orgId) sessionStorage.setItem('pc_org_id', orgId);
57
+ if (workspaceId) sessionStorage.setItem('pc_workspace_id', workspaceId);
58
+ if (orgSlug) sessionStorage.setItem('pc_org_slug', orgSlug);
59
+
60
+ // Extract and set email from JWT for sidebar and consent manager
61
+ const email = decoded.contact || decoded.primary_email;
62
+ if (email && typeof email === 'string') {
63
+ sessionStorage.setItem('email', email);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Use slug prop if provided, otherwise fall back to JWT-derived slug
69
+ const effectiveSlug = slug || orgSlug || '';
70
+
71
+ if (accessToken && orgId && workspaceId && effectiveSlug) {
72
+ // Prefer organisation_name from JWT; fall back to slug-derived name
73
+ const decoded = decodeJwtToken(accessToken);
74
+ const orgNameFromJwt = decoded?.organisation_name;
75
+ const orgName =
76
+ typeof orgNameFromJwt === 'string' && orgNameFromJwt.trim() !== ''
77
+ ? orgNameFromJwt
78
+ : effectiveSlug
79
+ .split('-')
80
+ .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
81
+ .join(' ');
82
+
83
+ setOrganization({
84
+ uuid: orgId,
85
+ suid: '',
86
+ name: orgName,
87
+ slug: effectiveSlug,
88
+ description: '',
89
+ domain: '',
90
+ workspaces: [{
91
+ uuid: workspaceId,
92
+ suid: '',
93
+ name: '',
94
+ description: '',
95
+ workspace_type: '',
96
+ status: '',
97
+ }],
98
+ ai_summary: {},
99
+ icon_url: null,
100
+ activated: true,
101
+ allowed_domains: [],
102
+ });
103
+ setIsLoadingOrg(false);
104
+ } else {
105
+ // If organization info is not available, set loading to false
106
+ setIsLoadingOrg(false);
107
+ }
108
+ }, [slug, authAccessToken]);
109
+
110
+ useEffect(() => {
111
+ // Don't check auth while still loading organization
112
+ if (isLoadingOrg || !organization) {
113
+ return;
114
+ }
115
+
116
+ // Prevent multiple simultaneous auth checks
117
+ if (authCheckInProgressRef.current) {
118
+ return;
119
+ }
120
+
121
+ // Check authentication status with token refresh capability
122
+ const checkAuth = async () => {
123
+ authCheckInProgressRef.current = true;
124
+
125
+ try {
126
+ // Check if user is authenticated, attempt refresh if needed
127
+ await checkAndRefreshAuth(
128
+ organization.slug || slug,
129
+ organization?.uuid,
130
+ organization?.workspaces[0]?.uuid,
131
+ consentClient
132
+ );
133
+
134
+ // User is authenticated, allow access
135
+ hasRedirectedRef.current = false;
136
+ setIsCheckingAuth(false);
137
+ } catch (error) {
138
+ // Auth check failed - user will need to provide tokens via props
139
+ setIsCheckingAuth(false);
140
+ } finally {
141
+ authCheckInProgressRef.current = false;
142
+ }
143
+ };
144
+
145
+ // Check auth after organization is loaded
146
+ checkAuth();
147
+ }, [slug, organization, isLoadingOrg, consentClient, authAccessToken]);
148
+
149
+ // Show loading state while checking authentication or loading organization
150
+ if (isLoadingOrg || !organization || isCheckingAuth) {
151
+ return (
152
+ <div style={{
153
+ ...pcStyles.flex,
154
+ ...pcStyles.hScreen,
155
+ ...pcStyles.wScreen,
156
+ ...pcStyles.itemsCenter,
157
+ ...pcStyles.justifyCenter,
158
+ }}>
159
+ <PCLoader variant="spinner" color="info" size="lg" />
160
+ </div>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <ThemeProvider defaultTheme={defaultTheme}>
166
+ <OrganizationProvider organization={organization}>
167
+ <PrivacyCenterLayoutComponent onBack={onBack}>
168
+ {children}
169
+ </PrivacyCenterLayoutComponent>
170
+ </OrganizationProvider>
171
+ </ThemeProvider>
172
+ );
173
+ }
@@ -0,0 +1,55 @@
1
+ import { I18nextProvider } from 'react-i18next';
2
+ import i18n from './lib/i18n';
3
+ import { PrivacyCenterLayout } from './PrivacyCenterLayout';
4
+ import { PrivacyCenterHomePage } from './PrivacyCenterHomePage';
5
+ import { BaseUrlProvider } from './context/BaseUrlContext';
6
+ import { AuthProvider } from './context/AuthContext';
7
+ import { NavigationProvider } from './context/NavigationContext';
8
+ import type { InternalPage } from './context/NavigationContext';
9
+ import { PageChromeProvider } from './context/PageChromeContext';
10
+ import { injectPCStyles } from './styles/injectStyles';
11
+
12
+ // Inject Privacy Center CSS styles on module load
13
+ injectPCStyles();
14
+
15
+ interface RedactoPrivacyCenterProps {
16
+ baseUrl: string; // Consent server base URL
17
+ slug: string;
18
+ theme?: 'light' | 'dark';
19
+ accessToken: string;
20
+ refreshToken: string;
21
+ onError: (error: Error) => boolean;
22
+ initialPage?: InternalPage;
23
+ onBack?: 'back' | 'signout';
24
+ }
25
+
26
+ export function RedactoPrivacyCenter({
27
+ baseUrl,
28
+ slug,
29
+ theme = 'light',
30
+ accessToken,
31
+ refreshToken,
32
+ onError,
33
+ initialPage = 'consent-manager',
34
+ onBack,
35
+ }: RedactoPrivacyCenterProps) {
36
+ return (
37
+ <I18nextProvider i18n={i18n}>
38
+ <BaseUrlProvider baseUrl={baseUrl}>
39
+ <AuthProvider
40
+ accessToken={accessToken}
41
+ refreshToken={refreshToken}
42
+ onError={onError}
43
+ >
44
+ <NavigationProvider initialPage={initialPage}>
45
+ <PageChromeProvider>
46
+ <PrivacyCenterLayout defaultTheme={theme} slug={slug} onBack={onBack}>
47
+ <PrivacyCenterHomePage />
48
+ </PrivacyCenterLayout>
49
+ </PageChromeProvider>
50
+ </NavigationProvider>
51
+ </AuthProvider>
52
+ </BaseUrlProvider>
53
+ </I18nextProvider>
54
+ );
55
+ }