@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.
- package/CHANGELOG.md +19 -1
- package/LICENSE +176 -0
- package/README.md +98 -1
- package/dist/index.js +231 -96
- package/dist/index.mjs +231 -96
- package/dist/privacy-center.d.mts +649 -0
- package/dist/privacy-center.d.ts +649 -0
- package/dist/privacy-center.js +20824 -0
- package/dist/privacy-center.mjs +20789 -0
- package/package.json +47 -3
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +6 -4
- package/src/RedactoNoticeConsent/api/index.ts +2 -0
- package/src/RedactoNoticeConsent/api/types.ts +2 -0
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +128 -62
- package/src/RedactoNoticeConsent/language-codes.ts +97 -0
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +30 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +173 -0
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +55 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +476 -0
- package/src/RedactoPrivacyCenter/api/client.ts +524 -0
- package/src/RedactoPrivacyCenter/api/config.ts +33 -0
- package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
- package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
- package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
- package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
- package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
- package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +469 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +426 -0
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +732 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +238 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +304 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +1596 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +174 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +337 -0
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +330 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +304 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +232 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +408 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
- package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +159 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +16 -0
- package/src/RedactoPrivacyCenter/components/types.ts +76 -0
- package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
- package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
- package/src/RedactoPrivacyCenter/config/table.ts +25 -0
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +137 -0
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +41 -0
- package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +96 -0
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
- package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
- package/src/RedactoPrivacyCenter/context/types.ts +14 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +566 -0
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +161 -0
- package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
- package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
- package/src/RedactoPrivacyCenter/lib/queries.ts +134 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +531 -0
- package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +626 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +255 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +280 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +280 -0
- package/src/RedactoPrivacyCenter/styles/base.css +117 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1725 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +813 -0
- package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
- package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +367 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +476 -0
- package/src/RedactoPrivacyCenter/table/index.ts +2 -0
- package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +75 -0
- package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +93 -0
- package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
- package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
- package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
- package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +107 -0
- package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +132 -0
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +31 -0
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +106 -0
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +173 -0
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +387 -0
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +149 -0
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +41 -0
- package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
- package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
- package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +53 -0
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +72 -0
- package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
- package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
- package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
- package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
- package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
- package/src/privacy-center.ts +108 -0
- package/tsconfig.json +2 -0
- package/tsup.config.ts +16 -1
- 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
|
+
}
|