@redacto.io/consent-sdk-react 10.0.0-beta.1 → 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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +78 -0
  4. package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
  5. package/dist/index.d.mts +32 -4
  6. package/dist/index.d.ts +32 -4
  7. package/dist/index.js +340 -230
  8. package/dist/index.mjs +339 -231
  9. package/dist/privacy-center.d.mts +55 -12
  10. package/dist/privacy-center.d.ts +55 -12
  11. package/dist/privacy-center.js +848 -950
  12. package/dist/privacy-center.mjs +698 -802
  13. package/package.json +1 -1
  14. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +87 -0
  15. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +27 -0
  16. package/src/RedactoNoticeAssisted/api/index.ts +7 -1
  17. package/src/RedactoNoticeAssisted/api/types.ts +2 -0
  18. package/src/RedactoNoticeAssisted/i18n.ts +0 -55
  19. package/src/RedactoNoticeAssisted/types.ts +2 -0
  20. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +42 -8
  21. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +74 -33
  22. package/src/RedactoNoticeConsent/api/index.ts +88 -175
  23. package/src/RedactoNoticeConsent/api/sandbox.ts +150 -0
  24. package/src/RedactoNoticeConsent/api/types.ts +58 -43
  25. package/src/RedactoNoticeConsent/language-codes.ts +0 -2
  26. package/src/RedactoNoticeConsent/types.ts +28 -2
  27. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +85 -5
  28. package/src/RedactoNoticeConsentInline/api/index.ts +10 -1
  29. package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +72 -0
  30. package/src/RedactoNoticeConsentInline/api/types.ts +2 -0
  31. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +37 -2
  32. package/src/RedactoPrivacyCenter/api/actions.ts +3 -1
  33. package/src/RedactoPrivacyCenter/api/client.ts +122 -25
  34. package/src/RedactoPrivacyCenter/api/fetcher.ts +24 -9
  35. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +28 -0
  36. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +47 -45
  37. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +2 -1
  38. package/src/RedactoPrivacyCenter/components/Form.tsx +55 -30
  39. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
  40. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +25 -13
  41. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +8 -6
  42. package/src/RedactoPrivacyCenter/components/constant.ts +3 -3
  43. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +42 -6
  44. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +131 -39
  45. package/src/RedactoPrivacyCenter/context/types.ts +38 -0
  46. package/src/RedactoPrivacyCenter/lib/api.ts +3 -0
  47. package/src/RedactoPrivacyCenter/lib/constants.ts +12 -6
  48. package/src/RedactoPrivacyCenter/lib/locales.ts +0 -3
  49. package/src/RedactoPrivacyCenter/lib/sandbox.ts +104 -0
  50. package/src/RedactoPrivacyCenter/lib/types.ts +25 -1
  51. package/src/RedactoPrivacyCenter/lib/utils.ts +15 -0
  52. package/src/RedactoPrivacyCenter/locales/as/translation.json +3 -3
  53. package/src/RedactoPrivacyCenter/locales/bn/translation.json +3 -3
  54. package/src/RedactoPrivacyCenter/locales/brx/translation.json +3 -3
  55. package/src/RedactoPrivacyCenter/locales/doi/translation.json +3 -3
  56. package/src/RedactoPrivacyCenter/locales/en/translation.json +4 -3
  57. package/src/RedactoPrivacyCenter/locales/gom/translation.json +3 -3
  58. package/src/RedactoPrivacyCenter/locales/gu/translation.json +3 -3
  59. package/src/RedactoPrivacyCenter/locales/hi/translation.json +3 -3
  60. package/src/RedactoPrivacyCenter/locales/kn/translation.json +3 -3
  61. package/src/RedactoPrivacyCenter/locales/ks/translation.json +3 -3
  62. package/src/RedactoPrivacyCenter/locales/mai/translation.json +3 -3
  63. package/src/RedactoPrivacyCenter/locales/ml/translation.json +3 -3
  64. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +3 -3
  65. package/src/RedactoPrivacyCenter/locales/mr/translation.json +3 -3
  66. package/src/RedactoPrivacyCenter/locales/ne/translation.json +3 -3
  67. package/src/RedactoPrivacyCenter/locales/or/translation.json +3 -3
  68. package/src/RedactoPrivacyCenter/locales/pa/translation.json +3 -3
  69. package/src/RedactoPrivacyCenter/locales/sa/translation.json +3 -3
  70. package/src/RedactoPrivacyCenter/locales/sat/translation.json +3 -3
  71. package/src/RedactoPrivacyCenter/locales/sd/translation.json +3 -3
  72. package/src/RedactoPrivacyCenter/locales/ta/translation.json +3 -3
  73. package/src/RedactoPrivacyCenter/locales/te/translation.json +3 -3
  74. package/src/RedactoPrivacyCenter/locales/ur/translation.json +3 -3
  75. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +7 -2
  76. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +17 -0
  77. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +83 -0
  78. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +33 -9
  79. package/src/shared/notice-consent-status.ts +190 -0
  80. package/src/shared/sandbox.ts +6 -0
  81. package/tests/Form.test.tsx +142 -5
  82. package/tests/RevokeWarningMessage.test.tsx +151 -0
  83. package/tests/consentClient.test.ts +46 -0
  84. package/src/RedactoPrivacyCenter/locales/bho/translation.json +0 -340
@@ -1,5 +1,11 @@
1
1
  import { apiFetch, ApiError } from "./fetcher";
2
2
  import type { ApiFetchOptions, ApiResponse } from "./fetcher";
3
+ import {
4
+ isSandboxMode,
5
+ sandboxAuthHeaders,
6
+ sandboxContact,
7
+ sandboxQueryIdentity,
8
+ } from "../lib/sandbox";
3
9
  import type {
4
10
  CaseMessage,
5
11
  IdentityListResponse,
@@ -33,7 +39,11 @@ export type BaseRequest = {
33
39
  * integrations without a deployed ledger keep working.
34
40
  */
35
41
  export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
36
- const useLedger = Boolean(ledgerBaseUrl);
42
+ const ledgerConfigured = Boolean(ledgerBaseUrl);
43
+ // Sandbox mode never routes to the Go ledger (it has no sandbox / test::
44
+ // support). A sandbox token forces the legacy consent-server path regardless
45
+ // of ledgerBaseUrl, so isolation can't be bypassed by also configuring a ledger.
46
+ const useLedger = () => ledgerConfigured && !isSandboxMode();
37
47
  const ledgerBase = (ledgerBaseUrl ?? "").replace(/\/$/, "");
38
48
  /**
39
49
  * Privacy Center API fetch wrapper that uses Authorization: Bearer header.
@@ -72,15 +82,26 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
72
82
  }
73
83
  }
74
84
 
75
- // Add Authorization Bearer token for privacy center APIs
76
- const token = authToken || (getAuthToken ? await getAuthToken() : null);
77
- if (token) {
78
- const trimmedToken = token.trim();
79
- // Format token with Bearer prefix if not already present
80
- const bearerToken = trimmedToken.startsWith('Bearer ')
81
- ? trimmedToken
82
- : `Bearer ${trimmedToken}`;
83
- requestHeaders.set("Authorization", bearerToken);
85
+ // Sandbox PoC: send only the X-Consent-Token credential (no JWT bearer). Set
86
+ // last so it wins over any caller-supplied Authorization header. The acting
87
+ // identity travels in the request payload -- query params on reads, the body
88
+ // on writes -- never a header.
89
+ const sandboxHeaders = sandboxAuthHeaders();
90
+ if (sandboxHeaders) {
91
+ Object.entries(sandboxHeaders).forEach(([key, value]) => {
92
+ requestHeaders.set(key, value);
93
+ });
94
+ } else {
95
+ // Add Authorization Bearer token for privacy center APIs
96
+ const token = authToken || (getAuthToken ? await getAuthToken() : null);
97
+ if (token) {
98
+ const trimmedToken = token.trim();
99
+ // Format token with Bearer prefix if not already present
100
+ const bearerToken = trimmedToken.startsWith('Bearer ')
101
+ ? trimmedToken
102
+ : `Bearer ${trimmedToken}`;
103
+ requestHeaders.set("Authorization", bearerToken);
104
+ }
84
105
  }
85
106
 
86
107
  // Build fetch options
@@ -180,7 +201,7 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
180
201
  }
181
202
 
182
203
  function ledgerUrl(baseRequest: BaseRequest, path: string): string {
183
- if (!useLedger) {
204
+ if (!useLedger()) {
184
205
  throw new Error("ledgerUrl called without a configured ledgerBaseUrl");
185
206
  }
186
207
  return `${ledgerBase}/public/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}${path}`;
@@ -191,13 +212,47 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
191
212
  }
192
213
 
193
214
  function notificationUrl(baseRequest: BaseRequest, path = ''): string {
194
- return privacyCenterUrl(baseRequest, `/notifications${path}`);
215
+ const identitySuffix = sandboxIdentitySuffix();
216
+ if (!identitySuffix) {
217
+ return privacyCenterUrl(baseRequest, `/notifications${path}`);
218
+ }
219
+ const separator = path.includes('?') ? `&${identitySuffix.slice(1)}` : identitySuffix;
220
+ return privacyCenterUrl(baseRequest, `/notifications${path}${separator}`);
195
221
  }
196
222
 
197
223
  function resolveAuthToken(authToken: string | undefined, baseRequest: BaseRequest): string {
198
224
  return authToken || baseRequest.authToken;
199
225
  }
200
226
 
227
+ /**
228
+ * Append the sandbox acting identity (`primary_email` / `primary_mobile`) to a
229
+ * query-param bag when sandbox mode is active; a no-op otherwise. The server
230
+ * resolves the acting test identity from these query params on reads (there is
231
+ * no subject header). The credential is the X-Consent-Token header set in
232
+ * privacyCenterApiFetch.
233
+ */
234
+ function appendSandboxIdentity(queryParams: URLSearchParams): void {
235
+ const identity = sandboxQueryIdentity();
236
+ if (identity) {
237
+ Object.entries(identity).forEach(([key, value]) => {
238
+ queryParams.append(key, value);
239
+ });
240
+ }
241
+ }
242
+
243
+ /**
244
+ * The sandbox acting-identity query suffix (`?primary_email=...` /
245
+ * `?primary_mobile=...`) for endpoints that build a bare URL with no other
246
+ * query params (the case-communication reads/POSTs). Empty string outside
247
+ * sandbox mode, so the JWT path is unchanged.
248
+ */
249
+ function sandboxIdentitySuffix(): string {
250
+ const queryParams = new URLSearchParams();
251
+ appendSandboxIdentity(queryParams);
252
+ const queryString = queryParams.toString();
253
+ return queryString ? `?${queryString}` : "";
254
+ }
255
+
201
256
  return {
202
257
  /**
203
258
  * Get form data (privacy center)
@@ -247,13 +302,23 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
247
302
  const url = privacyCenterUrl(baseRequest, '/case');
248
303
  const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
249
304
 
305
+ // Sandbox mode: the acting test identity is the single `contact` field
306
+ // (the server has no subject header; it namespaces this identity `test::`).
307
+ // Merge it into the body only when sandbox mode is active and the payload
308
+ // is a plain object; the JWT path is left byte-for-byte unchanged.
309
+ const contact = sandboxContact();
310
+ const requestPayload =
311
+ contact && payload && typeof payload === "object"
312
+ ? { ...(payload as Record<string, unknown>), contact }
313
+ : payload;
314
+
250
315
  // Case management uses session-based auth (X-Auth-Token)
251
316
  // Privacy center uses JWT Bearer tokens (Authorization: Bearer)
252
317
  if (useSessionAuth) {
253
318
  return apiFetch(url, {
254
319
  ...fetchOptions,
255
320
  method: "POST",
256
- body: JSON.stringify(payload),
321
+ body: JSON.stringify(requestPayload),
257
322
  getAuthToken,
258
323
  });
259
324
  }
@@ -262,7 +327,7 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
262
327
  return privacyCenterApiFetch(url, {
263
328
  ...fetchOptions,
264
329
  method: "POST",
265
- body: JSON.stringify(payload),
330
+ body: JSON.stringify(requestPayload),
266
331
  authToken: resolveAuthToken(authToken, baseRequest),
267
332
  });
268
333
  },
@@ -388,9 +453,12 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
388
453
  if (params.status) queryParams.append("status", params.status);
389
454
  if (params.product_uuid) queryParams.append("product_uuid", params.product_uuid);
390
455
  if (params.language) queryParams.append("language", params.language);
456
+ // Sandbox mode: the acting identity travels as query params
457
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
458
+ appendSandboxIdentity(queryParams);
391
459
  const queryString = queryParams.toString();
392
460
  const suffix = `/user-consents${queryString ? `?${queryString}` : ""}`;
393
- const url = useLedger
461
+ const url = useLedger()
394
462
  ? ledgerUrl(baseRequest, suffix)
395
463
  : privacyCenterUrl(baseRequest, suffix);
396
464
  return privacyCenterApiFetch(url, {
@@ -426,9 +494,12 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
426
494
  if (params.search) queryParams.append("search", params.search);
427
495
  if (params.language) queryParams.append("language", params.language);
428
496
  if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
497
+ // Sandbox mode: the acting identity travels as query params
498
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
499
+ appendSandboxIdentity(queryParams);
429
500
  const queryString = queryParams.toString();
430
501
  const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
431
- const url = useLedger
502
+ const url = useLedger()
432
503
  ? ledgerUrl(baseRequest, suffix)
433
504
  : privacyCenterUrl(baseRequest, suffix);
434
505
  return privacyCenterApiFetch(url, {
@@ -467,9 +538,10 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
467
538
  dataElementUuids?: string[],
468
539
  productUuid?: string,
469
540
  noticeUuid?: string,
470
- dataElements?: Array<{ uuid: string; selected: boolean }>
541
+ dataElements?: Array<{ uuid: string; selected: boolean }>,
542
+ language?: string
471
543
  ): Promise<unknown> {
472
- if (!useLedger) {
544
+ if (!useLedger()) {
473
545
  const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
474
546
  const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
475
547
  if (nominatorContact) {
@@ -481,6 +553,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
481
553
  if (productUuid) {
482
554
  body.product_uuid = productUuid;
483
555
  }
556
+ if (language) {
557
+ body.language = language;
558
+ }
484
559
  return privacyCenterApiFetch(url, {
485
560
  ...options,
486
561
  method: "POST",
@@ -507,7 +582,7 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
507
582
  }
508
583
  return { uuid: de.uuid, selected };
509
584
  });
510
- const body = {
585
+ const body: Record<string, unknown> = {
511
586
  notice_uuid: noticeUuid,
512
587
  partial: true,
513
588
  declined: false,
@@ -520,6 +595,7 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
520
595
  data_elements: elements,
521
596
  },
522
597
  ],
598
+ ...(language ? { language } : {}),
523
599
  };
524
600
  const url = ledgerUrl(baseRequest, "/submit-consent");
525
601
  return privacyCenterApiFetch(url, {
@@ -545,6 +621,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
545
621
  if (offset !== undefined) queryParams.append("offset", offset.toString());
546
622
  if (limit !== undefined) queryParams.append("limit", limit.toString());
547
623
  if (language) queryParams.append("language", language);
624
+ // Sandbox mode: the acting identity travels as query params
625
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
626
+ appendSandboxIdentity(queryParams);
548
627
  const queryString = queryParams.toString();
549
628
  const url = privacyCenterUrl(baseRequest, `/activities${queryString ? `?${queryString}` : ""}`);
550
629
  return privacyCenterApiFetch<unknown>(url, {
@@ -623,7 +702,7 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
623
702
  // (paginated list under /public/...); otherwise the legacy Python
624
703
  // privacy-center list is used. normalizeReceipts tolerates both envelopes.
625
704
  const receiptsSuffix = `/receipts${queryString ? `?${queryString}` : ""}`;
626
- const url = useLedger
705
+ const url = useLedger()
627
706
  ? ledgerUrl(baseRequest, receiptsSuffix)
628
707
  : privacyCenterUrl(baseRequest, receiptsSuffix);
629
708
  return privacyCenterApiFetch<unknown>(url, {
@@ -648,13 +727,22 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
648
727
  // The ledger serves the receipt PDF at /receipts/{uuid}/download, the
649
728
  // legacy Python service at /receipts/{uuid}/pdf. The receiptUuid always
650
729
  // comes from the same-mode list above, so list and download stay consistent.
651
- const url = useLedger
730
+ const url = useLedger()
652
731
  ? ledgerUrl(baseRequest, `/receipts/${receiptUuid}/download`)
653
732
  : privacyCenterUrl(baseRequest, `/receipts/${receiptUuid}/pdf`);
654
733
  const token = resolveAuthToken(authToken, baseRequest);
655
734
  const requestHeaders = new Headers();
656
735
  requestHeaders.set("Accept", "application/pdf");
657
- if (token) {
736
+ // Sandbox PoC: send the single X-Consent-Token header instead of the JWT
737
+ // bearer. The receipt PDF download identifies the principal by receiptUuid
738
+ // (already scoped to the sandbox subject via the same-mode list), so no
739
+ // extra identity payload is needed here.
740
+ const sandboxHeaders = sandboxAuthHeaders();
741
+ if (sandboxHeaders) {
742
+ Object.entries(sandboxHeaders).forEach(([key, value]) => {
743
+ requestHeaders.set(key, value);
744
+ });
745
+ } else if (token) {
658
746
  const trimmedToken = token.trim();
659
747
  requestHeaders.set(
660
748
  "Authorization",
@@ -697,6 +785,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
697
785
  if (offset !== undefined) queryParams.append("offset", offset.toString());
698
786
  if (limit !== undefined) queryParams.append("limit", limit.toString());
699
787
  if (language) queryParams.append("language", language);
788
+ // Sandbox mode: the acting identity travels as query params
789
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
790
+ appendSandboxIdentity(queryParams);
700
791
  const queryString = queryParams.toString();
701
792
  const url = privacyCenterUrl(baseRequest, `/cases?${queryString}`);
702
793
  return privacyCenterApiFetch<unknown>(url, {
@@ -715,7 +806,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
715
806
  caseUuid: string,
716
807
  authToken?: string
717
808
  ): Promise<CaseMessage[]> {
718
- const url = caseUrl(baseRequest, caseUuid, '/messages');
809
+ // Sandbox mode: the acting identity travels as query params
810
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
811
+ const url = caseUrl(baseRequest, caseUuid, `/messages${sandboxIdentitySuffix()}`);
719
812
  return privacyCenterApiFetch<CaseMessage[]>(url, {
720
813
  authToken: resolveAuthToken(authToken, baseRequest),
721
814
  });
@@ -731,7 +824,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
731
824
  documentUuids: string[],
732
825
  authToken?: string
733
826
  ): Promise<CaseMessage> {
734
- const url = caseUrl(baseRequest, caseUuid, '/messages');
827
+ // Sandbox mode: the acting identity travels as query params
828
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
829
+ const url = caseUrl(baseRequest, caseUuid, `/messages${sandboxIdentitySuffix()}`);
735
830
  return privacyCenterApiFetch<CaseMessage>(url, {
736
831
  method: 'POST',
737
832
  body: JSON.stringify({ body, document_uuids: documentUuids }),
@@ -748,7 +843,9 @@ export function createConsentClient(baseUrl: string, ledgerBaseUrl?: string) {
748
843
  file: File,
749
844
  authToken?: string
750
845
  ): Promise<UploadDocumentResponse> {
751
- const url = caseUrl(baseRequest, caseUuid, '/upload-document');
846
+ // Sandbox mode: the acting identity travels as query params
847
+ // (primary_email / primary_mobile); the credential is the X-Consent-Token header.
848
+ const url = caseUrl(baseRequest, caseUuid, `/upload-document${sandboxIdentitySuffix()}`);
752
849
  const formData = new FormData();
753
850
  formData.append('file', file, file.name); // field name must be exactly "file"
754
851
  // "payload" is required by the privacy-center endpoint (omitting it causes 422)
@@ -1,3 +1,5 @@
1
+ import { sandboxAuthHeaders } from "../lib/sandbox";
2
+
1
3
  /**
2
4
  * Options for apiFetch function
3
5
  */
@@ -125,16 +127,29 @@ export async function apiFetch<T>(
125
127
  }
126
128
  }
127
129
 
128
- // Add auth token AFTER custom headers to ensure it's never overridden
130
+ // Add auth AFTER custom headers so it's never overridden. Only for calls that
131
+ // opt into auth (a `getAuthToken` is supplied) — public calls like org
132
+ // branding (which hit the user server, not consent-server) stay unauthenticated
133
+ // and must NOT receive the sandbox headers.
129
134
  if (getAuthToken) {
130
- const token = await getAuthToken();
131
- if (token) {
132
- // Use the exact header format that works in other parts of the codebase
133
- // Other working API calls use 'X-Auth-Token' (capital A and T, rest lowercase)
134
- const trimmedToken = token.trim();
135
- requestHeaders.set("X-Auth-Token", trimmedToken);
136
- // Also set lowercase for compatibility (some servers are case-sensitive)
137
- requestHeaders.set("x-auth-token", trimmedToken);
135
+ // Sandbox PoC: send the single X-Consent-Token header instead of the session
136
+ // token. The acting identity travels in the request payload (injected by the
137
+ // client call sites), not a header.
138
+ const sandboxHeaders = sandboxAuthHeaders();
139
+ if (sandboxHeaders) {
140
+ Object.entries(sandboxHeaders).forEach(([key, value]) => {
141
+ requestHeaders.set(key, value);
142
+ });
143
+ } else {
144
+ const token = await getAuthToken();
145
+ if (token) {
146
+ // Use the exact header format that works in other parts of the codebase
147
+ // Other working API calls use 'X-Auth-Token' (capital A and T, rest lowercase)
148
+ const trimmedToken = token.trim();
149
+ requestHeaders.set("X-Auth-Token", trimmedToken);
150
+ // Also set lowercase for compatibility (some servers are case-sensitive)
151
+ requestHeaders.set("x-auth-token", trimmedToken);
152
+ }
138
153
  }
139
154
  }
140
155
 
@@ -214,6 +214,34 @@ export default function AppShell({
214
214
  var(--pc-bg-shell) 100%
215
215
  );
216
216
  }
217
+ .pc-shell-user-menu-identity {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 2px;
221
+ min-width: 0;
222
+ flex: 1;
223
+ }
224
+ .pc-shell-user-menu-identity-email {
225
+ font-size: 13px;
226
+ font-weight: 600;
227
+ color: var(--pc-text-primary);
228
+ line-height: 1.3;
229
+ overflow: hidden;
230
+ text-overflow: ellipsis;
231
+ white-space: nowrap;
232
+ min-width: 0;
233
+ }
234
+ .pc-shell-user-menu-identity-id {
235
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
236
+ font-size: 11px;
237
+ font-weight: 500;
238
+ color: var(--pc-text-secondary);
239
+ line-height: 1.3;
240
+ overflow: hidden;
241
+ text-overflow: ellipsis;
242
+ white-space: nowrap;
243
+ min-width: 0;
244
+ }
217
245
  .pc-popover.pc-shell-user-menu-popover {
218
246
  border-radius: 12px !important;
219
247
  overflow: hidden !important;
@@ -1,11 +1,12 @@
1
1
  import { useEffect, useState, type CSSProperties } from 'react';
2
- import { ChevronDown, Users } from 'lucide-react';
2
+ import { Users } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { PCAvatar, PCPopover } from '../../ui';
5
5
  import { useBranding } from '../../context/BrandingContext';
6
6
  import { useOrganization } from '../../context/OrganizationContext';
7
7
  import { useProfileSwitch } from '../../context/ProfileSwitchContext';
8
- import { getVar } from '../../lib/utils';
8
+ import { useAuth } from '../../context/AuthContext';
9
+ import { decodeJwtToken, getVar } from '../../lib/utils';
9
10
  import { usePrivacyCenterExitAction } from './usePrivacyCenterExitAction';
10
11
 
11
12
  type UserMenuProps = {
@@ -35,6 +36,7 @@ export function UserMenu({ onBack }: UserMenuProps) {
35
36
  const { t } = useTranslation();
36
37
  const branding = useBranding();
37
38
  const { organization } = useOrganization();
39
+ const { accessToken } = useAuth();
38
40
  const [email, setEmail] = useState('');
39
41
 
40
42
  useEffect(() => {
@@ -45,20 +47,20 @@ export function UserMenu({ onBack }: UserMenuProps) {
45
47
  usePrivacyCenterExitAction(onBack);
46
48
  const { canSwitch, switchProfile } = useProfileSwitch();
47
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) || '';
55
+
48
56
  const displayName = branding?.brand_name || organization?.name || '';
49
57
  const triggerAvatarName = email ? initialsFromEmail(email) : displayName || '?';
50
58
 
51
- const triggerStyle: CSSProperties = {
52
- display: 'inline-flex',
53
- alignItems: 'center',
54
- gap: 6,
55
- padding: '4px 6px 4px 4px',
56
- borderRadius: 9999,
57
- background: 'transparent',
58
- border: `1px solid ${getVar('--pc-border-color')}`,
59
- color: getVar('--pc-text-secondary'),
60
- cursor: 'pointer',
61
- transition: 'background 150ms ease, border-color 150ms ease, box-shadow 150ms ease',
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%',
62
64
  };
63
65
 
64
66
  // Shared style for the popover's action rows (switch profile, back/sign out).
@@ -81,24 +83,25 @@ export function UserMenu({ onBack }: UserMenuProps) {
81
83
  };
82
84
 
83
85
  const trigger = (
84
- <div
85
- style={triggerStyle}
86
- role="presentation"
87
- onMouseEnter={(e) => {
88
- (e.currentTarget as HTMLDivElement).style.boxShadow =
89
- '0 0 0 2px color-mix(in srgb, var(--pc-primary) 18%, transparent)';
90
- }}
91
- onMouseLeave={(e) => {
92
- (e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
93
- }}
94
- >
86
+ <button type="button" aria-label={t('accountMenu')} className="pc-navbar-avatar-btn">
95
87
  {branding?.icon_url ? (
96
- <PCAvatar src={branding.icon_url} name={triggerAvatarName} size="sm" color="info" />
88
+ <PCAvatar
89
+ src={branding.icon_url}
90
+ name={triggerAvatarName}
91
+ size="md"
92
+ color="info"
93
+ framed={false}
94
+ style={avatarFillStyle}
95
+ />
97
96
  ) : (
98
- <PCAvatar name={triggerAvatarName} size="sm" color="info" />
97
+ <PCAvatar
98
+ name={triggerAvatarName}
99
+ size="md"
100
+ color="info"
101
+ style={avatarFillStyle}
102
+ />
99
103
  )}
100
- <ChevronDown size={14} strokeWidth={2} />
101
- </div>
104
+ </button>
102
105
  );
103
106
 
104
107
  const content = (
@@ -155,7 +158,7 @@ export function UserMenu({ onBack }: UserMenuProps) {
155
158
  </div>
156
159
 
157
160
  {/* Identity card */}
158
- {email && (
161
+ {(email || orgUserId) && (
159
162
  <div
160
163
  style={{
161
164
  padding: '12px 16px',
@@ -168,22 +171,21 @@ export function UserMenu({ onBack }: UserMenuProps) {
168
171
  : 'none',
169
172
  }}
170
173
  >
171
- <PCAvatar name={initialsFromEmail(email)} size="sm" color="info" />
172
- <div
173
- title={email}
174
- style={{
175
- fontSize: 13,
176
- fontWeight: 600,
177
- color: getVar('--pc-text-primary'),
178
- lineHeight: 1.3,
179
- overflow: 'hidden',
180
- textOverflow: 'ellipsis',
181
- whiteSpace: 'nowrap',
182
- minWidth: 0,
183
- flex: 1,
184
- }}
185
- >
186
- {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}
187
189
  </div>
188
190
  </div>
189
191
  )}
@@ -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})`;