@redacto.io/consent-sdk-react 0.0.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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/README.md +50 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +1166 -0
- package/dist/index.mjs +1139 -0
- package/package.json +46 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +368 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +765 -0
- package/src/RedactoNoticeConsent/api/index.ts +143 -0
- package/src/RedactoNoticeConsent/api/types.ts +181 -0
- package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeConsent/index.ts +1 -0
- package/src/RedactoNoticeConsent/styles.ts +352 -0
- package/src/RedactoNoticeConsent/types.ts +41 -0
- package/src/RedactoNoticeConsent/useMediaQuery.ts +36 -0
- package/src/index.ts +1 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/images.d.ts +19 -0
- package/tests/mocks.ts +114 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@redacto.io/consent-sdk-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.cjs",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.cjs",
|
|
11
|
+
"import": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"description": "",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"jwt-decode": "^4.0.0",
|
|
23
|
+
"react": "^19.1.0",
|
|
24
|
+
"react-dom": "^19.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@changesets/cli": "^2.29.4",
|
|
28
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
29
|
+
"@testing-library/react": "^16.3.0",
|
|
30
|
+
"@types/react": "^19.1.3",
|
|
31
|
+
"@types/react-dom": "^19.1.3",
|
|
32
|
+
"@vitest/coverage-v8": "3.1.3",
|
|
33
|
+
"jsdom": "^26.1.0",
|
|
34
|
+
"tsup": "^8.4.0",
|
|
35
|
+
"typescript": "5.8.2",
|
|
36
|
+
"vitest": "^3.1.3"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"test": "vitest",
|
|
42
|
+
"test:watch": "vitest watch",
|
|
43
|
+
"release": "pnpm run build && changeset publish",
|
|
44
|
+
"coverage": "vitest run --coverage"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
act,
|
|
7
|
+
} from "@testing-library/react";
|
|
8
|
+
import { vi, expect, it, describe, beforeEach, afterEach } from "vitest";
|
|
9
|
+
import { RedactoNoticeConsent } from "./RedactoNoticeConsent";
|
|
10
|
+
import * as api from "./api";
|
|
11
|
+
import type { ConsentContent } from "./api/types";
|
|
12
|
+
import { defaultProps, mockConsentContent } from "../../tests/mocks";
|
|
13
|
+
import "@testing-library/jest-dom";
|
|
14
|
+
|
|
15
|
+
// Mock the API functions
|
|
16
|
+
vi.mock("./api", () => ({
|
|
17
|
+
fetchConsentContent: vi.fn(),
|
|
18
|
+
submitConsentEvent: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock the logo import correctly for ES modules
|
|
22
|
+
vi.mock("./assets/redacto-logo.png", () => ({
|
|
23
|
+
default: "mocked-logo.png",
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe("RedactoNoticeConsent", () => {
|
|
27
|
+
const mockedApi = vi.mocked(api, { partial: true });
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockedApi.fetchConsentContent.mockResolvedValue(mockConsentContent);
|
|
31
|
+
mockedApi.submitConsentEvent.mockResolvedValue(undefined);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders loading state initially", () => {
|
|
39
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
40
|
+
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders consent content after loading", async () => {
|
|
44
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
45
|
+
await waitFor(
|
|
46
|
+
() => {
|
|
47
|
+
expect(screen.getByText("Your Privacy Matters")).toBeInTheDocument();
|
|
48
|
+
expect(
|
|
49
|
+
screen.getByText(/We care about your privacy/i)
|
|
50
|
+
).toBeInTheDocument();
|
|
51
|
+
expect(
|
|
52
|
+
screen.getByRole("link", { name: /Privacy Policy/i })
|
|
53
|
+
).toBeInTheDocument();
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole("link", { name: /Vendors List/i })
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText("Manage What You Share")).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText("Marketing")).toBeInTheDocument();
|
|
59
|
+
},
|
|
60
|
+
{ timeout: 3000 }
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("displays logo from content", async () => {
|
|
65
|
+
const { unmount } = render(<RedactoNoticeConsent {...defaultProps} />);
|
|
66
|
+
await waitFor(
|
|
67
|
+
() => {
|
|
68
|
+
const logo = screen.getByAltText("logo");
|
|
69
|
+
expect(logo).toHaveAttribute("src", "https://example.com/logo.png");
|
|
70
|
+
},
|
|
71
|
+
{ timeout: 3000 }
|
|
72
|
+
);
|
|
73
|
+
unmount();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("displays fallback logo when no content logo provided", async () => {
|
|
77
|
+
const noLogoContent: ConsentContent = {
|
|
78
|
+
...mockConsentContent,
|
|
79
|
+
detail: {
|
|
80
|
+
...mockConsentContent.detail,
|
|
81
|
+
active_config: {
|
|
82
|
+
...mockConsentContent.detail.active_config,
|
|
83
|
+
logo_url: "",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
mockedApi.fetchConsentContent.mockResolvedValueOnce(noLogoContent);
|
|
88
|
+
const { unmount } = render(<RedactoNoticeConsent {...defaultProps} />);
|
|
89
|
+
await waitFor(
|
|
90
|
+
() => {
|
|
91
|
+
const logo = screen.getByAltText("logo");
|
|
92
|
+
expect(logo).toHaveAttribute("src", "mocked-logo.png");
|
|
93
|
+
},
|
|
94
|
+
{ timeout: 3000 }
|
|
95
|
+
);
|
|
96
|
+
unmount();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles language selection", async () => {
|
|
100
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
101
|
+
await waitFor(
|
|
102
|
+
() => {
|
|
103
|
+
expect(screen.getByRole("button", { name: "en" })).toBeInTheDocument();
|
|
104
|
+
},
|
|
105
|
+
{ timeout: 3000 }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
fireEvent.click(screen.getByRole("button", { name: "en" }));
|
|
110
|
+
});
|
|
111
|
+
await waitFor(
|
|
112
|
+
() => {
|
|
113
|
+
expect(screen.getByRole("option", { name: "es" })).toBeInTheDocument();
|
|
114
|
+
},
|
|
115
|
+
{ timeout: 3000 }
|
|
116
|
+
);
|
|
117
|
+
await act(async () => {
|
|
118
|
+
fireEvent.click(screen.getByText("es"));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await waitFor(
|
|
122
|
+
() => {
|
|
123
|
+
expect(
|
|
124
|
+
screen.getByText(/Nos importa tu privacidad/i)
|
|
125
|
+
).toBeInTheDocument();
|
|
126
|
+
expect(
|
|
127
|
+
screen.getByRole("link", { name: /Política de Privacidad/i })
|
|
128
|
+
).toBeInTheDocument();
|
|
129
|
+
expect(
|
|
130
|
+
screen.getByText("Gestionar Consentimiento")
|
|
131
|
+
).toBeInTheDocument();
|
|
132
|
+
},
|
|
133
|
+
{ timeout: 3000 }
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("toggles purpose collapse", async () => {
|
|
138
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
139
|
+
await waitFor(
|
|
140
|
+
() => {
|
|
141
|
+
expect(screen.getByText("Marketing")).toBeInTheDocument();
|
|
142
|
+
},
|
|
143
|
+
{ timeout: 3000 }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const marketingHeading = screen.getByText("Marketing");
|
|
147
|
+
const collapseDiv = marketingHeading
|
|
148
|
+
.closest('div[style*="cursor: pointer"]')
|
|
149
|
+
?.querySelector("svg");
|
|
150
|
+
if (!collapseDiv) throw new Error("Collapse div not found");
|
|
151
|
+
|
|
152
|
+
await act(async () => {
|
|
153
|
+
fireEvent.click(collapseDiv);
|
|
154
|
+
});
|
|
155
|
+
await waitFor(
|
|
156
|
+
() => {
|
|
157
|
+
expect(screen.getByText("Email")).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText("Phone")).toBeInTheDocument();
|
|
159
|
+
},
|
|
160
|
+
{ timeout: 3000 }
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await act(async () => {
|
|
164
|
+
fireEvent.click(collapseDiv);
|
|
165
|
+
});
|
|
166
|
+
await waitFor(
|
|
167
|
+
() => {
|
|
168
|
+
expect(screen.queryByText("Email")).not.toBeInTheDocument();
|
|
169
|
+
},
|
|
170
|
+
{ timeout: 3000 }
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("handles purpose checkbox interaction", async () => {
|
|
175
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
176
|
+
await waitFor(
|
|
177
|
+
() => {
|
|
178
|
+
expect(screen.getByText("Marketing")).toBeInTheDocument();
|
|
179
|
+
},
|
|
180
|
+
{ timeout: 3000 }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const marketingHeading = screen.getByText("Marketing");
|
|
184
|
+
const purposeCheckbox = marketingHeading
|
|
185
|
+
.closest('div[style*="justify-content: space-between"]')
|
|
186
|
+
?.querySelector('input[type="checkbox"]');
|
|
187
|
+
if (!purposeCheckbox) throw new Error("Purpose checkbox not found");
|
|
188
|
+
|
|
189
|
+
expect(purposeCheckbox).not.toBeChecked();
|
|
190
|
+
await act(async () => {
|
|
191
|
+
fireEvent.click(purposeCheckbox);
|
|
192
|
+
});
|
|
193
|
+
expect(purposeCheckbox).toBeChecked();
|
|
194
|
+
|
|
195
|
+
await act(async () => {
|
|
196
|
+
fireEvent.click(purposeCheckbox);
|
|
197
|
+
});
|
|
198
|
+
expect(purposeCheckbox).not.toBeChecked();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("disables required data element checkboxes", async () => {
|
|
202
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
203
|
+
await waitFor(
|
|
204
|
+
() => {
|
|
205
|
+
expect(screen.getByText("Marketing")).toBeInTheDocument();
|
|
206
|
+
},
|
|
207
|
+
{ timeout: 3000 }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const marketingHeading = screen.getByText("Marketing");
|
|
211
|
+
const collapseDiv = marketingHeading
|
|
212
|
+
.closest('div[style*="cursor: pointer"]')
|
|
213
|
+
?.querySelector("svg");
|
|
214
|
+
if (!collapseDiv) throw new Error("Collapse div not found");
|
|
215
|
+
await act(async () => {
|
|
216
|
+
fireEvent.click(collapseDiv);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await waitFor(
|
|
220
|
+
() => {
|
|
221
|
+
const emailText = screen.getByText("Email");
|
|
222
|
+
const emailCheckbox = emailText
|
|
223
|
+
.closest('div[style*="align-items: center"]')
|
|
224
|
+
?.querySelector('input[type="checkbox"]');
|
|
225
|
+
if (!emailCheckbox) throw new Error("Email checkbox not found");
|
|
226
|
+
|
|
227
|
+
const phoneText = screen.getByText(/Phone/);
|
|
228
|
+
const phoneCheckbox = phoneText
|
|
229
|
+
.closest('div[style*="align-items: center"]')
|
|
230
|
+
?.querySelector('input[type="checkbox"]');
|
|
231
|
+
if (!phoneCheckbox) throw new Error("Phone checkbox not found");
|
|
232
|
+
|
|
233
|
+
expect(phoneCheckbox).toBeDisabled();
|
|
234
|
+
expect(phoneCheckbox).toBeChecked();
|
|
235
|
+
|
|
236
|
+
expect(emailCheckbox).not.toBeDisabled();
|
|
237
|
+
fireEvent.click(emailCheckbox);
|
|
238
|
+
expect(emailCheckbox).toBeChecked();
|
|
239
|
+
},
|
|
240
|
+
{ timeout: 3000 }
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("calls submitConsentEvent on accept", async () => {
|
|
245
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
246
|
+
await waitFor(
|
|
247
|
+
() => {
|
|
248
|
+
expect(screen.getByText("Accept")).toBeInTheDocument();
|
|
249
|
+
},
|
|
250
|
+
{ timeout: 3000 }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await act(async () => {
|
|
254
|
+
fireEvent.click(screen.getByText("Accept"));
|
|
255
|
+
});
|
|
256
|
+
await waitFor(
|
|
257
|
+
() => {
|
|
258
|
+
expect(api.submitConsentEvent).toHaveBeenCalledWith({
|
|
259
|
+
accessToken: "mock-token",
|
|
260
|
+
noticeUuid: "notice-123",
|
|
261
|
+
purposes: [
|
|
262
|
+
{
|
|
263
|
+
uuid: "purpose-1",
|
|
264
|
+
name: "Marketing",
|
|
265
|
+
description: "We use data for marketing purposes.",
|
|
266
|
+
industries: "Marketing",
|
|
267
|
+
data_elements: [
|
|
268
|
+
{
|
|
269
|
+
uuid: "element-1",
|
|
270
|
+
name: "Email",
|
|
271
|
+
required: false,
|
|
272
|
+
description: null,
|
|
273
|
+
industries: null,
|
|
274
|
+
enabled: true,
|
|
275
|
+
selected: false,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
uuid: "element-2",
|
|
279
|
+
name: "Phone",
|
|
280
|
+
required: true,
|
|
281
|
+
description: null,
|
|
282
|
+
industries: null,
|
|
283
|
+
enabled: true,
|
|
284
|
+
selected: true,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
selected: false,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
declined: false,
|
|
291
|
+
});
|
|
292
|
+
expect(defaultProps.onAccept).toHaveBeenCalled();
|
|
293
|
+
},
|
|
294
|
+
{ timeout: 3000 }
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("calls onDecline on decline", async () => {
|
|
299
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
300
|
+
await waitFor(
|
|
301
|
+
() => {
|
|
302
|
+
expect(screen.getByText("Decline")).toBeInTheDocument();
|
|
303
|
+
},
|
|
304
|
+
{ timeout: 3000 }
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await act(async () => {
|
|
308
|
+
fireEvent.click(screen.getByText("Decline"));
|
|
309
|
+
});
|
|
310
|
+
await waitFor(
|
|
311
|
+
() => {
|
|
312
|
+
expect(defaultProps.onDecline).toHaveBeenCalled();
|
|
313
|
+
},
|
|
314
|
+
{ timeout: 3000 }
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("handles 409 conflict error (already consented)", async () => {
|
|
319
|
+
mockedApi.fetchConsentContent.mockRejectedValueOnce({ status: 409 });
|
|
320
|
+
await act(async () => {
|
|
321
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
322
|
+
});
|
|
323
|
+
await waitFor(
|
|
324
|
+
() => {
|
|
325
|
+
expect(defaultProps.onAccept).toHaveBeenCalled();
|
|
326
|
+
expect(
|
|
327
|
+
screen.queryByText("Your Privacy Matters")
|
|
328
|
+
).not.toBeInTheDocument();
|
|
329
|
+
},
|
|
330
|
+
{ timeout: 3000 }
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("handles other errors", async () => {
|
|
335
|
+
mockedApi.fetchConsentContent.mockRejectedValueOnce(
|
|
336
|
+
new Error("Network Error")
|
|
337
|
+
);
|
|
338
|
+
await act(async () => {
|
|
339
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
340
|
+
});
|
|
341
|
+
await waitFor(
|
|
342
|
+
() => {
|
|
343
|
+
expect(defaultProps.onError).toHaveBeenCalledWith(expect.any(Error));
|
|
344
|
+
},
|
|
345
|
+
{ timeout: 3000 }
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("applies custom styles from settings", async () => {
|
|
350
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
351
|
+
await waitFor(
|
|
352
|
+
() => {
|
|
353
|
+
const modal = screen.getByRole("dialog");
|
|
354
|
+
expect(modal).toHaveStyle({
|
|
355
|
+
backgroundColor: "#ffffff",
|
|
356
|
+
borderRadius: "8px",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const acceptButton = screen.getByText("Accept");
|
|
360
|
+
expect(acceptButton).toHaveStyle({
|
|
361
|
+
backgroundColor: "#4f87ff",
|
|
362
|
+
color: "#ffffff",
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
{ timeout: 3000 }
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|