@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "9.5.0-beta.0",
3
+ "version": "10.0.0-beta.1",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,11 @@ vi.mock("./api", () => ({
26
26
  fetchTTSAudioUrls: vi.fn(),
27
27
  initiateGuardianVerification: vi.fn(),
28
28
  verifyGuardianStatus: vi.fn(),
29
+ // Defaults to resolving undefined, so the ledger consent-state overlay is
30
+ // skipped and the banner falls back to the Python-provided values the
31
+ // existing tests assert on.
32
+ fetchNoticeConsentStatus: vi.fn(),
33
+ buildLedgerConsentOverlay: vi.fn(),
29
34
  }));
30
35
 
31
36
  // Mock jwt-decode
@@ -11,8 +11,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
11
11
 
12
12
  // API imports
13
13
  import {
14
+ buildLedgerConsentOverlay,
14
15
  clearApiCache,
15
16
  fetchConsentContent,
17
+ fetchNoticeConsentStatus,
16
18
  fetchTTSAudioUrls,
17
19
  initiateGuardianVerification,
18
20
  submitConsentEvent,
@@ -1296,6 +1298,7 @@ export const RedactoNoticeConsent = ({
1296
1298
  accessToken,
1297
1299
  refreshToken,
1298
1300
  baseUrl,
1301
+ ledgerBaseUrl,
1299
1302
  language = "en",
1300
1303
  blockUI = true,
1301
1304
  onAccept,
@@ -1838,9 +1841,31 @@ export const RedactoNoticeConsent = ({
1838
1841
  );
1839
1842
  setCollapsedPurposes(initialCollapsedState);
1840
1843
 
1841
- // Initialize selected states - handle reconsent pre-population
1842
- const isReconsentRequired = consentContentData.detail.reconsent_required;
1843
- const purposeSelections = consentContentData.detail.purpose_selections;
1844
+ // Initialize selected states - handle reconsent pre-population.
1845
+ // Approach A: overlay the prior-consent / reconsent state from the Go
1846
+ // ledger's check-consent. Consents live in the ledger post-cutover, so
1847
+ // Python's notice-validation overlay is empty; the notice text/config
1848
+ // still comes from Python (above). Falls back to the Python-provided
1849
+ // values when the ledger is unavailable, and is skipped for the
1850
+ // magic-link flow (specific_uuid), which pre-fills from a specific event.
1851
+ let isReconsentRequired = consentContentData.detail.reconsent_required;
1852
+ let purposeSelections = consentContentData.detail.purpose_selections;
1853
+ if (ledgerBaseUrl && !applicationId) {
1854
+ const ledgerStatus = await fetchNoticeConsentStatus({
1855
+ accessToken,
1856
+ ledgerBaseUrl,
1857
+ noticeUuid: noticeId,
1858
+ signal: abortControllerRef.current?.signal,
1859
+ });
1860
+ if (ledgerStatus) {
1861
+ const overlay = buildLedgerConsentOverlay(
1862
+ ledgerStatus,
1863
+ activeConfig.purposes
1864
+ );
1865
+ isReconsentRequired = overlay?.reconsent_required ?? false;
1866
+ purposeSelections = overlay?.purpose_selections;
1867
+ }
1868
+ }
1844
1869
 
1845
1870
  // Categorize purposes for reconsent UI
1846
1871
  const newCategorizedPurposes = isReconsentRequired
@@ -2121,6 +2146,7 @@ export const RedactoNoticeConsent = ({
2121
2146
  await submitConsentEvent({
2122
2147
  accessToken,
2123
2148
  baseUrl,
2149
+ ledgerBaseUrl,
2124
2150
  noticeUuid: content.notice_uuid,
2125
2151
  purposes: content.purposes.map((purpose) => {
2126
2152
  // Check if this is an already-consented purpose that was modified
@@ -3,10 +3,14 @@ import { parseApiError } from "../../shared/api-errors";
3
3
  import type {
4
4
  ConsentContent,
5
5
  ConsentEventPayload,
6
+ DataElementSelection,
6
7
  FetchConsentContentParams,
8
+ FetchNoticeConsentStatusParams,
7
9
  FetchTTSAudioUrlsParams,
8
10
  InitiateGuardianVerificationParams,
9
11
  InitiateGuardianVerificationResponse,
12
+ NoticeConsentStatusResponse,
13
+ PurposeSelection,
10
14
  RedactoJwtPayload,
11
15
  SubmitConsentEventParams,
12
16
  TTSAudioUrlsResponse,
@@ -128,6 +132,7 @@ export const fetchConsentContent = async ({
128
132
  export const submitConsentEvent = async ({
129
133
  accessToken,
130
134
  baseUrl,
135
+ ledgerBaseUrl,
131
136
  noticeUuid,
132
137
  purposes,
133
138
  declined,
@@ -151,7 +156,9 @@ export const submitConsentEvent = async ({
151
156
  throw new Error("Invalid token: missing organization or workspace UUID");
152
157
  }
153
158
 
154
- const apiBaseUrl = baseUrl || BASE_URL;
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;
155
162
 
156
163
  const validatedPurposes = purposes.map((purpose) => {
157
164
  if (!purpose.uuid) {
@@ -209,6 +216,128 @@ export const submitConsentEvent = async ({
209
216
  apiCache.clear();
210
217
  };
211
218
 
219
+ /**
220
+ * Fetch the principal's current consent status for a notice from the Go
221
+ * ledger (`check-consent`). The principal resolves from the JWT, so the body
222
+ * is empty. Returns null on any failure so the caller can fall back to the
223
+ * Python-provided overlay (graceful degradation). This is how the banner
224
+ * learns prior-consent / reconsent state post-cutover, since Python's notice
225
+ * validation reads its own (now-empty) consent DB.
226
+ */
227
+ export const fetchNoticeConsentStatus = async ({
228
+ accessToken,
229
+ ledgerBaseUrl,
230
+ noticeUuid,
231
+ signal,
232
+ }: FetchNoticeConsentStatusParams): Promise<NoticeConsentStatusResponse | null> => {
233
+ if (!accessToken || !ledgerBaseUrl || !noticeUuid) {
234
+ return null;
235
+ }
236
+
237
+ const decodedToken = decodeTokenSafely(accessToken);
238
+ if (!decodedToken) {
239
+ return null;
240
+ }
241
+
242
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
243
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
244
+ return null;
245
+ }
246
+
247
+ try {
248
+ const response = await fetch(
249
+ `${ledgerBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/check-consent`,
250
+ {
251
+ method: "POST",
252
+ headers: {
253
+ Authorization: `Bearer ${accessToken}`,
254
+ "Content-Type": "application/json",
255
+ },
256
+ body: "{}",
257
+ signal,
258
+ }
259
+ );
260
+
261
+ if (!response.ok) {
262
+ return null;
263
+ }
264
+
265
+ const data = (await response.json()) as
266
+ | NoticeConsentStatusResponse
267
+ | { detail?: NoticeConsentStatusResponse };
268
+ // The ledger returns the response directly; tolerate a {detail} envelope too.
269
+ const body =
270
+ data && typeof data === "object" && "detail" in data && data.detail
271
+ ? data.detail
272
+ : (data as NoticeConsentStatusResponse);
273
+
274
+ if (!body || !Array.isArray(body.purposes)) {
275
+ return null;
276
+ }
277
+ return body;
278
+ } catch {
279
+ // Network / abort / parse failure — degrade to the Python overlay.
280
+ return null;
281
+ }
282
+ };
283
+
284
+ /**
285
+ * Translate the ledger's notice consent status into the
286
+ * `{ purpose_selections, reconsent_required }` shape the banner already
287
+ * consumes, so prior-consent pre-fill and reconsent categorization work off
288
+ * the ledger. Returns null for a first-time principal (no consent record —
289
+ * every purpose INACTIVE), preserving the normal first-time consent flow.
290
+ *
291
+ * `needs_reconsent` is derived from `status` (anything not ACTIVE needs
292
+ * reconsent: expired / withdrawn / declined / inactive). NOTE: notice-version
293
+ * reconsent is NOT covered — the ledger's check-consent has no version
294
+ * awareness yet, so a republished-notice version bump won't re-prompt until
295
+ * that lands on the ledger (tracked fast-follow).
296
+ */
297
+ export const buildLedgerConsentOverlay = (
298
+ status: NoticeConsentStatusResponse,
299
+ configPurposes: ConsentContent["detail"]["active_config"]["purposes"]
300
+ ): {
301
+ reconsent_required: boolean;
302
+ purpose_selections: Record<string, PurposeSelection>;
303
+ } | null => {
304
+ const byUuid = new Map(status.purposes.map((p) => [p.uuid, p]));
305
+ const hasHistory = status.purposes.some(
306
+ (p) => (p.status || "INACTIVE") !== "INACTIVE"
307
+ );
308
+ if (!hasHistory) {
309
+ return null;
310
+ }
311
+
312
+ const purpose_selections: Record<string, PurposeSelection> = {};
313
+ for (const cp of configPurposes) {
314
+ const lp = byUuid.get(cp.uuid);
315
+ const purposeStatus = lp?.status || "INACTIVE";
316
+ const selectedByUuid = new Map(
317
+ (lp?.data_elements || []).map((de) => [de.uuid, de.selected])
318
+ );
319
+ const data_elements: Record<string, DataElementSelection> = {};
320
+ for (const de of cp.data_elements) {
321
+ data_elements[de.uuid] = {
322
+ selected: selectedByUuid.get(de.uuid) ?? false,
323
+ enabled: de.enabled,
324
+ required: de.required,
325
+ };
326
+ }
327
+ purpose_selections[cp.uuid] = {
328
+ selected: lp?.selected ?? false,
329
+ status: purposeStatus,
330
+ needs_reconsent: purposeStatus !== "ACTIVE",
331
+ data_elements,
332
+ };
333
+ }
334
+
335
+ const reconsent_required = configPurposes.some(
336
+ (cp) => (purpose_selections[cp.uuid]?.status || "INACTIVE") !== "ACTIVE"
337
+ );
338
+ return { reconsent_required, purpose_selections };
339
+ };
340
+
212
341
  export const fetchTTSAudioUrls = async ({
213
342
  accessToken,
214
343
  baseUrl,
@@ -19,6 +19,38 @@ export type DataElementSelection = {
19
19
  required: boolean;
20
20
  };
21
21
 
22
+ // ─── Ledger notice check-consent ────────────────────────────────────────────
23
+ // Mirrors the Go ledger's `NoticeConsentStatusResponse`. Used to overlay the
24
+ // banner's prior-consent / reconsent state from the ledger, since Python's
25
+ // notice-validation overlay is empty after the consent cutover.
26
+ export type NoticeConsentStatusDataElement = {
27
+ uuid: string;
28
+ name: string;
29
+ selected: boolean;
30
+ };
31
+
32
+ export type NoticeConsentStatusPurpose = {
33
+ uuid: string;
34
+ name: string;
35
+ selected: boolean;
36
+ status: string; // ACTIVE | EXPIRED | WITHDRAW | DECLINED | INACTIVE
37
+ expiry_datetime?: string | null;
38
+ data_elements: NoticeConsentStatusDataElement[];
39
+ };
40
+
41
+ export type NoticeConsentStatusResponse = {
42
+ consented: boolean;
43
+ all_mandatory_active: boolean;
44
+ purposes: NoticeConsentStatusPurpose[];
45
+ };
46
+
47
+ export type FetchNoticeConsentStatusParams = {
48
+ accessToken: string;
49
+ ledgerBaseUrl?: string;
50
+ noticeUuid: string;
51
+ signal?: AbortSignal;
52
+ };
53
+
22
54
  export type ConsentContent = {
23
55
  code: number;
24
56
  status: string;
@@ -199,6 +231,7 @@ export type Purpose = {
199
231
  export type SubmitConsentEventParams = {
200
232
  accessToken: string;
201
233
  baseUrl?: string;
234
+ ledgerBaseUrl?: string;
202
235
  noticeUuid: string;
203
236
  purposes: Array<Purpose>;
204
237
  declined: boolean;
@@ -5,6 +5,7 @@ export type Props = Readonly<{
5
5
  accessToken: string;
6
6
  refreshToken: string;
7
7
  baseUrl?: string;
8
+ ledgerBaseUrl?: string;
8
9
  settings?: Partial<Settings>;
9
10
  language?: string;
10
11
  blockUI?: boolean;
@@ -12,6 +12,7 @@ const RedactoConsentInlineComponent = ({
12
12
  notice_uuid,
13
13
  accessToken,
14
14
  baseUrl,
15
+ ledgerBaseUrl,
15
16
  language = "en",
16
17
  onAccept,
17
18
  onError,
@@ -320,6 +321,7 @@ const RedactoConsentInlineComponent = ({
320
321
  workspace_uuid,
321
322
  accessToken: tokenToUse,
322
323
  baseUrl,
324
+ ledgerBaseUrl,
323
325
  noticeUuid: content.notice_uuid,
324
326
  purposes: content.purposes.map((purpose) => {
325
327
  // Check if any required data element is selected
@@ -382,6 +384,7 @@ const RedactoConsentInlineComponent = ({
382
384
  org_uuid,
383
385
  workspace_uuid,
384
386
  baseUrl,
387
+ ledgerBaseUrl,
385
388
  selectedDataElements,
386
389
  selectedPurposes,
387
390
  applicationId,
@@ -48,12 +48,15 @@ export const submitConsentEvent = async ({
48
48
  workspace_uuid,
49
49
  accessToken,
50
50
  baseUrl,
51
+ ledgerBaseUrl,
51
52
  noticeUuid,
52
53
  purposes,
53
54
  declined,
54
55
  meta_data,
55
56
  }: SubmitConsentEventParams): Promise<void> => {
56
- const apiBaseUrl = baseUrl || BASE_URL;
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;
57
60
 
58
61
  const payload: ConsentEventPayload = {
59
62
  notice_uuid: noticeUuid,
@@ -173,6 +173,7 @@ export type SubmitConsentEventParams = {
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;
@@ -8,6 +8,7 @@ export type Props = Readonly<{
8
8
  accessToken?: string;
9
9
  refreshToken?: string;
10
10
  baseUrl?: string;
11
+ ledgerBaseUrl?: string;
11
12
  settings?: Partial<Settings>;
12
13
  language?: string;
13
14
  onAccept?: () => void;
@@ -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
- <DataAvailabilityProvider>
168
- <AppShell onBack={onBack}>
169
- {children}
170
- </AppShell>
171
- </DataAvailabilityProvider>
168
+ <SelectUserGate>
169
+ <DataAvailabilityProvider>
170
+ <AppShell onBack={onBack}>
171
+ {children}
172
+ </AppShell>
173
+ </DataAvailabilityProvider>
174
+ </SelectUserGate>
172
175
  </OrganizationProvider>
173
176
  );
174
177
  }
@@ -15,6 +15,7 @@ injectPCStyles();
15
15
 
16
16
  interface RedactoPrivacyCenterProps {
17
17
  baseUrl: string; // Consent server base URL
18
+ ledgerBaseUrl?: string; // Go ledger base URL — serves user-consents; omit to use the legacy Python privacy-center consent endpoints
18
19
  slug: string;
19
20
  accessToken: string;
20
21
  refreshToken: string;
@@ -25,6 +26,7 @@ interface RedactoPrivacyCenterProps {
25
26
 
26
27
  export function RedactoPrivacyCenter({
27
28
  baseUrl,
29
+ ledgerBaseUrl,
28
30
  slug,
29
31
  accessToken,
30
32
  refreshToken,
@@ -35,7 +37,7 @@ export function RedactoPrivacyCenter({
35
37
  return (
36
38
  <I18nextProvider i18n={i18n}>
37
39
  <BrandingProvider slug={slug} baseUrl={baseUrl}>
38
- <BaseUrlProvider baseUrl={baseUrl}>
40
+ <BaseUrlProvider baseUrl={baseUrl} ledgerBaseUrl={ledgerBaseUrl}>
39
41
  <AuthProvider
40
42
  accessToken={accessToken}
41
43
  refreshToken={refreshToken}
@@ -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,
@@ -351,6 +352,8 @@ export async function manageConsentServer(params: {
351
352
  nominatorContact?: string;
352
353
  dataElementUuids?: string[];
353
354
  productUuid?: string;
355
+ noticeUuid?: string;
356
+ dataElements?: Array<{ uuid: string; selected: boolean }>;
354
357
  }): Promise<boolean | null> {
355
358
  try {
356
359
  if (!params.consentClient) {
@@ -359,6 +362,11 @@ export async function manageConsentServer(params: {
359
362
  // Privacy center endpoints use JWT Bearer tokens, not session-based auth
360
363
  const baseRequest = buildBaseRequest(params.organizationId, params.workspaceId, params.authToken);
361
364
 
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.
362
370
  const result = await params.consentClient.manageConsent(
363
371
  baseRequest,
364
372
  params.purposeId,
@@ -368,9 +376,14 @@ export async function manageConsentServer(params: {
368
376
  undefined,
369
377
  params.nominatorContact,
370
378
  params.dataElementUuids,
371
- params.productUuid
379
+ params.productUuid,
380
+ params.noticeUuid,
381
+ params.dataElements
372
382
  );
373
- return (result as { success: boolean })?.success ?? null;
383
+ if (result && typeof result === 'object' && 'success' in result) {
384
+ return Boolean((result as LegacyManageConsentResponse).success);
385
+ }
386
+ return true;
374
387
  } catch (error: any) {
375
388
  // Normalize nominator-specific errors into stable error codes
376
389
  const errorMsg = error?.message || '';