@redacto.io/consent-sdk-react 10.0.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 +14 -0
- package/dist/index.d.mts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.js +6 -8
- package/dist/index.mjs +6 -8
- package/dist/privacy-center.d.mts +60 -27
- package/dist/privacy-center.d.ts +60 -27
- package/dist/privacy-center.js +1872 -1363
- package/dist/privacy-center.mjs +1796 -1282
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1 -0
- package/src/RedactoNoticeConsent/api/index.ts +4 -4
- package/src/RedactoNoticeConsent/api/types.ts +3 -2
- package/src/RedactoNoticeConsent/types.ts +1 -1
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +1 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +4 -4
- package/src/RedactoNoticeConsentInline/api/types.ts +2 -1
- package/src/RedactoNoticeConsentInline/types.ts +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +1 -1
- package/src/RedactoPrivacyCenter/api/actions.ts +10 -4
- package/src/RedactoPrivacyCenter/api/client.ts +101 -21
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +64 -35
- package/src/RedactoPrivacyCenter/components/Form.tsx +7 -3
- 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/context/BaseUrlContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
- package/src/RedactoPrivacyCenter/context/types.ts +1 -1
- package/src/RedactoPrivacyCenter/lib/types.ts +54 -2
- 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 +53 -7
- package/tests/consentClient.test.ts +171 -0
- package/tests/manageConsentServer.test.ts +38 -0
package/package.json
CHANGED
|
@@ -131,6 +131,7 @@ export const fetchConsentContent = async ({
|
|
|
131
131
|
|
|
132
132
|
export const submitConsentEvent = async ({
|
|
133
133
|
accessToken,
|
|
134
|
+
baseUrl,
|
|
134
135
|
ledgerBaseUrl,
|
|
135
136
|
noticeUuid,
|
|
136
137
|
purposes,
|
|
@@ -155,10 +156,9 @@ export const submitConsentEvent = async ({
|
|
|
155
156
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const apiBaseUrl = ledgerBaseUrl;
|
|
159
|
+
// Fall back to the consent server when no ledger is configured, so existing
|
|
160
|
+
// integrations that only pass baseUrl remain backwards compatible.
|
|
161
|
+
const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
|
|
162
162
|
|
|
163
163
|
const validatedPurposes = purposes.map((purpose) => {
|
|
164
164
|
if (!purpose.uuid) {
|
|
@@ -46,7 +46,7 @@ export type NoticeConsentStatusResponse = {
|
|
|
46
46
|
|
|
47
47
|
export type FetchNoticeConsentStatusParams = {
|
|
48
48
|
accessToken: string;
|
|
49
|
-
ledgerBaseUrl
|
|
49
|
+
ledgerBaseUrl?: string;
|
|
50
50
|
noticeUuid: string;
|
|
51
51
|
signal?: AbortSignal;
|
|
52
52
|
};
|
|
@@ -230,7 +230,8 @@ export type Purpose = {
|
|
|
230
230
|
|
|
231
231
|
export type SubmitConsentEventParams = {
|
|
232
232
|
accessToken: string;
|
|
233
|
-
|
|
233
|
+
baseUrl?: string;
|
|
234
|
+
ledgerBaseUrl?: string;
|
|
234
235
|
noticeUuid: string;
|
|
235
236
|
purposes: Array<Purpose>;
|
|
236
237
|
declined: boolean;
|
|
@@ -47,16 +47,16 @@ export const submitConsentEvent = async ({
|
|
|
47
47
|
org_uuid,
|
|
48
48
|
workspace_uuid,
|
|
49
49
|
accessToken,
|
|
50
|
+
baseUrl,
|
|
50
51
|
ledgerBaseUrl,
|
|
51
52
|
noticeUuid,
|
|
52
53
|
purposes,
|
|
53
54
|
declined,
|
|
54
55
|
meta_data,
|
|
55
56
|
}: SubmitConsentEventParams): Promise<void> => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const apiBaseUrl = ledgerBaseUrl;
|
|
57
|
+
// Fall back to the consent server when no ledger is configured, so existing
|
|
58
|
+
// integrations that only pass baseUrl remain backwards compatible.
|
|
59
|
+
const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
|
|
60
60
|
|
|
61
61
|
const payload: ConsentEventPayload = {
|
|
62
62
|
notice_uuid: noticeUuid,
|
|
@@ -172,7 +172,8 @@ export type SubmitConsentEventParams = {
|
|
|
172
172
|
org_uuid: string;
|
|
173
173
|
workspace_uuid: string;
|
|
174
174
|
accessToken: string;
|
|
175
|
-
|
|
175
|
+
baseUrl?: string;
|
|
176
|
+
ledgerBaseUrl?: string;
|
|
176
177
|
noticeUuid: string;
|
|
177
178
|
purposes: Array<Purpose>;
|
|
178
179
|
declined: boolean;
|
|
@@ -12,6 +12,7 @@ import { useAuth } from './context/AuthContext';
|
|
|
12
12
|
import { PCLoader } from './ui';
|
|
13
13
|
import { pcStyles } from './styles/pcStyles';
|
|
14
14
|
import AppShell from './components/AppShell/AppShell';
|
|
15
|
+
import { SelectUserGate } from './components/SelectUserGate';
|
|
15
16
|
|
|
16
17
|
interface PrivacyCenterLayoutProps {
|
|
17
18
|
children: ReactNode;
|
|
@@ -164,11 +165,13 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
|
|
|
164
165
|
|
|
165
166
|
return (
|
|
166
167
|
<OrganizationProvider organization={organization}>
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
<SelectUserGate>
|
|
169
|
+
<DataAvailabilityProvider>
|
|
170
|
+
<AppShell onBack={onBack}>
|
|
171
|
+
{children}
|
|
172
|
+
</AppShell>
|
|
173
|
+
</DataAvailabilityProvider>
|
|
174
|
+
</SelectUserGate>
|
|
172
175
|
</OrganizationProvider>
|
|
173
176
|
);
|
|
174
177
|
}
|
|
@@ -15,7 +15,7 @@ injectPCStyles();
|
|
|
15
15
|
|
|
16
16
|
interface RedactoPrivacyCenterProps {
|
|
17
17
|
baseUrl: string; // Consent server base URL
|
|
18
|
-
ledgerBaseUrl
|
|
18
|
+
ledgerBaseUrl?: string; // Go ledger base URL — serves user-consents; omit to use the legacy Python privacy-center consent endpoints
|
|
19
19
|
slug: string;
|
|
20
20
|
accessToken: string;
|
|
21
21
|
refreshToken: string;
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
GetCaseMessagesParams,
|
|
25
25
|
MessageDocument,
|
|
26
26
|
GetNotificationsServerParams,
|
|
27
|
+
LegacyManageConsentResponse,
|
|
27
28
|
NotificationActionBaseParams,
|
|
28
29
|
NotificationActionResponse,
|
|
29
30
|
NotificationByUuidServerParams,
|
|
@@ -361,10 +362,12 @@ export async function manageConsentServer(params: {
|
|
|
361
362
|
// Privacy center endpoints use JWT Bearer tokens, not session-based auth
|
|
362
363
|
const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
|
|
363
364
|
|
|
364
|
-
//
|
|
365
|
-
// throws otherwise
|
|
366
|
-
// ConsentEventResponse
|
|
367
|
-
|
|
365
|
+
// Ledger mode routes to the Go ledger's submit-consent (partial), which
|
|
366
|
+
// resolves on 2xx and throws otherwise — a resolved call is success (the
|
|
367
|
+
// ledger returns a ConsentEventResponse, no `success` flag). Legacy mode
|
|
368
|
+
// hits the Python manage-consent endpoint, whose unwrapped response
|
|
369
|
+
// carries a `success` flag that must be honored even on 2xx.
|
|
370
|
+
const result = await params.consentClient.manageConsent(
|
|
368
371
|
baseRequest,
|
|
369
372
|
params.purposeId,
|
|
370
373
|
params.contact,
|
|
@@ -377,6 +380,9 @@ export async function manageConsentServer(params: {
|
|
|
377
380
|
params.noticeUuid,
|
|
378
381
|
params.dataElements
|
|
379
382
|
);
|
|
383
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
384
|
+
return Boolean((result as LegacyManageConsentResponse).success);
|
|
385
|
+
}
|
|
380
386
|
return true;
|
|
381
387
|
} catch (error: any) {
|
|
382
388
|
// Normalize nominator-specific errors into stable error codes
|
|
@@ -2,11 +2,13 @@ import { apiFetch, ApiError } from "./fetcher";
|
|
|
2
2
|
import type { ApiFetchOptions, ApiResponse } from "./fetcher";
|
|
3
3
|
import type {
|
|
4
4
|
CaseMessage,
|
|
5
|
+
IdentityListResponse,
|
|
5
6
|
MessageDocument,
|
|
6
7
|
NotificationActionResponse,
|
|
7
8
|
NotificationListResponse,
|
|
8
9
|
NotificationQueryParams,
|
|
9
10
|
NotificationSummary,
|
|
11
|
+
SelectPrincipalResponse,
|
|
10
12
|
UnreadCountResponse,
|
|
11
13
|
UploadDocumentResponse,
|
|
12
14
|
} from "../lib/types";
|
|
@@ -25,13 +27,14 @@ export type BaseRequest = {
|
|
|
25
27
|
* Creates a client instance with a specific base URL.
|
|
26
28
|
* @param baseUrl - Consent server base URL (Python service, e.g. http://host/consent)
|
|
27
29
|
* @param ledgerBaseUrl - Go ledger base URL (e.g. http://host:8090/consent-ledger).
|
|
28
|
-
* Used for user-consents reads
|
|
30
|
+
* Used for user-consents reads/writes served from the ledger's `/public/...` paths.
|
|
31
|
+
* When omitted, those calls fall back to the legacy Python
|
|
32
|
+
* `/dsar/privacy-center` endpoints and payloads (pre-ledger behavior), so
|
|
33
|
+
* integrations without a deployed ledger keep working.
|
|
29
34
|
*/
|
|
30
|
-
export function createConsentClient(baseUrl: string, ledgerBaseUrl
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const ledgerBase = ledgerBaseUrl.replace(/\/$/, "");
|
|
35
|
+
export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
|
|
36
|
+
const useLedger = Boolean(ledgerBaseUrl);
|
|
37
|
+
const ledgerBase = (ledgerBaseUrl ?? "").replace(/\/$/, "");
|
|
35
38
|
/**
|
|
36
39
|
* Privacy Center API fetch wrapper that uses Authorization: Bearer header.
|
|
37
40
|
* Privacy center endpoints require JWT tokens in Authorization header, not X-Auth-Token.
|
|
@@ -177,6 +180,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
function ledgerUrl(baseRequest: BaseRequest, path: string): string {
|
|
183
|
+
if (!useLedger) {
|
|
184
|
+
throw new Error("ledgerUrl called without a configured ledgerBaseUrl");
|
|
185
|
+
}
|
|
180
186
|
return `${ledgerBase}/public/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}${path}`;
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -384,7 +390,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
384
390
|
if (params.language) queryParams.append("language", params.language);
|
|
385
391
|
const queryString = queryParams.toString();
|
|
386
392
|
const suffix = `/user-consents${queryString ? `?${queryString}` : ""}`;
|
|
387
|
-
const url =
|
|
393
|
+
const url = useLedger
|
|
394
|
+
? ledgerUrl(baseRequest, suffix)
|
|
395
|
+
: privacyCenterUrl(baseRequest, suffix);
|
|
388
396
|
return privacyCenterApiFetch(url, {
|
|
389
397
|
...options,
|
|
390
398
|
method: "GET",
|
|
@@ -420,7 +428,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
420
428
|
if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
|
|
421
429
|
const queryString = queryParams.toString();
|
|
422
430
|
const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
|
|
423
|
-
const url =
|
|
431
|
+
const url = useLedger
|
|
432
|
+
? ledgerUrl(baseRequest, suffix)
|
|
433
|
+
: privacyCenterUrl(baseRequest, suffix);
|
|
424
434
|
return privacyCenterApiFetch(url, {
|
|
425
435
|
...options,
|
|
426
436
|
method: "GET",
|
|
@@ -432,18 +442,23 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
432
442
|
/**
|
|
433
443
|
* Manage consent (privacy center) - revoke, regrant, or renew.
|
|
434
444
|
*
|
|
435
|
-
* The Go ledger has no manage-consent route, so
|
|
436
|
-
* `partial` submit-consent on the ledger — mirroring
|
|
437
|
-
* relay (`build_consent_management_payload`): the
|
|
438
|
-
* re-submitted with `selected: false` to revoke or
|
|
439
|
-
* regrant/renew, and `partial: true` so the rest of the
|
|
440
|
-
* are left untouched. The principal is resolved from the
|
|
441
|
-
* `contact`/identifiers are sent in the body, and
|
|
442
|
-
* omitted so the ledger mints a fresh event.
|
|
445
|
+
* The Go ledger has no manage-consent route, so with a ledger configured
|
|
446
|
+
* this is expressed as a `partial` submit-consent on the ledger — mirroring
|
|
447
|
+
* the Python dual-write relay (`build_consent_management_payload`): the
|
|
448
|
+
* affected purpose is re-submitted with `selected: false` to revoke or
|
|
449
|
+
* `selected: true` to regrant/renew, and `partial: true` so the rest of the
|
|
450
|
+
* notice's purposes are left untouched. The principal is resolved from the
|
|
451
|
+
* JWT, so no `contact`/identifiers are sent in the body, and
|
|
452
|
+
* `consent_event_uuid` is omitted so the ledger mints a fresh event.
|
|
453
|
+
*
|
|
454
|
+
* Without a ledger, the legacy Python `/manage-consent?status=` endpoint is
|
|
455
|
+
* used with its `{ purpose_uuid, contact, ... }` payload.
|
|
443
456
|
*/
|
|
444
457
|
async manageConsent(
|
|
445
458
|
baseRequest: BaseRequest,
|
|
446
459
|
purposeId: string,
|
|
460
|
+
// Sent to the legacy Python endpoint only; the ledger resolves the
|
|
461
|
+
// principal from the JWT, so no identifiers are sent in ledger mode.
|
|
447
462
|
contact: string,
|
|
448
463
|
status: 'revoke' | 'regrant' | 'renew',
|
|
449
464
|
authToken?: string,
|
|
@@ -454,6 +469,25 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
454
469
|
noticeUuid?: string,
|
|
455
470
|
dataElements?: Array<{ uuid: string; selected: boolean }>
|
|
456
471
|
): Promise<unknown> {
|
|
472
|
+
if (!useLedger) {
|
|
473
|
+
const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
|
|
474
|
+
const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
|
|
475
|
+
if (nominatorContact) {
|
|
476
|
+
body.nominator_contact = nominatorContact;
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(dataElementUuids) && dataElementUuids.length > 0) {
|
|
479
|
+
body.data_element_uuids = dataElementUuids;
|
|
480
|
+
}
|
|
481
|
+
if (productUuid) {
|
|
482
|
+
body.product_uuid = productUuid;
|
|
483
|
+
}
|
|
484
|
+
return privacyCenterApiFetch(url, {
|
|
485
|
+
...options,
|
|
486
|
+
method: "POST",
|
|
487
|
+
body: JSON.stringify(body),
|
|
488
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
457
491
|
if (!noticeUuid) {
|
|
458
492
|
throw new Error("notice_uuid is required to manage consent via the ledger");
|
|
459
493
|
}
|
|
@@ -519,6 +553,44 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
519
553
|
});
|
|
520
554
|
},
|
|
521
555
|
|
|
556
|
+
/**
|
|
557
|
+
* List all data principals that share this session's contact
|
|
558
|
+
* (shared-contact profile picker). Identity is derived server-side from the
|
|
559
|
+
* JWT — the caller sends nothing but its bearer token.
|
|
560
|
+
*/
|
|
561
|
+
async listIdentities(
|
|
562
|
+
baseRequest: BaseRequest,
|
|
563
|
+
authToken?: string,
|
|
564
|
+
options?: Omit<ApiFetchOptions, "getAuthToken">
|
|
565
|
+
): Promise<IdentityListResponse> {
|
|
566
|
+
const url = privacyCenterUrl(baseRequest, '/identities');
|
|
567
|
+
return privacyCenterApiFetch<IdentityListResponse>(url, {
|
|
568
|
+
...options,
|
|
569
|
+
cache: 'no-store',
|
|
570
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Re-scope the session to a chosen principal uuid (must be one of the
|
|
576
|
+
* principals sharing this session's contact). Returns fresh access/refresh
|
|
577
|
+
* tokens scoped to that principal.
|
|
578
|
+
*/
|
|
579
|
+
async selectPrincipal(
|
|
580
|
+
baseRequest: BaseRequest,
|
|
581
|
+
principalUuid: string,
|
|
582
|
+
authToken?: string,
|
|
583
|
+
options?: Omit<ApiFetchOptions, "getAuthToken">
|
|
584
|
+
): Promise<SelectPrincipalResponse> {
|
|
585
|
+
const url = privacyCenterUrl(baseRequest, '/identities/select');
|
|
586
|
+
return privacyCenterApiFetch<SelectPrincipalResponse>(url, {
|
|
587
|
+
...options,
|
|
588
|
+
method: 'POST',
|
|
589
|
+
body: JSON.stringify({ uuid: principalUuid }),
|
|
590
|
+
authToken: resolveAuthToken(authToken, baseRequest),
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
|
|
522
594
|
/**
|
|
523
595
|
* Get receipts (privacy center) — consent receipts for the authenticated user
|
|
524
596
|
*/
|
|
@@ -547,8 +619,13 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
547
619
|
if (params.created_before) queryParams.append("created_before", params.created_before);
|
|
548
620
|
if (params.language) queryParams.append("language", params.language);
|
|
549
621
|
const queryString = queryParams.toString();
|
|
550
|
-
//
|
|
551
|
-
|
|
622
|
+
// With a ledger configured, receipts are served by the Go ledger
|
|
623
|
+
// (paginated list under /public/...); otherwise the legacy Python
|
|
624
|
+
// privacy-center list is used. normalizeReceipts tolerates both envelopes.
|
|
625
|
+
const receiptsSuffix = `/receipts${queryString ? `?${queryString}` : ""}`;
|
|
626
|
+
const url = useLedger
|
|
627
|
+
? ledgerUrl(baseRequest, receiptsSuffix)
|
|
628
|
+
: privacyCenterUrl(baseRequest, receiptsSuffix);
|
|
552
629
|
return privacyCenterApiFetch<unknown>(url, {
|
|
553
630
|
...options,
|
|
554
631
|
method: "GET",
|
|
@@ -568,9 +645,12 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl: string) {
|
|
|
568
645
|
receiptUuid: string,
|
|
569
646
|
authToken?: string
|
|
570
647
|
): Promise<Blob> {
|
|
571
|
-
// The ledger serves the receipt PDF at /receipts/{uuid}/download
|
|
572
|
-
//
|
|
573
|
-
|
|
648
|
+
// The ledger serves the receipt PDF at /receipts/{uuid}/download, the
|
|
649
|
+
// legacy Python service at /receipts/{uuid}/pdf. The receiptUuid always
|
|
650
|
+
// comes from the same-mode list above, so list and download stay consistent.
|
|
651
|
+
const url = useLedger
|
|
652
|
+
? ledgerUrl(baseRequest, `/receipts/${receiptUuid}/download`)
|
|
653
|
+
: privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
|
|
574
654
|
const token = resolveAuthToken(authToken, baseRequest);
|
|
575
655
|
const requestHeaders = new Headers();
|
|
576
656
|
requestHeaders.set("Accept", "application/pdf");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2
|
-
import { ChevronDown } from 'lucide-react';
|
|
2
|
+
import { ChevronDown, 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';
|
|
7
|
+
import { useProfileSwitch } from '../../context/ProfileSwitchContext';
|
|
6
8
|
import { getVar } from '../../lib/utils';
|
|
7
9
|
import { usePrivacyCenterExitAction } from './usePrivacyCenterExitAction';
|
|
8
10
|
|
|
@@ -30,6 +32,7 @@ function initialsFromEmail(email: string): string {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function UserMenu({ onBack }: UserMenuProps) {
|
|
35
|
+
const { t } = useTranslation();
|
|
33
36
|
const branding = useBranding();
|
|
34
37
|
const { organization } = useOrganization();
|
|
35
38
|
const [email, setEmail] = useState('');
|
|
@@ -40,6 +43,7 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
40
43
|
|
|
41
44
|
const { handleAction, label: actionLabel, Icon: ActionIcon } =
|
|
42
45
|
usePrivacyCenterExitAction(onBack);
|
|
46
|
+
const { canSwitch, switchProfile } = useProfileSwitch();
|
|
43
47
|
|
|
44
48
|
const displayName = branding?.brand_name || organization?.name || '';
|
|
45
49
|
const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
|
|
@@ -57,6 +61,25 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
57
61
|
transition: 'background 150ms ease, border-color 150ms ease, box-shadow 150ms ease',
|
|
58
62
|
};
|
|
59
63
|
|
|
64
|
+
// Shared style for the popover's action rows (switch profile, back/sign out).
|
|
65
|
+
const menuItemStyle: CSSProperties = {
|
|
66
|
+
display: 'flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
gap: 10,
|
|
69
|
+
width: '100%',
|
|
70
|
+
padding: '10px 14px',
|
|
71
|
+
border: 'none',
|
|
72
|
+
borderRadius: 8,
|
|
73
|
+
background: 'transparent',
|
|
74
|
+
color: getVar('--pc-text-primary'),
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
fontFamily: 'inherit',
|
|
77
|
+
fontSize: 14,
|
|
78
|
+
fontWeight: 500,
|
|
79
|
+
textAlign: 'left',
|
|
80
|
+
transition: 'background 150ms ease',
|
|
81
|
+
};
|
|
82
|
+
|
|
60
83
|
const trigger = (
|
|
61
84
|
<div
|
|
62
85
|
style={triggerStyle}
|
|
@@ -139,7 +162,10 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
139
162
|
display: 'flex',
|
|
140
163
|
alignItems: 'center',
|
|
141
164
|
gap: 10,
|
|
142
|
-
borderBottom:
|
|
165
|
+
borderBottom:
|
|
166
|
+
onBack || canSwitch
|
|
167
|
+
? `1px solid ${getVar('--pc-border-color')}`
|
|
168
|
+
: 'none',
|
|
143
169
|
}}
|
|
144
170
|
>
|
|
145
171
|
<PCAvatar name={initialsFromEmail(email)} size="sm" color="info" />
|
|
@@ -162,40 +188,43 @@ export function UserMenu({ onBack }: UserMenuProps) {
|
|
|
162
188
|
</div>
|
|
163
189
|
)}
|
|
164
190
|
|
|
165
|
-
{/* Action
|
|
166
|
-
{onBack && (
|
|
191
|
+
{/* Action rows */}
|
|
192
|
+
{(canSwitch || onBack) && (
|
|
167
193
|
<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
|
-
|
|
194
|
+
{canSwitch && (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
role="menuitem"
|
|
198
|
+
onClick={switchProfile}
|
|
199
|
+
style={menuItemStyle}
|
|
200
|
+
onMouseEnter={(e) => {
|
|
201
|
+
e.currentTarget.style.background = getVar('--pc-bg-hover');
|
|
202
|
+
}}
|
|
203
|
+
onMouseLeave={(e) => {
|
|
204
|
+
e.currentTarget.style.background = 'transparent';
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<Users size={16} strokeWidth={2} />
|
|
208
|
+
<span>{t('switchProfile')}</span>
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
{onBack && (
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
role="menuitem"
|
|
215
|
+
onClick={handleAction}
|
|
216
|
+
style={menuItemStyle}
|
|
217
|
+
onMouseEnter={(e) => {
|
|
218
|
+
e.currentTarget.style.background = getVar('--pc-bg-hover');
|
|
219
|
+
}}
|
|
220
|
+
onMouseLeave={(e) => {
|
|
221
|
+
e.currentTarget.style.background = 'transparent';
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<ActionIcon size={16} strokeWidth={2} />
|
|
225
|
+
<span>{actionLabel}</span>
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
199
228
|
</div>
|
|
200
229
|
)}
|
|
201
230
|
</div>
|
|
@@ -143,7 +143,7 @@ export default function Form({
|
|
|
143
143
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
144
144
|
const { t } = useTranslation();
|
|
145
145
|
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
146
|
-
const { consentClient, baseUrl } = useBaseUrl();
|
|
146
|
+
const { consentClient, baseUrl, ledgerBaseUrl } = useBaseUrl();
|
|
147
147
|
const organizationId = organization?.uuid;
|
|
148
148
|
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
149
149
|
|
|
@@ -283,10 +283,13 @@ export default function Form({
|
|
|
283
283
|
// an arbitrary contact, which the JWT-scoped ledger endpoint cannot serve,
|
|
284
284
|
// so those keep the Python form/data groups. Returns the form data unchanged
|
|
285
285
|
// when ineligible or when the ledger read fails (graceful fallback to
|
|
286
|
-
// whatever groups form/data supplied).
|
|
286
|
+
// whatever groups form/data supplied). Without a configured ledgerBaseUrl,
|
|
287
|
+
// consents still live in Python and form/data groups are authoritative, so
|
|
288
|
+
// the overlay must not run (fetchUserConsents would return the legacy Python
|
|
289
|
+
// shape, which consentGroupToFormProductGroup does not map).
|
|
287
290
|
const withLedgerPickerGroups = useCallback(
|
|
288
291
|
async (form: FormData, language?: string): Promise<FormData> => {
|
|
289
|
-
if (isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
|
|
292
|
+
if (!ledgerBaseUrl || isPopup || isCaseManagementContext || !organizationId || !workspaceId) {
|
|
290
293
|
return form;
|
|
291
294
|
}
|
|
292
295
|
const consents = await fetchUserConsents({
|
|
@@ -313,6 +316,7 @@ export default function Form({
|
|
|
313
316
|
};
|
|
314
317
|
},
|
|
315
318
|
[
|
|
319
|
+
ledgerBaseUrl,
|
|
316
320
|
isPopup,
|
|
317
321
|
isCaseManagementContext,
|
|
318
322
|
organizationId,
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
email,
|
|
15
|
+
candidates,
|
|
16
|
+
onSelect,
|
|
17
|
+
busyOrgUserId,
|
|
18
|
+
error,
|
|
19
|
+
}: SelectUserProps) => {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const subtitle = email
|
|
22
|
+
? t('emailLinkedToMultipleProfiles', { email })
|
|
23
|
+
: t('contactLinkedToMultipleProfiles');
|
|
24
|
+
|
|
25
|
+
// A candidate with no uuid can't be selected (it's the value we submit),
|
|
26
|
+
// so drop it rather than render an unclickable row.
|
|
27
|
+
const selectable = candidates.filter((candidate) => candidate.uuid);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="pc-select-user">
|
|
31
|
+
<div className="pc-select-user__header">
|
|
32
|
+
<PCTitle as="h2">{t('chooseYourProfile')}</PCTitle>
|
|
33
|
+
<PCText as="p" size="base" color="secondary" align="center">
|
|
34
|
+
{subtitle}
|
|
35
|
+
</PCText>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="pc-select-user__list">
|
|
39
|
+
{selectable.map((candidate) => {
|
|
40
|
+
const principalUuid = candidate.uuid || '';
|
|
41
|
+
const primaryLabel =
|
|
42
|
+
candidate.primary_email || candidate.primary_mobile || candidate.uuid;
|
|
43
|
+
const secondaryLabel =
|
|
44
|
+
candidate.primary_mobile || candidate.primary_email || '';
|
|
45
|
+
const isBusy = busyOrgUserId === principalUuid;
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
key={candidate.uuid}
|
|
49
|
+
type="button"
|
|
50
|
+
className="pc-select-user__item"
|
|
51
|
+
onClick={() => onSelect(principalUuid)}
|
|
52
|
+
disabled={!principalUuid || busyOrgUserId !== null}
|
|
53
|
+
>
|
|
54
|
+
<span className="pc-select-user__item-text">
|
|
55
|
+
<span className="pc-select-user__item-primary">
|
|
56
|
+
{primaryLabel}
|
|
57
|
+
</span>
|
|
58
|
+
{secondaryLabel ? (
|
|
59
|
+
<span className="pc-select-user__item-secondary">
|
|
60
|
+
{secondaryLabel}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
</span>
|
|
64
|
+
{isBusy ? (
|
|
65
|
+
<span className="pc-select-user__item-status">
|
|
66
|
+
{t('signingInEllipsis')}
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
69
|
+
<ChevronRight size={18} className="pc-select-user__item-chevron" />
|
|
70
|
+
)}
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{error ? (
|
|
77
|
+
<p className="pc-select-user__error" role="alert">
|
|
78
|
+
{error}
|
|
79
|
+
</p>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|