@redacto.io/consent-sdk-react 3.6.0 → 5.0.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 +12 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +36 -7
- package/dist/index.mjs +36 -7
- package/dist/privacy-center.d.mts +134 -51
- package/dist/privacy-center.d.ts +134 -51
- package/dist/privacy-center.js +6391 -2741
- package/dist/privacy-center.mjs +6135 -2482
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +5 -5
- package/src/RedactoNoticeConsent/language-codes.ts +37 -1
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +81 -50
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +4 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +68 -36
- package/src/RedactoPrivacyCenter/api/client.ts +125 -14
- 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 +1 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +6 -13
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +6 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -1
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +2 -2
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +2 -3
- package/src/RedactoPrivacyCenter/components/Form.tsx +152 -11
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +4 -6
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +9 -2
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +12 -12
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -1
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +15 -1
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +8 -8
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +7 -11
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +1 -1
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +4 -24
- package/src/RedactoPrivacyCenter/components/constant.ts +3 -7
- package/src/RedactoPrivacyCenter/components/types.ts +12 -4
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +30 -16
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +3 -6
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +35 -3
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/types.ts +14 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +101 -3
- package/src/RedactoPrivacyCenter/lib/constants.ts +46 -1
- package/src/RedactoPrivacyCenter/lib/i18n.ts +1 -1
- package/src/RedactoPrivacyCenter/lib/queries.ts +106 -2
- package/src/RedactoPrivacyCenter/lib/types.ts +186 -1
- package/src/RedactoPrivacyCenter/lib/utils.ts +1 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +69 -2
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +93 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +725 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -6
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +5 -3
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +9 -12
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -2
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +24 -18
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +30 -20
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +17 -13
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +3 -2
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +3 -1
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +10 -4
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +44 -32
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +5 -3
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +4 -2
- package/src/privacy-center.ts +1 -0
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@ import logo from "./assets/redacto-logo.png";
|
|
|
32
32
|
|
|
33
33
|
// Constants
|
|
34
34
|
import { DPO_AUDIO_ELEMENT_MAP } from "./constants";
|
|
35
|
-
import { toBcp47Code } from "./language-codes";
|
|
35
|
+
import { toBcp47Code, toNativeLabel } from "./language-codes";
|
|
36
36
|
|
|
37
37
|
// Utility imports
|
|
38
38
|
import { injectCheckboxStyles } from "./injectStyles";
|
|
@@ -3780,9 +3780,9 @@ export const RedactoNoticeConsent = ({
|
|
|
3780
3780
|
aria-expanded={isLanguageDropdownOpen}
|
|
3781
3781
|
aria-haspopup="listbox"
|
|
3782
3782
|
aria-controls="language-listbox"
|
|
3783
|
-
aria-label={`Select language. Current language: ${selectedLanguage}`}
|
|
3783
|
+
aria-label={`Select language. Current language: ${toNativeLabel(selectedLanguage)}`}
|
|
3784
3784
|
>
|
|
3785
|
-
{selectedLanguage}
|
|
3785
|
+
{toNativeLabel(selectedLanguage)}
|
|
3786
3786
|
<svg
|
|
3787
3787
|
xmlns="http://www.w3.org/2000/svg"
|
|
3788
3788
|
width="16"
|
|
@@ -3829,9 +3829,9 @@ export const RedactoNoticeConsent = ({
|
|
|
3829
3829
|
role="option"
|
|
3830
3830
|
aria-selected={selectedLanguage === lang}
|
|
3831
3831
|
tabIndex={0}
|
|
3832
|
-
aria-label={`Select ${lang} language`}
|
|
3832
|
+
aria-label={`Select ${toNativeLabel(lang)} language`}
|
|
3833
3833
|
>
|
|
3834
|
-
{lang}
|
|
3834
|
+
{toNativeLabel(lang)}
|
|
3835
3835
|
</li>
|
|
3836
3836
|
))}
|
|
3837
3837
|
</ul>
|
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps English language names to "Native script (English)" display labels.
|
|
3
|
+
* English itself is kept as-is; all others show the native script first.
|
|
4
|
+
*/
|
|
5
|
+
export const LANGUAGE_NATIVE_LABELS: Record<string, string> = {
|
|
6
|
+
English: "English",
|
|
7
|
+
Assamese: "অসমীয়া (Assamese)",
|
|
8
|
+
Bengali: "বাংলা (Bengali)",
|
|
9
|
+
Bhojpuri: "भोजपुरी (Bhojpuri)",
|
|
10
|
+
Bodo: "बड़ो (Bodo)",
|
|
11
|
+
Dogri: "डोगरी (Dogri)",
|
|
12
|
+
Gujarati: "ગુજરાતી (Gujarati)",
|
|
13
|
+
Hindi: "हिन्दी (Hindi)",
|
|
14
|
+
Kannada: "ಕನ್ನಡ (Kannada)",
|
|
15
|
+
Kashmiri: "كٲشُر (Kashmiri)",
|
|
16
|
+
"Goan Konkani": "कोंकणी (Goan Konkani)",
|
|
17
|
+
Maithili: "मैथिली (Maithili)",
|
|
18
|
+
Malayalam: "മലയാളം (Malayalam)",
|
|
19
|
+
Manipuri: "ꯃꯩꯇꯩꯂꯣꯟ (Manipuri)",
|
|
20
|
+
Marathi: "मराठी (Marathi)",
|
|
21
|
+
Nepali: "नेपाली (Nepali)",
|
|
22
|
+
Odia: "ଓଡ଼ିଆ (Odia)",
|
|
23
|
+
Punjabi: "ਪੰਜਾਬੀ (Punjabi)",
|
|
24
|
+
Sanskrit: "संस्कृतम् (Sanskrit)",
|
|
25
|
+
Santali: "ᱥᱟᱱᱛᱟᱲᱤ (Santali)",
|
|
26
|
+
Sindhi: "سنڌي (Sindhi)",
|
|
27
|
+
Tamil: "தமிழ் (Tamil)",
|
|
28
|
+
Telugu: "తెలుగు (Telugu)",
|
|
29
|
+
Urdu: "اردو (Urdu)",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Returns the "Native (English)" display label for a language name, falling back to the name itself. */
|
|
33
|
+
export const toNativeLabel = (name: string): string =>
|
|
34
|
+
LANGUAGE_NATIVE_LABELS[name] ?? name;
|
|
35
|
+
|
|
1
36
|
/**
|
|
2
37
|
* Maps display language names (as used in supported_languages_and_translations
|
|
3
38
|
* keys and the language dropdown) to BCP-47 codes for the submit-consent payload.
|
|
@@ -9,13 +44,14 @@ const LANGUAGE_NAME_TO_BCP47: Record<string, string> = {
|
|
|
9
44
|
English: "en",
|
|
10
45
|
Assamese: "as",
|
|
11
46
|
Bengali: "bn",
|
|
47
|
+
Bhojpuri: "bho",
|
|
12
48
|
Bodo: "brx",
|
|
13
49
|
Dogri: "doi",
|
|
14
50
|
Gujarati: "gu",
|
|
15
51
|
Hindi: "hi",
|
|
16
52
|
Kannada: "kn",
|
|
17
53
|
Kashmiri: "ks",
|
|
18
|
-
Konkani: "gom",
|
|
54
|
+
"Goan Konkani": "gom",
|
|
19
55
|
Maithili: "mai",
|
|
20
56
|
Malayalam: "ml",
|
|
21
57
|
Manipuri: "mni-Mtei",
|
|
@@ -87,9 +87,6 @@ const RedactoConsentInlineComponent = ({
|
|
|
87
87
|
onValidationChange?.(areAllRequiredElementsChecked);
|
|
88
88
|
}, [areAllRequiredElementsChecked, onValidationChange]);
|
|
89
89
|
|
|
90
|
-
const acceptDisabled =
|
|
91
|
-
isSubmitting || !areAllRequiredElementsChecked || !accessToken;
|
|
92
|
-
|
|
93
90
|
useEffect(() => {
|
|
94
91
|
if (hasAlreadyConsented) {
|
|
95
92
|
onAccept?.();
|
|
@@ -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
|
+
};
|
|
@@ -4,6 +4,7 @@ import { useInternalNavigation } from './context/NavigationContext';
|
|
|
4
4
|
import { PrivacyCenterConsentManagerPage } from './PrivacyCenterConsentManagerPage';
|
|
5
5
|
import { PrivacyCenterFormPage } from './PrivacyCenterFormPage';
|
|
6
6
|
import { PrivacyCenterActivityPage } from './PrivacyCenterActivityPage';
|
|
7
|
+
import { PrivacyCenterCaseDetailsPage } from './PrivacyCenterCaseDetailsPage';
|
|
7
8
|
|
|
8
9
|
export function PrivacyCenterHomePage() {
|
|
9
10
|
const { currentPage } = useInternalNavigation();
|
|
@@ -21,6 +22,8 @@ export function PrivacyCenterHomePage() {
|
|
|
21
22
|
return <PrivacyCenterFormPage />;
|
|
22
23
|
case 'activity':
|
|
23
24
|
return <PrivacyCenterActivityPage />;
|
|
25
|
+
case 'case-details':
|
|
26
|
+
return <PrivacyCenterCaseDetailsPage />;
|
|
24
27
|
default:
|
|
25
28
|
return <PrivacyCenterConsentManagerPage />;
|
|
26
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useRef, ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useState, useRef, type ReactNode } from 'react';
|
|
2
2
|
import { OrganizationProvider } from './context/OrganizationContext';
|
|
3
3
|
import { ThemeProvider } from './context/theme';
|
|
4
4
|
import type { OrganizationDetail } from './lib/types';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
decodeJwtToken,
|
|
8
8
|
} from './lib/utils';
|
|
9
9
|
import { useBaseUrl } from './context/BaseUrlContext';
|
|
10
|
+
import { useAuth } from './context/AuthContext';
|
|
10
11
|
import { PCLoader } from './ui';
|
|
11
12
|
import { pcStyles } from './styles/pcStyles';
|
|
12
13
|
import PrivacyCenterLayoutComponent from './components/PrivacyCenterLayout';
|
|
@@ -20,6 +21,10 @@ interface PrivacyCenterLayoutProps {
|
|
|
20
21
|
|
|
21
22
|
export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: PrivacyCenterLayoutProps) {
|
|
22
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();
|
|
23
28
|
const [organization, setOrganization] = useState<OrganizationDetail | null>(null);
|
|
24
29
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
|
25
30
|
const [isLoadingOrg, setIsLoadingOrg] = useState(true);
|
|
@@ -27,58 +32,84 @@ export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: Pr
|
|
|
27
32
|
const authCheckInProgressRef = useRef(false);
|
|
28
33
|
|
|
29
34
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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,
|
|
52
92
|
suid: '',
|
|
53
|
-
name:
|
|
54
|
-
slug: slug,
|
|
93
|
+
name: '',
|
|
55
94
|
description: '',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
allowed_domains: [],
|
|
69
|
-
});
|
|
70
|
-
setIsLoadingOrg(false);
|
|
71
|
-
} else {
|
|
72
|
-
// If organization info is not available, set loading to false
|
|
73
|
-
// User will need to authenticate first
|
|
74
|
-
setIsLoadingOrg(false);
|
|
75
|
-
}
|
|
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);
|
|
76
107
|
}
|
|
77
|
-
}, [slug]);
|
|
108
|
+
}, [slug, authAccessToken]);
|
|
78
109
|
|
|
79
110
|
useEffect(() => {
|
|
80
111
|
// Don't check auth while still loading organization
|
|
81
|
-
if (isLoadingOrg || !
|
|
112
|
+
if (isLoadingOrg || !organization) {
|
|
82
113
|
return;
|
|
83
114
|
}
|
|
84
115
|
|
|
@@ -93,8 +124,8 @@ export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: Pr
|
|
|
93
124
|
|
|
94
125
|
try {
|
|
95
126
|
// Check if user is authenticated, attempt refresh if needed
|
|
96
|
-
|
|
97
|
-
slug,
|
|
127
|
+
await checkAndRefreshAuth(
|
|
128
|
+
organization.slug || slug,
|
|
98
129
|
organization?.uuid,
|
|
99
130
|
organization?.workspaces[0]?.uuid,
|
|
100
131
|
consentClient
|
|
@@ -113,7 +144,7 @@ export function PrivacyCenterLayout({ children, defaultTheme, slug, onBack }: Pr
|
|
|
113
144
|
|
|
114
145
|
// Check auth after organization is loaded
|
|
115
146
|
checkAuth();
|
|
116
|
-
}, [slug, organization, isLoadingOrg, consentClient]);
|
|
147
|
+
}, [slug, organization, isLoadingOrg, consentClient, authAccessToken]);
|
|
117
148
|
|
|
118
149
|
// Show loading state while checking authentication or loading organization
|
|
119
150
|
if (isLoadingOrg || !organization || isCheckingAuth) {
|
|
@@ -7,6 +7,10 @@ import { AuthProvider } from './context/AuthContext';
|
|
|
7
7
|
import { NavigationProvider } from './context/NavigationContext';
|
|
8
8
|
import type { InternalPage } from './context/NavigationContext';
|
|
9
9
|
import { PageChromeProvider } from './context/PageChromeContext';
|
|
10
|
+
import { injectPCStyles } from './styles/injectStyles';
|
|
11
|
+
|
|
12
|
+
// Inject Privacy Center CSS styles on module load
|
|
13
|
+
injectPCStyles();
|
|
10
14
|
|
|
11
15
|
interface RedactoPrivacyCenterProps {
|
|
12
16
|
baseUrl: string; // Consent server base URL
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { BaseRequest } from './client';
|
|
2
|
-
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
3
2
|
|
|
4
3
|
export type ConsentApiErrorCode = 'NOMINATOR_NOT_FOUND' | 'NOMINEE_MUST_SPECIFY_NOMINATOR';
|
|
5
4
|
|
|
@@ -14,17 +13,30 @@ export class ConsentApiError extends Error {
|
|
|
14
13
|
|
|
15
14
|
import type {
|
|
16
15
|
ActivityDetail,
|
|
16
|
+
CaseActionBaseParams,
|
|
17
17
|
CaseHistoryDetail,
|
|
18
|
+
CaseMessage,
|
|
18
19
|
ConsentAction,
|
|
19
20
|
CreateCaseResponse,
|
|
20
21
|
FormData,
|
|
22
|
+
GetCaseDocumentParams,
|
|
23
|
+
GetCaseMessagesParams,
|
|
24
|
+
MessageDocument,
|
|
21
25
|
OtpResponse,
|
|
22
26
|
OtpVerificationResponse,
|
|
23
27
|
Pagination,
|
|
28
|
+
SendCaseMessageParams,
|
|
29
|
+
SubmitDocumentForRequestParams,
|
|
24
30
|
TokenExchangeResponse,
|
|
31
|
+
UploadCaseDocumentParams,
|
|
32
|
+
UploadDocumentResponse,
|
|
25
33
|
UserConsentDetail,
|
|
26
34
|
} from '../lib/types';
|
|
27
35
|
|
|
36
|
+
function buildBaseRequest(organizationId: string, workspaceId: string, authToken?: string): BaseRequest {
|
|
37
|
+
return { organisationUuid: organizationId, workspaceUuid: workspaceId, authToken: authToken || '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* Check if an error is an authentication/JWT related error
|
|
30
42
|
*/
|
|
@@ -70,11 +82,7 @@ export async function getFormDataServer(
|
|
|
70
82
|
if (!consentClient) {
|
|
71
83
|
throw new Error('Consent client is required');
|
|
72
84
|
}
|
|
73
|
-
const baseRequest
|
|
74
|
-
organisationUuid: organizationId,
|
|
75
|
-
workspaceUuid: workspaceId,
|
|
76
|
-
authToken: authToken || '',
|
|
77
|
-
};
|
|
85
|
+
const baseRequest = buildBaseRequest(organizationId, workspaceId, authToken);
|
|
78
86
|
|
|
79
87
|
// If useSessionAuth is true (case management), use X-Auth-Token
|
|
80
88
|
// Otherwise (privacy center), use JWT Bearer token
|
|
@@ -99,11 +107,7 @@ export async function createCaseRequestServer(
|
|
|
99
107
|
if (!consentClient) {
|
|
100
108
|
throw new Error('Consent client is required');
|
|
101
109
|
}
|
|
102
|
-
const baseRequest
|
|
103
|
-
organisationUuid: organizationId,
|
|
104
|
-
workspaceUuid: workspaceId,
|
|
105
|
-
authToken: authToken || '',
|
|
106
|
-
};
|
|
110
|
+
const baseRequest = buildBaseRequest(organizationId, workspaceId, authToken);
|
|
107
111
|
|
|
108
112
|
// If useSessionAuth is true (case management), use X-Auth-Token
|
|
109
113
|
// Otherwise (privacy center), use JWT Bearer token
|
|
@@ -238,11 +242,7 @@ export async function fetchProductsServer(params: {
|
|
|
238
242
|
throw new Error('Consent client is required');
|
|
239
243
|
}
|
|
240
244
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
241
|
-
const baseRequest
|
|
242
|
-
organisationUuid: params.organizationId,
|
|
243
|
-
workspaceUuid: params.workspaceId,
|
|
244
|
-
authToken: params.authToken || '',
|
|
245
|
-
};
|
|
245
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
246
246
|
|
|
247
247
|
const result = await params.consentClient.getProducts(
|
|
248
248
|
baseRequest,
|
|
@@ -273,11 +273,7 @@ export async function fetchUserConsentsServer(params: {
|
|
|
273
273
|
throw new Error('Consent client is required');
|
|
274
274
|
}
|
|
275
275
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
276
|
-
const baseRequest
|
|
277
|
-
organisationUuid: params.organizationId,
|
|
278
|
-
workspaceUuid: params.workspaceId,
|
|
279
|
-
authToken: params.authToken || '',
|
|
280
|
-
};
|
|
276
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
281
277
|
|
|
282
278
|
const result = await params.consentClient.getUserConsents(
|
|
283
279
|
baseRequest,
|
|
@@ -313,11 +309,7 @@ export async function manageConsentServer(params: {
|
|
|
313
309
|
throw new Error('Consent client is required');
|
|
314
310
|
}
|
|
315
311
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
316
|
-
const baseRequest
|
|
317
|
-
organisationUuid: params.organizationId,
|
|
318
|
-
workspaceUuid: params.workspaceId,
|
|
319
|
-
authToken: params.authToken || '',
|
|
320
|
-
};
|
|
312
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
321
313
|
|
|
322
314
|
const result = await params.consentClient.manageConsent(
|
|
323
315
|
baseRequest,
|
|
@@ -358,11 +350,7 @@ export async function fetchActivitiesServer(params: {
|
|
|
358
350
|
}
|
|
359
351
|
|
|
360
352
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
361
|
-
const baseRequest
|
|
362
|
-
organisationUuid: params.organizationId,
|
|
363
|
-
workspaceUuid: params.workspaceId,
|
|
364
|
-
authToken: params.authToken || '',
|
|
365
|
-
};
|
|
353
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
366
354
|
|
|
367
355
|
const result = await params.consentClient.getActivities(
|
|
368
356
|
baseRequest,
|
|
@@ -392,11 +380,7 @@ export async function fetchCaseHistoryServer(params: {
|
|
|
392
380
|
throw new Error('Consent client is required');
|
|
393
381
|
}
|
|
394
382
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
395
|
-
const baseRequest
|
|
396
|
-
organisationUuid: params.organizationId,
|
|
397
|
-
workspaceUuid: params.workspaceId,
|
|
398
|
-
authToken: params.authToken || '',
|
|
399
|
-
};
|
|
383
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
400
384
|
|
|
401
385
|
const result = await params.consentClient.getCaseHistory(
|
|
402
386
|
baseRequest,
|
|
@@ -442,3 +426,51 @@ export async function fetchCaseHistoryServer(params: {
|
|
|
442
426
|
return handleServerActionError(error, 'fetching case history');
|
|
443
427
|
}
|
|
444
428
|
}
|
|
429
|
+
|
|
430
|
+
// ─── Case Communication ──────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
type ConsentClient = NonNullable<CaseActionBaseParams['consentClient']>;
|
|
433
|
+
|
|
434
|
+
async function runCaseAction<T>(
|
|
435
|
+
params: CaseActionBaseParams,
|
|
436
|
+
label: string,
|
|
437
|
+
fn: (client: ConsentClient, baseRequest: BaseRequest) => Promise<T>
|
|
438
|
+
): Promise<T | null> {
|
|
439
|
+
try {
|
|
440
|
+
if (!params.consentClient) throw new Error('Consent client is required');
|
|
441
|
+
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
442
|
+
return await fn(params.consentClient, baseRequest);
|
|
443
|
+
} catch (error: any) {
|
|
444
|
+
return handleServerActionError(error, label);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function getCaseMessagesServer(params: GetCaseMessagesParams): Promise<CaseMessage[] | null> {
|
|
449
|
+
return runCaseAction(params, 'fetching case messages', (client, base) =>
|
|
450
|
+
client.getCaseMessages(base, params.caseUuid, params.authToken)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function sendCaseMessageServer(params: SendCaseMessageParams): Promise<CaseMessage | null> {
|
|
455
|
+
return runCaseAction(params, 'sending case message', (client, base) =>
|
|
456
|
+
client.sendCaseMessage(base, params.caseUuid, params.body, params.documentUuids, params.authToken)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function uploadCaseDocumentServer(params: UploadCaseDocumentParams): Promise<UploadDocumentResponse | null> {
|
|
461
|
+
return runCaseAction(params, 'uploading case document', (client, base) =>
|
|
462
|
+
client.uploadCaseDocument(base, params.caseUuid, params.file, params.authToken)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function submitDocumentForRequestServer(params: SubmitDocumentForRequestParams): Promise<CaseMessage | null> {
|
|
467
|
+
return runCaseAction(params, 'submitting document for request', (client, base) =>
|
|
468
|
+
client.submitDocumentForRequest(base, params.caseUuid, params.requestEventUuid, params.documentUuid, params.body, params.authToken)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function getCaseDocumentServer(params: GetCaseDocumentParams): Promise<MessageDocument | null> {
|
|
473
|
+
return runCaseAction(params, 'fetching case document', (client, base) =>
|
|
474
|
+
client.getCaseDocument(base, params.caseUuid, params.documentUuid, params.authToken)
|
|
475
|
+
);
|
|
476
|
+
}
|