@redacto.io/consent-sdk-react 10.0.0-beta.0 → 10.0.0-beta.11
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 +123 -0
- package/README.md +78 -0
- package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
- package/dist/index.d.mts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +345 -237
- package/dist/index.mjs +344 -238
- package/dist/privacy-center.d.mts +113 -37
- package/dist/privacy-center.d.ts +113 -37
- package/dist/privacy-center.js +2317 -1910
- package/dist/privacy-center.mjs +2245 -1835
- package/package.json +1 -1
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +87 -0
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +27 -0
- package/src/RedactoNoticeAssisted/api/index.ts +7 -1
- package/src/RedactoNoticeAssisted/api/types.ts +2 -0
- package/src/RedactoNoticeAssisted/i18n.ts +0 -55
- package/src/RedactoNoticeAssisted/types.ts +2 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +42 -8
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +75 -33
- package/src/RedactoNoticeConsent/api/index.ts +90 -177
- package/src/RedactoNoticeConsent/api/sandbox.ts +150 -0
- package/src/RedactoNoticeConsent/api/types.ts +60 -44
- package/src/RedactoNoticeConsent/language-codes.ts +0 -2
- package/src/RedactoNoticeConsent/types.ts +29 -3
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +86 -5
- package/src/RedactoNoticeConsentInline/api/index.ts +14 -5
- package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +72 -0
- package/src/RedactoNoticeConsentInline/api/types.ts +4 -1
- package/src/RedactoNoticeConsentInline/types.ts +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +38 -3
- package/src/RedactoPrivacyCenter/api/actions.ts +13 -5
- package/src/RedactoPrivacyCenter/api/client.ts +216 -39
- package/src/RedactoPrivacyCenter/api/fetcher.ts +24 -9
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +28 -0
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +107 -76
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +2 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +53 -24
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +95 -0
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +195 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +42 -6
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +131 -39
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
- package/src/RedactoPrivacyCenter/context/types.ts +39 -1
- package/src/RedactoPrivacyCenter/lib/api.ts +3 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +12 -6
- package/src/RedactoPrivacyCenter/lib/locales.ts +0 -3
- package/src/RedactoPrivacyCenter/lib/sandbox.ts +104 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +78 -2
- package/src/RedactoPrivacyCenter/lib/utils.ts +15 -0
- package/src/RedactoPrivacyCenter/locales/as/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 +8 -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 +112 -2
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +17 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +83 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +33 -9
- package/src/shared/notice-consent-status.ts +190 -0
- package/src/shared/sandbox.ts +6 -0
- package/tests/Form.test.tsx +190 -7
- package/tests/RevokeWarningMessage.test.tsx +151 -0
- package/tests/consentClient.test.ts +217 -0
- package/tests/manageConsentServer.test.ts +38 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +0 -334
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Users } from 'lucide-react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import { PCAvatar, PCPopover } from '../../ui';
|
|
4
5
|
import { useBranding } from '../../context/BrandingContext';
|
|
5
6
|
import { useOrganization } from '../../context/OrganizationContext';
|
|
6
|
-
import {
|
|
7
|
+
import { useProfileSwitch } from '../../context/ProfileSwitchContext';
|
|
8
|
+
import { useAuth } from '../../context/AuthContext';
|
|
9
|
+
import { decodeJwtToken, getVar } from '../../lib/utils';
|
|
7
10
|
import { usePrivacyCenterExitAction } from './usePrivacyCenterExitAction';
|
|
8
11
|
|
|
9
12
|
type UserMenuProps = {
|
|
@@ -30,8 +33,10 @@ function initialsFromEmail(email: string): string {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function UserMenu({ onBack }: UserMenuProps) {
|
|
36
|
+
const { t } = useTranslation();
|
|
33
37
|
const branding = useBranding();
|
|
34
38
|
const { organization } = useOrganization();
|
|
39
|
+
const { accessToken } = useAuth();
|
|
35
40
|
const [email, setEmail] = useState('');
|
|
36
41
|
|
|
37
42
|
useEffect(() => {
|
|
@@ -40,42 +45,63 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
40
45
|
|
|
41
46
|
const { handleAction, label: actionLabel, Icon: ActionIcon } =
|
|
42
47
|
usePrivacyCenterExitAction(onBack);
|
|
48
|
+
const { canSwitch, switchProfile } = useProfileSwitch();
|
|
49
|
+
|
|
50
|
+
// The signed-in principal's org_user_id — unique per profile, unlike the
|
|
51
|
+
// email a shared contact reuses across profiles — so the menu can show which
|
|
52
|
+
// profile is active. Read from the (live) token, which re-scopes on switch.
|
|
53
|
+
const orgUserId =
|
|
54
|
+
(decodeJwtToken(accessToken)?.user_data?.org_user_id as string) || '';
|
|
43
55
|
|
|
44
56
|
const displayName = branding?.brand_name || organization?.name || '';
|
|
45
57
|
const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
// The .pc-navbar-avatar-btn control draws the ring and surface, so the avatar fills
|
|
60
|
+
// it and draws neither (see `framed={false}` below).
|
|
61
|
+
const avatarFillStyle: CSSProperties = {
|
|
62
|
+
width: '100%',
|
|
63
|
+
height: '100%',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Shared style for the popover's action rows (switch profile, back/sign out).
|
|
67
|
+
const menuItemStyle: CSSProperties = {
|
|
68
|
+
display: 'flex',
|
|
49
69
|
alignItems: 'center',
|
|
50
|
-
gap:
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
gap: 10,
|
|
71
|
+
width: '100%',
|
|
72
|
+
padding: '10px 14px',
|
|
73
|
+
border: 'none',
|
|
74
|
+
borderRadius: 8,
|
|
53
75
|
background: 'transparent',
|
|
54
|
-
|
|
55
|
-
color: getVar('--pc-text-secondary'),
|
|
76
|
+
color: getVar('--pc-text-primary'),
|
|
56
77
|
cursor: 'pointer',
|
|
57
|
-
|
|
78
|
+
fontFamily: 'inherit',
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
fontWeight: 500,
|
|
81
|
+
textAlign: 'left',
|
|
82
|
+
transition: 'background 150ms ease',
|
|
58
83
|
};
|
|
59
84
|
|
|
60
85
|
const trigger = (
|
|
61
|
-
<
|
|
62
|
-
style={triggerStyle}
|
|
63
|
-
role="presentation"
|
|
64
|
-
onMouseEnter={(e) => {
|
|
65
|
-
(e.currentTarget as HTMLDivElement).style.boxShadow =
|
|
66
|
-
'0 0 0 2px color-mix(in srgb, var(--pc-primary) 18%, transparent)';
|
|
67
|
-
}}
|
|
68
|
-
onMouseLeave={(e) => {
|
|
69
|
-
(e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
86
|
+
<button type="button" aria-label={t('accountMenu')} className="pc-navbar-avatar-btn">
|
|
72
87
|
{branding?.icon_url ? (
|
|
73
|
-
<PCAvatar
|
|
88
|
+
<PCAvatar
|
|
89
|
+
src={branding.icon_url}
|
|
90
|
+
name={triggerAvatarName}
|
|
91
|
+
size="md"
|
|
92
|
+
color="info"
|
|
93
|
+
framed={false}
|
|
94
|
+
style={avatarFillStyle}
|
|
95
|
+
/>
|
|
74
96
|
) : (
|
|
75
|
-
<PCAvatar
|
|
97
|
+
<PCAvatar
|
|
98
|
+
name={triggerAvatarName}
|
|
99
|
+
size="md"
|
|
100
|
+
color="info"
|
|
101
|
+
style={avatarFillStyle}
|
|
102
|
+
/>
|
|
76
103
|
)}
|
|
77
|
-
|
|
78
|
-
</div>
|
|
104
|
+
</button>
|
|
79
105
|
);
|
|
80
106
|
|
|
81
107
|
const content = (
|
|
@@ -132,70 +158,75 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
132
158
|
</div>
|
|
133
159
|
|
|
134
160
|
{/* Identity card */}
|
|
135
|
-
{email && (
|
|
161
|
+
{(email || orgUserId) && (
|
|
136
162
|
<div
|
|
137
163
|
style={{
|
|
138
164
|
padding: '12px 16px',
|
|
139
165
|
display: 'flex',
|
|
140
166
|
alignItems: 'center',
|
|
141
167
|
gap: 10,
|
|
142
|
-
borderBottom:
|
|
168
|
+
borderBottom:
|
|
169
|
+
onBack || canSwitch
|
|
170
|
+
? `1px solid ${getVar('--pc-border-color')}`
|
|
171
|
+
: 'none',
|
|
143
172
|
}}
|
|
144
173
|
>
|
|
145
|
-
<PCAvatar name={
|
|
146
|
-
<div
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
{email}
|
|
174
|
+
<PCAvatar name={triggerAvatarName} size="sm" color="info" />
|
|
175
|
+
<div className="pc-shell-user-menu-identity">
|
|
176
|
+
{email ? (
|
|
177
|
+
<div title={email} className="pc-shell-user-menu-identity-email">
|
|
178
|
+
{email}
|
|
179
|
+
</div>
|
|
180
|
+
) : null}
|
|
181
|
+
{orgUserId ? (
|
|
182
|
+
<div
|
|
183
|
+
title={orgUserId}
|
|
184
|
+
className="pc-shell-user-menu-identity-id"
|
|
185
|
+
>
|
|
186
|
+
{t('userIdLabel', { id: orgUserId })}
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
161
189
|
</div>
|
|
162
190
|
</div>
|
|
163
191
|
)}
|
|
164
192
|
|
|
165
|
-
{/* Action
|
|
166
|
-
{onBack && (
|
|
193
|
+
{/* Action rows */}
|
|
194
|
+
{(canSwitch || onBack) && (
|
|
167
195
|
<div style={{ padding: '6px 8px 8px' }}>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
{canSwitch && (
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
role="menuitem"
|
|
200
|
+
onClick={switchProfile}
|
|
201
|
+
style={menuItemStyle}
|
|
202
|
+
onMouseEnter={(e) => {
|
|
203
|
+
e.currentTarget.style.background = getVar('--pc-bg-hover');
|
|
204
|
+
}}
|
|
205
|
+
onMouseLeave={(e) => {
|
|
206
|
+
e.currentTarget.style.background = 'transparent';
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<Users size={16} strokeWidth={2} />
|
|
210
|
+
<span>{t('switchProfile')}</span>
|
|
211
|
+
</button>
|
|
212
|
+
)}
|
|
213
|
+
{onBack && (
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
role="menuitem"
|
|
217
|
+
onClick={handleAction}
|
|
218
|
+
style={menuItemStyle}
|
|
219
|
+
onMouseEnter={(e) => {
|
|
220
|
+
e.currentTarget.style.background = getVar('--pc-bg-hover');
|
|
221
|
+
}}
|
|
222
|
+
onMouseLeave={(e) => {
|
|
223
|
+
e.currentTarget.style.background = 'transparent';
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<ActionIcon size={16} strokeWidth={2} />
|
|
227
|
+
<span>{actionLabel}</span>
|
|
228
|
+
</button>
|
|
229
|
+
)}
|
|
199
230
|
</div>
|
|
200
231
|
)}
|
|
201
232
|
</div>
|
|
@@ -776,6 +776,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
776
776
|
productUuid,
|
|
777
777
|
noticeUuid,
|
|
778
778
|
dataElements,
|
|
779
|
+
language: langParam,
|
|
779
780
|
});
|
|
780
781
|
|
|
781
782
|
if (response) {
|
|
@@ -798,7 +799,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
798
799
|
handleCloseModal();
|
|
799
800
|
}
|
|
800
801
|
},
|
|
801
|
-
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient, selectedRecord]
|
|
802
|
+
[organizationId, workspaceId, contact, fetchData, handleCloseModal, t, authContext, consentClient, selectedRecord, langParam]
|
|
802
803
|
);
|
|
803
804
|
|
|
804
805
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
@@ -10,10 +10,10 @@ import { PiUploadBold } from 'react-icons/pi';
|
|
|
10
10
|
import { toast } from '../utils/toast';
|
|
11
11
|
import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCText, PCTextarea } from '../ui';
|
|
12
12
|
import { PCModal } from '../ui/PCModal';
|
|
13
|
-
import { buildGroupDataElements, buildGroupPurposes, consentGroupToFormProductGroup } from './groupHelpers';
|
|
13
|
+
import { buildGroupDataElements, buildGroupPurposes, consentGroupToFormProductGroup, legacyFlatGroup } from './groupHelpers';
|
|
14
14
|
import { pcStyles } from '../styles/pcStyles';
|
|
15
15
|
import { createCaseRequest, fetchUserConsents, getFormData } from '../lib/api';
|
|
16
|
-
import { FORM_PICKER_GROUP_LIMIT } from '../lib/constants';
|
|
16
|
+
import { FORM_PICKER_GROUP_LIMIT, LEGACY_PRODUCT_GROUP_UUID } from '../lib/constants';
|
|
17
17
|
import { ensureValidToken } from '../lib/utils';
|
|
18
18
|
import { useAuth } from '../context/AuthContext';
|
|
19
19
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
@@ -235,12 +235,31 @@ export default function Form({
|
|
|
235
235
|
|
|
236
236
|
// Union of direct + nominated product groups exposed by the backend.
|
|
237
237
|
// Drives both the product-cards step and the per-product picker beneath.
|
|
238
|
+
// When the backend exposes no groups at all but did return the legacy flat
|
|
239
|
+
// `purposes` list (pre-grouping Python deployments, and the popup /
|
|
240
|
+
// case-management contexts that cannot read user-consents), fall back to a
|
|
241
|
+
// single synthetic group so the purposes stay reachable instead of the
|
|
242
|
+
// picker claiming the principal has no consents.
|
|
238
243
|
const allGroups = useMemo<FormProductGroup[]>(() => {
|
|
239
244
|
if (!data) {
|
|
240
245
|
return [];
|
|
241
246
|
}
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
const groups = [
|
|
248
|
+
...(data.direct_groups ?? []),
|
|
249
|
+
...(data.nominated_groups ?? []),
|
|
250
|
+
];
|
|
251
|
+
if (groups.length > 0) {
|
|
252
|
+
return groups;
|
|
253
|
+
}
|
|
254
|
+
const flatGroup = legacyFlatGroup(data, t('yourData'));
|
|
255
|
+
// `buildGroupPurposes` is what the panel renders from — it drops purposes
|
|
256
|
+
// with no data elements, so a flat list that survives nothing must keep
|
|
257
|
+
// showing the empty state rather than an unusable panel.
|
|
258
|
+
if (buildGroupPurposes(flatGroup).length === 0) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
return [flatGroup];
|
|
262
|
+
}, [data, t]);
|
|
244
263
|
|
|
245
264
|
// TECH-3712 UX: when the DP has exactly one consent group total
|
|
246
265
|
// (direct + nominated combined), the product-selection step is friction —
|
|
@@ -275,16 +294,20 @@ export default function Form({
|
|
|
275
294
|
[],
|
|
276
295
|
);
|
|
277
296
|
|
|
278
|
-
// Approach A: source the DSAR request-form product picker from
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
297
|
+
// Approach A: source the DSAR request-form product picker from user-consents
|
|
298
|
+
// rather than from Python's form/data, which stopped emitting product groups
|
|
299
|
+
// after the consent cutover. `consentClient.getUserConsents` serves the same
|
|
300
|
+
// grouped payload from either backend — the Go ledger's
|
|
301
|
+
// `/public/.../user-consents` when a ledgerBaseUrl is configured, Python's
|
|
302
|
+
// `/dsar/privacy-center/user-consents` otherwise (the same source the
|
|
303
|
+
// Manage Consent view reads in both modes) — so the overlay runs regardless
|
|
304
|
+
// of which backend the SDK points at. Only the end-user privacy-center path
|
|
305
|
+
// is eligible: popup / case-management resolve an arbitrary contact, which
|
|
306
|
+
// the JWT-scoped user-consents endpoint cannot serve, so those keep the
|
|
307
|
+
// form/data groups. Returns the form data unchanged when ineligible, when
|
|
308
|
+
// the read fails, or when it comes back with no groups at all — an empty
|
|
309
|
+
// result must never overwrite groups form/data did supply.
|
|
310
|
+
const withUserConsentPickerGroups = useCallback(
|
|
288
311
|
async (form: FormData, language?: string): Promise<FormData> => {
|
|
289
312
|
if (isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
|
|
290
313
|
return form;
|
|
@@ -301,15 +324,15 @@ export default function Form({
|
|
|
301
324
|
nominatedLimit: FORM_PICKER_GROUP_LIMIT,
|
|
302
325
|
language,
|
|
303
326
|
});
|
|
304
|
-
|
|
327
|
+
const direct = consents?.direct ?? [];
|
|
328
|
+
const nominated = consents?.nominated ?? [];
|
|
329
|
+
if (direct.length === 0 && nominated.length === 0) {
|
|
305
330
|
return form;
|
|
306
331
|
}
|
|
307
332
|
return {
|
|
308
333
|
...form,
|
|
309
|
-
direct_groups:
|
|
310
|
-
nominated_groups:
|
|
311
|
-
consentGroupToFormProductGroup,
|
|
312
|
-
),
|
|
334
|
+
direct_groups: direct.map(consentGroupToFormProductGroup),
|
|
335
|
+
nominated_groups: nominated.map(consentGroupToFormProductGroup),
|
|
313
336
|
};
|
|
314
337
|
},
|
|
315
338
|
[
|
|
@@ -362,7 +385,7 @@ export default function Form({
|
|
|
362
385
|
return;
|
|
363
386
|
}
|
|
364
387
|
|
|
365
|
-
setData(await
|
|
388
|
+
setData(await withUserConsentPickerGroups(translatedFormData, langValue));
|
|
366
389
|
|
|
367
390
|
if (translatedFormData.grievance_options) {
|
|
368
391
|
const currentGrievanceSelections = { ...selectedGrievances };
|
|
@@ -381,7 +404,7 @@ export default function Form({
|
|
|
381
404
|
} finally {
|
|
382
405
|
setIsTranslating(false);
|
|
383
406
|
}
|
|
384
|
-
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t,
|
|
407
|
+
}, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, selectedGrievances, t, withUserConsentPickerGroups]);
|
|
385
408
|
|
|
386
409
|
// Clear product / org-level selections when request type changes.
|
|
387
410
|
// Per-product state (selectedPurposes, selectedCorrectionElements,
|
|
@@ -534,7 +557,7 @@ export default function Form({
|
|
|
534
557
|
}
|
|
535
558
|
}
|
|
536
559
|
|
|
537
|
-
const pickerData = await
|
|
560
|
+
const pickerData = await withUserConsentPickerGroups(
|
|
538
561
|
displayData,
|
|
539
562
|
langParam || undefined,
|
|
540
563
|
);
|
|
@@ -561,7 +584,7 @@ export default function Form({
|
|
|
561
584
|
};
|
|
562
585
|
|
|
563
586
|
void initializePrivacyCenterForm();
|
|
564
|
-
}, [organizationId, workspaceId, isPopup, authContext, consentClient,
|
|
587
|
+
}, [organizationId, workspaceId, isPopup, authContext, consentClient, withUserConsentPickerGroups]);
|
|
565
588
|
|
|
566
589
|
/**
|
|
567
590
|
* Maps translated correction element names back to English names using UUID lookup.
|
|
@@ -667,7 +690,13 @@ export default function Form({
|
|
|
667
690
|
}
|
|
668
691
|
|
|
669
692
|
const isProductScoped = isProductScopedRequestType(requestType);
|
|
670
|
-
|
|
693
|
+
// The synthetic legacy-flat group has no server-side product, so it must
|
|
694
|
+
// not anchor the case — those submissions keep the pre-grouping
|
|
695
|
+
// org-level shape (`product_uuid` is optional on the backend).
|
|
696
|
+
const isLegacyFlatSelection =
|
|
697
|
+
selectedGroup?.product_uuid === LEGACY_PRODUCT_GROUP_UUID;
|
|
698
|
+
const anchorGroup =
|
|
699
|
+
isProductScoped && !isLegacyFlatSelection ? selectedGroup : null;
|
|
671
700
|
|
|
672
701
|
const requestDetails: {
|
|
673
702
|
purposes: UpdatePurpose[];
|
|
@@ -32,7 +32,7 @@ export const ModifyConsentModal = ({
|
|
|
32
32
|
action: 'revoke' as ConsentAction,
|
|
33
33
|
buttonLabel: t('revokeConsent'),
|
|
34
34
|
buttonVariant: 'danger' as const,
|
|
35
|
-
warningMessage: t('revokeConsentWarning'),
|
|
35
|
+
warningMessage: selectedRecord.revoke_warning_message || t('revokeConsentWarning'),
|
|
36
36
|
description: nominatorLabel
|
|
37
37
|
? t('aboutToModifyConsentOnBehalf', { nominator: nominatorLabel })
|
|
38
38
|
: t('aboutToModifyConsent'),
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ChevronRight } from 'lucide-react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { PCText, PCTitle } from '../ui';
|
|
4
|
+
import type { SelectUserProps } from '../lib/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* "Choose your profile" picker, shown when the signed-in contact maps to more
|
|
8
|
+
* than one data principal (shared contact). Selecting one re-scopes the session
|
|
9
|
+
* to that principal uuid. Purely presentational — SelectUserGate owns the fetch
|
|
10
|
+
* and the re-scope. Structural styles live in styles/injectStyles.ts
|
|
11
|
+
* (.pc-select-user*).
|
|
12
|
+
*/
|
|
13
|
+
export const SelectUser = ({
|
|
14
|
+
candidates,
|
|
15
|
+
onSelect,
|
|
16
|
+
busyOrgUserId,
|
|
17
|
+
error,
|
|
18
|
+
}: SelectUserProps) => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
|
|
21
|
+
// A candidate with no uuid can't be selected (it's the value we submit),
|
|
22
|
+
// so drop it rather than render an unclickable row.
|
|
23
|
+
const selectable = candidates.filter((candidate) => candidate.uuid);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="pc-select-user">
|
|
27
|
+
<div className="pc-select-user__header">
|
|
28
|
+
<PCTitle as="h2">{t('chooseYourProfile')}</PCTitle>
|
|
29
|
+
<PCText as="p" size="base" color="secondary" align="center">
|
|
30
|
+
{t('loginIdentifierLinkedToMultipleProfiles')}
|
|
31
|
+
</PCText>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="pc-select-user__list">
|
|
35
|
+
{selectable.map((candidate) => {
|
|
36
|
+
const principalUuid = candidate.uuid || '';
|
|
37
|
+
const email = candidate.primary_email || '';
|
|
38
|
+
const mobile = candidate.primary_mobile || '';
|
|
39
|
+
const orgUserId = candidate.org_user_id || '';
|
|
40
|
+
// org_user_id is unique per principal (ledger-enforced within the
|
|
41
|
+
// org/workspace), so it leads the row as the reliable way to tell
|
|
42
|
+
// shared-contact profiles apart when the email/mobile are identical.
|
|
43
|
+
// Fall back to a contact when a principal has no org_user_id, so the
|
|
44
|
+
// row still shows something identifiable.
|
|
45
|
+
const primaryLabel = orgUserId
|
|
46
|
+
? t('userIdLabel', { id: orgUserId })
|
|
47
|
+
: email || mobile || candidate.uuid;
|
|
48
|
+
// Email and mobile drop below the id. Skip any value that already
|
|
49
|
+
// surfaced as the primary label (the fallback case) or duplicates the
|
|
50
|
+
// other contact, so we never echo the same value twice.
|
|
51
|
+
const secondaryLabels = [email, mobile].filter(
|
|
52
|
+
(value, index, all) =>
|
|
53
|
+
value !== '' &&
|
|
54
|
+
value !== primaryLabel &&
|
|
55
|
+
all.indexOf(value) === index
|
|
56
|
+
);
|
|
57
|
+
const isBusy = busyOrgUserId === principalUuid;
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
key={candidate.uuid}
|
|
61
|
+
type="button"
|
|
62
|
+
className="pc-select-user__item"
|
|
63
|
+
onClick={() => onSelect(principalUuid)}
|
|
64
|
+
disabled={!principalUuid || busyOrgUserId !== null}
|
|
65
|
+
>
|
|
66
|
+
<span className="pc-select-user__item-text">
|
|
67
|
+
<span className="pc-select-user__item-primary">
|
|
68
|
+
{primaryLabel}
|
|
69
|
+
</span>
|
|
70
|
+
{secondaryLabels.map((label) => (
|
|
71
|
+
<span key={label} className="pc-select-user__item-secondary">
|
|
72
|
+
{label}
|
|
73
|
+
</span>
|
|
74
|
+
))}
|
|
75
|
+
</span>
|
|
76
|
+
{isBusy ? (
|
|
77
|
+
<span className="pc-select-user__item-status">
|
|
78
|
+
{t('signingInEllipsis')}
|
|
79
|
+
</span>
|
|
80
|
+
) : (
|
|
81
|
+
<ChevronRight size={18} className="pc-select-user__item-chevron" />
|
|
82
|
+
)}
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{error ? (
|
|
89
|
+
<p className="pc-select-user__error" role="alert">
|
|
90
|
+
{error}
|
|
91
|
+
</p>
|
|
92
|
+
) : null}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|