react-lgpd-consent 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # react-lgpd-consent 🍪
2
2
 
3
- [![NPM Version](https://img.shields.io/npm/v/react-lgpd-consent?style=for-the-badge&color=blue)](https://www.npmjs.com/package/react-lgpd-consent)
3
+ [![NPM Version](https://img.shields.io/npm/v/react-lgpd-consent?style=for-the-badge&color=blue)](https://www.n useEffect(() => {
4
+ if (consented && preferences.analytics) {
5
+ loadScript(
6
+ 'ga',
7
+ 'https://www.googletagmanager.com/gtag/js?id=GA_ID',
8
+ 'analytics' // Aguarda consentimento finalizado
9
+ )
10
+ }
11
+ }, [preferences, consented])package/react-lgpd-consent)
4
12
  [![License](https://img.shields.io/npm/l/react-lgpd-consent?style=for-the-badge)](https://github.com/lucianoedipo/react-lgpd-consent/blob/main/LICENSE)
5
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org/)
6
14
  [![React](https://img.shields.io/badge/React-18%2B-61DAFB?style=for-the-badge&logo=react)](https://reactjs.org/)
@@ -23,6 +31,7 @@ Solução moderna, acessível e personalizável para gerenciar consentimento de
23
31
  - 🚫 **Banner Bloqueante**: Modo opcional para exigir interação antes de continuar
24
32
  - 🎨 **Sistema de Temas**: Temas customizáveis para integração visual perfeita
25
33
  - ⚡ **Carregamento Condicional**: Scripts só executam após consentimento explícito
34
+ - 🔌 **Modal Automático**: Modal de preferências incluído automaticamente com lazy loading
26
35
 
27
36
  ## 🚀 Instalação
28
37
 
@@ -40,7 +49,27 @@ pnpm add react-lgpd-consent
40
49
  npm install @mui/material js-cookie
41
50
  ```
42
51
 
43
- ## 📖 Uso Básico
52
+ ## Exemplo Completo
53
+
54
+ ```tsx
55
+ import { ConsentProvider, CookieBanner } from 'react-lgpd-consent'
56
+
57
+ function App() {
58
+ return (
59
+ <ConsentProvider>
60
+ <div>
61
+ <h1>Meu Site</h1>
62
+ <p>Conteúdo do site...</p>
63
+
64
+ {/* Banner de cookies */}
65
+ <CookieBanner policyLinkUrl="/privacy-policy" blocking={true} />
66
+ </div>
67
+ </ConsentProvider>
68
+ )
69
+ }
70
+ ```
71
+
72
+ ## �📖 Uso Básico
44
73
 
45
74
  ### 1. Setup do Provider
46
75
 
@@ -69,6 +98,7 @@ function Layout() {
69
98
  policyLinkUrl="/politica-privacidade"
70
99
  blocking={true} // Padrão: bloqueia até decisão
71
100
  />
101
+ {/* Modal de preferências incluído automaticamente! */}
72
102
  </>
73
103
  )
74
104
  }
@@ -92,10 +122,14 @@ function MyComponent() {
92
122
  }
93
123
  ```
94
124
 
125
+ > ✅ **O modal de preferências é incluído automaticamente pelo ConsentProvider!** Não é mais necessário renderizá-lo manualmente.
126
+
127
+ ````
128
+
95
129
  ### 4. Carregamento Condicional de Scripts
96
130
 
97
131
  ```tsx
98
- import { ConsentGate, loadConditionalScript } from 'react-lgpd-consent'
132
+ import { ConsentGate, loadScript } from 'react-lgpd-consent'
99
133
 
100
134
  function Analytics() {
101
135
  return (
@@ -119,7 +153,7 @@ function MyComponent() {
119
153
  }
120
154
  }, [preferences, consented])
121
155
  }
122
- ```
156
+ ````
123
157
 
124
158
  ## 🎨 Customização
125
159
 
@@ -242,41 +276,87 @@ const temaCustomizado = createTheme({
242
276
  })
243
277
  ```
244
278
 
245
- ## ⚡ Carregamento Condicional
279
+ ## ⚡ Carregamento Inteligente de Scripts
246
280
 
247
- ### Função loadConditionalScript
281
+ ### Função loadScript
248
282
 
249
- Para scripts que devem aguardar consentimento específico:
283
+ Scripts aguardam automaticamente o **consentimento finalizado** (banner fechado ou preferências salvas):
250
284
 
251
285
  ```tsx
252
- import { loadConditionalScript } from 'react-lgpd-consent'
286
+ import { loadScript } from 'react-lgpd-consent'
253
287
 
254
- // Carrega script apenas quando analytics for aceito
255
- await loadConditionalScript(
288
+ // Carrega script apenas após consentimento para analytics
289
+ await loadScript(
256
290
  'gtag',
257
291
  'https://www.googletagmanager.com/gtag/js?id=GA_ID',
258
- () => preferences.analytics,
259
- { timeout: 10000 }, // timeout opcional
292
+ 'analytics', // Categoria obrigatória
260
293
  )
294
+
295
+ // Script geral (sempre carrega após consentimento)
296
+ await loadScript('custom-script', 'https://example.com/script.js')
297
+ ```
298
+
299
+ ### Comportamento Inteligente
300
+
301
+ - **Aguarda decisão final**: Não executa durante mudanças no modal de preferências
302
+ - **Só executa após salvar**: Scripts só rodam quando o usuário finaliza as preferências
303
+ - **Baseado em categoria**: Respeita as permissões por categoria
304
+
305
+ ## 🎨 Personalização Total
306
+
307
+ ### Modal de Preferências Customizado
308
+
309
+ Substitua completamente o modal padrão com seu próprio componente:
310
+
311
+ ```tsx
312
+ import { ConsentProvider, useConsent } from 'react-lgpd-consent'
313
+ import MeuModalCustomizado from './MeuModalCustomizado'
314
+
315
+ function App() {
316
+ return (
317
+ <ConsentProvider
318
+ PreferencesModalComponent={MeuModalCustomizado}
319
+ preferencesModalProps={{ variant: 'custom' }}
320
+ >
321
+ <MeuApp />
322
+ </ConsentProvider>
323
+ )
324
+ }
325
+
326
+ // Seu componente customizado
327
+ function MeuModalCustomizado({ variant }) {
328
+ const { isModalOpen, closePreferences, setPreference } = useConsent()
329
+
330
+ return (
331
+ <MyCustomDialog open={isModalOpen} onClose={closePreferences}>
332
+ {/* Seu design personalizado aqui */}
333
+ </MyCustomDialog>
334
+ )
335
+ }
261
336
  ```
262
337
 
263
- ### Parâmetros
338
+ ### Desabilitar Modal Automático
264
339
 
265
- - `id`: Identificador único para o script
266
- - `src`: URL do script a ser carregado
267
- - `condition`: Função que retorna boolean indicando se deve carregar
268
- - `options`: Configurações opcionais (timeout, etc.)
340
+ Para controle total, desabilite o modal automático:
341
+
342
+ ```tsx
343
+ <ConsentProvider disableAutomaticModal>
344
+ <MeuApp />
345
+ {/* Renderize seus componentes customizados onde quiser */}
346
+ <MeuModalTotalmenteCustomizado />
347
+ </ConsentProvider>
348
+ ```
269
349
 
270
350
  ## �🔧 API Completa
271
351
 
272
352
  ### Components
273
353
 
274
- | Componente | Descrição | Props Principais |
275
- | ------------------ | -------------------------------------- | ------------------------------------------------------------ |
276
- | `ConsentProvider` | Provider principal do contexto | `initialState`, `texts`, `theme`, `cookie`, callbacks |
277
- | `CookieBanner` | Banner de consentimento | `policyLinkUrl`, `blocking`, `debug`, pass-through MUI props |
278
- | `PreferencesModal` | Modal de preferências detalhadas | `DialogProps` (MUI pass-through) |
279
- | `ConsentGate` | Renderização condicional por categoria | `category`, `children` |
354
+ | Componente | Descrição | Props Principais |
355
+ | ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
356
+ | `ConsentProvider` | Provider principal do contexto | `initialState`, `texts`, `theme`, `PreferencesModalComponent`, `disableAutomaticModal`, callbacks |
357
+ | `CookieBanner` | Banner de consentimento | `policyLinkUrl`, `blocking`, `debug`, pass-through MUI props |
358
+ | `PreferencesModal` | Modal de preferências (incluído automaticamente) | `DialogProps` (MUI pass-through) - **Opcional** |
359
+ | `ConsentGate` | Renderização condicional por categoria | `category`, `children` |
280
360
 
281
361
  ### Hook `useConsent()`
282
362
 
@@ -304,12 +384,9 @@ console.log(texts.banner.title) // "Política de Cookies"
304
384
 
305
385
  ### Utilitários
306
386
 
307
- - `loadScript(id, src, attrs?)` - Carrega scripts dinamicamente
308
- - `loadConditionalScript(id, src, condition, options?)` - Carrega scripts condicionalmente
387
+ - `loadScript(id, src, category?, attrs?)` - Carrega scripts com consentimento inteligente
309
388
  - `defaultConsentTheme` - Tema padrão do Material-UI
310
- - Tipos TypeScript completos exportados
311
-
312
- ## 🌐 SSR / Next.js
389
+ - Tipos TypeScript completos exportados## 🌐 SSR / Next.js
313
390
 
314
391
  Para evitar flash de conteúdo em SSR:
315
392
 
@@ -0,0 +1,6 @@
1
+ import {
2
+ PreferencesModal
3
+ } from "./chunk-K3EVSUMQ.js";
4
+ export {
5
+ PreferencesModal
6
+ };
@@ -0,0 +1,368 @@
1
+ // src/components/PreferencesModal.tsx
2
+ import Button from "@mui/material/Button";
3
+ import Dialog from "@mui/material/Dialog";
4
+ import DialogActions from "@mui/material/DialogActions";
5
+ import DialogContent from "@mui/material/DialogContent";
6
+ import DialogTitle from "@mui/material/DialogTitle";
7
+ import FormControlLabel from "@mui/material/FormControlLabel";
8
+ import FormGroup from "@mui/material/FormGroup";
9
+ import Switch from "@mui/material/Switch";
10
+ import Typography from "@mui/material/Typography";
11
+
12
+ // src/context/ConsentContext.tsx
13
+ import * as React from "react";
14
+ import { ThemeProvider } from "@mui/material/styles";
15
+
16
+ // src/utils/cookieUtils.ts
17
+ import Cookies from "js-cookie";
18
+ var DEFAULT_COOKIE_OPTS = {
19
+ name: "cookieConsent",
20
+ maxAgeDays: 365,
21
+ sameSite: "Lax",
22
+ secure: true,
23
+ path: "/"
24
+ };
25
+ function readConsentCookie(name = DEFAULT_COOKIE_OPTS.name) {
26
+ if (typeof document === "undefined") return null;
27
+ const raw = Cookies.get(name);
28
+ if (!raw) return null;
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function writeConsentCookie(state, opts) {
36
+ if (typeof document === "undefined") return;
37
+ const o = { ...DEFAULT_COOKIE_OPTS, ...opts };
38
+ Cookies.set(o.name, JSON.stringify(state), {
39
+ expires: o.maxAgeDays,
40
+ sameSite: o.sameSite,
41
+ secure: o.secure,
42
+ path: o.path
43
+ });
44
+ }
45
+ function removeConsentCookie(opts) {
46
+ if (typeof document === "undefined") return;
47
+ const o = { ...DEFAULT_COOKIE_OPTS, ...opts };
48
+ Cookies.remove(o.name, { path: o.path });
49
+ }
50
+
51
+ // src/utils/theme.ts
52
+ import { createTheme } from "@mui/material/styles";
53
+ var defaultConsentTheme = createTheme({
54
+ palette: {
55
+ primary: {
56
+ main: "#1976d2",
57
+ // Azul institucional
58
+ contrastText: "#ffffff"
59
+ },
60
+ secondary: {
61
+ main: "#dc004e",
62
+ // Rosa/vermelho para ações importantes
63
+ contrastText: "#ffffff"
64
+ },
65
+ background: {
66
+ default: "#fafafa",
67
+ paper: "#ffffff"
68
+ },
69
+ text: {
70
+ primary: "#333333",
71
+ secondary: "#666666"
72
+ },
73
+ action: {
74
+ hover: "rgba(25, 118, 210, 0.04)"
75
+ }
76
+ },
77
+ typography: {
78
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
79
+ body2: {
80
+ fontSize: "0.875rem",
81
+ lineHeight: 1.43
82
+ },
83
+ button: {
84
+ fontWeight: 500,
85
+ textTransform: "none"
86
+ // Manter capitalização original
87
+ }
88
+ },
89
+ components: {
90
+ MuiButton: {
91
+ styleOverrides: {
92
+ root: {
93
+ borderRadius: 8,
94
+ paddingX: 16,
95
+ paddingY: 8
96
+ },
97
+ contained: {
98
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
99
+ "&:hover": {
100
+ boxShadow: "0 4px 8px rgba(0,0,0,0.15)"
101
+ }
102
+ }
103
+ }
104
+ },
105
+ MuiPaper: {
106
+ styleOverrides: {
107
+ root: {
108
+ borderRadius: 12
109
+ }
110
+ }
111
+ },
112
+ MuiDialog: {
113
+ styleOverrides: {
114
+ paper: {
115
+ borderRadius: 16
116
+ }
117
+ }
118
+ }
119
+ }
120
+ });
121
+
122
+ // src/context/ConsentContext.tsx
123
+ import { jsx, jsxs } from "react/jsx-runtime";
124
+ var PreferencesModal = React.lazy(
125
+ () => import("./PreferencesModal-LWZAP5OT.js").then((m) => ({
126
+ default: m.PreferencesModal
127
+ }))
128
+ );
129
+ var DEFAULT_PREFERENCES = {
130
+ analytics: false,
131
+ marketing: false
132
+ };
133
+ var DEFAULT_TEXTS = {
134
+ bannerMessage: "Utilizamos cookies para melhorar sua experi\xEAncia.",
135
+ acceptAll: "Aceitar todos",
136
+ declineAll: "Recusar",
137
+ preferences: "Prefer\xEAncias",
138
+ policyLink: "Saiba mais",
139
+ modalTitle: "Prefer\xEAncias de Cookies",
140
+ modalIntro: "Ajuste as categorias de cookies. Cookies necess\xE1rios s\xE3o sempre utilizados para funcionalidades b\xE1sicas.",
141
+ save: "Salvar prefer\xEAncias",
142
+ necessaryAlwaysOn: "Cookies necess\xE1rios (sempre ativos)"
143
+ };
144
+ function reducer(state, action) {
145
+ switch (action.type) {
146
+ case "ACCEPT_ALL":
147
+ return {
148
+ consented: true,
149
+ preferences: { analytics: true, marketing: true },
150
+ isModalOpen: false
151
+ };
152
+ case "REJECT_ALL":
153
+ return {
154
+ consented: true,
155
+ preferences: { analytics: false, marketing: false },
156
+ isModalOpen: false
157
+ };
158
+ case "SET_CATEGORY":
159
+ return {
160
+ ...state,
161
+ preferences: {
162
+ ...state.preferences,
163
+ [action.category]: action.value
164
+ }
165
+ };
166
+ case "OPEN_MODAL":
167
+ return { ...state, isModalOpen: true };
168
+ case "CLOSE_MODAL":
169
+ return { ...state, isModalOpen: false, consented: true };
170
+ // houve interação
171
+ case "RESET":
172
+ return {
173
+ consented: false,
174
+ preferences: { ...DEFAULT_PREFERENCES },
175
+ isModalOpen: false
176
+ };
177
+ case "HYDRATE":
178
+ return { ...action.state };
179
+ default:
180
+ return state;
181
+ }
182
+ }
183
+ var StateCtx = React.createContext(null);
184
+ var ActionsCtx = React.createContext(null);
185
+ var TextsCtx = React.createContext(DEFAULT_TEXTS);
186
+ function ConsentProvider({
187
+ initialState,
188
+ texts: textsProp,
189
+ theme,
190
+ PreferencesModalComponent,
191
+ preferencesModalProps = {},
192
+ disableAutomaticModal = false,
193
+ onConsentGiven,
194
+ onPreferencesSaved,
195
+ cookie: cookieOpts,
196
+ children
197
+ }) {
198
+ const texts = React.useMemo(
199
+ () => ({ ...DEFAULT_TEXTS, ...textsProp ?? {} }),
200
+ [textsProp]
201
+ );
202
+ const cookie = React.useMemo(
203
+ () => ({ ...DEFAULT_COOKIE_OPTS, ...cookieOpts ?? {} }),
204
+ [cookieOpts]
205
+ );
206
+ const appliedTheme = React.useMemo(
207
+ () => theme || defaultConsentTheme,
208
+ [theme]
209
+ );
210
+ const boot = React.useMemo(() => {
211
+ if (initialState) return { ...initialState, isModalOpen: false };
212
+ const saved = readConsentCookie(cookie.name);
213
+ return saved ?? {
214
+ consented: false,
215
+ preferences: { ...DEFAULT_PREFERENCES },
216
+ isModalOpen: false
217
+ };
218
+ }, [initialState, cookie.name]);
219
+ const [state, dispatch] = React.useReducer(reducer, boot);
220
+ React.useEffect(() => {
221
+ if (state.consented) writeConsentCookie(state, cookie);
222
+ }, [state, cookie]);
223
+ const prevConsented = React.useRef(state.consented);
224
+ React.useEffect(() => {
225
+ if (!prevConsented.current && state.consented && onConsentGiven) {
226
+ setTimeout(() => onConsentGiven(state), 150);
227
+ }
228
+ prevConsented.current = state.consented;
229
+ }, [state, onConsentGiven]);
230
+ const prevPrefs = React.useRef(state.preferences);
231
+ React.useEffect(() => {
232
+ if (state.consented && onPreferencesSaved && prevPrefs.current !== state.preferences) {
233
+ setTimeout(() => onPreferencesSaved(state.preferences), 150);
234
+ prevPrefs.current = state.preferences;
235
+ }
236
+ }, [state, onPreferencesSaved]);
237
+ const api = React.useMemo(() => {
238
+ const acceptAll = () => dispatch({ type: "ACCEPT_ALL" });
239
+ const rejectAll = () => dispatch({ type: "REJECT_ALL" });
240
+ const setPreference = (category, value) => dispatch({ type: "SET_CATEGORY", category, value });
241
+ const openPreferences = () => dispatch({ type: "OPEN_MODAL" });
242
+ const closePreferences = () => dispatch({ type: "CLOSE_MODAL" });
243
+ const resetConsent = () => {
244
+ removeConsentCookie(cookie);
245
+ dispatch({ type: "RESET" });
246
+ };
247
+ return {
248
+ consented: !!state.consented,
249
+ preferences: state.preferences,
250
+ isModalOpen: state.isModalOpen,
251
+ acceptAll,
252
+ rejectAll,
253
+ setPreference,
254
+ openPreferences,
255
+ closePreferences,
256
+ resetConsent
257
+ };
258
+ }, [state, cookie]);
259
+ return /* @__PURE__ */ jsx(ThemeProvider, { theme: appliedTheme, children: /* @__PURE__ */ jsx(StateCtx.Provider, { value: state, children: /* @__PURE__ */ jsx(ActionsCtx.Provider, { value: api, children: /* @__PURE__ */ jsxs(TextsCtx.Provider, { value: texts, children: [
260
+ children,
261
+ !disableAutomaticModal && /* @__PURE__ */ jsx(React.Suspense, { fallback: null, children: PreferencesModalComponent ? /* @__PURE__ */ jsx(PreferencesModalComponent, { ...preferencesModalProps }) : /* @__PURE__ */ jsx(PreferencesModal, {}) })
262
+ ] }) }) }) });
263
+ }
264
+ function useConsentStateInternal() {
265
+ const ctx = React.useContext(StateCtx);
266
+ if (!ctx)
267
+ throw new Error("useConsentState must be used within ConsentProvider");
268
+ return ctx;
269
+ }
270
+ function useConsentActionsInternal() {
271
+ const ctx = React.useContext(ActionsCtx);
272
+ if (!ctx)
273
+ throw new Error("useConsentActions must be used within ConsentProvider");
274
+ return ctx;
275
+ }
276
+ function useConsentTextsInternal() {
277
+ const ctx = React.useContext(TextsCtx);
278
+ return ctx;
279
+ }
280
+
281
+ // src/hooks/useConsent.ts
282
+ function useConsent() {
283
+ const state = useConsentStateInternal();
284
+ const actions = useConsentActionsInternal();
285
+ return {
286
+ consented: state.consented,
287
+ preferences: state.preferences,
288
+ isModalOpen: state.isModalOpen,
289
+ acceptAll: actions.acceptAll,
290
+ rejectAll: actions.rejectAll,
291
+ setPreference: actions.setPreference,
292
+ openPreferences: actions.openPreferences,
293
+ closePreferences: actions.closePreferences,
294
+ resetConsent: actions.resetConsent
295
+ };
296
+ }
297
+ function useConsentTexts() {
298
+ return useConsentTextsInternal();
299
+ }
300
+
301
+ // src/components/PreferencesModal.tsx
302
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
303
+ function PreferencesModal2({
304
+ DialogProps: DialogProps2
305
+ }) {
306
+ const { preferences, setPreference, closePreferences, isModalOpen } = useConsent();
307
+ const texts = useConsentTexts();
308
+ const open = DialogProps2?.open ?? isModalOpen ?? false;
309
+ return /* @__PURE__ */ jsxs2(
310
+ Dialog,
311
+ {
312
+ "aria-labelledby": "cookie-pref-title",
313
+ open,
314
+ onClose: closePreferences,
315
+ ...DialogProps2,
316
+ children: [
317
+ /* @__PURE__ */ jsx2(DialogTitle, { id: "cookie-pref-title", children: texts.modalTitle }),
318
+ /* @__PURE__ */ jsxs2(DialogContent, { dividers: true, children: [
319
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", sx: { mb: 2 }, children: texts.modalIntro }),
320
+ /* @__PURE__ */ jsxs2(FormGroup, { children: [
321
+ /* @__PURE__ */ jsx2(
322
+ FormControlLabel,
323
+ {
324
+ control: /* @__PURE__ */ jsx2(
325
+ Switch,
326
+ {
327
+ checked: preferences.analytics,
328
+ onChange: (e) => setPreference("analytics", e.target.checked)
329
+ }
330
+ ),
331
+ label: "Cookies Anal\xEDticos (medem uso do site)"
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsx2(
335
+ FormControlLabel,
336
+ {
337
+ control: /* @__PURE__ */ jsx2(
338
+ Switch,
339
+ {
340
+ checked: preferences.marketing,
341
+ onChange: (e) => setPreference("marketing", e.target.checked)
342
+ }
343
+ ),
344
+ label: "Cookies de Marketing/Publicidade"
345
+ }
346
+ ),
347
+ /* @__PURE__ */ jsx2(
348
+ FormControlLabel,
349
+ {
350
+ control: /* @__PURE__ */ jsx2(Switch, { checked: true, disabled: true }),
351
+ label: texts.necessaryAlwaysOn
352
+ }
353
+ )
354
+ ] })
355
+ ] }),
356
+ /* @__PURE__ */ jsx2(DialogActions, { children: /* @__PURE__ */ jsx2(Button, { variant: "contained", onClick: closePreferences, children: texts.save }) })
357
+ ]
358
+ }
359
+ );
360
+ }
361
+
362
+ export {
363
+ defaultConsentTheme,
364
+ PreferencesModal2 as PreferencesModal,
365
+ ConsentProvider,
366
+ useConsent,
367
+ useConsentTexts
368
+ };