@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.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +22 -25
  2. package/CHANGELOG.md +16 -2
  3. package/dist/index.d.mts +13 -4
  4. package/dist/index.d.ts +13 -4
  5. package/dist/index.js +116 -11
  6. package/dist/index.mjs +116 -11
  7. package/dist/privacy-center.d.mts +76 -19
  8. package/dist/privacy-center.d.ts +76 -19
  9. package/dist/privacy-center.js +2022 -1378
  10. package/dist/privacy-center.mjs +1946 -1297
  11. package/package.json +1 -1
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +5 -0
  13. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +29 -3
  14. package/src/RedactoNoticeConsent/api/index.ts +130 -1
  15. package/src/RedactoNoticeConsent/api/types.ts +33 -0
  16. package/src/RedactoNoticeConsent/types.ts +1 -0
  17. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +3 -0
  18. package/src/RedactoNoticeConsentInline/api/index.ts +4 -1
  19. package/src/RedactoNoticeConsentInline/api/types.ts +1 -0
  20. package/src/RedactoNoticeConsentInline/types.ts +1 -0
  21. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
  22. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +3 -1
  23. package/src/RedactoPrivacyCenter/api/actions.ts +15 -2
  24. package/src/RedactoPrivacyCenter/api/client.ts +146 -20
  25. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
  26. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +12 -1
  27. package/src/RedactoPrivacyCenter/components/Form.tsx +63 -7
  28. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +83 -0
  29. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +193 -0
  30. package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
  31. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +64 -1
  32. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +8 -2
  33. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
  34. package/src/RedactoPrivacyCenter/context/types.ts +1 -0
  35. package/src/RedactoPrivacyCenter/lib/api.ts +8 -0
  36. package/src/RedactoPrivacyCenter/lib/constants.ts +9 -0
  37. package/src/RedactoPrivacyCenter/lib/types.ts +63 -1
  38. package/src/RedactoPrivacyCenter/locales/as/translation.json +7 -1
  39. package/src/RedactoPrivacyCenter/locales/bho/translation.json +7 -1
  40. package/src/RedactoPrivacyCenter/locales/bn/translation.json +7 -1
  41. package/src/RedactoPrivacyCenter/locales/brx/translation.json +7 -1
  42. package/src/RedactoPrivacyCenter/locales/doi/translation.json +7 -1
  43. package/src/RedactoPrivacyCenter/locales/en/translation.json +7 -1
  44. package/src/RedactoPrivacyCenter/locales/gom/translation.json +7 -1
  45. package/src/RedactoPrivacyCenter/locales/gu/translation.json +7 -1
  46. package/src/RedactoPrivacyCenter/locales/hi/translation.json +7 -1
  47. package/src/RedactoPrivacyCenter/locales/kn/translation.json +7 -1
  48. package/src/RedactoPrivacyCenter/locales/ks/translation.json +7 -1
  49. package/src/RedactoPrivacyCenter/locales/mai/translation.json +7 -1
  50. package/src/RedactoPrivacyCenter/locales/ml/translation.json +7 -1
  51. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +7 -1
  52. package/src/RedactoPrivacyCenter/locales/mr/translation.json +7 -1
  53. package/src/RedactoPrivacyCenter/locales/ne/translation.json +7 -1
  54. package/src/RedactoPrivacyCenter/locales/or/translation.json +7 -1
  55. package/src/RedactoPrivacyCenter/locales/pa/translation.json +7 -1
  56. package/src/RedactoPrivacyCenter/locales/sa/translation.json +7 -1
  57. package/src/RedactoPrivacyCenter/locales/sat/translation.json +7 -1
  58. package/src/RedactoPrivacyCenter/locales/sd/translation.json +7 -1
  59. package/src/RedactoPrivacyCenter/locales/ta/translation.json +7 -1
  60. package/src/RedactoPrivacyCenter/locales/te/translation.json +7 -1
  61. package/src/RedactoPrivacyCenter/locales/ur/translation.json +7 -1
  62. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +105 -0
  63. package/tests/Form.test.tsx +58 -7
  64. package/tests/consentClient.test.ts +171 -0
  65. package/tests/manageConsentServer.test.ts +38 -0
  66. 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 { DataElement, FormProductGroup, Purpose } from '../lib/types';
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(() => createConsentClient(baseUrl), [baseUrl]);
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);
@@ -14,5 +14,6 @@ export type AuthContextValue = Readonly<{
14
14
 
15
15
  export type BaseUrlContextValue = Readonly<{
16
16
  baseUrl: string;
17
+ ledgerBaseUrl?: string;
17
18
  consentClient: ReturnType<typeof createConsentClient>;
18
19
  }>;
@@ -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
- pagination: Pagination;
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
  }