@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +4 -4
  3. package/dist/index.d.ts +4 -4
  4. package/dist/index.js +36 -7
  5. package/dist/index.mjs +36 -7
  6. package/dist/privacy-center.d.mts +134 -51
  7. package/dist/privacy-center.d.ts +134 -51
  8. package/dist/privacy-center.js +6391 -2741
  9. package/dist/privacy-center.mjs +6135 -2482
  10. package/package.json +1 -1
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +5 -5
  12. package/src/RedactoNoticeConsent/language-codes.ts +37 -1
  13. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  14. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  15. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  16. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  17. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +81 -50
  18. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +4 -0
  19. package/src/RedactoPrivacyCenter/api/actions.ts +68 -36
  20. package/src/RedactoPrivacyCenter/api/client.ts +125 -14
  21. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  22. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  23. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  24. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  25. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  26. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  27. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  28. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  29. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  30. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  31. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  32. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +1 -0
  33. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +6 -13
  34. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +6 -0
  35. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -1
  36. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +2 -2
  37. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +2 -3
  38. package/src/RedactoPrivacyCenter/components/Form.tsx +152 -11
  39. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +4 -6
  40. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +9 -2
  41. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -0
  42. package/src/RedactoPrivacyCenter/components/Pagination.tsx +12 -12
  43. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -1
  44. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +15 -1
  45. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +8 -8
  46. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +7 -11
  47. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +1 -1
  48. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +1 -1
  49. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +4 -24
  50. package/src/RedactoPrivacyCenter/components/constant.ts +3 -7
  51. package/src/RedactoPrivacyCenter/components/types.ts +12 -4
  52. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +30 -16
  53. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +3 -6
  54. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +35 -3
  55. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +1 -1
  56. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  57. package/src/RedactoPrivacyCenter/lib/api.ts +101 -3
  58. package/src/RedactoPrivacyCenter/lib/constants.ts +46 -1
  59. package/src/RedactoPrivacyCenter/lib/i18n.ts +1 -1
  60. package/src/RedactoPrivacyCenter/lib/queries.ts +106 -2
  61. package/src/RedactoPrivacyCenter/lib/types.ts +186 -1
  62. package/src/RedactoPrivacyCenter/lib/utils.ts +1 -0
  63. package/src/RedactoPrivacyCenter/locales/as/translation.json +93 -1
  64. package/src/RedactoPrivacyCenter/locales/bho/translation.json +93 -1
  65. package/src/RedactoPrivacyCenter/locales/bn/translation.json +93 -1
  66. package/src/RedactoPrivacyCenter/locales/brx/translation.json +93 -1
  67. package/src/RedactoPrivacyCenter/locales/doi/translation.json +93 -1
  68. package/src/RedactoPrivacyCenter/locales/en/translation.json +69 -2
  69. package/src/RedactoPrivacyCenter/locales/gom/translation.json +93 -1
  70. package/src/RedactoPrivacyCenter/locales/gu/translation.json +93 -1
  71. package/src/RedactoPrivacyCenter/locales/hi/translation.json +93 -1
  72. package/src/RedactoPrivacyCenter/locales/kn/translation.json +93 -1
  73. package/src/RedactoPrivacyCenter/locales/ks/translation.json +93 -1
  74. package/src/RedactoPrivacyCenter/locales/mai/translation.json +93 -1
  75. package/src/RedactoPrivacyCenter/locales/ml/translation.json +93 -1
  76. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +93 -1
  77. package/src/RedactoPrivacyCenter/locales/mr/translation.json +93 -1
  78. package/src/RedactoPrivacyCenter/locales/ne/translation.json +93 -1
  79. package/src/RedactoPrivacyCenter/locales/or/translation.json +93 -1
  80. package/src/RedactoPrivacyCenter/locales/pa/translation.json +93 -1
  81. package/src/RedactoPrivacyCenter/locales/sa/translation.json +93 -1
  82. package/src/RedactoPrivacyCenter/locales/sat/translation.json +93 -1
  83. package/src/RedactoPrivacyCenter/locales/sd/translation.json +93 -1
  84. package/src/RedactoPrivacyCenter/locales/ta/translation.json +93 -1
  85. package/src/RedactoPrivacyCenter/locales/te/translation.json +93 -1
  86. package/src/RedactoPrivacyCenter/locales/ur/translation.json +93 -1
  87. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +725 -0
  88. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -6
  89. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +5 -3
  90. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +9 -12
  91. package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -2
  92. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +24 -18
  93. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +30 -20
  94. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +17 -13
  95. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +3 -2
  96. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +3 -1
  97. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -2
  98. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +10 -4
  99. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +44 -32
  100. package/src/RedactoPrivacyCenter/ui/PCText.tsx +5 -3
  101. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -2
  102. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +4 -2
  103. package/src/privacy-center.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "3.6.0",
3
+ "version": "5.0.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -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?.();
@@ -12,7 +12,6 @@ export const fetchConsentContent = async ({
12
12
  workspace_uuid,
13
13
  notice_uuid,
14
14
  baseUrl,
15
- language = "en",
16
15
  specific_uuid,
17
16
  }: FetchConsentContentParams): Promise<ConsentContent> => {
18
17
  try {
@@ -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
- // Load organization info from sessionStorage
31
- if (typeof window !== 'undefined' && slug) {
32
- const accessToken = sessionStorage.getItem('access_token');
33
- const orgId = sessionStorage.getItem('pc_org_id');
34
- const workspaceId = sessionStorage.getItem('pc_workspace_id');
35
- const orgSlug = sessionStorage.getItem('pc_org_slug');
36
-
37
- // Create organization object from sessionStorage
38
- if (accessToken && orgId && workspaceId && orgSlug === slug) {
39
- // Prefer organisation_name from JWT; fall back to slug-derived name
40
- const decoded = decodeJwtToken(accessToken);
41
- const orgNameFromJwt = decoded?.organisation_name;
42
- const orgName =
43
- typeof orgNameFromJwt === 'string' && orgNameFromJwt.trim() !== ''
44
- ? orgNameFromJwt
45
- : slug
46
- .split('-')
47
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
48
- .join(' ');
49
-
50
- setOrganization({
51
- uuid: orgId,
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: orgName,
54
- slug: slug,
93
+ name: '',
55
94
  description: '',
56
- domain: '',
57
- workspaces: [{
58
- uuid: workspaceId,
59
- suid: '',
60
- name: '',
61
- description: '',
62
- workspace_type: '',
63
- status: '',
64
- }],
65
- ai_summary: {},
66
- icon_url: null,
67
- activated: true,
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 || !slug || !organization) {
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
- const isAuthenticated = await checkAndRefreshAuth(
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: 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: 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: 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: 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: 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: 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: 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
+ }