@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
@@ -6,7 +6,6 @@ export const LANGUAGE_NATIVE_LABELS: Record<string, string> = {
6
6
  English: "English",
7
7
  Assamese: "অসমীয়া (Assamese)",
8
8
  Bengali: "বাংলা (Bengali)",
9
- Bhojpuri: "भोजपुरी (Bhojpuri)",
10
9
  Bodo: "बड़ो (Bodo)",
11
10
  Dogri: "डोगरी (Dogri)",
12
11
  Gujarati: "ગુજરાતી (Gujarati)",
@@ -44,7 +43,6 @@ const LANGUAGE_NAME_TO_BCP47: Record<string, string> = {
44
43
  English: "en",
45
44
  Assamese: "as",
46
45
  Bengali: "bn",
47
- Bhojpuri: "bho",
48
46
  Bodo: "brx",
49
47
  Dogri: "doi",
50
48
  Gujarati: "gu",
@@ -2,8 +2,34 @@ import type { Settings, DpoInfoTranslation } from "./api/types";
2
2
 
3
3
  export type Props = Readonly<{
4
4
  noticeId: string;
5
- accessToken: string;
6
- refreshToken: string;
5
+ /**
6
+ * JWT access token. Optional so sandbox integrations (see `token`) can
7
+ * omit it. Required in the normal JWT flow.
8
+ */
9
+ accessToken?: string;
10
+ /** JWT refresh token. Optional; accepted for source-compat, currently unused. */
11
+ refreshToken?: string;
12
+ /**
13
+ * Sandbox PoC: a pasted static token. When set, the SDK runs against the
14
+ * sandbox with no JWT — it sends the single `X-Consent-Token` header, carries
15
+ * the acting identity (`email` || `mobile`) in each request's own payload,
16
+ * takes org/workspace from `organisationUuid`/`workspaceUuid`, and does no
17
+ * token refresh.
18
+ */
19
+ token?: string;
20
+ /** Sandbox test subject email (carried in the request payload when set). */
21
+ email?: string;
22
+ /** Sandbox test subject mobile (fallback subject when `email` is absent). */
23
+ mobile?: string;
24
+ /**
25
+ * Sandbox UCIC: the client's own user id / org_user_id, e.g. a UUID or
26
+ * customer code; optional third identifier alongside email/mobile.
27
+ */
28
+ ucic?: string;
29
+ /** Sandbox org UUID (replaces the JWT-decoded org in sandbox mode). */
30
+ organisationUuid?: string;
31
+ /** Sandbox workspace UUID (replaces the JWT-decoded workspace in sandbox mode). */
32
+ workspaceUuid?: string;
7
33
  baseUrl?: string;
8
34
  ledgerBaseUrl?: string;
9
35
  settings?: Partial<Settings>;
@@ -1,5 +1,9 @@
1
1
  import { useEffect, useMemo, useState, useRef, useCallback, memo } from "react";
2
2
  import { fetchConsentContent, submitConsentEvent } from "./api";
3
+ import {
4
+ fetchNoticeConsentStatus,
5
+ isNoticeFullyConsented,
6
+ } from "../shared/notice-consent-status";
3
7
  import type { ConsentContent } from "./api/types";
4
8
  import type { Props, TranslationObject } from "./types";
5
9
  import { styles } from "./styles";
@@ -36,6 +40,14 @@ const RedactoConsentInlineComponent = ({
36
40
  Record<string, boolean>
37
41
  >({});
38
42
  const [hasAlreadyConsented, setHasAlreadyConsented] = useState(false);
43
+ // When a ledger is configured, block the auto-submit effect until the ledger
44
+ // check-consent read settles, so an already-consented principal is not
45
+ // re-submitted. Starts true only when a ledger check will actually run
46
+ // (skipped for the magic-link specific_uuid flow); stays false otherwise so
47
+ // the legacy Python-409 path is unaffected.
48
+ const [isCheckingConsent, setIsCheckingConsent] = useState<boolean>(
49
+ Boolean(ledgerBaseUrl) && !applicationId
50
+ );
39
51
  const [fetchError, setFetchError] = useState<Error | null>(null);
40
52
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
41
53
 
@@ -149,18 +161,26 @@ const RedactoConsentInlineComponent = ({
149
161
  return defaultText;
150
162
  };
151
163
 
152
- const fetchNotice = async () => {
164
+ const fetchNotice = async (signal?: AbortSignal) => {
153
165
  setIsLoading(true);
154
166
  setFetchError(null);
167
+ setHasAlreadyConsented(false);
155
168
  try {
156
169
  const consentContentData = await fetchConsentContent({
157
170
  org_uuid,
158
171
  workspace_uuid,
159
172
  notice_uuid,
173
+ accessToken,
160
174
  baseUrl,
161
175
  language,
162
176
  specific_uuid: applicationId,
177
+ signal,
163
178
  });
179
+
180
+ if (signal?.aborted) {
181
+ return;
182
+ }
183
+
164
184
  const activeConfig = consentContentData.detail.active_config;
165
185
  setContent(activeConfig);
166
186
  if (activeConfig.default_language) {
@@ -203,6 +223,13 @@ const RedactoConsentInlineComponent = ({
203
223
  );
204
224
  setSelectedPurposes(initialPurposeState);
205
225
  } catch (err: unknown) {
226
+ if (
227
+ signal?.aborted ||
228
+ (err instanceof Error && err.name === "AbortError")
229
+ ) {
230
+ return;
231
+ }
232
+
206
233
  const error = err as Error & { status?: number };
207
234
  setFetchError(error);
208
235
  if (error.status === 409) {
@@ -211,14 +238,60 @@ const RedactoConsentInlineComponent = ({
211
238
  onError?.(error);
212
239
  }
213
240
  } finally {
214
- setIsLoading(false);
241
+ if (!signal?.aborted) {
242
+ setIsLoading(false);
243
+ }
215
244
  }
216
245
  };
217
246
 
218
247
  useEffect(() => {
219
- fetchNotice();
248
+ const controller = new AbortController();
249
+ void fetchNotice(controller.signal);
250
+
251
+ return () => {
252
+ controller.abort();
253
+ };
220
254
  // eslint-disable-next-line react-hooks/exhaustive-deps
221
- }, [org_uuid, workspace_uuid, notice_uuid, language, applicationId]);
255
+ }, [org_uuid, workspace_uuid, notice_uuid, language, applicationId, accessToken]);
256
+
257
+ // Post-cutover, consent records live in the Go ledger, so Python's
258
+ // get-notice no longer returns 409 CONSENT_ALREADY_PROVIDED. When a ledger
259
+ // is configured, ask it directly whether this principal has already fully
260
+ // consented — the same check-consent read the banner uses. Skipped for the
261
+ // magic-link specific_uuid flow (which always re-submits against a specific
262
+ // application) and when no ledger is set (the legacy 409 path governs).
263
+ useEffect(() => {
264
+ if (!ledgerBaseUrl || applicationId) {
265
+ return;
266
+ }
267
+ if (!accessToken || !content) {
268
+ return;
269
+ }
270
+ let active = true;
271
+ const controller = new AbortController();
272
+ const checkLedgerConsent = async () => {
273
+ try {
274
+ const ledgerStatus = await fetchNoticeConsentStatus({
275
+ accessToken,
276
+ ledgerBaseUrl,
277
+ noticeUuid: notice_uuid,
278
+ signal: controller.signal,
279
+ });
280
+ if (active && ledgerStatus && isNoticeFullyConsented(ledgerStatus)) {
281
+ setHasAlreadyConsented(true);
282
+ }
283
+ } finally {
284
+ if (active) {
285
+ setIsCheckingConsent(false);
286
+ }
287
+ }
288
+ };
289
+ checkLedgerConsent();
290
+ return () => {
291
+ active = false;
292
+ controller.abort();
293
+ };
294
+ }, [ledgerBaseUrl, applicationId, accessToken, content, notice_uuid]);
222
295
 
223
296
  const togglePurposeCollapse = (purposeUuid: string) => {
224
297
  setCollapsedPurposes((prev) => ({
@@ -392,13 +465,18 @@ const RedactoConsentInlineComponent = ({
392
465
  ]
393
466
  );
394
467
 
395
- // Auto-submit consent when all conditions are met
468
+ // Auto-submit consent when all conditions are met. Waits for the ledger
469
+ // check-consent read to settle (isCheckingConsent) and never fires for an
470
+ // already-consented principal, so the cutover cannot trigger a duplicate
471
+ // submission.
396
472
  useEffect(() => {
397
473
  if (
398
474
  accessToken &&
399
475
  content &&
400
476
  areAllRequiredElementsChecked &&
401
477
  !isSubmitting &&
478
+ !isCheckingConsent &&
479
+ !hasAlreadyConsented &&
402
480
  !hasSubmittedRef.current
403
481
  ) {
404
482
  hasSubmittedRef.current = true;
@@ -417,6 +495,8 @@ const RedactoConsentInlineComponent = ({
417
495
  content,
418
496
  areAllRequiredElementsChecked,
419
497
  isSubmitting,
498
+ isCheckingConsent,
499
+ hasAlreadyConsented,
420
500
  handleAcceptWithToken,
421
501
  ]);
422
502
 
@@ -12,13 +12,19 @@ export const fetchConsentContent = async ({
12
12
  org_uuid,
13
13
  workspace_uuid,
14
14
  notice_uuid,
15
+ accessToken,
15
16
  baseUrl,
17
+ language,
16
18
  specific_uuid,
19
+ signal,
17
20
  }: FetchConsentContentParams): Promise<ConsentContent> => {
18
21
  const apiBaseUrl = baseUrl || BASE_URL;
22
+ const noticesUrl = `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices`;
19
23
 
20
24
  const url = new URL(
21
- `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/notices/get-notice/${notice_uuid}`
25
+ accessToken
26
+ ? `${noticesUrl}/${notice_uuid}`
27
+ : `${noticesUrl}/get-notice/${notice_uuid}`
22
28
  );
23
29
 
24
30
  if (specific_uuid) {
@@ -29,7 +35,10 @@ export const fetchConsentContent = async ({
29
35
  method: "GET",
30
36
  headers: {
31
37
  "Content-Type": "application/json",
38
+ "Accept-Language": language || "en",
39
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
32
40
  },
41
+ signal,
33
42
  });
34
43
 
35
44
  if (!response.ok) {
@@ -0,0 +1,72 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { fetchConsentContent } from "./index";
3
+
4
+ const LEDGER = "https://ledger.example.test/consent-ledger";
5
+ const PYTHON = "https://api.example.test/consent";
6
+ const TOKEN = "opaque-token";
7
+
8
+ const scope = (base: string) =>
9
+ `${base}/public/organisations/org-1/workspaces/ws-1/notices`;
10
+
11
+ const requestedUrl = (fetchMock: ReturnType<typeof vi.fn>): string =>
12
+ String(fetchMock.mock.calls[0][0]);
13
+
14
+ const requestedHeaders = (
15
+ fetchMock: ReturnType<typeof vi.fn>
16
+ ): Record<string, string> =>
17
+ (fetchMock.mock.calls[0][1] as RequestInit).headers as Record<string, string>;
18
+
19
+ describe("inline notice read routing", () => {
20
+ const fetchMock = vi.fn();
21
+ const base = { org_uuid: "org-1", workspace_uuid: "ws-1", notice_uuid: "ntc-1" };
22
+
23
+ beforeEach(() => {
24
+ fetchMock.mockReset();
25
+ fetchMock.mockResolvedValue({
26
+ ok: true,
27
+ status: 200,
28
+ json: async () => ({}),
29
+ } as Response);
30
+ vi.stubGlobal("fetch", fetchMock);
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.unstubAllGlobals();
35
+ });
36
+
37
+ it("reads the authenticated notice endpoint when a token is available", async () => {
38
+ await fetchConsentContent({ ...base, accessToken: TOKEN, baseUrl: LEDGER });
39
+ expect(requestedUrl(fetchMock)).toBe(`${scope(LEDGER)}/ntc-1`);
40
+ expect(requestedHeaders(fetchMock).Authorization).toBe(`Bearer ${TOKEN}`);
41
+ });
42
+
43
+ it("falls back to the tokenless get-notice endpoint without a token", async () => {
44
+ await fetchConsentContent({ ...base, baseUrl: PYTHON });
45
+ expect(requestedUrl(fetchMock)).toBe(`${scope(PYTHON)}/get-notice/ntc-1`);
46
+ expect(requestedHeaders(fetchMock).Authorization).toBeUndefined();
47
+ });
48
+
49
+ it("keeps specific_uuid on the authenticated read", async () => {
50
+ await fetchConsentContent({
51
+ ...base,
52
+ accessToken: TOKEN,
53
+ baseUrl: LEDGER,
54
+ specific_uuid: "app-1",
55
+ });
56
+ expect(requestedUrl(fetchMock)).toBe(`${scope(LEDGER)}/ntc-1?specific_uuid=app-1`);
57
+ });
58
+
59
+ it("sends the requested language on both paths", async () => {
60
+ await fetchConsentContent({ ...base, baseUrl: PYTHON, language: "hi" });
61
+ expect(requestedHeaders(fetchMock)["Accept-Language"]).toBe("hi");
62
+
63
+ fetchMock.mockClear();
64
+ await fetchConsentContent({
65
+ ...base,
66
+ accessToken: TOKEN,
67
+ baseUrl: LEDGER,
68
+ language: "hi",
69
+ });
70
+ expect(requestedHeaders(fetchMock)["Accept-Language"]).toBe("hi");
71
+ });
72
+ });
@@ -128,9 +128,11 @@ export type FetchConsentContentParams = {
128
128
  org_uuid: string;
129
129
  workspace_uuid: string;
130
130
  notice_uuid: string;
131
+ accessToken?: string;
131
132
  baseUrl?: string;
132
133
  language?: string;
133
134
  specific_uuid?: string;
135
+ signal?: AbortSignal;
134
136
  };
135
137
 
136
138
  export type ConsentEventPayload = {
@@ -17,8 +17,31 @@ interface RedactoPrivacyCenterProps {
17
17
  baseUrl: string; // Consent server base URL
18
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
- accessToken: string;
21
- refreshToken: string;
20
+ /** JWT access token. Optional so sandbox mode (see `token`) can omit it. */
21
+ accessToken?: string;
22
+ /** JWT refresh token. Optional; used by the OTP-refresh loop in the JWT flow. */
23
+ refreshToken?: string;
24
+ /**
25
+ * Sandbox PoC: a pasted static token. When set, the Privacy Center runs
26
+ * against the sandbox with no JWT — it sends the single `X-Consent-Token`
27
+ * header, carries the acting identity (`email` || `mobile`) in each request's
28
+ * own payload, takes org/workspace from `organisationUuid`/`workspaceUuid`,
29
+ * and never calls `/otp/refresh`.
30
+ */
31
+ token?: string;
32
+ /** Sandbox test subject email (carried in the request payload when set). */
33
+ email?: string;
34
+ /** Sandbox test subject mobile (fallback subject when `email` is absent). */
35
+ mobile?: string;
36
+ /**
37
+ * Sandbox UCIC: the client's own user id / org_user_id, e.g. a UUID or
38
+ * customer code; optional third identifier alongside email/mobile.
39
+ */
40
+ ucic?: string;
41
+ /** Sandbox org UUID (replaces the JWT-decoded org in sandbox mode). */
42
+ organisationUuid?: string;
43
+ /** Sandbox workspace UUID (replaces the JWT-decoded workspace in sandbox mode). */
44
+ workspaceUuid?: string;
22
45
  onError: (error: Error) => boolean;
23
46
  initialPage?: InternalPage;
24
47
  onBack?: 'back' | 'signout';
@@ -30,6 +53,12 @@ export function RedactoPrivacyCenter({
30
53
  slug,
31
54
  accessToken,
32
55
  refreshToken,
56
+ token,
57
+ email,
58
+ mobile,
59
+ ucic,
60
+ organisationUuid,
61
+ workspaceUuid,
33
62
  onError,
34
63
  initialPage = 'consent-manager',
35
64
  onBack,
@@ -41,6 +70,12 @@ export function RedactoPrivacyCenter({
41
70
  <AuthProvider
42
71
  accessToken={accessToken}
43
72
  refreshToken={refreshToken}
73
+ token={token}
74
+ sandboxUcic={ucic}
75
+ sandboxSubject={email || mobile}
76
+ organisationUuid={organisationUuid}
77
+ workspaceUuid={workspaceUuid}
78
+ slug={slug}
44
79
  onError={onError}
45
80
  >
46
81
  <NavigationProvider initialPage={initialPage}>
@@ -354,6 +354,7 @@ export async function manageConsentServer(params: {
354
354
  productUuid?: string;
355
355
  noticeUuid?: string;
356
356
  dataElements?: Array<{ uuid: string; selected: boolean }>;
357
+ language?: string;
357
358
  }): Promise<boolean | null> {
358
359
  try {
359
360
  if (!params.consentClient) {
@@ -378,7 +379,8 @@ export async function manageConsentServer(params: {
378
379
  params.dataElementUuids,
379
380
  params.productUuid,
380
381
  params.noticeUuid,
381
- params.dataElements
382
+ params.dataElements,
383
+ params.language
382
384
  );
383
385
  if (result && typeof result === 'object' && 'success' in result) {
384
386
  return Boolean((result as LegacyManageConsentResponse).success);