@redacto.io/consent-sdk-react 9.5.0-beta.0 → 10.0.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.
- package/.turbo/turbo-build.log +22 -25
- package/CHANGELOG.md +16 -2
- package/dist/index.d.mts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +116 -11
- package/dist/index.mjs +116 -11
- package/dist/privacy-center.d.mts +76 -19
- package/dist/privacy-center.d.ts +76 -19
- package/dist/privacy-center.js +2022 -1378
- package/dist/privacy-center.mjs +1946 -1297
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -3
- package/src/RedactoNoticeConsent/api/index.ts +130 -1
- package/src/RedactoNoticeConsent/api/types.ts +33 -0
- package/src/RedactoNoticeConsent/types.ts +1 -0
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +4 -1
- package/src/RedactoNoticeConsentInline/api/types.ts +1 -0
- package/src/RedactoNoticeConsentInline/types.ts +1 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
- package/src/RedactoPrivacyCenter/api/actions.ts +15 -2
- package/src/RedactoPrivacyCenter/api/client.ts +146 -20
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +63 -7
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +83 -0
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +193 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
- package/src/RedactoPrivacyCenter/context/types.ts +1 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +8 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +9 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +63 -1
- package/src/RedactoPrivacyCenter/locales/as/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +7 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +105 -0
- package/tests/Form.test.tsx +58 -7
- package/tests/consentClient.test.ts +171 -0
- package/tests/manageConsentServer.test.ts +38 -0
- package/tests/mocks.ts +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { useOrganization } from '../context/OrganizationContext';
|
|
10
|
+
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
11
|
+
import { useAuth } from '../context/AuthContext';
|
|
12
|
+
import { ProfileSwitchProvider } from '../context/ProfileSwitchContext';
|
|
13
|
+
import { PCLoader } from '../ui';
|
|
14
|
+
import { decodeJwtToken } from '../lib/utils';
|
|
15
|
+
import type {
|
|
16
|
+
GatePhase,
|
|
17
|
+
IdentityCandidate,
|
|
18
|
+
SelectUserGateProps,
|
|
19
|
+
} from '../lib/types';
|
|
20
|
+
import { PICKED_ORG_USER_KEY } from './constant';
|
|
21
|
+
import { SelectUser } from './SelectUser';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shared-contact profile gate. On mount it asks the backend which principals
|
|
25
|
+
* share the signed-in contact; when more than one exists (and the session isn't
|
|
26
|
+
* already pinned to a chosen one), it renders the "choose your profile" picker
|
|
27
|
+
* instead of the consent view. Picking re-scopes the session via updateTokens,
|
|
28
|
+
* then renders children. It sits OUTSIDE DataAvailabilityProvider so no consent
|
|
29
|
+
* fetch runs against an unscoped token, and degrades to children on any error.
|
|
30
|
+
*
|
|
31
|
+
* Once resolved it also exposes `switchProfile` via ProfileSwitchContext, so a
|
|
32
|
+
* signed-in shared contact can re-open the picker and move to another profile.
|
|
33
|
+
*/
|
|
34
|
+
export const SelectUserGate = ({ children }: SelectUserGateProps) => {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const { organization } = useOrganization();
|
|
37
|
+
const organizationId = organization?.uuid || '';
|
|
38
|
+
const workspaceId = organization?.workspaces?.[0]?.uuid || '';
|
|
39
|
+
const { consentClient } = useBaseUrl();
|
|
40
|
+
const { accessToken, updateTokens } = useAuth();
|
|
41
|
+
|
|
42
|
+
const [phase, setPhase] = useState<GatePhase>('checking');
|
|
43
|
+
const [candidates, setCandidates] = useState<IdentityCandidate[]>([]);
|
|
44
|
+
const [busyOrgUserId, setBusyOrgUserId] = useState<string | null>(null);
|
|
45
|
+
const [selectError, setSelectError] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
// The token's principal. Routine refreshes (checkAndRefreshAuth,
|
|
48
|
+
// refreshOtpToken) hand back a fresh token string for the SAME principal;
|
|
49
|
+
// keying the gate check off this decoded id rather than the raw token keeps
|
|
50
|
+
// those refreshes from re-running the check and flashing the picker/spinner.
|
|
51
|
+
const tokenOrgUserId =
|
|
52
|
+
(decodeJwtToken(accessToken)?.user_data?.org_user_id as string) || '';
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
|
|
57
|
+
if (!organizationId || !workspaceId || !accessToken) {
|
|
58
|
+
setPhase('resolved');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Already pinned to a chosen principal this session? Pass straight through
|
|
63
|
+
// to children — but STILL resolve the sibling list in the background so the
|
|
64
|
+
// user menu can offer "switch profile". The default/unscoped token has
|
|
65
|
+
// org_user_id = the contact and no stored pick, so the picker still shows.
|
|
66
|
+
const picked =
|
|
67
|
+
typeof window !== 'undefined'
|
|
68
|
+
? sessionStorage.getItem(PICKED_ORG_USER_KEY)
|
|
69
|
+
: null;
|
|
70
|
+
const isPinned = Boolean(picked) && picked === tokenOrgUserId;
|
|
71
|
+
|
|
72
|
+
// Only block on the spinner when the picker might actually open; a pinned
|
|
73
|
+
// session renders children immediately and resolves siblings quietly.
|
|
74
|
+
setPhase(isPinned ? 'resolved' : 'checking');
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const res = await consentClient.listIdentities({
|
|
78
|
+
organisationUuid: organizationId,
|
|
79
|
+
workspaceUuid: workspaceId,
|
|
80
|
+
authToken: accessToken,
|
|
81
|
+
});
|
|
82
|
+
if (cancelled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const list = res?.identities ?? [];
|
|
86
|
+
setCandidates(list);
|
|
87
|
+
if (list.length > 1 && !isPinned) {
|
|
88
|
+
setPhase('picking');
|
|
89
|
+
} else {
|
|
90
|
+
setPhase('resolved');
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Ledger/endpoint unavailable — never block the Privacy Center.
|
|
94
|
+
if (!cancelled) {
|
|
95
|
+
setCandidates([]);
|
|
96
|
+
setPhase('resolved');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
};
|
|
104
|
+
// consentClient is a stable factory result; re-running on its identity would
|
|
105
|
+
// risk a loop. Re-evaluate only when the tenant or the token's principal
|
|
106
|
+
// changes — not on every raw-token refresh. accessToken is read inside for
|
|
107
|
+
// the fetch but intentionally omitted so same-principal refreshes are no-ops.
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
}, [organizationId, workspaceId, tokenOrgUserId]);
|
|
110
|
+
|
|
111
|
+
const handleSelect = async (principalUuid: string) => {
|
|
112
|
+
if (!principalUuid || busyOrgUserId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setBusyOrgUserId(principalUuid);
|
|
116
|
+
setSelectError(null);
|
|
117
|
+
try {
|
|
118
|
+
const res = await consentClient.selectPrincipal(
|
|
119
|
+
{
|
|
120
|
+
organisationUuid: organizationId,
|
|
121
|
+
workspaceUuid: workspaceId,
|
|
122
|
+
authToken: accessToken,
|
|
123
|
+
},
|
|
124
|
+
principalUuid,
|
|
125
|
+
);
|
|
126
|
+
if (!res?.token || !res?.refresh_token) {
|
|
127
|
+
throw new Error('select-principal returned no tokens');
|
|
128
|
+
}
|
|
129
|
+
// The picker selects by opaque uuid, but the "already pinned" check
|
|
130
|
+
// compares against the token's own org_user_id. Derive the pin from the
|
|
131
|
+
// NEW token so it matches on later reloads — it's the signed-in
|
|
132
|
+
// principal's own id, not a co-principal's, so nothing leaks.
|
|
133
|
+
if (typeof window !== 'undefined') {
|
|
134
|
+
const pinnedOrgUserId = decodeJwtToken(res.token)?.user_data
|
|
135
|
+
?.org_user_id as string | undefined;
|
|
136
|
+
if (pinnedOrgUserId) {
|
|
137
|
+
sessionStorage.setItem(PICKED_ORG_USER_KEY, pinnedOrgUserId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Resolve before swapping the token so the picker doesn't flash back.
|
|
141
|
+
setPhase('resolved');
|
|
142
|
+
updateTokens(res.token, res.refresh_token);
|
|
143
|
+
} catch {
|
|
144
|
+
setBusyOrgUserId(null);
|
|
145
|
+
setSelectError(t('couldNotSwitchProfile'));
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Re-open the picker so a shared contact can move to another of its profiles.
|
|
150
|
+
// Drops the session pin and shows the picker with the already-loaded
|
|
151
|
+
// candidates; picking re-scopes the token exactly like the first choice.
|
|
152
|
+
const switchProfile = useCallback(() => {
|
|
153
|
+
if (typeof window !== 'undefined') {
|
|
154
|
+
sessionStorage.removeItem(PICKED_ORG_USER_KEY);
|
|
155
|
+
}
|
|
156
|
+
setSelectError(null);
|
|
157
|
+
setBusyOrgUserId(null);
|
|
158
|
+
setPhase('picking');
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const switchValue = useMemo(
|
|
162
|
+
() => ({ canSwitch: candidates.length > 1, switchProfile }),
|
|
163
|
+
[candidates.length, switchProfile],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
let inner: ReactNode;
|
|
167
|
+
if (phase === 'checking') {
|
|
168
|
+
inner = (
|
|
169
|
+
<div className="pc-select-user-loading">
|
|
170
|
+
<PCLoader variant="spinner" color="info" size="lg" />
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
} else if (phase === 'picking') {
|
|
174
|
+
const decoded = decodeJwtToken(accessToken);
|
|
175
|
+
const email =
|
|
176
|
+
(decoded?.user_data?.primary_email as string) ||
|
|
177
|
+
(decoded?.primary_email as string) ||
|
|
178
|
+
null;
|
|
179
|
+
inner = (
|
|
180
|
+
<SelectUser
|
|
181
|
+
email={email}
|
|
182
|
+
candidates={candidates}
|
|
183
|
+
onSelect={handleSelect}
|
|
184
|
+
busyOrgUserId={busyOrgUserId}
|
|
185
|
+
error={selectError}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
inner = <>{children}</>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return <ProfileSwitchProvider value={switchValue}>{inner}</ProfileSwitchProvider>;
|
|
193
|
+
};
|
|
@@ -39,3 +39,14 @@ export const MARKER_TINT: Record<BadgeVariant, MarkerTint> = {
|
|
|
39
39
|
// Internal error thrown when an activity page fails to load (surfaced to the
|
|
40
40
|
// user via the i18n `failedToFetchActivityData` toast).
|
|
41
41
|
export const ACTIVITY_FETCH_ERROR_MESSAGE = 'Failed to fetch activity data';
|
|
42
|
+
|
|
43
|
+
// ─── Shared-contact profile picker ───────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// sessionStorage key pinning the principal the user chose this session, so
|
|
46
|
+
// routine token refreshes skip the picker.
|
|
47
|
+
export const PICKED_ORG_USER_KEY = 'pc_picked_org_user_id';
|
|
48
|
+
|
|
49
|
+
// The shared-contact picker/switch user-facing strings are localized via i18n —
|
|
50
|
+
// see the chooseYourProfile / emailLinkedToMultipleProfiles /
|
|
51
|
+
// contactLinkedToMultipleProfiles / signingInEllipsis / couldNotSwitchProfile /
|
|
52
|
+
// switchProfile keys in locales/*/translation.json.
|
|
@@ -1,4 +1,67 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ConsentDataElement,
|
|
3
|
+
ConsentGroup,
|
|
4
|
+
DataElement,
|
|
5
|
+
FormProductGroup,
|
|
6
|
+
Purpose,
|
|
7
|
+
PurposeItem,
|
|
8
|
+
} from '../lib/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map a Go-ledger `ConsentGroup` (from the user-consents endpoint) onto the
|
|
12
|
+
* DSAR request form's `FormProductGroup`. Approach A: the request-form product
|
|
13
|
+
* picker is now sourced from the ledger instead of Python's `form/data`, since
|
|
14
|
+
* consents live in the ledger after the cutover.
|
|
15
|
+
*
|
|
16
|
+
* The shapes are near-identical; the only adaptations are:
|
|
17
|
+
* - purpose id rename: ledger `purpose_uuid` -> form `uuid` (which the picker
|
|
18
|
+
* keys on and the submit payload sends back as `purpose_uuid`),
|
|
19
|
+
* - the ledger data-element's `selected` stands in for the form's
|
|
20
|
+
* `given_consent`,
|
|
21
|
+
* - fields the picker never reads (industries, validity, descriptions) are
|
|
22
|
+
* defaulted.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: the ledger caps inline purposes at 50 per group (`has_more_purposes`).
|
|
25
|
+
* A product with more than 50 purposes would need the per-group purposes
|
|
26
|
+
* endpoint to assemble a complete CORRECTION data-element list (follow-up).
|
|
27
|
+
*/
|
|
28
|
+
export const consentGroupToFormProductGroup = (
|
|
29
|
+
group: ConsentGroup,
|
|
30
|
+
): FormProductGroup => ({
|
|
31
|
+
product_uuid: group.product_uuid,
|
|
32
|
+
product_name: group.product_name,
|
|
33
|
+
product_description: group.product_description,
|
|
34
|
+
nominator: group.nominator,
|
|
35
|
+
purposes: (group.purposes ?? []).map(purposeItemToFormPurpose),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const purposeItemToFormPurpose = (purpose: PurposeItem): Purpose => ({
|
|
39
|
+
uuid: purpose.purpose_uuid,
|
|
40
|
+
purpose_uuid: purpose.purpose_uuid,
|
|
41
|
+
name: purpose.name,
|
|
42
|
+
description: purpose.description ?? '',
|
|
43
|
+
industries: '',
|
|
44
|
+
selected: false,
|
|
45
|
+
given_consent: false,
|
|
46
|
+
status: purpose.status,
|
|
47
|
+
validity: null,
|
|
48
|
+
enabled: true,
|
|
49
|
+
data_elements: (purpose.data_elements ?? []).map(
|
|
50
|
+
consentDataElementToFormDataElement,
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const consentDataElementToFormDataElement = (
|
|
55
|
+
element: ConsentDataElement,
|
|
56
|
+
): DataElement => ({
|
|
57
|
+
uuid: element.uuid,
|
|
58
|
+
name: element.name,
|
|
59
|
+
description: '',
|
|
60
|
+
enabled: element.enabled,
|
|
61
|
+
required: element.required,
|
|
62
|
+
given_consent: element.selected,
|
|
63
|
+
selected: element.selected,
|
|
64
|
+
});
|
|
2
65
|
|
|
3
66
|
/**
|
|
4
67
|
* Build the per-purpose state used by `PurposeSelectionSection` for a single
|
|
@@ -9,18 +9,24 @@ export const BaseUrlContext = createContext<BaseUrlContextValue | null>(null);
|
|
|
9
9
|
export const BaseUrlProvider = ({
|
|
10
10
|
children,
|
|
11
11
|
baseUrl,
|
|
12
|
+
ledgerBaseUrl,
|
|
12
13
|
}: {
|
|
13
14
|
children: ReactNode;
|
|
14
15
|
baseUrl: string; // Consent server base URL
|
|
16
|
+
ledgerBaseUrl?: string; // Go ledger base URL — serves user-consents
|
|
15
17
|
}) => {
|
|
16
|
-
const consentClient = useMemo(
|
|
18
|
+
const consentClient = useMemo(
|
|
19
|
+
() => createConsentClient(baseUrl, ledgerBaseUrl),
|
|
20
|
+
[baseUrl, ledgerBaseUrl]
|
|
21
|
+
);
|
|
17
22
|
|
|
18
23
|
const value: BaseUrlContextValue = useMemo(
|
|
19
24
|
() => ({
|
|
20
25
|
baseUrl,
|
|
26
|
+
ledgerBaseUrl,
|
|
21
27
|
consentClient,
|
|
22
28
|
}),
|
|
23
|
-
[baseUrl, consentClient]
|
|
29
|
+
[baseUrl, ledgerBaseUrl, consentClient]
|
|
24
30
|
);
|
|
25
31
|
|
|
26
32
|
return (
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { ProfileSwitchValue } from '../lib/types';
|
|
3
|
+
|
|
4
|
+
// Lets any descendant of SelectUserGate (e.g. the user menu) re-open the
|
|
5
|
+
// shared-contact profile picker. Defaults to a no-op so consumers rendered
|
|
6
|
+
// outside the gate degrade gracefully.
|
|
7
|
+
const ProfileSwitchContext = createContext<ProfileSwitchValue>({
|
|
8
|
+
canSwitch: false,
|
|
9
|
+
switchProfile: () => {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const ProfileSwitchProvider = ProfileSwitchContext.Provider;
|
|
13
|
+
|
|
14
|
+
export const useProfileSwitch = (): ProfileSwitchValue =>
|
|
15
|
+
useContext(ProfileSwitchContext);
|
|
@@ -392,6 +392,8 @@ export const manageConsent = async ({
|
|
|
392
392
|
nominatorContact,
|
|
393
393
|
dataElementUuids,
|
|
394
394
|
productUuid,
|
|
395
|
+
noticeUuid,
|
|
396
|
+
dataElements,
|
|
395
397
|
}: {
|
|
396
398
|
organizationId: string;
|
|
397
399
|
workspaceId: string;
|
|
@@ -403,6 +405,8 @@ export const manageConsent = async ({
|
|
|
403
405
|
nominatorContact?: string;
|
|
404
406
|
dataElementUuids?: string[];
|
|
405
407
|
productUuid?: string;
|
|
408
|
+
noticeUuid?: string;
|
|
409
|
+
dataElements?: Array<{ uuid: string; selected: boolean }>;
|
|
406
410
|
}): Promise<boolean | null> => {
|
|
407
411
|
try {
|
|
408
412
|
// Ensure we have a valid token before making the API call
|
|
@@ -423,6 +427,8 @@ export const manageConsent = async ({
|
|
|
423
427
|
nominatorContact,
|
|
424
428
|
dataElementUuids,
|
|
425
429
|
productUuid,
|
|
430
|
+
noticeUuid,
|
|
431
|
+
dataElements,
|
|
426
432
|
});
|
|
427
433
|
return result;
|
|
428
434
|
} catch (error: any) {
|
|
@@ -445,6 +451,8 @@ export const manageConsent = async ({
|
|
|
445
451
|
nominatorContact,
|
|
446
452
|
dataElementUuids,
|
|
447
453
|
productUuid,
|
|
454
|
+
noticeUuid,
|
|
455
|
+
dataElements,
|
|
448
456
|
});
|
|
449
457
|
}
|
|
450
458
|
console.error(`Error ${action} consent:`, error);
|
|
@@ -196,6 +196,15 @@ export const MAX_REFRESH_BACKOFF_MS = 300_000; // 5m
|
|
|
196
196
|
export const REFRESH_TOKEN_STORAGE_KEY = 'redacto_pc_refresh_token';
|
|
197
197
|
export const LEGACY_REFRESH_TOKEN_COOKIE = 'refresh_token';
|
|
198
198
|
|
|
199
|
+
// ─── DSAR request form ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
// Max product groups requested from the Go ledger's user-consents when
|
|
202
|
+
// sourcing the DSAR request-form product picker (Approach A). The ledger caps
|
|
203
|
+
// user-consents page size at 100; the picker needs all of a principal's
|
|
204
|
+
// groups, so we request the max. A principal with >100 product groups would
|
|
205
|
+
// need group-level pagination (follow-up).
|
|
206
|
+
export const FORM_PICKER_GROUP_LIMIT = 100;
|
|
207
|
+
|
|
199
208
|
// ─── Notification Hub ──────────────────────────────────────────────────────
|
|
200
209
|
|
|
201
210
|
export const POLL_INTERVAL = 30_000;
|
|
@@ -256,6 +256,10 @@ export type PurposeItem = {
|
|
|
256
256
|
method: string;
|
|
257
257
|
data_elements: ConsentDataElement[];
|
|
258
258
|
link_reason: string | null;
|
|
259
|
+
// UUID of the notice version this purpose was consented under. Served by the
|
|
260
|
+
// Go ledger's user-consents response; required to revoke/regrant via the
|
|
261
|
+
// ledger's submit-consent endpoint (which has no manage-consent route).
|
|
262
|
+
notice_uuid: string | null;
|
|
259
263
|
};
|
|
260
264
|
|
|
261
265
|
export type ConsentGroup = {
|
|
@@ -345,6 +349,12 @@ export type ManageConsentResponse = {
|
|
|
345
349
|
};
|
|
346
350
|
};
|
|
347
351
|
|
|
352
|
+
// Unwrapped `detail` of the legacy Python manage-consent response; the Go
|
|
353
|
+
// ledger returns a ConsentEventResponse with no `success` flag instead.
|
|
354
|
+
export type LegacyManageConsentResponse = {
|
|
355
|
+
success: boolean;
|
|
356
|
+
};
|
|
357
|
+
|
|
348
358
|
export type ManageConsentParams = {
|
|
349
359
|
organizationId: string;
|
|
350
360
|
workspaceId: string;
|
|
@@ -405,9 +415,17 @@ export type Receipt = {
|
|
|
405
415
|
created_at: string;
|
|
406
416
|
};
|
|
407
417
|
|
|
418
|
+
// Mirrors the Go ledger's `ConsentReceiptListResponse` (offset-paginated page
|
|
419
|
+
// of the principal's receipts): flat `total`/`skip`/`limit`, not a nested
|
|
420
|
+
// `pagination` object. Without a configured ledgerBaseUrl the legacy Python
|
|
421
|
+
// list is called instead, which returns a `{ data, pagination }` envelope —
|
|
422
|
+
// `normalizeReceipts` in ReceiptsList tolerates either shape; this type
|
|
423
|
+
// documents the ledger contract.
|
|
408
424
|
export type ReceiptListResponse = {
|
|
409
425
|
data: Receipt[];
|
|
410
|
-
|
|
426
|
+
total: number;
|
|
427
|
+
skip: number;
|
|
428
|
+
limit: number;
|
|
411
429
|
};
|
|
412
430
|
|
|
413
431
|
export type ActivityResponse = {
|
|
@@ -804,3 +822,47 @@ export type NotificationRefreshButtonProps = Readonly<{
|
|
|
804
822
|
onRefresh: () => void;
|
|
805
823
|
iconSize?: number;
|
|
806
824
|
}>;
|
|
825
|
+
|
|
826
|
+
export type IdentityCandidate = Readonly<{
|
|
827
|
+
uuid: string;
|
|
828
|
+
org_user_id?: string | null;
|
|
829
|
+
primary_email?: string | null;
|
|
830
|
+
primary_mobile?: string | null;
|
|
831
|
+
created_at?: string | null;
|
|
832
|
+
}>;
|
|
833
|
+
|
|
834
|
+
export type IdentityListResponse = Readonly<{
|
|
835
|
+
identities: IdentityCandidate[];
|
|
836
|
+
}>;
|
|
837
|
+
|
|
838
|
+
export type SelectPrincipalResponse = Readonly<{
|
|
839
|
+
token: string;
|
|
840
|
+
refresh_token: string;
|
|
841
|
+
expires_at?: string;
|
|
842
|
+
}>;
|
|
843
|
+
|
|
844
|
+
// Discrete phases of the shared-contact profile gate: `checking` probes the
|
|
845
|
+
// backend for sibling principals, `picking` shows the profile picker, and
|
|
846
|
+
// `resolved` passes through to children.
|
|
847
|
+
export type GatePhase = 'checking' | 'picking' | 'resolved';
|
|
848
|
+
|
|
849
|
+
export type SelectUserProps = Readonly<{
|
|
850
|
+
email: string | null;
|
|
851
|
+
candidates: IdentityCandidate[];
|
|
852
|
+
onSelect: (orgUserId: string) => void;
|
|
853
|
+
busyOrgUserId: string | null;
|
|
854
|
+
error?: string | null;
|
|
855
|
+
}>;
|
|
856
|
+
|
|
857
|
+
export type SelectUserGateProps = Readonly<{
|
|
858
|
+
children: React.ReactNode;
|
|
859
|
+
}>;
|
|
860
|
+
|
|
861
|
+
// Exposed by SelectUserGate so a signed-in session can re-open the profile
|
|
862
|
+
// picker (shared contact). `canSwitch` is true only when the contact maps to
|
|
863
|
+
// more than one principal; `switchProfile` drops the pinned choice and shows
|
|
864
|
+
// the picker again.
|
|
865
|
+
export type ProfileSwitchValue = Readonly<{
|
|
866
|
+
canSwitch: boolean;
|
|
867
|
+
switchProfile: () => void;
|
|
868
|
+
}>;
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "প্ৰ'ফাইল সলনি কৰক",
|
|
335
|
+
"chooseYourProfile": "আপোনাৰ প্ৰ'ফাইল বাছনি কৰক",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "এই যোগাযোগটো একাধিক প্ৰ'ফাইলৰ সৈতে সংযুক্ত। আপুনি চাব বিচৰাটো বাছনি কৰক।",
|
|
338
|
+
"signingInEllipsis": "ছাইন ইন হৈ আছে…",
|
|
339
|
+
"couldNotSwitchProfile": "সেই প্ৰ'ফাইললৈ সলনি কৰিব পৰা নগ'ল। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক।"
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "प्रोफाइल बदलीं",
|
|
335
|
+
"chooseYourProfile": "आपन प्रोफाइल चुनीं",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "ई संपर्क एक से ढेर प्रोफाइल से जुड़ल बा। जवन रउआ देखल चाहत बानी ओकरा के चुनीं।",
|
|
338
|
+
"signingInEllipsis": "साइन इन हो रहल बा…",
|
|
339
|
+
"couldNotSwitchProfile": "ओह प्रोफाइल पर स्विच ना हो सकल। कृपया फेर से कोसिस करीं।"
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "প্রোফাইল পরিবর্তন করুন",
|
|
335
|
+
"chooseYourProfile": "আপনার প্রোফাইল বেছে নিন",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "এই যোগাযোগটি একাধিক প্রোফাইলের সাথে যুক্ত। আপনি যেটি দেখতে চান সেটি বেছে নিন।",
|
|
338
|
+
"signingInEllipsis": "সাইন ইন হচ্ছে…",
|
|
339
|
+
"couldNotSwitchProfile": "সেই প্রোফাইলে স্যুইচ করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।"
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "प्रोफाइल सोलाय",
|
|
335
|
+
"chooseYourProfile": "नोंथांनि प्रफाइल सायख",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} आ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "बे सोमोन्दोआ मोनसेनिख्रुइ बांसिन प्रफाइलजों नांजाब दं। नोंथाङा नायनो लुबैयोब्ला बेखौ सायख।",
|
|
338
|
+
"signingInEllipsis": "साइन इन जायगासिनो दं…",
|
|
339
|
+
"couldNotSwitchProfile": "बै प्रफाइलआव स्विच खालामनो हायाखै। अननानै फिन नाजा।"
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "प्रोफाइल बदलो",
|
|
335
|
+
"chooseYourProfile": "अपनी प्रोफाइल चुनो",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "एह् संपर्क इक कोला मता प्रोफाइलें कन्नै जुड़्या दा ऐ। जेह्ड़ी तुसें दिक्खनी ऐ ओह् चुनो।",
|
|
338
|
+
"signingInEllipsis": "साइन इन होआ करदा ऐ…",
|
|
339
|
+
"couldNotSwitchProfile": "ओह् प्रोफाइल पर स्विच नेईं होई सकेआ। कृपा करियै दुबारा कोशिश करो।"
|
|
334
340
|
}
|
|
@@ -344,5 +344,11 @@
|
|
|
344
344
|
"forProduct": "For {{productName}}",
|
|
345
345
|
"actingOnBehalfOfShort": "acting on behalf of {{nominator}}",
|
|
346
346
|
"nominationChipSuffix": "· {{nominator}}",
|
|
347
|
-
"selectPurposesForThisProduct": "Select purposes within this product."
|
|
347
|
+
"selectPurposesForThisProduct": "Select purposes within this product.",
|
|
348
|
+
"switchProfile": "Switch profile",
|
|
349
|
+
"chooseYourProfile": "Choose your profile",
|
|
350
|
+
"emailLinkedToMultipleProfiles": "{{email}} is linked to more than one profile. Choose the one you want to view.",
|
|
351
|
+
"contactLinkedToMultipleProfiles": "This contact is linked to more than one profile. Choose the one you want to view.",
|
|
352
|
+
"signingInEllipsis": "Signing in…",
|
|
353
|
+
"couldNotSwitchProfile": "Could not switch to that profile. Please try again."
|
|
348
354
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "प्रोफायल बदला",
|
|
335
|
+
"chooseYourProfile": "तुमचें प्रोफायल निवडात",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} एकापरस चड प्रोफायलांक जोडलां. तुमकां जें पळोवपाक जाय तें निवडात.",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "हो संपर्क एकापरस चड प्रोफायलांक जोडला. तुमकां जें पळोवपाक जाय तें निवडात.",
|
|
338
|
+
"signingInEllipsis": "साइन इन जावन आसा…",
|
|
339
|
+
"couldNotSwitchProfile": "त्या प्रोफायलाचेर स्विच करूंक जालें ना. उपकार करून परत यत्न करात."
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "પ્રોફાઇલ બદલો",
|
|
335
|
+
"chooseYourProfile": "તમારી પ્રોફાઇલ પસંદ કરો",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "આ સંપર્ક એક કરતાં વધુ પ્રોફાઇલ સાથે જોડાયેલ છે. તમે જે જોવા માંગો છો તે પસંદ કરો.",
|
|
338
|
+
"signingInEllipsis": "સાઇન ઇન થઈ રહ્યું છે…",
|
|
339
|
+
"couldNotSwitchProfile": "તે પ્રોફાઇલ પર સ્વિચ કરી શકાયું નહીં. કૃપા કરીને ફરી પ્રયાસ કરો."
|
|
334
340
|
}
|
|
@@ -333,5 +333,11 @@
|
|
|
333
333
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
334
334
|
"someRequestsFailed": "Some requests failed",
|
|
335
335
|
"retry": "Retry",
|
|
336
|
-
"retrying": "Retrying..."
|
|
336
|
+
"retrying": "Retrying...",
|
|
337
|
+
"switchProfile": "प्रोफ़ाइल बदलें",
|
|
338
|
+
"chooseYourProfile": "अपनी प्रोफ़ाइल चुनें",
|
|
339
|
+
"emailLinkedToMultipleProfiles": "{{email}} एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
|
|
340
|
+
"contactLinkedToMultipleProfiles": "यह संपर्क एक से अधिक प्रोफ़ाइल से जुड़ा है। जिसे आप देखना चाहते हैं उसे चुनें।",
|
|
341
|
+
"signingInEllipsis": "साइन इन हो रहा है…",
|
|
342
|
+
"couldNotSwitchProfile": "उस प्रोफ़ाइल पर स्विच नहीं किया जा सका। कृपया पुनः प्रयास करें।"
|
|
337
343
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "ಪ್ರೊಫೈಲ್ ಬದಲಾಯಿಸಿ",
|
|
335
|
+
"chooseYourProfile": "ನಿಮ್ಮ ಪ್ರೊಫೈಲ್ ಆಯ್ಕೆಮಾಡಿ",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "ಈ ಸಂಪರ್ಕವು ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಪ್ರೊಫೈಲ್ಗಳಿಗೆ ಲಿಂಕ್ ಆಗಿದೆ. ನೀವು ವೀಕ್ಷಿಸಲು ಬಯಸುವುದನ್ನು ಆಯ್ಕೆಮಾಡಿ.",
|
|
338
|
+
"signingInEllipsis": "ಸೈನ್ ಇನ್ ಆಗುತ್ತಿದೆ…",
|
|
339
|
+
"couldNotSwitchProfile": "ಆ ಪ್ರೊಫೈಲ್ಗೆ ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ."
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "پروفائل بدلاو",
|
|
335
|
+
"chooseYourProfile": "پننہٕ پروفائل ژارٕو",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "یہ رابطہ چھُ اکہ کھۄتہ زیادٕ پروفائلن سٟتؠ جوڑنہ آمُت۔ یُس توہہِ وُچھُن چھِو یژھان سُ ژارٕو۔",
|
|
338
|
+
"signingInEllipsis": "سائن اِن گژھان چھُ…",
|
|
339
|
+
"couldNotSwitchProfile": "تہِ پروفائلس منٛز سوِچ کرُن آو نہ۔ مہربانی کرِتھ دوبارٕ کوشش کریو۔"
|
|
334
340
|
}
|
|
@@ -330,5 +330,11 @@
|
|
|
330
330
|
"casesCreatedCount_other": "{{count}} cases created",
|
|
331
331
|
"someRequestsFailed": "Some requests failed",
|
|
332
332
|
"retry": "Retry",
|
|
333
|
-
"retrying": "Retrying..."
|
|
333
|
+
"retrying": "Retrying...",
|
|
334
|
+
"switchProfile": "प्रोफाइल बदलू",
|
|
335
|
+
"chooseYourProfile": "अपन प्रोफाइल चुनू",
|
|
336
|
+
"emailLinkedToMultipleProfiles": "{{email}} एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
|
|
337
|
+
"contactLinkedToMultipleProfiles": "ई संपर्क एकाधिक प्रोफाइलसँ जुड़ल अछि। जे अहाँ देखय चाहैत छी ओकरा चुनू।",
|
|
338
|
+
"signingInEllipsis": "साइन इन भ' रहल अछि…",
|
|
339
|
+
"couldNotSwitchProfile": "ओ प्रोफाइल पर स्विच नहि भ' सकल। कृपया फेर सँ प्रयास करू।"
|
|
334
340
|
}
|