@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.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +423 -136
- package/dist/index.mjs +423 -136
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +123 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +936 -671
- package/src/RedactoNoticeConsent/api/index.ts +67 -53
- package/src/RedactoNoticeConsent/api/types.ts +67 -13
- package/src/RedactoNoticeConsent/constants.ts +20 -0
- package/src/RedactoNoticeConsent/styles.ts +3 -6
- package/src/RedactoNoticeConsent/types.ts +11 -5
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +30 -26
- package/src/RedactoNoticeConsentInline/api/index.ts +65 -27
- package/src/RedactoNoticeConsentInline/api/types.ts +28 -11
- package/src/RedactoNoticeConsentInline/types.ts +9 -4
- package/src/types/react-jsx.d.ts +26 -0
- package/src/utils/isLegacyApi.ts +12 -0
- package/tests/mocks.ts +53 -7
- package/tsconfig.json +1 -5
|
@@ -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
|
|
174
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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]:
|
|
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
|
-
|
|
41
|
+
privacy_center_anchor_text?: string;
|
|
42
|
+
dpo_info?: DpoInfoTranslation;
|
|
39
43
|
[key: string]:
|
|
40
44
|
| string
|
|
41
45
|
| PurposeTranslations
|
|
42
46
|
| DataElementTranslations
|
|
43
|
-
|
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
};
|