@redacto.io/consent-sdk-react 9.1.0 → 9.2.0-beta.1

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 (64) hide show
  1. package/.turbo/turbo-build.log +10 -9
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.js +2 -4
  4. package/dist/index.mjs +2 -4
  5. package/dist/privacy-center.d.mts +56 -2
  6. package/dist/privacy-center.d.ts +56 -2
  7. package/dist/privacy-center.js +4840 -1874
  8. package/dist/privacy-center.mjs +5124 -2158
  9. package/package.json +1 -1
  10. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +2 -4
  11. package/src/RedactoNoticeConsent/api/types.ts +1 -0
  12. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +3 -1
  13. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  14. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
  15. package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
  16. package/src/RedactoPrivacyCenter/api/client.ts +79 -0
  17. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +19 -13
  18. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +25 -0
  19. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +39 -30
  20. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +2 -0
  21. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +10 -3
  22. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +21 -6
  23. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
  24. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +12 -0
  25. package/src/RedactoPrivacyCenter/components/Form.tsx +843 -162
  26. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
  27. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +284 -71
  28. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
  29. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
  30. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
  31. package/src/RedactoPrivacyCenter/components/types.ts +26 -2
  32. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
  33. package/src/RedactoPrivacyCenter/lib/api.ts +95 -1
  34. package/src/RedactoPrivacyCenter/lib/constants.ts +17 -0
  35. package/src/RedactoPrivacyCenter/lib/types.ts +115 -0
  36. package/src/RedactoPrivacyCenter/locales/as/translation.json +40 -1
  37. package/src/RedactoPrivacyCenter/locales/bho/translation.json +40 -1
  38. package/src/RedactoPrivacyCenter/locales/bn/translation.json +40 -1
  39. package/src/RedactoPrivacyCenter/locales/brx/translation.json +40 -1
  40. package/src/RedactoPrivacyCenter/locales/doi/translation.json +40 -1
  41. package/src/RedactoPrivacyCenter/locales/en/translation.json +46 -1
  42. package/src/RedactoPrivacyCenter/locales/gom/translation.json +40 -1
  43. package/src/RedactoPrivacyCenter/locales/gu/translation.json +40 -1
  44. package/src/RedactoPrivacyCenter/locales/hi/translation.json +40 -1
  45. package/src/RedactoPrivacyCenter/locales/kn/translation.json +40 -1
  46. package/src/RedactoPrivacyCenter/locales/ks/translation.json +40 -1
  47. package/src/RedactoPrivacyCenter/locales/mai/translation.json +40 -1
  48. package/src/RedactoPrivacyCenter/locales/ml/translation.json +40 -1
  49. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +40 -1
  50. package/src/RedactoPrivacyCenter/locales/mr/translation.json +40 -1
  51. package/src/RedactoPrivacyCenter/locales/ne/translation.json +40 -1
  52. package/src/RedactoPrivacyCenter/locales/or/translation.json +40 -1
  53. package/src/RedactoPrivacyCenter/locales/pa/translation.json +40 -1
  54. package/src/RedactoPrivacyCenter/locales/sa/translation.json +40 -1
  55. package/src/RedactoPrivacyCenter/locales/sat/translation.json +40 -1
  56. package/src/RedactoPrivacyCenter/locales/sd/translation.json +40 -1
  57. package/src/RedactoPrivacyCenter/locales/ta/translation.json +40 -1
  58. package/src/RedactoPrivacyCenter/locales/te/translation.json +40 -1
  59. package/src/RedactoPrivacyCenter/locales/ur/translation.json +40 -1
  60. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
  61. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +3 -1
  62. package/tests/Form.test.tsx +1170 -0
  63. package/tests/ProductSelectionSection.test.tsx +293 -0
  64. package/tests/RequestSubmit.test.tsx +295 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "9.1.0",
3
+ "version": "9.2.0-beta.1",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -4130,13 +4130,11 @@ export const RedactoNoticeConsent = ({
4130
4130
  content.dpo_info.dpo_text || ""
4131
4131
  )}
4132
4132
  </span>{" "}
4133
- {content.dpo_info.dpo_url && (
4133
+ {content.dpo_info.dpo_email && (
4134
4134
  <a
4135
4135
  id="dpo-dpo-link"
4136
4136
  style={{ ...styles.link, ...linkStyle }}
4137
- href={content.dpo_info.dpo_url}
4138
- target="_blank"
4139
- rel="noopener noreferrer"
4137
+ href={`mailto:${content.dpo_info.dpo_email}`}
4140
4138
  >
4141
4139
  {getTranslatedDpoText(
4142
4140
  "dpo_anchor_text",
@@ -261,6 +261,7 @@ export type DpoInfo = {
261
261
  dpo_text: string;
262
262
  dpo_anchor_text: string;
263
263
  dpo_url: string;
264
+ dpo_email?: string;
264
265
  };
265
266
 
266
267
  export type DpoInfoTranslation = {
@@ -144,7 +144,9 @@ export const PrivacyCenterCaseDetailsPage = () => {
144
144
 
145
145
  {/* Tab content */}
146
146
  <div className="pc-case-tab-content">
147
- {activeTab === 'messages' && <MessagesTab caseUuid={selectedCase.uuid} />}
147
+ {activeTab === 'messages' && (
148
+ <MessagesTab caseUuid={selectedCase.uuid} caseStatus={selectedCase.status} />
149
+ )}
148
150
  {activeTab === 'request-details' && (
149
151
  <RequestDetailsTab selectedCase={selectedCase} request_details={request_details} requestTypeLabel={requestTypeLabel} />
150
152
  )}
@@ -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 { PrivacyCenterMyReceiptPage } from './PrivacyCenterMyReceiptPage';
7
8
  import { PrivacyCenterCaseDetailsPage } from './PrivacyCenterCaseDetailsPage';
8
9
  import { NotificationsPage } from './NotificationsPage';
9
10
 
@@ -23,6 +24,8 @@ export function PrivacyCenterHomePage() {
23
24
  return <PrivacyCenterFormPage />;
24
25
  case 'activity':
25
26
  return <PrivacyCenterActivityPage />;
27
+ case 'receipts':
28
+ return <PrivacyCenterMyReceiptPage />;
26
29
  case 'case-details':
27
30
  return <PrivacyCenterCaseDetailsPage />;
28
31
  case 'notifications':
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react';
2
+ import i18n from './lib/i18n';
3
+ import ReceiptsList from './components/ReceiptsList';
4
+
5
+ export function PrivacyCenterMyReceiptPage() {
6
+ useEffect(() => {
7
+ if (!i18n.language) {
8
+ i18n.changeLanguage('en');
9
+ }
10
+ }, []);
11
+
12
+ return (
13
+ <div
14
+ style={{
15
+ width: '100%',
16
+ display: 'flex',
17
+ flexDirection: 'column',
18
+ gap: '0.5rem',
19
+ }}
20
+ >
21
+ <div style={{ paddingTop: '0.25rem' }}>
22
+ <ReceiptsList />
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -419,6 +419,72 @@ export async function fetchActivitiesServer(params: {
419
419
  }
420
420
  }
421
421
 
422
+ export async function fetchReceiptsServer(params: {
423
+ organizationId: string;
424
+ workspaceId: string;
425
+ skip?: number;
426
+ limit?: number;
427
+ notice_uuid?: string;
428
+ event_type?: string;
429
+ status?: string;
430
+ created_after?: string;
431
+ created_before?: string;
432
+ language?: string;
433
+ authToken?: string;
434
+ consentClient?: ReturnType<typeof import('./client').createConsentClient>;
435
+ }): Promise<unknown | null> {
436
+ try {
437
+ if (!params.consentClient) {
438
+ throw new Error('Consent client is required');
439
+ }
440
+ // Privacy center endpoints use JWT Bearer tokens, not session-based auth
441
+ const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
442
+
443
+ const result = await params.consentClient.getReceipts(
444
+ baseRequest,
445
+ {
446
+ skip: params.skip,
447
+ limit: params.limit,
448
+ notice_uuid: params.notice_uuid,
449
+ event_type: params.event_type,
450
+ status: params.status,
451
+ created_after: params.created_after,
452
+ created_before: params.created_before,
453
+ language: params.language,
454
+ },
455
+ params.authToken
456
+ );
457
+ return result;
458
+ } catch (error: any) {
459
+ return handleServerActionError(error, 'fetching receipts');
460
+ }
461
+ }
462
+
463
+ export async function fetchReceiptPdfServer(params: {
464
+ organizationId: string;
465
+ workspaceId: string;
466
+ receiptUuid: string;
467
+ authToken?: string;
468
+ consentClient?: ReturnType<typeof import('./client').createConsentClient>;
469
+ }): Promise<Blob | null> {
470
+ try {
471
+ if (!params.consentClient) {
472
+ throw new Error('Consent client is required');
473
+ }
474
+ const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
475
+
476
+ const result = await params.consentClient.getReceiptPdf(
477
+ baseRequest,
478
+ params.receiptUuid,
479
+ params.authToken
480
+ );
481
+ return result;
482
+ } catch (error: any) {
483
+ // Re-throw 404s (receipt doesn't exist or not yours) so the UI can show a targeted message
484
+ return handleServerActionError(error, 'downloading receipt PDF', true);
485
+ }
486
+ }
487
+
422
488
  export async function fetchCaseHistoryServer(params: {
423
489
  organizationId: string;
424
490
  workspaceId: string;
@@ -464,6 +464,85 @@ export function createConsentClient(baseUrl: string) {
464
464
  });
465
465
  },
466
466
 
467
+ /**
468
+ * Get receipts (privacy center) — consent receipts for the authenticated user
469
+ */
470
+ async getReceipts(
471
+ baseRequest: BaseRequest,
472
+ params: {
473
+ skip?: number;
474
+ limit?: number;
475
+ notice_uuid?: string;
476
+ event_type?: string;
477
+ status?: string;
478
+ created_after?: string;
479
+ created_before?: string;
480
+ language?: string;
481
+ },
482
+ authToken?: string,
483
+ options?: Omit<ApiFetchOptions, "getAuthToken">
484
+ ): Promise<unknown> {
485
+ const queryParams = new URLSearchParams();
486
+ if (params.skip !== undefined) queryParams.append("skip", params.skip.toString());
487
+ if (params.limit !== undefined) queryParams.append("limit", params.limit.toString());
488
+ if (params.notice_uuid) queryParams.append("notice_uuid", params.notice_uuid);
489
+ if (params.event_type) queryParams.append("event_type", params.event_type);
490
+ if (params.status) queryParams.append("status", params.status);
491
+ if (params.created_after) queryParams.append("created_after", params.created_after);
492
+ if (params.created_before) queryParams.append("created_before", params.created_before);
493
+ if (params.language) queryParams.append("language", params.language);
494
+ const queryString = queryParams.toString();
495
+ const url = privacyCenterUrl(baseRequest, `/receipts${queryString ? `?${queryString}` : ""}`);
496
+ return privacyCenterApiFetch<unknown>(url, {
497
+ ...options,
498
+ method: "GET",
499
+ cache: "no-store",
500
+ authToken: resolveAuthToken(authToken, baseRequest),
501
+ });
502
+ },
503
+
504
+ /**
505
+ * Download a single receipt as a PDF (privacy center).
506
+ * Returns raw PDF bytes as a Blob — the caller builds an object URL to open/save it.
507
+ * Uses a direct fetch (not privacyCenterApiFetch) because the response is binary, not JSON,
508
+ * and the Authorization header must be sent (anchor navigation wouldn't carry it).
509
+ */
510
+ async getReceiptPdf(
511
+ baseRequest: BaseRequest,
512
+ receiptUuid: string,
513
+ authToken?: string
514
+ ): Promise<Blob> {
515
+ const url = privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
516
+ const token = resolveAuthToken(authToken, baseRequest);
517
+ const requestHeaders = new Headers();
518
+ requestHeaders.set("Accept", "application/pdf");
519
+ if (token) {
520
+ const trimmedToken = token.trim();
521
+ requestHeaders.set(
522
+ "Authorization",
523
+ trimmedToken.startsWith("Bearer ") ? trimmedToken : `Bearer ${trimmedToken}`
524
+ );
525
+ }
526
+ const response = await fetch(url, { method: "GET", headers: requestHeaders });
527
+ if (!response.ok) {
528
+ let errorMessage = `Request failed with status ${response.status}`;
529
+ try {
530
+ const errorData = (await response.json()) as { detail?: unknown; error_code?: string } | null;
531
+ if (errorData?.detail) {
532
+ errorMessage =
533
+ typeof errorData.detail === "string" ? errorData.detail : JSON.stringify(errorData.detail);
534
+ } else if (errorData?.error_code) {
535
+ errorMessage = errorData.error_code;
536
+ }
537
+ } catch {
538
+ // non-JSON error body — keep default message
539
+ }
540
+ // Prepend HTTP status so callers can branch on it (e.g. 404 = receipt not found)
541
+ throw new Error(`[${response.status}] ${errorMessage}`);
542
+ }
543
+ return response.blob();
544
+ },
545
+
467
546
  /**
468
547
  * Get case history (privacy center)
469
548
  */
@@ -3,15 +3,19 @@ import { IoAttach } from 'react-icons/io5';
3
3
  import { PCButton, PCLoader } from '../../ui';
4
4
  import type { DocumentRequestCardProps } from '../../lib/types';
5
5
  import { UPLOAD_ACTIVE_STATUSES, DOCUMENT_REQUEST_STATUS_LABEL, STATUS_COLORS } from '../../lib/constants';
6
+ import { FinalizedTooltipWrapper } from './FinalizedTooltipWrapper';
6
7
 
7
8
  export const DocumentRequestCard = ({
8
9
  message,
9
10
  onUpload,
10
11
  isSubmitting,
12
+ isCaseFinalized,
11
13
  }: DocumentRequestCardProps) => {
12
14
  const { t } = useTranslation();
13
15
  const meta = message.document_request_metadata;
14
- if (!meta) return null;
16
+ if (!meta) {
17
+ return null;
18
+ }
15
19
 
16
20
  const canUpload = UPLOAD_ACTIVE_STATUSES.includes(meta.document_request_status);
17
21
  const statusInfo = DOCUMENT_REQUEST_STATUS_LABEL[meta.document_request_status];
@@ -46,18 +50,20 @@ export const DocumentRequestCard = ({
46
50
  </div>
47
51
 
48
52
  {canUpload && (
49
- <PCButton
50
- variant="outline"
51
- size="sm"
52
- disabled={isSubmitting}
53
- onClick={() => onUpload(message.uuid)}
54
- className="pc-doc-request-upload-btn"
55
- >
56
- {isSubmitting
57
- ? <PCLoader variant="spinner" size="sm" />
58
- : <IoAttach size={14} />}
59
- {t('uploadAndSubmit')}
60
- </PCButton>
53
+ <FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
54
+ <PCButton
55
+ variant="outline"
56
+ size="sm"
57
+ disabled={isSubmitting || isCaseFinalized}
58
+ onClick={isCaseFinalized ? undefined : () => onUpload(message.uuid)}
59
+ className="pc-doc-request-upload-btn"
60
+ >
61
+ {isSubmitting
62
+ ? <PCLoader variant="spinner" size="sm" />
63
+ : <IoAttach size={14} />}
64
+ {t('uploadAndSubmit')}
65
+ </PCButton>
66
+ </FinalizedTooltipWrapper>
61
67
  )}
62
68
 
63
69
  {meta.document_request_status === 'rejected' && meta.rejection_reason && (
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties } from 'react';
2
+ import { PCTooltip } from '../../ui';
3
+ import { useCaseFinalizedTooltip } from '../../lib/constants';
4
+ import type { FinalizedTooltipWrapperProps } from '../../lib/types';
5
+
6
+ const AUTO_WIDTH_TRIGGER_STYLE: CSSProperties = { width: 'auto' };
7
+
8
+ export const FinalizedTooltipWrapper = ({
9
+ enabled,
10
+ autoWidth = false,
11
+ children,
12
+ }: FinalizedTooltipWrapperProps) => {
13
+ const tooltip = useCaseFinalizedTooltip();
14
+ if (!enabled) {
15
+ return children;
16
+ }
17
+ return (
18
+ <PCTooltip
19
+ content={tooltip}
20
+ triggerStyle={autoWidth ? AUTO_WIDTH_TRIGGER_STYLE : undefined}
21
+ >
22
+ {children}
23
+ </PCTooltip>
24
+ );
25
+ };
@@ -3,6 +3,7 @@ import { IoAttach, IoSend, IoDocumentText } from 'react-icons/io5';
3
3
  import { PCButton, PCLoader } from '../../ui';
4
4
  import type { MessageComposerProps } from '../../lib/types';
5
5
  import { formatFileSize } from './utils';
6
+ import { FinalizedTooltipWrapper } from './FinalizedTooltipWrapper';
6
7
 
7
8
  export const MessageComposer = ({
8
9
  messageText,
@@ -13,6 +14,7 @@ export const MessageComposer = ({
13
14
  onSend,
14
15
  isSending,
15
16
  isUploading,
17
+ isCaseFinalized,
16
18
  }: MessageComposerProps) => {
17
19
  const { t } = useTranslation();
18
20
 
@@ -46,38 +48,45 @@ export const MessageComposer = ({
46
48
  )}
47
49
 
48
50
  <div className="pc-composer-textarea-wrap">
49
- <textarea
50
- value={messageText}
51
- onChange={(e) => onMessageTextChange(e.target.value)}
52
- onKeyDown={handleKeyDown}
53
- placeholder={t('writeMessagePlaceholder')}
54
- rows={3}
55
- className="pc-composer-textarea"
56
- />
51
+ <FinalizedTooltipWrapper enabled={isCaseFinalized}>
52
+ <textarea
53
+ value={messageText}
54
+ onChange={(e) => onMessageTextChange(e.target.value)}
55
+ onKeyDown={handleKeyDown}
56
+ placeholder={t('writeMessagePlaceholder')}
57
+ rows={3}
58
+ className="pc-composer-textarea"
59
+ disabled={isCaseFinalized}
60
+ />
61
+ </FinalizedTooltipWrapper>
57
62
  </div>
58
63
  <div className="pc-composer-actions">
59
- <PCButton
60
- variant="outline"
61
- size="sm"
62
- disabled={isUploading || isSending}
63
- onClick={onAttachClick}
64
- className="pc-composer-btn"
65
- >
66
- {isUploading
67
- ? <PCLoader variant="spinner" size="sm" />
68
- : <IoAttach size={16} />}
69
- {t('attachFile')}
70
- </PCButton>
71
- <PCButton
72
- size="sm"
73
- disabled={(!messageText.trim() && !pendingUpload) || isSending || isUploading}
74
- onClick={onSend}
75
- className="pc-composer-btn"
76
- >
77
- {isSending ? <PCLoader variant="spinner" size="sm" /> : null}
78
- {t('send')}
79
- <IoSend size={14} />
80
- </PCButton>
64
+ <FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
65
+ <PCButton
66
+ variant="outline"
67
+ size="sm"
68
+ disabled={isUploading || isSending || isCaseFinalized}
69
+ onClick={onAttachClick}
70
+ className="pc-composer-btn"
71
+ >
72
+ {isUploading
73
+ ? <PCLoader variant="spinner" size="sm" />
74
+ : <IoAttach size={16} />}
75
+ {t('attachFile')}
76
+ </PCButton>
77
+ </FinalizedTooltipWrapper>
78
+ <FinalizedTooltipWrapper enabled={isCaseFinalized} autoWidth>
79
+ <PCButton
80
+ size="sm"
81
+ disabled={(!messageText.trim() && !pendingUpload) || isSending || isUploading || isCaseFinalized}
82
+ onClick={onSend}
83
+ className="pc-composer-btn"
84
+ >
85
+ {isSending ? <PCLoader variant="spinner" size="sm" /> : null}
86
+ {t('send')}
87
+ <IoSend size={14} />
88
+ </PCButton>
89
+ </FinalizedTooltipWrapper>
81
90
  </div>
82
91
  <div className="pc-composer-footer">
83
92
  {t('messagesSecureFooter')}
@@ -13,6 +13,7 @@ export const MessageItem = ({
13
13
  message,
14
14
  onUploadForRequest,
15
15
  isSubmittingForRequest,
16
+ isCaseFinalized,
16
17
  }: MessageItemProps) => {
17
18
  const { t } = useTranslation();
18
19
  const isPrincipal = message.sender_role === 'data_principal';
@@ -37,6 +38,7 @@ export const MessageItem = ({
37
38
  message={message}
38
39
  onUpload={onUploadForRequest}
39
40
  isSubmitting={isSubmittingForRequest}
41
+ isCaseFinalized={isCaseFinalized}
40
42
  />
41
43
  </>
42
44
  );
@@ -10,15 +10,18 @@ import {
10
10
  useUploadCaseDocument,
11
11
  useSubmitDocumentForRequest,
12
12
  } from '../../lib/queries';
13
- import { STATUS_COLORS } from '../../lib/constants';
13
+ import { STATUS_COLORS, useCaseFinalizedTooltip } from '../../lib/constants';
14
14
  import type { MessagesTabProps } from '../../lib/types';
15
15
  import { MessageItem } from './MessageItem';
16
16
  import { MessageComposer } from './MessageComposer';
17
17
  import { useMessagePolling } from './useMessagePolling';
18
18
  import { useFileUpload } from './useFileUpload';
19
+ import { isTerminalCaseStatus } from './utils';
19
20
 
20
- export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
21
+ export const MessagesTab = ({ caseUuid, caseStatus }: MessagesTabProps) => {
22
+ const isCaseFinalized = isTerminalCaseStatus(caseStatus);
21
23
  const { t } = useTranslation();
24
+ const caseFinalizedTooltip = useCaseFinalizedTooltip();
22
25
  const { organization } = useOrganization();
23
26
  const { accessToken, refreshToken, onError, updateTokens } = useAuth();
24
27
  const authContext = useMemo(
@@ -83,7 +86,9 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
83
86
  }}
84
87
  >
85
88
  <span>🔒</span>
86
- <span className="pc-messages-notice-text">{t('secureMessagesNotice')}</span>
89
+ <span className="pc-messages-notice-text">
90
+ {isCaseFinalized ? caseFinalizedTooltip : t('secureMessagesNotice')}
91
+ </span>
87
92
  <button
88
93
  type="button"
89
94
  onClick={() => refetch()}
@@ -128,6 +133,7 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
128
133
  message={msg}
129
134
  onUploadForRequest={triggerUploadForRequest}
130
135
  isSubmittingForRequest={submittingForRequest === msg.uuid}
136
+ isCaseFinalized={isCaseFinalized}
131
137
  />
132
138
  ))}
133
139
  </div>
@@ -141,6 +147,7 @@ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
141
147
  onSend={handleSend}
142
148
  isSending={isSending}
143
149
  isUploading={isUploading}
150
+ isCaseFinalized={isCaseFinalized}
144
151
  />
145
152
  </div>
146
153
  );
@@ -1,14 +1,29 @@
1
- import { MAX_FILE_SIZE, ALLOWED_MIME_TYPES } from '../../lib/constants';
1
+ import { MAX_FILE_SIZE, ALLOWED_MIME_TYPES, TERMINAL_CASE_STATUSES } from '../../lib/constants';
2
2
  import type { CaseStatusVariant } from '../../lib/types';
3
3
 
4
+ export const isTerminalCaseStatus = (status: string | undefined | null): boolean => {
5
+ if (!status) {
6
+ return false;
7
+ }
8
+ return (TERMINAL_CASE_STATUSES as readonly string[]).includes(status.toLowerCase());
9
+ };
10
+
4
11
  export const getVar = (v: string) => `var(${v})`;
5
12
 
6
13
  export const getCaseStatusVariant = (status: string): CaseStatusVariant => {
7
- const n = status.toLowerCase().replace(/_/g, '');
8
- if (n === 'completed') return 'success';
9
- if (n === 'rejected' || n === 'declined') return 'error';
10
- if (n === 'processing' || n === 'inprogress' || n === 'expired') return 'warning';
11
- if (n === 'initiated') return 'info';
14
+ const normalized = status.toLowerCase().replace(/_/g, '');
15
+ if (normalized === 'completed') {
16
+ return 'success';
17
+ }
18
+ if (normalized === 'rejected' || normalized === 'declined') {
19
+ return 'error';
20
+ }
21
+ if (normalized === 'processing' || normalized === 'inprogress' || normalized === 'expired') {
22
+ return 'warning';
23
+ }
24
+ if (normalized === 'initiated') {
25
+ return 'info';
26
+ }
12
27
  return 'secondary';
13
28
  };
14
29