@redacto.io/consent-sdk-react 9.2.0-beta.0 → 9.2.0-beta.2

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 (51) hide show
  1. package/.turbo/turbo-build.log +15 -10
  2. package/CHANGELOG.md +12 -0
  3. package/dist/privacy-center.d.mts +51 -1
  4. package/dist/privacy-center.d.ts +51 -1
  5. package/dist/privacy-center.js +3463 -1148
  6. package/dist/privacy-center.mjs +3399 -1084
  7. package/package.json +1 -1
  8. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  9. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
  10. package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
  11. package/src/RedactoPrivacyCenter/api/client.ts +79 -0
  12. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
  13. package/src/RedactoPrivacyCenter/components/Form.tsx +402 -259
  14. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
  15. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +95 -57
  16. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
  17. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
  18. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
  19. package/src/RedactoPrivacyCenter/components/types.ts +20 -2
  20. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
  21. package/src/RedactoPrivacyCenter/lib/api.ts +67 -8
  22. package/src/RedactoPrivacyCenter/lib/constants.ts +5 -0
  23. package/src/RedactoPrivacyCenter/lib/types.ts +74 -0
  24. package/src/RedactoPrivacyCenter/locales/as/translation.json +39 -1
  25. package/src/RedactoPrivacyCenter/locales/bho/translation.json +39 -1
  26. package/src/RedactoPrivacyCenter/locales/bn/translation.json +39 -1
  27. package/src/RedactoPrivacyCenter/locales/brx/translation.json +39 -1
  28. package/src/RedactoPrivacyCenter/locales/doi/translation.json +39 -1
  29. package/src/RedactoPrivacyCenter/locales/en/translation.json +44 -1
  30. package/src/RedactoPrivacyCenter/locales/gom/translation.json +39 -1
  31. package/src/RedactoPrivacyCenter/locales/gu/translation.json +39 -1
  32. package/src/RedactoPrivacyCenter/locales/hi/translation.json +39 -1
  33. package/src/RedactoPrivacyCenter/locales/kn/translation.json +39 -1
  34. package/src/RedactoPrivacyCenter/locales/ks/translation.json +39 -1
  35. package/src/RedactoPrivacyCenter/locales/mai/translation.json +39 -1
  36. package/src/RedactoPrivacyCenter/locales/ml/translation.json +39 -1
  37. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +39 -1
  38. package/src/RedactoPrivacyCenter/locales/mr/translation.json +39 -1
  39. package/src/RedactoPrivacyCenter/locales/ne/translation.json +39 -1
  40. package/src/RedactoPrivacyCenter/locales/or/translation.json +39 -1
  41. package/src/RedactoPrivacyCenter/locales/pa/translation.json +39 -1
  42. package/src/RedactoPrivacyCenter/locales/sa/translation.json +39 -1
  43. package/src/RedactoPrivacyCenter/locales/sat/translation.json +39 -1
  44. package/src/RedactoPrivacyCenter/locales/sd/translation.json +39 -1
  45. package/src/RedactoPrivacyCenter/locales/ta/translation.json +39 -1
  46. package/src/RedactoPrivacyCenter/locales/te/translation.json +39 -1
  47. package/src/RedactoPrivacyCenter/locales/ur/translation.json +39 -1
  48. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
  49. package/tests/Form.test.tsx +1144 -0
  50. package/tests/ProductSelectionSection.test.tsx +328 -0
  51. package/tests/RequestSubmit.test.tsx +135 -0
@@ -1,4 +1,4 @@
1
- import { Activity, MessageCircleWarning, ShieldCheck } from 'lucide-react';
1
+ import { Activity, MessageCircleWarning, ReceiptText, ShieldCheck } from 'lucide-react';
2
2
  import type { MenuItem } from './types';
3
3
 
4
4
  export const menuItemsPC: MenuItem[] = [
@@ -17,4 +17,9 @@ export const menuItemsPC: MenuItem[] = [
17
17
  href: 'activity',
18
18
  icon: <Activity size={18} strokeWidth={1.75} />,
19
19
  },
20
+ {
21
+ name: 'My Receipt',
22
+ href: 'receipts',
23
+ icon: <ReceiptText size={18} strokeWidth={1.75} />,
24
+ },
20
25
  ];
@@ -0,0 +1,56 @@
1
+ import type { DataElement, FormProductGroup, Purpose } from '../lib/types';
2
+
3
+ /**
4
+ * Build the per-purpose state used by `PurposeSelectionSection` for a single
5
+ * product group. Mirrors the legacy `data.purposes`-derived shape so the
6
+ * existing checkbox component works unchanged: data-elements default to
7
+ * `selected: false` and purposes are filtered to those with at least one
8
+ * data element.
9
+ *
10
+ * Lives in its own module so both `Form.tsx` (which seeds the per-group
11
+ * state Map) and `ProductSelectionSection.tsx` (which computes the
12
+ * activeCount shown on each product card) can share the same filter —
13
+ * otherwise the card header drifts from the list-body it summarises.
14
+ */
15
+ export const buildGroupPurposes = (group: FormProductGroup): Purpose[] =>
16
+ group.purposes
17
+ .filter((p): p is Purpose => p !== null && p !== undefined)
18
+ .map((p) => {
19
+ const validDataElements = Array.isArray(p.data_elements)
20
+ ? p.data_elements
21
+ .filter((de) => de !== null && de !== undefined && Boolean(de.uuid))
22
+ .map((de) => ({ ...de, selected: false }))
23
+ : [];
24
+ return {
25
+ ...p,
26
+ selected: false,
27
+ data_elements: validDataElements,
28
+ };
29
+ })
30
+ .filter((p) => p.data_elements.length > 0);
31
+
32
+ export type GroupCorrectionDataElement = DataElement & { purposeName: string };
33
+
34
+ /**
35
+ * Build a product-scoped deduped data-element list for grouped CORRECTION.
36
+ * Dedup is done by data-element UUID within the product group.
37
+ */
38
+ export const buildGroupDataElements = (
39
+ group: FormProductGroup,
40
+ ): GroupCorrectionDataElement[] => {
41
+ const byUuid = new Map<string, GroupCorrectionDataElement>();
42
+ for (const purpose of group.purposes) {
43
+ if (!purpose) {
44
+ continue;
45
+ }
46
+ const dataElements = Array.isArray(purpose.data_elements)
47
+ ? purpose.data_elements
48
+ : [];
49
+ for (const de of dataElements) {
50
+ if (de && de.uuid && !byUuid.has(de.uuid)) {
51
+ byUuid.set(de.uuid, { ...de, purposeName: purpose.name });
52
+ }
53
+ }
54
+ }
55
+ return Array.from(byUuid.values());
56
+ };
@@ -0,0 +1,203 @@
1
+ import { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { createColumnHelper } from '@tanstack/react-table';
4
+ import dayjs from 'dayjs';
5
+ import { Download } from 'lucide-react';
6
+ import { PCText, PCButton, PCBadge } from '../ui';
7
+ import { pcStyles } from '../styles/pcStyles';
8
+ import { STATUS_COLORS } from '../lib/constants';
9
+ import { getVar } from '../lib/utils';
10
+ import type { Receipt } from '../lib/types';
11
+
12
+ const columnHelper = createColumnHelper<Receipt>();
13
+
14
+ type EventPalette = { color: string; bg: string };
15
+ type BadgeVariant = 'success' | 'error' | 'warning' | 'info';
16
+
17
+ // Map the raw event_type enum to a status palette + a fallback i18n label.
18
+ // The localized `event_type_display` from the API is preferred when present.
19
+ const EVENT_PALETTE: Record<string, EventPalette> = {
20
+ GRANTED: STATUS_COLORS.success,
21
+ WITHDRAWN: STATUS_COLORS.warning,
22
+ DECLINED: STATUS_COLORS.error,
23
+ EXPIRED: STATUS_COLORS.warning,
24
+ };
25
+
26
+ const EVENT_LABEL_KEY: Record<string, string> = {
27
+ GRANTED: 'consentGranted',
28
+ WITHDRAWN: 'consentRevoked',
29
+ DECLINED: 'consentDeclined',
30
+ EXPIRED: 'consentExpired',
31
+ };
32
+
33
+ const FALLBACK_PALETTE: EventPalette = STATUS_COLORS.info;
34
+ const EVENT_BADGE_VARIANT: Record<string, BadgeVariant> = {
35
+ GRANTED: 'success',
36
+ WITHDRAWN: 'warning',
37
+ DECLINED: 'error',
38
+ EXPIRED: 'warning',
39
+ };
40
+
41
+ // First two alphanumerics of the product name, e.g. "Website" → "WE", "Mobile App" → "MO".
42
+ function getProductInitials(name: string): string {
43
+ const cleaned = name.replace(/[^a-zA-Z0-9]/g, '');
44
+ return cleaned.slice(0, 2).toUpperCase() || '?';
45
+ }
46
+
47
+ const cellPad = { paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom };
48
+
49
+ export const useReceiptColumns = ({
50
+ onDownload,
51
+ downloadingUuid,
52
+ }: {
53
+ onDownload: (receipt: Receipt) => void;
54
+ downloadingUuid: string | null;
55
+ }) => {
56
+ const { t } = useTranslation();
57
+ return useMemo(
58
+ () => [
59
+ columnHelper.accessor('event_type', {
60
+ id: 'event',
61
+ header: t('event'),
62
+ cell: (info) => {
63
+ const row = info.row.original;
64
+ const raw = String(row.event_type ?? '').toUpperCase();
65
+ const palette = EVENT_PALETTE[raw] ?? FALLBACK_PALETTE;
66
+ const badgeVariant = EVENT_BADGE_VARIANT[raw] ?? 'info';
67
+ const labelKey = EVENT_LABEL_KEY[raw];
68
+ const label = row.event_type_display || (labelKey ? t(labelKey) : row.event_type || '—');
69
+ return (
70
+ <div style={cellPad}>
71
+ <PCBadge
72
+ variant={badgeVariant}
73
+ size="sm"
74
+ style={{
75
+ display: 'inline-flex',
76
+ alignItems: 'center',
77
+ gap: '0.4375rem',
78
+ fontWeight: 600,
79
+ whiteSpace: 'nowrap',
80
+ }}
81
+ >
82
+ <span
83
+ aria-hidden="true"
84
+ style={{
85
+ width: 7,
86
+ height: 7,
87
+ borderRadius: '50%',
88
+ backgroundColor: palette.color,
89
+ flexShrink: 0,
90
+ }}
91
+ />
92
+ {label}
93
+ </PCBadge>
94
+ </div>
95
+ );
96
+ },
97
+ enableSorting: false,
98
+ enableColumnFilter: false,
99
+ }),
100
+ columnHelper.accessor('product_name', {
101
+ id: 'product',
102
+ header: t('product'),
103
+ cell: (info) => {
104
+ const row = info.row.original;
105
+ const name = row.product_name_display || row.product_name || '—';
106
+ return (
107
+ <div style={{ ...cellPad, display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
108
+ <span
109
+ aria-hidden="true"
110
+ style={{
111
+ display: 'inline-flex',
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ width: 32,
115
+ height: 32,
116
+ borderRadius: 8,
117
+ backgroundColor: getVar('--pc-bg-tertiary'),
118
+ color: getVar('--pc-text-secondary'),
119
+ border: `1px solid ${getVar('--pc-border-color')}`,
120
+ fontSize: '0.6875rem',
121
+ fontWeight: 600,
122
+ flexShrink: 0,
123
+ }}
124
+ >
125
+ {getProductInitials(name)}
126
+ </span>
127
+ <PCText size="sm" weight="medium" color="primary">
128
+ {name}
129
+ </PCText>
130
+ </div>
131
+ );
132
+ },
133
+ enableSorting: false,
134
+ enableColumnFilter: false,
135
+ }),
136
+ columnHelper.accessor('created_at', {
137
+ id: 'date',
138
+ header: t('date'),
139
+ cell: (info) => {
140
+ const d = dayjs(info.getValue());
141
+ return (
142
+ <div style={cellPad}>
143
+ <PCText size="sm" color="primary" weight="medium">
144
+ {d.isValid() ? d.format('MMM D, YYYY') : '—'}
145
+ </PCText>
146
+ </div>
147
+ );
148
+ },
149
+ enableSorting: false,
150
+ enableColumnFilter: false,
151
+ }),
152
+ columnHelper.display({
153
+ id: 'time',
154
+ header: t('time'),
155
+ cell: (info) => {
156
+ const d = dayjs(info.row.original.created_at);
157
+ return (
158
+ <div style={cellPad}>
159
+ <PCText size="sm" color="secondary">
160
+ {d.isValid() ? d.format('h:mm A') : '—'}
161
+ </PCText>
162
+ </div>
163
+ );
164
+ },
165
+ enableSorting: false,
166
+ enableColumnFilter: false,
167
+ }),
168
+ columnHelper.display({
169
+ id: 'download',
170
+ header: t('download'),
171
+ meta: { align: 'right' },
172
+ cell: (info) => {
173
+ const row = info.row.original;
174
+ const isDownloading = downloadingUuid === row.uuid;
175
+ return (
176
+ <div style={{ ...cellPad, display: 'flex', justifyContent: 'flex-end' }}>
177
+ <PCButton
178
+ variant="outline"
179
+ size="sm"
180
+ isLoading={isDownloading}
181
+ disabled={isDownloading || !row.uuid}
182
+ onClick={() => onDownload(row)}
183
+ style={{
184
+ display: 'inline-flex',
185
+ alignItems: 'center',
186
+ gap: '0.375rem',
187
+ minWidth: '6.75rem',
188
+ justifyContent: 'center',
189
+ }}
190
+ >
191
+ {!isDownloading && <Download size={15} strokeWidth={1.9} />}
192
+ {t('download')}
193
+ </PCButton>
194
+ </div>
195
+ );
196
+ },
197
+ enableSorting: false,
198
+ enableColumnFilter: false,
199
+ }),
200
+ ],
201
+ [t, onDownload, downloadingUuid]
202
+ );
203
+ };
@@ -1,5 +1,8 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { CaseRequest, ConsentModalRecord } from '../lib/types';
2
+ import type {
3
+ CaseRequest,
4
+ ConsentModalRecord,
5
+ } from '../lib/types';
3
6
 
4
7
  export type TableColumnAlign = 'left' | 'center' | 'right';
5
8
 
@@ -26,8 +29,23 @@ export type CaseDetailsModalProps = {
26
29
  selectedCase: CaseRequest | null;
27
30
  };
28
31
 
32
+ /**
33
+ * Single-case success surface. The Form's 1-form -> 1-case contract
34
+ * means there is no fan-out and no partial-failure UI — every submit
35
+ * produces exactly one `case_id`. `product_name` / `product_uuid` are
36
+ * populated for product-scoped request types (ACCESS / ERASURE /
37
+ * CORRECTION) and empty strings for org-level types (NOMINATION /
38
+ * GRIEVANCE), in which case the success card omits the product label.
39
+ */
40
+ export type RequestSubmitResult = Readonly<{
41
+ case_id: string;
42
+ uuid: string;
43
+ product_name: string;
44
+ product_uuid: string;
45
+ }>;
46
+
29
47
  export type RequestSubmitProps = Readonly<{
30
- caseId: string;
48
+ result: RequestSubmitResult;
31
49
  contact: string;
32
50
  onCreateRequest: () => void;
33
51
  revokeConsentOnFulfilment?: boolean;
@@ -1,13 +1,13 @@
1
1
  import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
2
  import type { CaseRequest } from '../lib/types';
3
3
 
4
- export type InternalPage = 'consent-manager' | 'form' | 'activity' | 'case-details' | 'notifications';
4
+ export type InternalPage = 'consent-manager' | 'form' | 'activity' | 'case-details' | 'notifications' | 'receipts';
5
5
 
6
6
  const NAV_STORAGE_KEY = 'pc_current_page';
7
7
  const CASE_UUID_STORAGE_KEY = 'pc_selected_case_uuid';
8
8
 
9
9
  const VALID_PAGES: ReadonlySet<string> = new Set<InternalPage>([
10
- 'consent-manager', 'form', 'activity', 'case-details', 'notifications',
10
+ 'consent-manager', 'form', 'activity', 'case-details', 'notifications', 'receipts',
11
11
  ]);
12
12
 
13
13
  function getStoredPage(fallback: InternalPage): InternalPage {
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  createCaseRequestServer,
3
3
  fetchActivitiesServer,
4
+ fetchReceiptsServer,
5
+ fetchReceiptPdfServer,
4
6
  fetchCaseHistoryServer,
5
7
  fetchProductsServer,
6
8
  fetchUserConsentsServer,
@@ -36,6 +38,8 @@ import type {
36
38
  UserConsentDetail,
37
39
  UserDataRequest,
38
40
  GroupPurposesOut,
41
+ ReceiptEventType,
42
+ ReceiptPurposeStatus,
39
43
  } from './types';
40
44
  import { ensureValidToken } from './utils';
41
45
 
@@ -169,6 +173,13 @@ export const getFormData = async (
169
173
  }
170
174
  };
171
175
 
176
+ /**
177
+ * Public helper for creating a case request. Returns `null` on any
178
+ * non-auth error (preserves the documented
179
+ * `Promise<CreateCaseResponse | null>` contract the SDK exposes to
180
+ * consumers) — does NOT throw. Auth errors trigger a single token
181
+ * refresh + retry before falling through.
182
+ */
172
183
  export const createCaseRequest = async (
173
184
  organizationId: string,
174
185
  workspaceId: string,
@@ -204,14 +215,23 @@ export const createCaseRequest = async (
204
215
  if (shouldRetry) {
205
216
  // Read from sessionStorage first (synchronously updated) then fall back to context
206
217
  const newAuthToken = sessionStorage.getItem('access_token') || authContext?.accessToken || '';
207
- return await createCaseRequestServer(
208
- organizationId,
209
- workspaceId,
210
- data,
211
- newAuthToken,
212
- false,
213
- consentClient
214
- );
218
+ try {
219
+ return await createCaseRequestServer(
220
+ organizationId,
221
+ workspaceId,
222
+ data,
223
+ newAuthToken,
224
+ false,
225
+ consentClient
226
+ );
227
+ } catch (retryError) {
228
+ // The post-refresh retry can still fail (network blip, 5xx,
229
+ // expired-again token). Honor the documented `Promise<… | null>`
230
+ // contract by swallowing here rather than letting the rethrow
231
+ // escape — callers (e.g. Form.tsx) rely on null-on-error.
232
+ console.error('Error creating case request after auth refresh:', retryError);
233
+ return null;
234
+ }
215
235
  }
216
236
  console.error('Error creating case request:', error);
217
237
  return null;
@@ -622,6 +642,45 @@ export const getCaseDocument = async (
622
642
  getCaseDocumentServer({ organizationId, workspaceId, caseUuid, documentUuid, authToken: token, consentClient })
623
643
  );
624
644
 
645
+ // ─── Receipts ────────────────────────────────────────────────────────────────
646
+
647
+ export type FetchReceiptsParams = {
648
+ skip?: number;
649
+ limit?: number;
650
+ notice_uuid?: string;
651
+ event_type?: ReceiptEventType;
652
+ status?: ReceiptPurposeStatus;
653
+ created_after?: string;
654
+ created_before?: string;
655
+ language?: string;
656
+ };
657
+
658
+ export const fetchReceipts = async (
659
+ organizationId: string,
660
+ workspaceId: string,
661
+ params?: FetchReceiptsParams,
662
+ authContext?: AuthContext,
663
+ consentClient?: BaseUrlContextValue['consentClient'],
664
+ ): Promise<unknown | null> =>
665
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching receipts:', (token) =>
666
+ fetchReceiptsServer({ organizationId, workspaceId, ...params, authToken: token, consentClient })
667
+ );
668
+
669
+ /**
670
+ * Download a single receipt as a PDF Blob. The UI builds an object URL from the
671
+ * Blob to open/save it (PDF UI is a later pass; the SDK function is ready now).
672
+ */
673
+ export const fetchReceiptPdf = async (
674
+ organizationId: string,
675
+ workspaceId: string,
676
+ receiptUuid: string,
677
+ authContext?: AuthContext,
678
+ consentClient?: BaseUrlContextValue['consentClient'],
679
+ ): Promise<Blob | null> =>
680
+ withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error downloading receipt PDF:', (token) =>
681
+ fetchReceiptPdfServer({ organizationId, workspaceId, receiptUuid, authToken: token, consentClient })
682
+ );
683
+
625
684
  // ─── Notification Hub ───────────────────────────────────────────────────────
626
685
 
627
686
  export const fetchNotifications = async (
@@ -94,12 +94,17 @@ export const PAGE_CHROME_BY_PAGE: Record<InternalPage, PageChromeConfig> = {
94
94
  titleKey: 'notificationHub',
95
95
  descriptionKey: 'notificationHubSubtitle',
96
96
  },
97
+ receipts: {
98
+ titleKey: 'myReceipt',
99
+ descriptionKey: 'myReceiptSubtitle',
100
+ },
97
101
  };
98
102
 
99
103
  const HREF_TO_NAME_KEY: Record<string, string> = {
100
104
  'consent-manager': 'consentManager',
101
105
  form: 'grievance',
102
106
  activity: 'activity',
107
+ receipts: 'myReceipt',
103
108
  };
104
109
 
105
110
  export const useMenuItemsPC = (): MenuItem[] => {
@@ -87,12 +87,42 @@ export type GrievanceOptions = {
87
87
  label: string;
88
88
  };
89
89
 
90
+ /**
91
+ * Product-scoped form-data group.
92
+ *
93
+ * Phase 2 of TECH-3712. Mirrors the backend `FormProductGroupOut`. Used to
94
+ * render per-product sections inside the privacy-center form. `nominator`
95
+ * is populated on nominated groups and `null` on direct (own-data) groups.
96
+ */
97
+ export type FormProductGroup = {
98
+ product_uuid: string;
99
+ product_name: string;
100
+ product_description: string | null;
101
+ nominator: NominatorInfo | null;
102
+ // Bare `Purpose[]` — matches the backend `FormProductGroupOut.purposes`
103
+ // contract (`List[PurposeConsentFormOut]`). The legacy `FormData.purposes`
104
+ // field below remains wrapped (`RawPurposeWrapper[]`) for back-compat.
105
+ purposes: Purpose[];
106
+ };
107
+
90
108
  export type FormData = {
91
109
  uuid: string;
92
110
  name: string;
93
111
  contact: string;
94
112
  grievance_options: GrievanceOptions[];
113
+ // Legacy flat list, retained for backwards compatibility with older SDK
114
+ // consumers. The backend continues to populate this alongside the grouped
115
+ // fields below.
95
116
  purposes: RawPurposeWrapper[];
117
+ // Phase 2 of TECH-3712. Product-grouped purposes for the data principal's
118
+ // own consents. Empty array when the backend has no direct groups to emit.
119
+ // Optional so legacy backends (pre-Phase 1) typecheck — the runtime
120
+ // grouped-render guard already handles the absent case.
121
+ direct_groups?: FormProductGroup[];
122
+ // Phase 2 of TECH-3712. Product-grouped purposes the data principal can
123
+ // act on as a nominee. Empty array when there are no nominator grants.
124
+ // Optional for the same backwards-compatibility reason as `direct_groups`.
125
+ nominated_groups?: FormProductGroup[];
96
126
  };
97
127
 
98
128
  export type FormDataResponse = {
@@ -156,6 +186,13 @@ export type UserDataRequest = {
156
186
  time_period: number | null;
157
187
  requestor_note: string;
158
188
  revoke_consent_on_fulfilment?: boolean;
189
+ // Phase 2 of TECH-3712. Optional product anchor for the case. When set,
190
+ // the case is created against the (product, optional nominator) consent
191
+ // group; when omitted, the case is created as a legacy org-level case.
192
+ product_uuid?: string;
193
+ // Phase 2 of TECH-3712. Optional data-principal anchor used when the
194
+ // requestor is acting on behalf of a nominator. Omit for direct cases.
195
+ data_principal_uuid?: string;
159
196
  };
160
197
 
161
198
  export type CreateCaseResponse = {
@@ -350,6 +387,29 @@ export type ActivityDetail = {
350
387
  pagination: Pagination;
351
388
  };
352
389
 
390
+ // ─── Receipts ────────────────────────────────────────────────────────────────
391
+
392
+ export type ReceiptEventType = 'GRANTED' | 'WITHDRAWN' | 'DECLINED' | 'EXPIRED';
393
+ export type ReceiptPurposeStatus = 'CONSENTED' | 'DECLINED' | 'WITHDRAWN';
394
+
395
+ /**
396
+ * A single consent receipt row. The backend returns localized `*_display`
397
+ * fields alongside the raw enum/value — render the `*_display`, branch on the raw.
398
+ */
399
+ export type Receipt = {
400
+ uuid: string;
401
+ event_type: ReceiptEventType | string;
402
+ event_type_display?: string | null;
403
+ product_name?: string | null;
404
+ product_name_display?: string | null;
405
+ created_at: string;
406
+ };
407
+
408
+ export type ReceiptListResponse = {
409
+ data: Receipt[];
410
+ pagination: Pagination;
411
+ };
412
+
353
413
  export type ActivityResponse = {
354
414
  code: number;
355
415
  status: string;
@@ -383,6 +443,20 @@ export type CaseRequest = {
383
443
  completed_at?: string;
384
444
  due_date?: string;
385
445
  request_details?: CaseRequestDetails;
446
+ // Phase 2 of TECH-3712. Product anchor (when the case was created against
447
+ // a product consent group). Undefined for legacy org-level cases.
448
+ product_uuid?: string;
449
+ // Phase 2 of TECH-3712. Product display name hydrated by the console list
450
+ // endpoint. Undefined on legacy cases or when the product is not joinable.
451
+ product_name?: string;
452
+ // Phase 2 of TECH-3712. Data-principal anchor for nominee-driven cases.
453
+ data_principal_uuid?: string;
454
+ // Phase 2 of TECH-3712. UUID of the user who initiated the case (may be
455
+ // the data principal themselves or a nominee acting on their behalf).
456
+ requestor_uuid?: string;
457
+ // Phase 2 of TECH-3712. UUID of the nominator whose grant authorised the
458
+ // nominee to file this case. Undefined for direct cases.
459
+ nominator_uuid?: string;
386
460
  };
387
461
 
388
462
  export type RequestDetailsTabProps = {
@@ -1,4 +1,25 @@
1
1
  {
2
+ "consented": "সন্মতি দিয়া হ’ল",
3
+ "declined": "প্ৰত্যাখ্যান কৰা হ’ল",
4
+ "withdrawn": "প্ৰত্যাহাৰ কৰা হ’ল",
5
+ "dateRange": "তাৰিখৰ পৰিসৰ",
6
+ "allTime": "সকলো সময়",
7
+ "customRange": "নিজস্ব পৰিসৰ",
8
+ "from": "পৰা",
9
+ "to": "লৈ",
10
+ "clearFilters": "ফিল্টাৰ পৰিষ্কাৰ কৰক",
11
+ "myReceipt": "মোৰ ৰচিদ",
12
+ "myReceiptSubtitle": "আপোনাৰ সন্মতিৰ ৰচিদ চাওক",
13
+ "event": "ঘটনা",
14
+ "date": "তাৰিখ",
15
+ "time": "সময়",
16
+ "download": "ডাউনল'ড কৰক",
17
+ "consentGranted": "সন্মতি দিয়া হ’ল",
18
+ "consentRevoked": "সন্মতি প্ৰত্যাহাৰ কৰা হ’ল",
19
+ "consentDeclined": "সন্মতি প্ৰত্যাখ্যান কৰা হ’ল",
20
+ "consentExpired": "সন্মতিৰ ম্যাদ উকলিল",
21
+ "failedToFetchReceipts": "ৰচিদ আনিবলৈ ব্যৰ্থ",
22
+ "failedToDownloadReceipt": "ৰচিদ ডাউনল'ড কৰিবলৈ ব্যৰ্থ",
2
23
  "yourPrivacyCenter": "আপোনাৰ গোপনীয়তা কেন্দ্ৰ",
3
24
  "yourRightsOverDataSimplified": "আপোনাৰ ডেটাত আপোনাৰ অধিকাৰ, সহজ ভাষাত",
4
25
  "description": "গোপনীয়তা আইনে আপোনাক আপোনাৰ তথ্য সংগ্ৰহ, ব্যৱহাৰ, শ্বেয়াৰ আৰু সংৰক্ষণ সন্দর্ভত কিছুমান অধিকাৰ প্ৰদান কৰে আৰু আমি সেই অধিকাৰ ব্যৱহাৰৰ বাবে আপোনাক স্বাগতম জনাইছো। অনুগ্ৰহ কৰি তলৰ ফৰ্মটো পূৰণ কৰক যাতে আমি আপোনাৰ অনুৰোধৰ প্ৰমাণীকৰণ কৰি প্ৰসেছ কৰিব পাৰোঁ। অধিক তথ্যৰ বাবে অনুগ্ৰহ কৰি আমাৰ গোপনীয়তা নীতি পঢ়ক।",
@@ -288,5 +309,22 @@
288
309
  "withdrawnWithCount": "{{count}} withdrawn",
289
310
  "expiredWithCount": "{{count}} expired",
290
311
  "declinedWithCount": "{{count}} declined",
291
- "productsAndConsentsSummary": "{{products}} products, {{consents}} consents"
312
+ "productsAndConsentsSummary": "{{products}} products, {{consents}} consents",
313
+ "selectProductsForRequest": "Select one or more products for your {{requestType}} request",
314
+ "selectProductsForRequestGeneric": "Select one or more products to continue",
315
+ "yourData": "Your data",
316
+ "actingOnBehalfOf": "Acting on behalf of {{nominator}}",
317
+ "activePurposes_one": "{{count}} উদ্দেশ্য",
318
+ "activePurposes_other": "{{count}} উদ্দেশ্য",
319
+ "autoSelected": "Auto-selected",
320
+ "noProductsAvailable": "No products available",
321
+ "noProductsAvailableDescription": "You have no consents to manage yet.",
322
+ "forProduct": "For {{productName}}",
323
+ "actingOnBehalfOfShort": "acting on behalf of {{nominator}}",
324
+ "selectPurposesForThisProduct": "Select purposes within this product.",
325
+ "casesCreatedCount_one": "{{count}} case created",
326
+ "casesCreatedCount_other": "{{count}} cases created",
327
+ "someRequestsFailed": "Some requests failed",
328
+ "retry": "Retry",
329
+ "retrying": "Retrying..."
292
330
  }
@@ -1,4 +1,25 @@
1
1
  {
2
+ "consented": "सहमति दिहल गइल",
3
+ "declined": "अस्वीकार कइल गइल",
4
+ "withdrawn": "वापस लिहल गइल",
5
+ "dateRange": "तारीख सीमा",
6
+ "allTime": "सब समय",
7
+ "customRange": "कस्टम सीमा",
8
+ "from": "से",
9
+ "to": "तक",
10
+ "clearFilters": "फिल्टर साफ करीं",
11
+ "myReceipt": "हमार रसीद",
12
+ "myReceiptSubtitle": "आपन सहमति के रसीद देखीं",
13
+ "event": "घटना",
14
+ "date": "तारीख",
15
+ "time": "समय",
16
+ "download": "डाउनलोड करीं",
17
+ "consentGranted": "सहमति दिहल गइल",
18
+ "consentRevoked": "सहमति वापस लिहल गइल",
19
+ "consentDeclined": "सहमति अस्वीकार कइल गइल",
20
+ "consentExpired": "सहमति के अवधि खतम",
21
+ "failedToFetchReceipts": "रसीद पावे में विफल",
22
+ "failedToDownloadReceipt": "रसीद डाउनलोड करे में विफल",
2
23
  "yourPrivacyCenter": "रउरा गोपनीयता केंद्र",
3
24
  "yourRightsOverDataSimplified": "रउरा डेटा पर अधिकार, सरल रूप में",
4
25
  "description": "एप्लिकेशन गोपनीयता कानून रउरा के कुछ अधिकार देला जवन हमनी के द्वारा रउरा जानकारी के संग्रह, उपयोग, साझा करे आ संग्रह करे से जुड़ल बा, आ हम रउरा के ई अधिकार के प्रयोग करे के स्वागत करिला। कृपया नीचे दिहल फॉर्म भर दीं ताकि हम रउरा अनुरोध के सत्यापित आ पूरा कर सकीं। अधिक जानकारी खातिर, कृपया हमार आंतरिक गोपनीयता नीति पढ़ीं।",
@@ -288,5 +309,22 @@
288
309
  "withdrawnWithCount": "{{count}} withdrawn",
289
310
  "expiredWithCount": "{{count}} expired",
290
311
  "declinedWithCount": "{{count}} declined",
291
- "productsAndConsentsSummary": "{{products}} products, {{consents}} consents"
312
+ "productsAndConsentsSummary": "{{products}} products, {{consents}} consents",
313
+ "selectProductsForRequest": "Select one or more products for your {{requestType}} request",
314
+ "selectProductsForRequestGeneric": "Select one or more products to continue",
315
+ "yourData": "Your data",
316
+ "actingOnBehalfOf": "Acting on behalf of {{nominator}}",
317
+ "activePurposes_one": "{{count}} उद्देश्य",
318
+ "activePurposes_other": "{{count}} उद्देश्य",
319
+ "autoSelected": "Auto-selected",
320
+ "noProductsAvailable": "No products available",
321
+ "noProductsAvailableDescription": "You have no consents to manage yet.",
322
+ "forProduct": "For {{productName}}",
323
+ "actingOnBehalfOfShort": "acting on behalf of {{nominator}}",
324
+ "selectPurposesForThisProduct": "Select purposes within this product.",
325
+ "casesCreatedCount_one": "{{count}} case created",
326
+ "casesCreatedCount_other": "{{count}} cases created",
327
+ "someRequestsFailed": "Some requests failed",
328
+ "retry": "Retry",
329
+ "retrying": "Retrying..."
292
330
  }