@redacto.io/consent-sdk-react 9.1.0 → 9.2.0-beta.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 (45) hide show
  1. package/.turbo/turbo-build.log +10 -14
  2. package/CHANGELOG.md +6 -0
  3. package/dist/index.js +2 -4
  4. package/dist/index.mjs +2 -4
  5. package/dist/privacy-center.js +860 -851
  6. package/dist/privacy-center.mjs +810 -801
  7. package/package.json +1 -1
  8. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +2 -4
  9. package/src/RedactoNoticeConsent/api/types.ts +1 -0
  10. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +3 -1
  11. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +19 -13
  12. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +25 -0
  13. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +39 -30
  14. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +2 -0
  15. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +10 -3
  16. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +21 -6
  17. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +12 -0
  18. package/src/RedactoPrivacyCenter/components/Form.tsx +15 -129
  19. package/src/RedactoPrivacyCenter/lib/constants.ts +12 -0
  20. package/src/RedactoPrivacyCenter/lib/types.ts +10 -0
  21. package/src/RedactoPrivacyCenter/locales/as/translation.json +1 -0
  22. package/src/RedactoPrivacyCenter/locales/bho/translation.json +1 -0
  23. package/src/RedactoPrivacyCenter/locales/bn/translation.json +1 -0
  24. package/src/RedactoPrivacyCenter/locales/brx/translation.json +1 -0
  25. package/src/RedactoPrivacyCenter/locales/doi/translation.json +1 -0
  26. package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
  27. package/src/RedactoPrivacyCenter/locales/gom/translation.json +1 -0
  28. package/src/RedactoPrivacyCenter/locales/gu/translation.json +1 -0
  29. package/src/RedactoPrivacyCenter/locales/hi/translation.json +1 -0
  30. package/src/RedactoPrivacyCenter/locales/kn/translation.json +1 -0
  31. package/src/RedactoPrivacyCenter/locales/ks/translation.json +1 -0
  32. package/src/RedactoPrivacyCenter/locales/mai/translation.json +1 -0
  33. package/src/RedactoPrivacyCenter/locales/ml/translation.json +1 -0
  34. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +1 -0
  35. package/src/RedactoPrivacyCenter/locales/mr/translation.json +1 -0
  36. package/src/RedactoPrivacyCenter/locales/ne/translation.json +1 -0
  37. package/src/RedactoPrivacyCenter/locales/or/translation.json +1 -0
  38. package/src/RedactoPrivacyCenter/locales/pa/translation.json +1 -0
  39. package/src/RedactoPrivacyCenter/locales/sa/translation.json +1 -0
  40. package/src/RedactoPrivacyCenter/locales/sat/translation.json +1 -0
  41. package/src/RedactoPrivacyCenter/locales/sd/translation.json +1 -0
  42. package/src/RedactoPrivacyCenter/locales/ta/translation.json +1 -0
  43. package/src/RedactoPrivacyCenter/locales/te/translation.json +1 -0
  44. package/src/RedactoPrivacyCenter/locales/ur/translation.json +1 -0
  45. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +3 -1
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.0",
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
  )}
@@ -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
 
@@ -145,6 +145,18 @@ export default function PurposeSelectionSection({
145
145
  ? t('whatDataToAccess')
146
146
  : t('whatDataToDelete')}
147
147
  </p>
148
+ <p style={{
149
+ marginTop: '0.25rem',
150
+ marginBottom: '0.5rem',
151
+ paddingLeft: getVar('--pc-spacing-sm'),
152
+ borderLeft: `2px solid ${getVar('--pc-primary')}`,
153
+ fontSize: pcStyles.textXs.fontSize,
154
+ color: getVar('--pc-text-secondary'),
155
+ lineHeight: 1.4,
156
+ }}>
157
+ {t('revocationNotDeletionNote') ||
158
+ 'Revoking consent does not delete data the organization already holds. You can still request access to or erasure of this data, including for purposes whose consent you have withdrawn.'}
159
+ </p>
148
160
  {!isErasure && (
149
161
  <PCCheckbox
150
162
  label={t('selectAll')}
@@ -9,7 +9,6 @@ import { AiOutlineClose } from 'react-icons/ai';
9
9
  import { PiUploadBold } from 'react-icons/pi';
10
10
  import { toast } from '../utils/toast';
11
11
  import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCTextarea } from '../ui';
12
- import { PCModal } from '../ui/PCModal';
13
12
  import { pcStyles } from '../styles/pcStyles';
14
13
  import { createCaseRequest, getFormData } from '../lib/api';
15
14
  import { ensureValidToken } from '../lib/utils';
@@ -41,8 +40,7 @@ import NominationDataSection from './DataRequest/NominationDataSection';
41
40
  import PurposeSelectionSection from './DataRequest/PurposeSelectionSection';
42
41
  import RequestSubmit from './RequestSubmit';
43
42
  import type { CaseHistoryProps } from './types';
44
- import { FiAlertCircle, FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
45
- import { MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL } from './constant';
43
+ import { FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
46
44
 
47
45
  const getVar = (varName: string): string => `var(${varName})`;
48
46
 
@@ -134,10 +132,6 @@ export default function Form({
134
132
  const [isTranslating, setIsTranslating] = useState(false);
135
133
  const [isRequestDetailsExpanded, setIsRequestDetailsExpanded] =
136
134
  useState(true);
137
- const [isErasureConsentModalOpen, setIsErasureConsentModalOpen] =
138
- useState(false);
139
- const [revokeConsentOnFulfilment, setRevokeConsentOnFulfilment] =
140
- useState(false);
141
135
 
142
136
  const resetFileInput = () => {
143
137
  if (fileInputRef.current) {
@@ -164,11 +158,13 @@ export default function Form({
164
158
 
165
159
  const uniqueElements = new Map();
166
160
  extractedPurposes.forEach((purpose) => {
167
- // Ensure data_elements is an array and filter out any null/undefined elements
168
- // Only include data elements with selected === true
161
+ // Ensure data_elements is an array and filter out any null/undefined elements.
162
+ // Include every data element the org holds for this user, regardless of current
163
+ // consent state. Revoking consent does not delete data (TECH-4089), so purposes
164
+ // with revoked/withdrawn consent must remain raisable for access/erasure/correction.
169
165
  const validDataElements = Array.isArray(purpose.data_elements)
170
166
  ? purpose.data_elements.filter(
171
- (element) => element !== null && element !== undefined && element.uuid && element.selected
167
+ (element) => element !== null && element !== undefined && element.uuid
172
168
  )
173
169
  : [];
174
170
 
@@ -188,13 +184,14 @@ export default function Form({
188
184
  Array.from(uniqueElements.values()) as DataElement[]
189
185
  );
190
186
 
191
- // Ensure data_elements are properly preserved with all properties
192
- // Only include data elements with selected === true, and filter out purposes with no data elements
187
+ // Ensure data_elements are properly preserved with all properties.
188
+ // Include data elements regardless of consent state (revocation ≠ deletion,
189
+ // TECH-4089); only drop purposes that genuinely have no data elements.
193
190
  const processedPurposes = extractedPurposes
194
191
  .map((p) => {
195
192
  const validDataElements = Array.isArray(p.data_elements)
196
193
  ? p.data_elements
197
- .filter((de) => de !== null && de !== undefined && de.selected)
194
+ .filter((de) => de !== null && de !== undefined && de.uuid)
198
195
  .map((de) => ({
199
196
  ...de,
200
197
  selected: false,
@@ -321,13 +318,14 @@ export default function Form({
321
318
  .map((p) => p?.purpose)
322
319
  .filter((p) => p !== null && p !== undefined);
323
320
 
324
- // Ensure data_elements are properly preserved with all properties
325
- // Only include data elements with selected === true, and filter out purposes with no data elements
321
+ // Ensure data_elements are properly preserved with all properties.
322
+ // Include data elements regardless of consent state (revocation ≠ deletion,
323
+ // TECH-4089); only drop purposes that genuinely have no data elements.
326
324
  const processedPurposes = extractedPurposes
327
325
  .map((p) => {
328
326
  const validDataElements = Array.isArray(p.data_elements)
329
327
  ? p.data_elements
330
- .filter((de) => de !== null && de !== undefined && de.selected)
328
+ .filter((de) => de !== null && de !== undefined && de.uuid)
331
329
  .map((de) => ({
332
330
  ...de,
333
331
  selected: false,
@@ -505,47 +503,13 @@ export default function Form({
505
503
  return translatedName;
506
504
  }, [data]);
507
505
 
508
- const selectedErasurePurposeList = useMemo(
509
- () =>
510
- erasurePurposes
511
- .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
512
- .map((p) => p.name)
513
- .filter(Boolean),
514
- [erasurePurposes]
515
- );
516
-
517
- const selectedErasurePurposeNames = useMemo(() => {
518
- if (
519
- selectedErasurePurposeList.length === 0 ||
520
- selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL
521
- ) {
522
- return t('revokeConsentModalYourPurposes');
523
- }
524
- return selectedErasurePurposeList.join(', ');
525
- }, [selectedErasurePurposeList, t]);
526
-
527
506
  const handleSubmitClick = () => {
528
- if (!isFormValid){
529
- return
530
- };
531
- if (requestType === RequestTypeEnum.ERASURE) {
532
- setIsErasureConsentModalOpen(true);
507
+ if (!isFormValid) {
533
508
  return;
534
509
  }
535
- setRevokeConsentOnFulfilment(false);
536
510
  void handleSubmit(false);
537
511
  };
538
512
 
539
- const handleErasureModalChoice = (revoke: boolean) => {
540
- setRevokeConsentOnFulfilment(revoke);
541
- setIsErasureConsentModalOpen(false);
542
- void handleSubmit(revoke);
543
- };
544
-
545
- const handleErasureModalClose = () => {
546
- setIsErasureConsentModalOpen(false);
547
- };
548
-
549
513
  const handleSubmit = async (revokeConsent: boolean = false) => {
550
514
  if (!isFormValid){
551
515
  return
@@ -923,8 +887,6 @@ export default function Form({
923
887
  caseId={caseId || ''}
924
888
  contact={formData?.contact}
925
889
  onCreateRequest={onCreateRequest}
926
- revokeConsentOnFulfilment={revokeConsentOnFulfilment}
927
- revokedPurposeNames={selectedErasurePurposeNames}
928
890
  />
929
891
  ) : (
930
892
  <div>
@@ -1511,82 +1473,6 @@ export default function Form({
1511
1473
  </div>
1512
1474
  </div>
1513
1475
  )}
1514
- <PCModal
1515
- isOpen={isErasureConsentModalOpen}
1516
- onClose={handleErasureModalClose}
1517
- size="md"
1518
- showCloseButton={false}
1519
- footer={
1520
- <div className="pc-erasure-modal-footer">
1521
- <PCButton
1522
- variant="secondary"
1523
- onClick={() => handleErasureModalChoice(false)}
1524
- disabled={isLoading}
1525
- >
1526
- {t('revokeConsentNo')}
1527
- </PCButton>
1528
- <PCButton
1529
- variant="primary"
1530
- onClick={() => handleErasureModalChoice(true)}
1531
- disabled={isLoading}
1532
- isLoading={isLoading}
1533
- >
1534
- {t('revokeConsentYes')}
1535
- </PCButton>
1536
- </div>
1537
- }
1538
- >
1539
- <div className="pc-erasure-modal-content">
1540
- <div className="pc-erasure-modal-header">
1541
- <div className="pc-erasure-modal-icon">
1542
- <FiAlertCircle size={24} />
1543
- </div>
1544
- <p className="pc-erasure-modal-title">
1545
- {t('revokeConsentModalTitle')}
1546
- </p>
1547
- <p className="pc-erasure-modal-question">
1548
- {t('revokeConsentModalQuestion')}
1549
- </p>
1550
- </div>
1551
-
1552
- <div className="pc-erasure-modal-purposes">
1553
- <p className="pc-erasure-modal-purposes-label">
1554
- {t('revokeConsentModalPurposesLabel')}
1555
- </p>
1556
- {selectedErasurePurposeList.length > 0 ? (
1557
- <>
1558
- {selectedErasurePurposeList
1559
- .slice(0, MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL)
1560
- .map((name) => (
1561
- <div key={name} className="pc-erasure-modal-purpose-item">
1562
- {name}
1563
- </div>
1564
- ))}
1565
- {selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL && (
1566
- <span className="pc-erasure-modal-purposes-placeholder">
1567
- {t('revokeConsentModalAndMore', {
1568
- count: selectedErasurePurposeList.length - MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL,
1569
- })}
1570
- </span>
1571
- )}
1572
- </>
1573
- ) : (
1574
- <span className="pc-erasure-modal-purposes-placeholder">
1575
- {t('revokeConsentModalYourPurposes')}
1576
- </span>
1577
- )}
1578
- </div>
1579
-
1580
- <div className="pc-erasure-modal-warning">
1581
- <p className="pc-erasure-modal-warning-text">
1582
- <span className="pc-erasure-modal-warning-label">
1583
- {t('revokeConsentModalIfYesLabel')}
1584
- </span>{' '}
1585
- {t('revokeConsentModalIfYesBody')}
1586
- </p>
1587
- </div>
1588
- </div>
1589
- </PCModal>
1590
1476
  </>
1591
1477
  );
1592
1478
  }
@@ -138,6 +138,18 @@ export const STATUS_COLORS = {
138
138
  infoBanner: { color: '#1d4ed8', bg: '#eff6ff', border: '#bfdbfe' },
139
139
  } as const;
140
140
 
141
+ // ─── Case statuses ────────────────────────────────────────────────────────────
142
+
143
+ export const TERMINAL_CASE_STATUSES = ['completed', 'rejected', 'declined', 'expired'] as const;
144
+
145
+ export const CASE_FINALIZED_TOOLTIP_KEY = 'caseFinalizedMessagesTooltip';
146
+ export const CASE_FINALIZED_TOOLTIP_FALLBACK = 'This case is finalized — messages can no longer be sent.';
147
+
148
+ export const useCaseFinalizedTooltip = () => {
149
+ const { t } = useTranslation();
150
+ return t(CASE_FINALIZED_TOOLTIP_KEY, CASE_FINALIZED_TOOLTIP_FALLBACK);
151
+ };
152
+
141
153
  // ─── Document request ─────────────────────────────────────────────────────────
142
154
 
143
155
  export const UPLOAD_ACTIVE_STATUSES: DocumentRequestStatus[] = ['pending', 'replacement_requested'];
@@ -509,6 +509,7 @@ export type MessageItemProps = Readonly<{
509
509
  message: CaseMessage;
510
510
  onUploadForRequest: (requestEventUuid: string) => void;
511
511
  isSubmittingForRequest: boolean;
512
+ isCaseFinalized: boolean;
512
513
  }>;
513
514
 
514
515
  export type ChatBubbleProps = Readonly<{
@@ -526,6 +527,7 @@ export type DocumentRequestCardProps = Readonly<{
526
527
  message: CaseMessage;
527
528
  onUpload: (requestEventUuid: string) => void;
528
529
  isSubmitting: boolean;
530
+ isCaseFinalized: boolean;
529
531
  }>
530
532
 
531
533
  export type StatusEventProps = Readonly<{
@@ -540,6 +542,7 @@ export type StatusEventProps = Readonly<{
540
542
 
541
543
  export type MessagesTabProps = Readonly<{
542
544
  caseUuid: string;
545
+ caseStatus: string;
543
546
  }>;
544
547
 
545
548
  export type MessageComposerProps = Readonly<{
@@ -551,6 +554,13 @@ export type MessageComposerProps = Readonly<{
551
554
  onSend: () => void;
552
555
  isSending: boolean;
553
556
  isUploading: boolean;
557
+ isCaseFinalized: boolean;
558
+ }>;
559
+
560
+ export type FinalizedTooltipWrapperProps = Readonly<{
561
+ enabled: boolean;
562
+ autoWidth?: boolean;
563
+ children: React.ReactElement;
554
564
  }>;
555
565
 
556
566
  export type CaseStatusVariant =
@@ -202,6 +202,7 @@
202
202
  "submitted": "দাখিল কৰা হৈছে",
203
203
  "responseDue": "প্ৰতিক্ৰিয়া বাকী",
204
204
  "secureMessagesNotice": "আপোনাৰ বাৰ্তাসমূহ কেৱল গোপনীয়তা দলক সুৰক্ষিতভাৱে প্ৰেৰণ কৰা হয়।",
205
+ "caseFinalizedMessagesTooltip": "এই গোচৰটো চূড়ান্ত কৰা হৈছে — এতিয়া বাৰ্তা পঠাব নোৱাৰি।",
205
206
  "noMessagesYet": "এতিয়ালৈকে কোনো বাৰ্তা নাই",
206
207
  "writeMessagePlaceholder": "গোপনীয়তা দলক বাৰ্তা লিখক...",
207
208
  "attachFile": "ফাইল সংলগ্ন কৰক",
@@ -202,6 +202,7 @@
202
202
  "submitted": "जमा भइल",
203
203
  "responseDue": "जवाब बाकी बा",
204
204
  "secureMessagesNotice": "रउआ संदेश बस गोपनीयता टीम के सुरक्षित तरीका से भेजल जाला।",
205
+ "caseFinalizedMessagesTooltip": "ई केस अंतिम हो गइल बा — अब संदेस ना भेजल जा सके।",
205
206
  "noMessagesYet": "अबहीं तक कवनो संदेश नइखे",
206
207
  "writeMessagePlaceholder": "गोपनीयता टीम के संदेश लिखीं...",
207
208
  "attachFile": "फाइल जोड़ीं",
@@ -202,6 +202,7 @@
202
202
  "submitted": "জমা দেওয়া হয়েছে",
203
203
  "responseDue": "প্রতিক্রিয়া বাকি",
204
204
  "secureMessagesNotice": "আপনার বার্তাগুলি শুধুমাত্র গোপনীয়তা দলের কাছে নিরাপদে পাঠানো হয়।",
205
+ "caseFinalizedMessagesTooltip": "এই কেসটি চূড়ান্ত করা হয়েছে — বার্তা আর পাঠানো যাবে না।",
205
206
  "noMessagesYet": "এখনো কোনো বার্তা নেই",
206
207
  "writeMessagePlaceholder": "গোপনীয়তা দলকে একটি বার্তা লিখুন...",
207
208
  "attachFile": "ফাইল সংযুক্ত করুন",
@@ -202,6 +202,7 @@
202
202
  "submitted": "दाखिल जादों",
203
203
  "responseDue": "फिननाय बाकी",
204
204
  "secureMessagesNotice": "नोंथांनि खौरांफोर सिगां गोपनीयथा हानजाखौ सुरक्षित बायदिजों दैथायदों।",
205
+ "caseFinalizedMessagesTooltip": "बे केसआ जोबनाय जायो — दानै खौरांखौ दैथाय हुनो हाया।",
205
206
  "noMessagesYet": "दानो सम कोनो खौरां गैया",
206
207
  "writeMessagePlaceholder": "गोपनीयथा हानजाखौ खौरां लिर...",
207
208
  "attachFile": "फाइल लाफाब",