@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.
- package/.turbo/turbo-build.log +15 -10
- package/CHANGELOG.md +12 -0
- package/dist/privacy-center.d.mts +51 -1
- package/dist/privacy-center.d.ts +51 -1
- package/dist/privacy-center.js +3463 -1148
- package/dist/privacy-center.mjs +3399 -1084
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
- package/src/RedactoPrivacyCenter/api/client.ts +79 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +402 -259
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +95 -57
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
- package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
- package/src/RedactoPrivacyCenter/components/types.ts +20 -2
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/lib/api.ts +67 -8
- package/src/RedactoPrivacyCenter/lib/constants.ts +5 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +74 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +44 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +39 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
- package/tests/Form.test.tsx +1144 -0
- package/tests/ProductSelectionSection.test.tsx +328 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
}
|