@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
@@ -1,7 +1,5 @@
1
- import { useState } from 'react';
2
1
  import { PCTitle, PCCollapse, PCTooltip } from '../../ui';
3
2
  import { pcStyles } from '../../styles/pcStyles';
4
- import { cn } from '../../utils/cn';
5
3
  import { PiCaretDownBold } from 'react-icons/pi';
6
4
  import { MdLineWeight } from 'react-icons/md';
7
5
 
@@ -9,11 +7,9 @@ const getVar = (varName: string): string => `var(${varName})`;
9
7
  import StatusBadge from '../../ui/StatusBadge';
10
8
  import { useInternalNavigation, type InternalPage } from '../../context/NavigationContext';
11
9
  import { useTranslation } from 'react-i18next';
12
- import type { BadgeType, MenuBadgeProps, SideMenuPCProps } from './types';
10
+ import type { MenuBadgeProps, SideMenuPCProps } from './types';
13
11
  import { badges } from './constants';
14
12
 
15
- export type { DropdownItem, MenuItem, SideMenuPCProps } from './types';
16
-
17
13
  const MenuBadge = ({ type, isDisabled }: MenuBadgeProps) => {
18
14
  const { t } = useTranslation();
19
15
  const badge = badges[type];
@@ -25,7 +21,7 @@ const MenuBadge = ({ type, isDisabled }: MenuBadgeProps) => {
25
21
  return (
26
22
  <span
27
23
  style={{
28
- marginLeft: pcStyles.mSm.marginLeft,
24
+ marginLeft: getVar('--pc-spacing-sm'),
29
25
  borderRadius: '9999px',
30
26
  paddingLeft: '0.5rem',
31
27
  paddingRight: '0.5rem',
@@ -150,7 +146,7 @@ export function SideMenuPC({ menuItems, isCollapsed, onItemSelect }: SideMenuPCP
150
146
  {item?.icon && (
151
147
  <span
152
148
  style={{
153
- marginRight: isCollapsed ? 0 : pcStyles.mSm.marginRight,
149
+ marginRight: isCollapsed ? 0 : getVar('--pc-spacing-sm'),
154
150
  display: 'inline-flex',
155
151
  height: '1.25rem',
156
152
  width: '1.25rem',
@@ -242,7 +238,7 @@ export function SideMenuPC({ menuItems, isCollapsed, onItemSelect }: SideMenuPCP
242
238
  {dropdownItem?.icon && (
243
239
  <span
244
240
  style={{
245
- marginRight: pcStyles.mSm.marginRight,
241
+ marginRight: getVar('--pc-spacing-sm'),
246
242
  display: 'inline-flex',
247
243
  height: '1.125rem',
248
244
  width: '1.125rem',
@@ -265,7 +261,7 @@ export function SideMenuPC({ menuItems, isCollapsed, onItemSelect }: SideMenuPCP
265
261
  />
266
262
  )}
267
263
  </div>
268
- <div style={{ marginLeft: pcStyles.mMd.marginLeft, ...pcStyles.flex, ...pcStyles.itemsCenter }}>
264
+ <div style={{ marginLeft: getVar('--pc-spacing-md'), ...pcStyles.flex, ...pcStyles.itemsCenter }}>
269
265
  {dropdownItem?.badge?.length ? (
270
266
  <StatusBadge status={dropdownItem?.badge} className="pc-status-badge" />
271
267
  ) : (
@@ -284,7 +280,7 @@ export function SideMenuPC({ menuItems, isCollapsed, onItemSelect }: SideMenuPCP
284
280
  renderItemWithTooltip(
285
281
  <div
286
282
  key={item.name + '-' + index}
287
- onClick={() => handleNavigate(item.href)}
283
+ onClick={() => item.href && handleNavigate(item.href)}
288
284
  style={{
289
285
  position: 'relative',
290
286
  marginTop: isCollapsed ? 0 : index === 0 ? 0 : '0.1875rem',
@@ -317,7 +313,7 @@ export function SideMenuPC({ menuItems, isCollapsed, onItemSelect }: SideMenuPCP
317
313
  {item?.icon && (
318
314
  <span
319
315
  style={{
320
- marginRight: isCollapsed ? 0 : pcStyles.mSm.marginRight,
316
+ marginRight: isCollapsed ? 0 : getVar('--pc-spacing-sm'),
321
317
  display: 'inline-flex',
322
318
  height: '1.25rem',
323
319
  width: '1.25rem',
@@ -1,4 +1,4 @@
1
- import { ReactElement } from 'react';
1
+ import type { ReactElement } from 'react';
2
2
 
3
3
  export type BadgeType = 'new' | 'coming';
4
4
 
@@ -52,7 +52,7 @@ export const useActivitiesColumns = () => {
52
52
 
53
53
  return (
54
54
  <div style={{ paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom }}>
55
- <StatusBadge status={status} variant="filled" className="pc-status-badge">
55
+ <StatusBadge status={status} className="pc-status-badge">
56
56
  {displayType}
57
57
  </StatusBadge>
58
58
  </div>
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { PCText, PCActionIcon, PCTooltip, PCBadge } from '../ui';
4
4
  import { pcStyles } from '../styles/pcStyles';
5
- import { FiEye, FiDownload } from 'react-icons/fi';
5
+ import { FiDownload } from 'react-icons/fi';
6
6
  import { createColumnHelper } from '@tanstack/react-table';
7
7
  import type { CaseRequest } from '../lib/types';
8
8
  import { formatDate } from './utils';
@@ -21,13 +21,7 @@ const getCaseStatusVariant = (status: string): 'success' | 'error' | 'warning' |
21
21
  const formatCaseStatus = (s: string) =>
22
22
  s.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
23
23
 
24
- export type UseCaseHistoryColumnsProps = {
25
- onViewDetails: (caseRequest: CaseRequest) => void;
26
- };
27
-
28
- export const useCaseHistoryColumns = ({
29
- onViewDetails,
30
- }: UseCaseHistoryColumnsProps) => {
24
+ export const useCaseHistoryColumns = () => {
31
25
  const { t } = useTranslation();
32
26
  return useMemo(
33
27
  () => [
@@ -133,7 +127,7 @@ export const useCaseHistoryColumns = ({
133
127
  meta: { align: 'right' },
134
128
  enableSorting: false,
135
129
  enableColumnFilter: false,
136
- cell: ({ row }) => {
130
+ cell: () => {
137
131
  return (
138
132
  <div style={{
139
133
  ...pcStyles.flex,
@@ -141,20 +135,6 @@ export const useCaseHistoryColumns = ({
141
135
  ...pcStyles.gapXs,
142
136
  width: '100%',
143
137
  }}>
144
- <PCTooltip content={t('viewDetails')}>
145
- <PCActionIcon
146
- variant="outline"
147
- size="sm"
148
- style={{
149
- color: getVar('--pc-text-secondary'),
150
- transition: 'color 0.2s ease',
151
- }}
152
- onClick={() => onViewDetails(row.original)}
153
- aria-label={t('viewDetails')}
154
- >
155
- <FiEye size={16} />
156
- </PCActionIcon>
157
- </PCTooltip>
158
138
  <PCTooltip content={t('downloadResponse')}>
159
139
  <PCActionIcon
160
140
  variant="outline"
@@ -174,6 +154,6 @@ export const useCaseHistoryColumns = ({
174
154
  },
175
155
  }),
176
156
  ],
177
- [onViewDetails, t]
157
+ [t]
178
158
  );
179
159
  };
@@ -1,10 +1,3 @@
1
- export const TABS = {
2
- ALL: 'All Requests',
3
- PROCESSING: 'Processing',
4
- COMPLETED: 'Completed',
5
- REJECTED: 'Rejected',
6
- };
7
-
8
1
  export const GRIEVANCE_TAB_IDS = {
9
2
  ALL: 'all',
10
3
  PROCESSING: 'processing',
@@ -18,3 +11,6 @@ export {
18
11
  INITIAL_PAGE,
19
12
  INITIAL_PAGE_SIZE,
20
13
  } from '../config/table';
14
+
15
+
16
+ export const MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL = 3;
@@ -1,6 +1,12 @@
1
- import { ReactNode } from 'react';
1
+ import type { ReactNode } from 'react';
2
2
  import type { CaseRequest, UserConsent } from '../lib/types';
3
3
 
4
+ export type TableColumnAlign = 'left' | 'center' | 'right';
5
+
6
+ export type TableColumnMeta = {
7
+ align?: TableColumnAlign;
8
+ };
9
+
4
10
  export type PrivacyCenterLayoutProps = {
5
11
  children: ReactNode;
6
12
  sidebarClassName?: string;
@@ -20,11 +26,13 @@ export type CaseDetailsModalProps = {
20
26
  selectedCase: CaseRequest | null;
21
27
  };
22
28
 
23
- export type RequestSubmitProps = {
29
+ export type RequestSubmitProps = Readonly<{
24
30
  caseId: string;
25
31
  contact: string;
26
32
  onCreateRequest: () => void;
27
- };
33
+ revokeConsentOnFulfilment?: boolean;
34
+ revokedPurposeNames?: string;
35
+ }>;
28
36
 
29
37
  export type PaginationProps = {
30
38
  currentPage: number;
@@ -47,7 +55,7 @@ export type ProductFilterOption = {
47
55
  };
48
56
 
49
57
  export type ConsentManagerProps = {
50
- variant?: import('rizzui').TableVariantProps;
58
+ variant?: 'modern' | 'minimal';
51
59
  };
52
60
 
53
61
  export type ModifyConsentModalProps = {
@@ -1,13 +1,8 @@
1
- import React, { createContext, useContext, ReactNode, useState, useEffect, useCallback, useMemo } from 'react';
2
- import { setRefreshTokenCookie, getCookieValue, decodeJwtToken } from '../lib/utils';
1
+ import { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from 'react';
2
+ import { setRefreshTokenCookie, decodeJwtToken } from '../lib/utils';
3
+ import type { AuthContextValue } from './types';
3
4
 
4
- export interface AuthContextValue {
5
- accessToken: string;
6
- refreshToken: string;
7
- onError: (error: Error) => boolean;
8
- // Internal: updates tokens after refresh
9
- updateTokens: (accessToken: string, refreshToken?: string) => void;
10
- }
5
+ export type { AuthContextValue } from './types';
11
6
 
12
7
  const AuthContext = createContext<AuthContextValue | null>(null);
13
8
 
@@ -30,8 +25,10 @@ const syncJwtDataToSessionStorage = (accessToken: string): void => {
30
25
  return;
31
26
  }
32
27
 
33
- // Extract email from JWT (contact or primary_email)
34
- const email = decoded.contact || decoded.primary_email;
28
+ // Extract email from JWT (contact or primary_email), fall back to existing sources
29
+ const email = decoded.contact || decoded.primary_email
30
+ || sessionStorage.getItem('email')
31
+ || (typeof localStorage !== 'undefined' ? localStorage.getItem('userEmail') : null);
35
32
  if (email && typeof email === 'string') {
36
33
  sessionStorage.setItem('email', email);
37
34
  }
@@ -61,17 +58,34 @@ export const AuthProvider = ({
61
58
  refreshToken: initialRefreshToken,
62
59
  onError,
63
60
  }: AuthProviderProps) => {
64
- const [accessToken, setAccessToken] = useState(initialAccessToken);
65
- const [refreshToken, setRefreshToken] = useState(initialRefreshToken);
61
+ // Sync to sessionStorage synchronously during initialization so child
62
+ // components (ConsentManager, SideBarPC) find the data when they mount.
63
+ // useEffect runs AFTER children render — too late for components that
64
+ // read sessionStorage during their render phase.
65
+ const [accessToken, setAccessToken] = useState(() => {
66
+ if (initialAccessToken && typeof initialAccessToken === 'string' && initialAccessToken.trim() !== '') {
67
+ if (typeof window !== 'undefined') {
68
+ sessionStorage.setItem('access_token', initialAccessToken);
69
+ syncJwtDataToSessionStorage(initialAccessToken);
70
+ }
71
+ }
72
+ return initialAccessToken;
73
+ });
74
+ const [refreshToken, setRefreshToken] = useState(() => {
75
+ if (initialRefreshToken && typeof initialRefreshToken === 'string' && initialRefreshToken.trim() !== '') {
76
+ if (typeof window !== 'undefined') {
77
+ setRefreshTokenCookie(initialRefreshToken);
78
+ }
79
+ }
80
+ return initialRefreshToken;
81
+ });
66
82
 
67
- // Sync props to state and storage when they change
68
- // Only sync if tokens are valid non-empty strings
83
+ // Re-sync when props change (for token refresh scenarios)
69
84
  useEffect(() => {
70
85
  if (initialAccessToken && typeof initialAccessToken === 'string' && initialAccessToken.trim() !== '') {
71
86
  setAccessToken(initialAccessToken);
72
87
  if (typeof window !== 'undefined') {
73
88
  sessionStorage.setItem('access_token', initialAccessToken);
74
- // Extract and store org/auth data from JWT
75
89
  syncJwtDataToSessionStorage(initialAccessToken);
76
90
  }
77
91
  }
@@ -1,11 +1,8 @@
1
- import React, { createContext, useContext, ReactNode, useMemo } from 'react';
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
2
2
  import { createConsentClient } from '../api/client';
3
- import type { BaseRequest } from '../api/client';
3
+ import type { BaseUrlContextValue } from './types';
4
4
 
5
- export interface BaseUrlContextValue {
6
- baseUrl: string;
7
- consentClient: ReturnType<typeof createConsentClient>;
8
- }
5
+ export type { BaseUrlContextValue } from './types';
9
6
 
10
7
  export const BaseUrlContext = createContext<BaseUrlContextValue | null>(null);
11
8
 
@@ -1,11 +1,13 @@
1
1
  import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
+ import type { CaseRequest } from '../lib/types';
2
3
 
3
- export type InternalPage = 'consent-manager' | 'form' | 'activity';
4
+ export type InternalPage = 'consent-manager' | 'form' | 'activity' | 'case-details';
4
5
 
5
6
  const NAV_STORAGE_KEY = 'pc_current_page';
7
+ const CASE_UUID_STORAGE_KEY = 'pc_selected_case_uuid';
6
8
 
7
9
  const VALID_PAGES: ReadonlySet<string> = new Set<InternalPage>([
8
- 'consent-manager', 'form', 'activity',
10
+ 'consent-manager', 'form', 'activity', 'case-details',
9
11
  ]);
10
12
 
11
13
  function getStoredPage(fallback: InternalPage): InternalPage {
@@ -21,9 +23,21 @@ function getStoredPage(fallback: InternalPage): InternalPage {
21
23
  return fallback;
22
24
  }
23
25
 
26
+ function getStoredCaseUuid(): string | null {
27
+ if (typeof window === 'undefined') return null;
28
+ try {
29
+ return sessionStorage.getItem(CASE_UUID_STORAGE_KEY);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
24
35
  interface NavigationContextType {
25
36
  currentPage: InternalPage;
26
37
  navigateTo: (page: InternalPage) => void;
38
+ selectedCase: CaseRequest | null;
39
+ setSelectedCase: (c: CaseRequest | null) => void;
40
+ pendingCaseUuid: string | null;
27
41
  }
28
42
 
29
43
  const NavigationContext = createContext<NavigationContextType | null>(null);
@@ -38,9 +52,27 @@ export const NavigationProvider = ({
38
52
  const [currentPage, setCurrentPage] = useState<InternalPage>(
39
53
  () => getStoredPage(initialPage)
40
54
  );
55
+ const [selectedCase, setSelectedCaseState] = useState<CaseRequest | null>(null);
56
+ const [pendingCaseUuid] = useState<string | null>(() => getStoredCaseUuid());
57
+
58
+ const setSelectedCase = useCallback((c: CaseRequest | null) => {
59
+ setSelectedCaseState(c);
60
+ try {
61
+ if (c?.uuid) {
62
+ sessionStorage.setItem(CASE_UUID_STORAGE_KEY, c.uuid);
63
+ } else {
64
+ sessionStorage.removeItem(CASE_UUID_STORAGE_KEY);
65
+ }
66
+ } catch {
67
+ // sessionStorage unavailable
68
+ }
69
+ }, []);
41
70
 
42
71
  const navigateTo = useCallback((page: InternalPage) => {
43
72
  setCurrentPage(page);
73
+ if (page !== 'case-details') {
74
+ setSelectedCaseState(null);
75
+ }
44
76
  try {
45
77
  sessionStorage.setItem(NAV_STORAGE_KEY, page);
46
78
  } catch {
@@ -49,7 +81,7 @@ export const NavigationProvider = ({
49
81
  }, []);
50
82
 
51
83
  return (
52
- <NavigationContext.Provider value={{ currentPage, navigateTo }}>
84
+ <NavigationContext.Provider value={{ currentPage, navigateTo, selectedCase, setSelectedCase, pendingCaseUuid }}>
53
85
  {children}
54
86
  </NavigationContext.Provider>
55
87
  );
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, ReactNode } from 'react';
1
+ import { createContext, useContext, type ReactNode } from 'react';
2
2
  import type { OrganizationDetail } from '../lib/types';
3
3
 
4
4
  export const OrganizationContext = createContext<{
@@ -0,0 +1,14 @@
1
+ import type { createConsentClient } from '../api/client';
2
+
3
+ export type AuthContextValue = Readonly<{
4
+ accessToken: string;
5
+ refreshToken: string;
6
+ onError: (error: Error) => boolean;
7
+ // Internal: updates tokens after refresh
8
+ updateTokens: (accessToken: string, refreshToken?: string) => void;
9
+ }>;
10
+
11
+ export type BaseUrlContextValue = Readonly<{
12
+ baseUrl: string;
13
+ consentClient: ReturnType<typeof createConsentClient>;
14
+ }>;
@@ -6,16 +6,21 @@ import {
6
6
  fetchUserConsentsServer,
7
7
  getFormDataServer,
8
8
  manageConsentServer,
9
- createOtpServer,
10
- verifyOtpServer,
9
+ getCaseMessagesServer,
10
+ sendCaseMessageServer,
11
+ uploadCaseDocumentServer,
12
+ submitDocumentForRequestServer,
13
+ getCaseDocumentServer,
11
14
  } from '../api/actions';
12
15
  import type { BaseUrlContextValue } from '../context/BaseUrlContext';
13
16
  import type {
14
17
  ActivityDetail,
15
18
  CaseHistoryDetail,
19
+ CaseMessage,
16
20
  CreateCaseResponse,
17
21
  FormData,
18
- OrganizationResponse,
22
+ MessageDocument,
23
+ UploadDocumentResponse,
19
24
  UserConsentDetail,
20
25
  UserDataRequest,
21
26
  } from './types';
@@ -466,3 +471,96 @@ export const fetchCaseHistory = async (
466
471
  return null;
467
472
  }
468
473
  };
474
+
475
+ // ─── Auth-retry helper ──────────────────────────────────────────────────────
476
+
477
+ /**
478
+ * Wraps an API call with token acquisition and single-retry-on-auth-error logic.
479
+ * `fn` receives a valid access token and returns the API result.
480
+ */
481
+ async function withAuthRetry<T>(
482
+ organizationId: string,
483
+ workspaceId: string,
484
+ authContext: AuthContext | undefined,
485
+ consentClient: BaseUrlContextValue['consentClient'] | undefined,
486
+ errorLabel: string,
487
+ fn: (token: string) => Promise<T>
488
+ ): Promise<T | null> {
489
+ try {
490
+ const accessToken = await ensureValidToken(organizationId, workspaceId, undefined, authContext, consentClient);
491
+ if (!accessToken) return null;
492
+ return await fn(accessToken);
493
+ } catch (error: any) {
494
+ const shouldRetry = await handleClientAuthError(error, organizationId, workspaceId, authContext, consentClient);
495
+ if (shouldRetry) {
496
+ const token = sessionStorage.getItem('access_token') || authContext?.accessToken || '';
497
+ return await fn(token);
498
+ }
499
+ console.error(errorLabel, error);
500
+ return null;
501
+ }
502
+ }
503
+
504
+ // ─── Case Communication ──────────────────────────────────────────────────────
505
+
506
+ export const getCaseMessages = async (
507
+ organizationId: string,
508
+ workspaceId: string,
509
+ caseUuid: string,
510
+ authContext?: AuthContext,
511
+ consentClient?: BaseUrlContextValue['consentClient']
512
+ ): Promise<CaseMessage[] | null> =>
513
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching case messages:', (token) =>
514
+ getCaseMessagesServer({ organizationId, workspaceId, caseUuid, authToken: token, consentClient })
515
+ );
516
+
517
+ export const sendCaseMessage = async (
518
+ organizationId: string,
519
+ workspaceId: string,
520
+ caseUuid: string,
521
+ body: string,
522
+ documentUuids: string[],
523
+ authContext?: AuthContext,
524
+ consentClient?: BaseUrlContextValue['consentClient']
525
+ ): Promise<CaseMessage | null> =>
526
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error sending case message:', (token) =>
527
+ sendCaseMessageServer({ organizationId, workspaceId, caseUuid, body, documentUuids, authToken: token, consentClient })
528
+ );
529
+
530
+ export const uploadCaseDocument = async (
531
+ organizationId: string,
532
+ workspaceId: string,
533
+ caseUuid: string,
534
+ file: File,
535
+ authContext?: AuthContext,
536
+ consentClient?: BaseUrlContextValue['consentClient']
537
+ ): Promise<UploadDocumentResponse | null> =>
538
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error uploading case document:', (token) =>
539
+ uploadCaseDocumentServer({ organizationId, workspaceId, caseUuid, file, authToken: token, consentClient })
540
+ );
541
+
542
+ export const submitDocumentForRequest = async (
543
+ organizationId: string,
544
+ workspaceId: string,
545
+ caseUuid: string,
546
+ requestEventUuid: string,
547
+ documentUuid: string,
548
+ body: string,
549
+ authContext?: AuthContext,
550
+ consentClient?: BaseUrlContextValue['consentClient']
551
+ ): Promise<CaseMessage | null> =>
552
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error submitting document for request:', (token) =>
553
+ submitDocumentForRequestServer({ organizationId, workspaceId, caseUuid, requestEventUuid, documentUuid, body, authToken: token, consentClient })
554
+ );
555
+
556
+ export const getCaseDocument = async (
557
+ organizationId: string,
558
+ workspaceId: string,
559
+ caseUuid: string,
560
+ documentUuid: string,
561
+ authContext?: AuthContext,
562
+ consentClient?: BaseUrlContextValue['consentClient']
563
+ ): Promise<MessageDocument | null> =>
564
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching case document:', (token) =>
565
+ getCaseDocumentServer({ organizationId, workspaceId, caseUuid, documentUuid, authToken: token, consentClient })
566
+ );
@@ -4,6 +4,7 @@ import { GRIEVANCE_TAB_IDS } from '../components/constant';
4
4
  import { menuItemsPC } from '../components/SideMenuBarPC/constants';
5
5
  import type { MenuItem } from '../components/SideMenuBarPC/types';
6
6
  import type { InternalPage } from '../context/NavigationContext';
7
+ import type { DocumentRequestStatus, DocumentRequestStatusInfo } from './types';
7
8
 
8
9
  export const useRequestTypeLabels = () => {
9
10
  const { t } = useTranslation();
@@ -36,7 +37,7 @@ export const languageOptions = [
36
37
  { value: 'sd', label: 'سنڌي (Sindhi)' },
37
38
  { value: 'doi', label: 'डोगरी (Dogri)' },
38
39
  { value: 'mni-Mtei', label: 'ꯃꯩꯇꯩꯂꯣꯟ (Manipuri)' },
39
- { value: 'gom', label: 'कोंकणी (Konkani)' },
40
+ { value: 'gom', label: 'कोंकणी (Goan Konkani)' },
40
41
  { value: 'sa', label: 'संस्कृतम् (Sanskrit)' },
41
42
  { value: 'bho', label: 'भोजपुरी (Bhojpuri)' },
42
43
  { value: 'brx', label: 'बड़ो (Bodo)' },
@@ -84,6 +85,10 @@ export const PAGE_CHROME_BY_PAGE: Record<InternalPage, PageChromeConfig> = {
84
85
  titleKey: 'activities',
85
86
  descriptionKey: 'activitiesSubtitle',
86
87
  },
88
+ 'case-details': {
89
+ titleKey: 'caseDetails',
90
+ descriptionKey: 'yourPrivacyCenter',
91
+ },
87
92
  };
88
93
 
89
94
  const HREF_TO_NAME_KEY: Record<string, string> = {
@@ -100,6 +105,46 @@ export const useMenuItemsPC = (): MenuItem[] => {
100
105
  }));
101
106
  };
102
107
 
108
+ // ─── Upload validation ────────────────────────────────────────────────────────
109
+
110
+ export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
111
+
112
+ export const ALLOWED_MIME_TYPES = [
113
+ 'application/pdf',
114
+ 'image/jpeg',
115
+ 'image/png',
116
+ 'image/webp',
117
+ 'application/msword',
118
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
119
+ 'application/vnd.ms-excel',
120
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
121
+ 'text/plain',
122
+ 'text/csv',
123
+ ];
124
+
125
+ // ─── Status colors ───────────────────────────────────────────────────────────
126
+
127
+ export const STATUS_COLORS = {
128
+ success: { color: '#16a34a', bg: '#f0fdf4', border: '#bbf7d0' },
129
+ error: { color: '#dc2626', bg: '#fef2f2', border: '#fecaca' },
130
+ warning: { color: '#d97706', bg: '#fffbeb', border: '#fde68a' },
131
+ info: { color: '#2563eb', bg: '#f8fbff', border: '#93c5fd' },
132
+ infoDark: '#1d4ed8',
133
+ infoBanner: { color: '#1d4ed8', bg: '#eff6ff', border: '#bfdbfe' },
134
+ } as const;
135
+
136
+ // ─── Document request ─────────────────────────────────────────────────────────
137
+
138
+ export const UPLOAD_ACTIVE_STATUSES: DocumentRequestStatus[] = ['pending', 'replacement_requested'];
139
+
140
+ export const DOCUMENT_REQUEST_STATUS_LABEL: Record<DocumentRequestStatus, DocumentRequestStatusInfo> = {
141
+ pending: { labelKey: 'docStatusPending', color: STATUS_COLORS.warning.color },
142
+ under_review: { labelKey: 'docStatusUnderReview', color: STATUS_COLORS.info.color },
143
+ approved: { labelKey: 'docStatusApproved', color: STATUS_COLORS.success.color },
144
+ rejected: { labelKey: 'docStatusRejected', color: STATUS_COLORS.error.color },
145
+ replacement_requested: { labelKey: 'docStatusReplacementRequested', color: STATUS_COLORS.warning.color },
146
+ };
147
+
103
148
  // Activity columns constants
104
149
  export const ACTIVITY_COLUMN_SIZES = {
105
150
  TITLE: 220,
@@ -62,7 +62,7 @@ i18n
62
62
  order: ['sessionStorage', 'navigator', 'htmlTag'],
63
63
  caches: ['sessionStorage'],
64
64
  lookupSessionStorage: 'i18nextLng',
65
- convertDetectedLanguage: (lng) => {
65
+ convertDetectedLanguage: (lng: string) => {
66
66
  // Extract base language code (e.g. 'en-US' -> 'en') so it matches
67
67
  // our supported language keys, but preserve script subtags like 'mni-Mtei'.
68
68
  const base = lng.split('-')[0];