@redacto.io/consent-sdk-react 3.2.0 → 3.4.0

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.
@@ -5,11 +5,13 @@ import type {
5
5
  FetchConsentContentParams,
6
6
  FetchTTSAudioUrlsParams,
7
7
  GuardianInfoPayload,
8
+ LegacyConsentEventPayload,
8
9
  RedactoJwtPayload,
9
10
  SubmitConsentEventParams,
10
11
  SubmitGuardianInfoParams,
11
12
  TTSAudioUrlsResponse,
12
13
  } from "./types";
14
+ import { isLegacyApi } from "../../utils/isLegacyApi";
13
15
 
14
16
  const BASE_URL = "https://api.redacto.io/consent";
15
17
 
@@ -45,16 +47,6 @@ function decodeTokenSafely(token: string): RedactoJwtPayload | null {
45
47
  }
46
48
  }
47
49
 
48
- // Abstract localStorage access for better testability
49
- const storage = {
50
- getItem: (key: string): string | null => {
51
- if (typeof window !== 'undefined') {
52
- return localStorage.getItem(key);
53
- }
54
- return null;
55
- }
56
- };
57
-
58
50
  // Function to clear API cache (useful for testing or when switching users)
59
51
  export const clearApiCache = (): void => {
60
52
  apiCache.clear();
@@ -67,6 +59,7 @@ export const fetchConsentContent = async ({
67
59
  language = "en",
68
60
  specific_uuid,
69
61
  validate_against = "all",
62
+ include_fully_consented_data = false,
70
63
  signal, // AbortSignal for request cancellation
71
64
  }: FetchConsentContentParams & { signal?: AbortSignal }): Promise<ConsentContent> => {
72
65
  try {
@@ -88,7 +81,7 @@ export const fetchConsentContent = async ({
88
81
  const apiBaseUrl = baseUrl || BASE_URL;
89
82
 
90
83
  // Create cache key for this request - include accessToken to ensure different users get different cache
91
- const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}`;
84
+ const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}-${include_fully_consented_data}`;
92
85
 
93
86
  // Check cache first (skip for reconsent requests as they need fresh data)
94
87
  if (validate_against === "all" && !specific_uuid) {
@@ -106,6 +99,9 @@ export const fetchConsentContent = async ({
106
99
  url.searchParams.append("specific_uuid", specific_uuid);
107
100
  }
108
101
  url.searchParams.append("validate_against", validate_against);
102
+ if (include_fully_consented_data) {
103
+ url.searchParams.append("include_fully_consented_data", "true");
104
+ }
109
105
 
110
106
  const response = await fetch(url.toString(), {
111
107
  method: "GET",
@@ -146,8 +142,6 @@ export const fetchConsentContent = async ({
146
142
 
147
143
  return data;
148
144
  } catch (error) {
149
- // Log error for debugging but don't expose sensitive information
150
- console.error("Error fetching consent content:", error instanceof Error ? error.message : error);
151
145
  throw error;
152
146
  }
153
147
  };
@@ -177,48 +171,72 @@ export const submitConsentEvent = async ({
177
171
  throw new Error("Invalid token: missing organization or workspace UUID");
178
172
  }
179
173
 
180
- const primaryEmail = storage.getItem("userEmail");
174
+ const apiBaseUrl = baseUrl || BASE_URL;
181
175
 
182
- // Validate purposes data
183
- const validatedPurposes = purposes.map((purpose) => {
184
- if (!purpose.uuid || !purpose.name) {
185
- throw new Error(`Invalid purpose data: missing uuid or name`);
186
- }
176
+ let payload: ConsentEventPayload | LegacyConsentEventPayload;
177
+
178
+ if (isLegacyApi(apiBaseUrl)) {
179
+ // @deprecated Legacy payload for production (.io) backend
180
+ const primaryEmail = typeof window !== "undefined" ? localStorage.getItem("userEmail") : null;
181
+ payload = {
182
+ primary_email: primaryEmail || undefined,
183
+ notice_uuid: noticeUuid,
184
+ source: "WEB",
185
+ declined: Boolean(declined),
186
+ consents: {
187
+ purposes: purposes.map((purpose) => {
188
+ if (!purpose.uuid) {
189
+ throw new Error(`Invalid purpose data: missing uuid`);
190
+ }
191
+ return {
192
+ uuid: purpose.uuid,
193
+ name: purpose.name,
194
+ description: purpose.description || "",
195
+ industries: purpose.industries || "",
196
+ selected: Boolean(purpose.selected),
197
+ data_elements: (purpose.data_elements || []).map((element) => ({
198
+ uuid: element.uuid,
199
+ name: element.name,
200
+ enabled: Boolean(element.enabled),
201
+ required: Boolean(element.required),
202
+ selected: element.required ? true : Boolean(element.selected),
203
+ })),
204
+ };
205
+ }),
206
+ },
207
+ meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
208
+ };
209
+ } else {
210
+ // New payload for staging (.tech) / localhost
211
+ const validatedPurposes = purposes.map((purpose) => {
212
+ if (!purpose.uuid) {
213
+ throw new Error(`Invalid purpose data: missing uuid`);
214
+ }
187
215
 
188
- return {
189
- uuid: purpose.uuid,
190
- name: purpose.name,
191
- description: purpose.description || "",
192
- industries: purpose.industries || "",
193
- selected: Boolean(purpose.selected),
194
- data_elements: (purpose.data_elements || []).map((element) => {
195
- if (!element.uuid || !element.name) {
196
- throw new Error(`Invalid data element: missing uuid or name`);
197
- }
198
-
199
- return {
216
+ // Only include enabled data elements (disabled elements are not selectable)
217
+ const dataElements = (purpose.data_elements || [])
218
+ .filter((element) => element.enabled)
219
+ .map((element) => ({
200
220
  uuid: element.uuid,
201
- name: element.name,
202
- enabled: Boolean(element.enabled),
203
- required: Boolean(element.required),
204
221
  selected: element.required ? true : Boolean(element.selected),
205
- };
206
- }),
207
- };
208
- });
222
+ }));
209
223
 
210
- const payload: ConsentEventPayload = {
211
- primary_email: primaryEmail || undefined,
212
- notice_uuid: noticeUuid,
213
- source: "WEB",
214
- declined: Boolean(declined),
215
- consents: {
216
- purposes: validatedPurposes,
217
- },
218
- meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
219
- };
224
+ return {
225
+ purpose_uuid: purpose.uuid,
226
+ selected: Boolean(purpose.selected),
227
+ data_elements: dataElements.length > 0 ? dataElements : undefined,
228
+ };
229
+ });
220
230
 
221
- const apiBaseUrl = baseUrl || BASE_URL;
231
+ payload = {
232
+ notice_uuid: noticeUuid,
233
+ purposes: validatedPurposes,
234
+ select_all_mandatory: false,
235
+ source: "WEB",
236
+ declined: Boolean(declined),
237
+ meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
238
+ };
239
+ }
222
240
 
223
241
  const response = await fetch(
224
242
  `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
@@ -257,8 +275,6 @@ export const submitConsentEvent = async ({
257
275
  apiCache.clear();
258
276
 
259
277
  } catch (error) {
260
- // Log error for debugging but don't expose sensitive information
261
- console.error("Error submitting consent event:", error instanceof Error ? error.message : error);
262
278
  throw error;
263
279
  }
264
280
  };
@@ -414,8 +430,6 @@ export const fetchTTSAudioUrls = async ({
414
430
 
415
431
  return data;
416
432
  } catch (error) {
417
- // Log error for debugging but don't expose sensitive information
418
- console.error("Error fetching TTS audio URLs:", error instanceof Error ? error.message : error);
419
433
  throw error;
420
434
  }
421
435
  };
@@ -49,7 +49,7 @@ export type ConsentContent = {
49
49
  decline_button_text: string;
50
50
  logo_url: string;
51
51
  privacy_policy_url: string;
52
- sub_processors_url: string;
52
+ privacy_center_url: string;
53
53
  primary_color: string;
54
54
  secondary_color: string;
55
55
  font_preference: string;
@@ -75,28 +75,28 @@ export type ConsentContent = {
75
75
  confirm_button_text: string;
76
76
  decline_button_text: string;
77
77
  privacy_policy_prefix_text: string;
78
- vendor_list_prefix_text: string;
79
78
  privacy_policy_anchor_text: string;
80
- vendors_list_anchor_text: string;
79
+ privacy_center_anchor_text?: string;
81
80
  purpose_section_heading: string;
82
81
  notice_banner_heading: string;
83
82
  data_elements: {
84
83
  [key: string]: string;
85
84
  };
86
85
  purposes: {
87
- [key: string]: string;
86
+ [key: string]: string | { name: string; description: string };
88
87
  };
88
+ dpo_info?: DpoInfoTranslation;
89
89
  };
90
90
  };
91
91
  created_at: string;
92
92
  updated_at: string;
93
93
  deployed_at: string;
94
94
  privacy_policy_prefix_text: string;
95
- vendor_list_prefix_text: string;
96
95
  privacy_policy_anchor_text: string;
97
- vendors_list_anchor_text: string;
96
+ privacy_center_anchor_text?: string;
98
97
  purpose_section_heading: string;
99
98
  notice_banner_heading: string;
99
+ dpo_info?: DpoInfo;
100
100
  };
101
101
  notice_type?: string;
102
102
  compliance_requirement?: string;
@@ -154,12 +154,30 @@ export type FetchConsentContentParams = {
154
154
  language?: string;
155
155
  specific_uuid?: string;
156
156
  validate_against?: "all" | "required";
157
+ include_fully_consented_data?: boolean;
157
158
  };
158
159
 
159
160
  export type ConsentEventPayload = {
160
- org_user_id?: string;
161
+ notice_uuid: string;
162
+ purposes: Array<{
163
+ purpose_uuid: string;
164
+ selected: boolean;
165
+ data_elements?: Array<{
166
+ uuid: string;
167
+ selected: boolean;
168
+ }>;
169
+ }>;
170
+ select_all_mandatory: boolean;
171
+ source?: string;
172
+ declined?: boolean;
173
+ meta_data?: {
174
+ specific_uuid: string;
175
+ };
176
+ };
177
+
178
+ // @deprecated Remove when production backend supports the new payload format.
179
+ export type LegacyConsentEventPayload = {
161
180
  primary_email?: string;
162
- primary_mobile?: string;
163
181
  notice_uuid: string;
164
182
  source: string;
165
183
  declined: boolean;
@@ -179,9 +197,7 @@ export type ConsentEventPayload = {
179
197
  }>;
180
198
  }>;
181
199
  };
182
- meta_data?: {
183
- specific_uuid: string;
184
- };
200
+ meta_data?: { specific_uuid: string };
185
201
  };
186
202
 
187
203
  export type DataElement = {
@@ -263,6 +279,31 @@ export type PurposeItemProps = {
263
279
  isAlreadyConsented: boolean;
264
280
  };
265
281
 
282
+ // DPO Info Types
283
+ export type DpoInfo = {
284
+ grievance_text: string;
285
+ grievance_anchor_text: string;
286
+ grievance_url: string;
287
+ grievance_email: string;
288
+ grievance_email_connector_text?: string;
289
+ dp_board_text: string;
290
+ dp_board_anchor_text: string;
291
+ dp_board_url: string;
292
+ dpo_text: string;
293
+ dpo_anchor_text: string;
294
+ dpo_url: string;
295
+ };
296
+
297
+ export type DpoInfoTranslation = {
298
+ grievance_text?: string;
299
+ grievance_anchor_text?: string;
300
+ grievance_email_connector_text?: string;
301
+ dp_board_text?: string;
302
+ dp_board_anchor_text?: string;
303
+ dpo_text?: string;
304
+ dpo_anchor_text?: string;
305
+ };
306
+
266
307
  // TTS Audio Types
267
308
  export type PurposeAudioUrls = {
268
309
  name_audio_url: string;
@@ -285,16 +326,29 @@ export type TTSAudioUrlsResponse = {
285
326
  confirm_button_text_audio_url: string;
286
327
  decline_button_text_audio_url: string;
287
328
  privacy_policy_prefix_text_audio_url: string;
288
- vendor_list_prefix_text_audio_url: string;
289
329
  privacy_policy_anchor_text_audio_url: string;
290
- vendors_list_anchor_text_audio_url: string;
330
+ privacy_center_anchor_text_audio_url?: string;
291
331
  purpose_section_heading_audio_url: string;
292
332
  notice_banner_heading_audio_url: string;
293
333
  purposes_audio: Record<string, PurposeAudioUrls>;
294
334
  data_elements_audio: Record<string, DataElementAudioUrls>;
335
+ dpo_info_audio?: DpoInfoAudio;
295
336
  };
296
337
  };
297
338
 
339
+ export type DpoInfoAudioKey = keyof DpoInfoAudio;
340
+
341
+ export type DpoInfoAudio = {
342
+ grievance_text_audio_url?: string;
343
+ grievance_anchor_text_audio_url?: string;
344
+ dp_board_text_audio_url?: string;
345
+ dp_board_anchor_text_audio_url?: string;
346
+ dpo_text_audio_url?: string;
347
+ dpo_anchor_text_audio_url?: string;
348
+ grievance_email_connector_text_audio_url?: string;
349
+ grievance_email_audio_url?: string;
350
+ };
351
+
298
352
  export type FetchTTSAudioUrlsParams = {
299
353
  accessToken: string;
300
354
  baseUrl?: string;
@@ -0,0 +1,20 @@
1
+ import type { DpoInfoAudioKey } from "./api/types";
2
+
3
+ /**
4
+ * Maps DPO info audio URL keys to their corresponding DOM element IDs.
5
+ * Used by both audioSequence and elementIdSequence builders to keep them in sync.
6
+ * Order determines playback sequence: grievance → dp board → dpo.
7
+ */
8
+ export const DPO_AUDIO_ELEMENT_MAP: ReadonlyArray<{
9
+ audioKey: DpoInfoAudioKey;
10
+ elementId: string;
11
+ }> = [
12
+ { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
13
+ { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
14
+ { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
15
+ { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
16
+ { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
17
+ { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
18
+ { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
19
+ { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" },
20
+ ] as const;
@@ -54,8 +54,7 @@ export const styles = {
54
54
  justifyContent: "space-between",
55
55
  alignItems: "center",
56
56
  paddingBottom: "15px",
57
- borderBottom: "1px solid #e5e7eb",
58
- boxShadow: "0 1px 2px rgba(0, 0, 0, 0.03)",
57
+ borderBottom: "none",
59
58
  backgroundColor: "#ffffff",
60
59
  margin: 0,
61
60
  boxSizing: "border-box",
@@ -114,7 +113,7 @@ export const styles = {
114
113
  },
115
114
 
116
115
  middleSection: {
117
- margin: "20px 0px",
116
+ margin: "0px",
118
117
  display: "flex",
119
118
  flexDirection: "column",
120
119
  gap: "12px",
@@ -130,7 +129,6 @@ export const styles = {
130
129
  fontSize: "16px",
131
130
  fontWeight: 400,
132
131
  lineHeight: "150%",
133
- letterSpacing: "0.2px",
134
132
  color: "#344054",
135
133
  margin: 0,
136
134
  padding: 0,
@@ -284,8 +282,7 @@ export const styles = {
284
282
  justifyContent: "space-between",
285
283
  gap: "29px",
286
284
  paddingTop: "15px",
287
- borderTop: "1px solid #e5e7eb",
288
- boxShadow: "0 -1px 2px rgba(0, 0, 0, 0.03)",
285
+ borderTop: "none",
289
286
  backgroundColor: "#ffffff",
290
287
  margin: 0,
291
288
  boxSizing: "border-box",
@@ -1,4 +1,4 @@
1
- import type { Settings } from "./api/types";
1
+ import type { Settings, DpoInfoTranslation } from "./api/types";
2
2
 
3
3
  export type Props = Readonly<{
4
4
  noticeId: string;
@@ -13,10 +13,14 @@ export type Props = Readonly<{
13
13
  onError?: (error: Error) => void;
14
14
  applicationId?: string;
15
15
  validateAgainst?: "all" | "required";
16
+ includeFullyConsentedData?: boolean;
17
+ reviewModeButtonText?: string;
16
18
  }>;
17
19
 
20
+ export type PurposeTranslation = string | { name: string; description: string };
21
+
18
22
  export type PurposeTranslations = {
19
- [purposeId: string]: string;
23
+ [purposeId: string]: PurposeTranslation;
20
24
  };
21
25
 
22
26
  export type DataElementTranslations = {
@@ -33,13 +37,15 @@ export type TranslationObject = {
33
37
  purpose_section_heading?: string;
34
38
  notice_banner_heading?: string;
35
39
  privacy_policy_prefix_text?: string;
36
- vendor_list_prefix_text?: string;
37
40
  privacy_policy_anchor_text?: string;
38
- vendors_list_anchor_text?: string;
41
+ privacy_center_anchor_text?: string;
42
+ dpo_info?: DpoInfoTranslation;
39
43
  [key: string]:
40
44
  | string
41
45
  | PurposeTranslations
42
46
  | DataElementTranslations
43
- | Record<string, string>
47
+ | DpoInfoTranslation
48
+ | Record<string, string | undefined>
49
+ | Record<string, string | { name: string; description: string }>
44
50
  | undefined;
45
51
  };
@@ -103,7 +103,7 @@ const RedactoConsentInlineComponent = ({
103
103
  ): string => {
104
104
  if (!content) return defaultText;
105
105
 
106
- if (selectedLanguage === content.default_language) {
106
+ if (selectedLanguage === "English" || selectedLanguage === "en" || selectedLanguage === "EN") {
107
107
  if (
108
108
  key === "privacy_policy_anchor_text" &&
109
109
  content.privacy_policy_anchor_text
@@ -127,7 +127,16 @@ const RedactoConsentInlineComponent = ({
127
127
 
128
128
  if (itemId) {
129
129
  if (key === "purposes.name" || key === "purposes.description") {
130
- return translationMap.purposes?.[itemId] || defaultText;
130
+ const purposeData = translationMap.purposes?.[itemId];
131
+ if (purposeData) {
132
+ if (typeof purposeData === "string") {
133
+ return key === "purposes.name" ? purposeData : defaultText;
134
+ }
135
+ return key === "purposes.name"
136
+ ? purposeData.name || defaultText
137
+ : purposeData.description || defaultText;
138
+ }
139
+ return defaultText;
131
140
  }
132
141
  if (key.startsWith("data_elements.")) {
133
142
  return translationMap.data_elements?.[itemId] || defaultText;
@@ -135,7 +144,7 @@ const RedactoConsentInlineComponent = ({
135
144
  }
136
145
 
137
146
  const value = translationMap[key];
138
- if (typeof value === "string") {
147
+ if (typeof value === "string" && value) {
139
148
  return value;
140
149
  }
141
150
 
@@ -154,26 +163,24 @@ const RedactoConsentInlineComponent = ({
154
163
  language,
155
164
  specific_uuid: applicationId,
156
165
  });
157
- setContent(consentContentData.detail.active_config);
158
- if (consentContentData.detail.active_config.default_language) {
159
- setSelectedLanguage(
160
- consentContentData.detail.active_config.default_language
161
- );
166
+ const activeConfig = consentContentData.detail.active_config;
167
+ setContent(activeConfig);
168
+ if (activeConfig.default_language) {
169
+ setSelectedLanguage(activeConfig.default_language);
162
170
  }
163
- const initialCollapsedState =
164
- consentContentData.detail.active_config.purposes.reduce(
165
- (acc, purpose) => ({
166
- ...acc,
167
- [purpose.uuid]: true,
168
- }),
169
- {}
170
- );
171
+ const initialCollapsedState = activeConfig.purposes.reduce(
172
+ (acc, purpose) => ({
173
+ ...acc,
174
+ [purpose.uuid]: true,
175
+ }),
176
+ {}
177
+ );
171
178
  setCollapsedPurposes(initialCollapsedState);
172
179
 
173
180
  // Initialize selected states
174
181
  // All elements start unchecked, but required elements cannot be unchecked once checked
175
182
  const initialDataElementState: Record<string, boolean> =
176
- consentContentData.detail.active_config.purposes.reduce(
183
+ activeConfig.purposes.reduce(
177
184
  (acc: Record<string, boolean>, purpose) => {
178
185
  purpose.data_elements.forEach((element) => {
179
186
  const combinedId = `${purpose.uuid}-${element.uuid}`;
@@ -187,7 +194,7 @@ const RedactoConsentInlineComponent = ({
187
194
 
188
195
  // Initialize purpose states - all start unchecked
189
196
  const initialPurposeState: Record<string, boolean> =
190
- consentContentData.detail.active_config.purposes.reduce(
197
+ activeConfig.purposes.reduce(
191
198
  (acc: Record<string, boolean>, purpose) => {
192
199
  return {
193
200
  ...acc,
@@ -198,7 +205,6 @@ const RedactoConsentInlineComponent = ({
198
205
  );
199
206
  setSelectedPurposes(initialPurposeState);
200
207
  } catch (err: unknown) {
201
- console.error(err);
202
208
  const error = err as Error & { status?: number };
203
209
  setFetchError(error);
204
210
  if (error.status === 409) {
@@ -360,13 +366,12 @@ const RedactoConsentInlineComponent = ({
360
366
  // Reset ref on success to allow re-submission if needed
361
367
  hasSubmittedRef.current = false;
362
368
  } catch (error) {
363
- console.error("Error submitting consent:", error);
364
369
  const err = error as Error & { status?: number };
365
- if (err.status === 500) {
366
- setErrorMessage(
367
- "An error occurred while submitting your consent. Please try again later."
368
- );
369
- }
370
+ const message =
371
+ err.status === 500
372
+ ? "An error occurred while submitting your consent. Please try again later."
373
+ : err.message || "Failed to submit consent. Please try again.";
374
+ setErrorMessage(message);
370
375
  // Reset ref on error to allow retry
371
376
  hasSubmittedRef.current = false;
372
377
  throw error;
@@ -400,7 +405,6 @@ const RedactoConsentInlineComponent = ({
400
405
  try {
401
406
  await handleAcceptWithToken(accessToken);
402
407
  } catch (err) {
403
- console.error("Failed to auto-submit consent:", err);
404
408
  hasSubmittedRef.current = false;
405
409
  onError?.(err as Error);
406
410
  }
@@ -1,9 +1,11 @@
1
1
  import type {
2
2
  ConsentContent,
3
3
  ConsentEventPayload,
4
+ LegacyConsentEventPayload,
4
5
  FetchConsentContentParams,
5
6
  SubmitConsentEventParams,
6
7
  } from "./types";
8
+ import { isLegacyApi } from "../../utils/isLegacyApi";
7
9
 
8
10
  const BASE_URL = "https://api.redacto.tech/consent";
9
11
 
@@ -58,7 +60,6 @@ export const fetchConsentContent = async ({
58
60
  const data = await response.json();
59
61
  return data;
60
62
  } catch (error) {
61
- console.error("Error fetching consent content:", error);
62
63
  throw error;
63
64
  }
64
65
  };
@@ -74,33 +75,71 @@ export const submitConsentEvent = async ({
74
75
  meta_data,
75
76
  }: SubmitConsentEventParams): Promise<void> => {
76
77
  try {
77
- const primaryEmail = localStorage.getItem("userEmail");
78
- const payload: ConsentEventPayload = {
79
- primary_email: primaryEmail || undefined,
80
- notice_uuid: noticeUuid,
81
- source: "WEB",
82
- declined,
83
- consents: {
84
- purposes: purposes.map((purpose) => ({
85
- uuid: purpose.uuid,
86
- name: purpose.name,
87
- description: purpose.description,
88
- industries: purpose.industries || "",
89
- selected: purpose.selected,
90
- data_elements: purpose.data_elements.map((element) => ({
91
- uuid: element.uuid,
92
- name: element.name,
93
- enabled: element.enabled,
94
- required: element.required,
95
- selected: element.required ? true : element.selected,
96
- })),
97
- })),
98
- },
99
- meta_data,
100
- };
101
-
102
78
  const apiBaseUrl = baseUrl || BASE_URL;
103
79
 
80
+ let payload: ConsentEventPayload | LegacyConsentEventPayload;
81
+
82
+ if (isLegacyApi(apiBaseUrl)) {
83
+ // @deprecated Legacy payload for production (.io) backend
84
+ const primaryEmail = typeof window !== "undefined" ? localStorage.getItem("userEmail") : null;
85
+ payload = {
86
+ primary_email: primaryEmail || undefined,
87
+ notice_uuid: noticeUuid,
88
+ source: "WEB",
89
+ declined: Boolean(declined),
90
+ consents: {
91
+ purposes: purposes.map((purpose) => {
92
+ if (!purpose.uuid) {
93
+ throw new Error(`Invalid purpose data: missing uuid`);
94
+ }
95
+ return {
96
+ uuid: purpose.uuid,
97
+ name: purpose.name,
98
+ description: purpose.description || "",
99
+ industries: purpose.industries || "",
100
+ selected: Boolean(purpose.selected),
101
+ data_elements: (purpose.data_elements || []).map((element) => ({
102
+ uuid: element.uuid,
103
+ name: element.name,
104
+ enabled: Boolean(element.enabled),
105
+ required: Boolean(element.required),
106
+ selected: element.required ? true : Boolean(element.selected),
107
+ })),
108
+ };
109
+ }),
110
+ },
111
+ meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
112
+ };
113
+ } else {
114
+ // New payload for staging (.tech) / localhost
115
+ payload = {
116
+ notice_uuid: noticeUuid,
117
+ purposes: purposes.map((purpose) => {
118
+ if (!purpose.uuid) {
119
+ throw new Error(`Invalid purpose data: missing uuid`);
120
+ }
121
+
122
+ // Only include enabled data elements (disabled elements are not selectable)
123
+ const dataElements = purpose.data_elements
124
+ .filter((element) => element.enabled)
125
+ .map((element) => ({
126
+ uuid: element.uuid,
127
+ selected: element.required ? true : Boolean(element.selected),
128
+ }));
129
+
130
+ return {
131
+ purpose_uuid: purpose.uuid,
132
+ selected: Boolean(purpose.selected),
133
+ data_elements: dataElements.length > 0 ? dataElements : undefined,
134
+ };
135
+ }),
136
+ select_all_mandatory: false,
137
+ source: "WEB",
138
+ declined,
139
+ meta_data,
140
+ };
141
+ }
142
+
104
143
  const response = await fetch(
105
144
  `${apiBaseUrl}/public/organisations/${org_uuid}/workspaces/${workspace_uuid}/submit-consent`,
106
145
  {
@@ -151,7 +190,6 @@ export const submitConsentEvent = async ({
151
190
  throw error;
152
191
  }
153
192
  } catch (error) {
154
- console.error("Error submitting consent event:", error);
155
193
  throw error;
156
194
  }
157
195
  };