@react-lgpd-consent/core 0.5.1 → 0.6.2

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/dist/index.js CHANGED
@@ -31,6 +31,15 @@ function createProjectPreferences(config, defaultValue = false) {
31
31
  });
32
32
  return preferences;
33
33
  }
34
+ function ensureNecessaryAlwaysOn(preferences) {
35
+ if (preferences.necessary === true) {
36
+ return { ...preferences, necessary: true };
37
+ }
38
+ return {
39
+ ...preferences,
40
+ necessary: true
41
+ };
42
+ }
34
43
  function validateProjectPreferences(preferences, config) {
35
44
  const validPreferences = {
36
45
  necessary: true
@@ -298,12 +307,22 @@ function setDebugLogging(enabled, level = 2 /* INFO */) {
298
307
  }
299
308
 
300
309
  // src/utils/cookieUtils.ts
310
+ var DEFAULT_STORAGE_NAMESPACE = "lgpd-consent";
311
+ var DEFAULT_STORAGE_VERSION = "1";
312
+ function buildConsentStorageKey(options) {
313
+ const namespaceRaw = options?.namespace?.trim() || DEFAULT_STORAGE_NAMESPACE;
314
+ const versionRaw = options?.version?.trim() || DEFAULT_STORAGE_VERSION;
315
+ const sanitizedNamespace = namespaceRaw.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase();
316
+ const sanitizedVersion = versionRaw.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase();
317
+ return `${sanitizedNamespace}__v${sanitizedVersion}`;
318
+ }
301
319
  var DEFAULT_COOKIE_OPTS = {
302
320
  name: "cookieConsent",
303
321
  maxAgeDays: 365,
304
322
  sameSite: "Lax",
305
323
  secure: typeof window !== "undefined" ? window.location.protocol === "https:" : false,
306
- path: "/"
324
+ path: "/",
325
+ domain: void 0
307
326
  };
308
327
  var COOKIE_SCHEMA_VERSION = "1.0";
309
328
  function readConsentCookie(name = DEFAULT_COOKIE_OPTS.name) {
@@ -357,10 +376,11 @@ function writeConsentCookie(state, config, opts, source = "banner") {
357
376
  }
358
377
  const now = (/* @__PURE__ */ new Date()).toISOString();
359
378
  const o = { ...DEFAULT_COOKIE_OPTS, ...opts };
379
+ const preferences = ensureNecessaryAlwaysOn(state.preferences);
360
380
  const cookieData = {
361
381
  version: COOKIE_SCHEMA_VERSION,
362
382
  consented: state.consented,
363
- preferences: state.preferences,
383
+ preferences,
364
384
  consentDate: state.consentDate || now,
365
385
  lastUpdate: now,
366
386
  source,
@@ -371,7 +391,8 @@ function writeConsentCookie(state, config, opts, source = "banner") {
371
391
  expires: o.maxAgeDays,
372
392
  sameSite: o.sameSite,
373
393
  secure: o.secure,
374
- path: o.path
394
+ path: o.path,
395
+ domain: o.domain
375
396
  });
376
397
  logger.info("Consent cookie saved", {
377
398
  consented: cookieData.consented,
@@ -386,12 +407,12 @@ function removeConsentCookie(opts) {
386
407
  }
387
408
  const o = { ...DEFAULT_COOKIE_OPTS, ...opts };
388
409
  logger.cookieOperation("delete", o.name);
389
- Cookies.remove(o.name, { path: o.path });
410
+ Cookies.remove(o.name, { path: o.path, domain: o.domain });
390
411
  logger.info("Consent cookie removed");
391
412
  }
392
413
 
393
414
  // src/utils/dataLayerEvents.ts
394
- var LIBRARY_VERSION = "0.5.0";
415
+ var LIBRARY_VERSION = "0.6.2";
395
416
  function ensureDataLayer() {
396
417
  if (typeof window === "undefined") return;
397
418
  if (!window.dataLayer) {
@@ -1027,6 +1048,207 @@ var GUIDANCE_PRESETS = {
1027
1048
  }
1028
1049
  };
1029
1050
 
1051
+ // src/utils/peerDepsCheck.ts
1052
+ function detectMultipleReactInstances() {
1053
+ if (typeof window === "undefined") return false;
1054
+ try {
1055
+ const reactSymbols = Object.getOwnPropertySymbols(window).map((sym) => String(sym)).filter((name) => name.includes("react"));
1056
+ if (reactSymbols.length > 1) {
1057
+ return true;
1058
+ }
1059
+ const ReactModule = window.React;
1060
+ if (ReactModule && Array.isArray(ReactModule)) {
1061
+ return true;
1062
+ }
1063
+ const hasMultipleVersions = window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 1;
1064
+ return hasMultipleVersions || false;
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+ function getPackageVersion(packageName) {
1070
+ if (typeof window === "undefined") return null;
1071
+ try {
1072
+ const pkg = window[packageName];
1073
+ if (pkg?.version) return pkg.version;
1074
+ const React6 = window.React;
1075
+ if (packageName === "react" && React6?.version) {
1076
+ return React6.version;
1077
+ }
1078
+ return null;
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ function isVersionInRange(version, minMajor, maxMajor) {
1084
+ const major = parseInt(version.split(".")[0], 10);
1085
+ return major >= minMajor && major <= maxMajor;
1086
+ }
1087
+ function checkPeerDeps(options = {}) {
1088
+ const { skipInProduction = true, logWarnings = true } = options;
1089
+ const result = {
1090
+ ok: true,
1091
+ warnings: [],
1092
+ errors: []
1093
+ };
1094
+ const isProduction = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1095
+ if (skipInProduction && isProduction) {
1096
+ return result;
1097
+ }
1098
+ if (typeof window === "undefined") {
1099
+ return result;
1100
+ }
1101
+ if (detectMultipleReactInstances()) {
1102
+ result.ok = false;
1103
+ const errorMsg = `
1104
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1105
+ \u2551 \u26A0\uFE0F ERRO: M\xFAltiplas inst\xE2ncias de React detectadas \u2551
1106
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1107
+
1108
+ \u{1F534} Problema:
1109
+ Seu projeto est\xE1 carregando mais de uma c\xF3pia do React, causando o erro:
1110
+ "Invalid hook call. Hooks can only be called inside of the body of a
1111
+ function component."
1112
+
1113
+ \u{1F50D} Causa prov\xE1vel:
1114
+ \u2022 pnpm/Yarn PnP sem hoisting adequado de peer dependencies
1115
+ \u2022 node_modules com React duplicado (npm/yarn cl\xE1ssico)
1116
+ \u2022 Webpack/Vite com m\xFAltiplas resolu\xE7\xF5es do mesmo pacote
1117
+
1118
+ \u2705 Solu\xE7\xF5es:
1119
+
1120
+ \u{1F4E6} PNPM (RECOMENDADO):
1121
+ Adicione ao package.json raiz:
1122
+ {
1123
+ "pnpm": {
1124
+ "overrides": {
1125
+ "react": "$react",
1126
+ "react-dom": "$react-dom"
1127
+ }
1128
+ }
1129
+ }
1130
+ Execute: pnpm install
1131
+
1132
+ \u{1F4E6} NPM/Yarn:
1133
+ Adicione ao package.json raiz:
1134
+ {
1135
+ "overrides": {
1136
+ "react": "^18.2.0 || ^19.0.0",
1137
+ "react-dom": "^18.2.0 || ^19.0.0"
1138
+ }
1139
+ }
1140
+ Execute: npm install (ou yarn install)
1141
+
1142
+ \u{1F527} Webpack:
1143
+ Adicione ao webpack.config.js:
1144
+ module.exports = {
1145
+ resolve: {
1146
+ alias: {
1147
+ react: path.resolve('./node_modules/react'),
1148
+ 'react-dom': path.resolve('./node_modules/react-dom'),
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ \u26A1 Vite:
1154
+ Adicione ao vite.config.js:
1155
+ export default {
1156
+ resolve: {
1157
+ dedupe: ['react', 'react-dom']
1158
+ }
1159
+ }
1160
+
1161
+ \u{1F4DA} Documenta\xE7\xE3o:
1162
+ https://github.com/lucianoedipo/react-lgpd-consent/blob/main/TROUBLESHOOTING.md#multiple-react-instances
1163
+
1164
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1165
+ `;
1166
+ result.errors.push(errorMsg);
1167
+ if (logWarnings) {
1168
+ console.error(errorMsg);
1169
+ }
1170
+ }
1171
+ const reactVersion = getPackageVersion("react");
1172
+ if (reactVersion) {
1173
+ if (!isVersionInRange(reactVersion, 18, 19)) {
1174
+ result.ok = false;
1175
+ const errorMsg = `
1176
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1177
+ \u2551 \u26A0\uFE0F AVISO: Vers\xE3o do React n\xE3o suportada \u2551
1178
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1179
+
1180
+ \u{1F4E6} Vers\xE3o detectada: React ${reactVersion}
1181
+ \u2705 Vers\xF5es suportadas: React 18.x ou 19.x
1182
+
1183
+ \u{1F50D} O react-lgpd-consent requer React 18.2.0+ ou React 19.x
1184
+
1185
+ \u2705 Solu\xE7\xE3o:
1186
+ Atualize o React para uma vers\xE3o suportada:
1187
+
1188
+ npm install react@^18.2.0 react-dom@^18.2.0
1189
+
1190
+ ou
1191
+
1192
+ npm install react@^19.0.0 react-dom@^19.0.0
1193
+
1194
+ \u{1F4DA} Documenta\xE7\xE3o:
1195
+ https://github.com/lucianoedipo/react-lgpd-consent/blob/main/TROUBLESHOOTING.md#react-version
1196
+
1197
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1198
+ `;
1199
+ result.errors.push(errorMsg);
1200
+ if (logWarnings) {
1201
+ console.error(errorMsg);
1202
+ }
1203
+ }
1204
+ }
1205
+ const muiVersion = window["@mui/material"]?.version;
1206
+ if (muiVersion) {
1207
+ if (!isVersionInRange(muiVersion, 5, 7)) {
1208
+ result.warnings.push(
1209
+ `MUI vers\xE3o ${muiVersion} detectada. Vers\xF5es suportadas: 5.15.0+, 6.x ou 7.x. Alguns componentes podem n\xE3o funcionar corretamente.`
1210
+ );
1211
+ if (logWarnings) {
1212
+ logger.warn(
1213
+ `
1214
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1215
+ \u2551 \u26A0\uFE0F AVISO: Vers\xE3o do Material-UI fora do range recomendado \u2551
1216
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1217
+
1218
+ \u{1F4E6} Vers\xE3o detectada: @mui/material ${muiVersion}
1219
+ \u2705 Vers\xF5es suportadas: 5.15.0+, 6.x, 7.x
1220
+
1221
+ \u{1F50D} Componentes de UI (@react-lgpd-consent/mui) podem apresentar problemas.
1222
+
1223
+ \u2705 Solu\xE7\xE3o:
1224
+ Atualize o MUI para uma vers\xE3o suportada:
1225
+
1226
+ npm install @mui/material@^7.0.0 @emotion/react @emotion/styled
1227
+
1228
+ ou mantenha 5.15.0+:
1229
+
1230
+ npm install @mui/material@^5.15.0 @emotion/react @emotion/styled
1231
+
1232
+ \u{1F4DA} Documenta\xE7\xE3o:
1233
+ https://github.com/lucianoedipo/react-lgpd-consent/blob/main/TROUBLESHOOTING.md#mui-version
1234
+
1235
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1236
+ `
1237
+ );
1238
+ }
1239
+ }
1240
+ }
1241
+ return result;
1242
+ }
1243
+ function runPeerDepsCheck() {
1244
+ const result = checkPeerDeps({ logWarnings: true });
1245
+ if (result.ok && result.warnings.length === 0) {
1246
+ logger.debug("\u2705 Peer dependencies check: OK");
1247
+ } else if (result.warnings.length > 0) {
1248
+ logger.warn("\u26A0\uFE0F Peer dependencies check: avisos detectados");
1249
+ }
1250
+ }
1251
+
1030
1252
  // src/utils/validation.ts
1031
1253
  var isDev = () => typeof process !== "undefined" && process.env.NODE_ENV !== "production";
1032
1254
  function validateConsentProviderProps(props) {
@@ -1299,10 +1521,11 @@ function useDesignTokens() {
1299
1521
  }
1300
1522
  function createFullConsentState(consented, preferences, source, projectConfig, isModalOpen = false, existingState) {
1301
1523
  const now = (/* @__PURE__ */ new Date()).toISOString();
1524
+ const enforcedPreferences = ensureNecessaryAlwaysOn(preferences);
1302
1525
  return {
1303
1526
  version: "1.0",
1304
1527
  consented,
1305
- preferences,
1528
+ preferences: enforcedPreferences,
1306
1529
  consentDate: existingState?.consentDate || now,
1307
1530
  lastUpdate: now,
1308
1531
  source,
@@ -1366,7 +1589,11 @@ function reducer(state, action) {
1366
1589
  });
1367
1590
  return newState;
1368
1591
  }
1369
- case "SET_CATEGORY":
1592
+ case "SET_CATEGORY": {
1593
+ if (action.category === "necessary") {
1594
+ logger.warn("Attempt to toggle necessary category ignored for compliance reasons.");
1595
+ return state;
1596
+ }
1370
1597
  logger.debug("Category preference changed", {
1371
1598
  category: action.category,
1372
1599
  value: action.value
@@ -1379,9 +1606,12 @@ function reducer(state, action) {
1379
1606
  },
1380
1607
  lastUpdate: (/* @__PURE__ */ new Date()).toISOString()
1381
1608
  };
1382
- case "SET_PREFERENCES":
1383
- logger.info("Preferences saved", { preferences: action.preferences });
1384
- return createFullConsentState(true, action.preferences, "modal", action.config, false, state);
1609
+ }
1610
+ case "SET_PREFERENCES": {
1611
+ const sanitized = ensureNecessaryAlwaysOn(action.preferences);
1612
+ logger.info("Preferences saved", { preferences: sanitized });
1613
+ return createFullConsentState(true, sanitized, "modal", action.config, false, state);
1614
+ }
1385
1615
  case "OPEN_MODAL":
1386
1616
  return { ...state, isModalOpen: true };
1387
1617
  case "CLOSE_MODAL":
@@ -1432,16 +1662,25 @@ function ConsentProvider({
1432
1662
  onConsentGiven,
1433
1663
  onPreferencesSaved,
1434
1664
  cookie: cookieOpts,
1665
+ storage,
1666
+ onConsentVersionChange,
1435
1667
  disableDeveloperGuidance,
1436
1668
  guidanceConfig,
1437
1669
  children,
1438
1670
  disableDiscoveryLog
1439
1671
  }) {
1440
1672
  const texts = React4.useMemo(() => ({ ...DEFAULT_TEXTS, ...textsProp ?? {} }), [textsProp]);
1441
- const cookie = React4.useMemo(
1442
- () => ({ ...DEFAULT_COOKIE_OPTS, ...cookieOpts ?? {} }),
1443
- [cookieOpts]
1444
- );
1673
+ const cookie = React4.useMemo(() => {
1674
+ const base = { ...DEFAULT_COOKIE_OPTS, ...cookieOpts ?? {} };
1675
+ base.name = cookieOpts?.name ?? buildConsentStorageKey({
1676
+ namespace: storage?.namespace,
1677
+ version: storage?.version
1678
+ });
1679
+ if (!base.domain && storage?.domain) {
1680
+ base.domain = storage.domain;
1681
+ }
1682
+ return base;
1683
+ }, [cookieOpts, storage?.domain, storage?.namespace, storage?.version]);
1445
1684
  const finalCategoriesConfig = React4.useMemo(() => {
1446
1685
  const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1447
1686
  if (!categories) return DEFAULT_PROJECT_CATEGORIES;
@@ -1459,6 +1698,12 @@ function ConsentProvider({
1459
1698
  }, [categories]);
1460
1699
  const didWarnAboutMissingUI = React4.useRef(false);
1461
1700
  useDeveloperGuidance(finalCategoriesConfig, disableDeveloperGuidance, guidanceConfig);
1701
+ React4.useEffect(() => {
1702
+ const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1703
+ if (!isProd && !disableDeveloperGuidance) {
1704
+ runPeerDepsCheck();
1705
+ }
1706
+ }, [disableDeveloperGuidance]);
1462
1707
  React4.useEffect(() => {
1463
1708
  const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
1464
1709
  if (!isProd && PreferencesModalComponent) {
@@ -1478,6 +1723,8 @@ function ConsentProvider({
1478
1723
  );
1479
1724
  }, [initialState, finalCategoriesConfig]);
1480
1725
  const [state, dispatch] = React4.useReducer(reducer, boot);
1726
+ const previousCookieRef = React4.useRef(cookie);
1727
+ const skipCookiePersistRef = React4.useRef(false);
1481
1728
  const [isHydrated, setIsHydrated] = React4.useState(false);
1482
1729
  const previousPreferencesRef = React4.useRef(state.preferences);
1483
1730
  React4.useEffect(() => {
@@ -1493,6 +1740,35 @@ function ConsentProvider({
1493
1740
  }
1494
1741
  setIsHydrated(true);
1495
1742
  }, [cookie.name, initialState, finalCategoriesConfig]);
1743
+ React4.useEffect(() => {
1744
+ const previousCookie = previousCookieRef.current;
1745
+ const isSameCookie = previousCookie.name === cookie.name && previousCookie.domain === cookie.domain && previousCookie.path === cookie.path;
1746
+ if (isSameCookie) {
1747
+ previousCookieRef.current = cookie;
1748
+ return;
1749
+ }
1750
+ skipCookiePersistRef.current = true;
1751
+ removeConsentCookie(previousCookie);
1752
+ const reset = () => {
1753
+ removeConsentCookie(cookie);
1754
+ dispatch({ type: "RESET", config: finalCategoriesConfig });
1755
+ };
1756
+ reset();
1757
+ if (onConsentVersionChange) {
1758
+ onConsentVersionChange({
1759
+ previousKey: previousCookie.name,
1760
+ nextKey: cookie.name,
1761
+ resetConsent: reset
1762
+ });
1763
+ }
1764
+ previousCookieRef.current = cookie;
1765
+ }, [cookie, finalCategoriesConfig, onConsentVersionChange, dispatch]);
1766
+ function resetSkipCookiePersistOnConsentRevoked() {
1767
+ if (skipCookiePersistRef.current && !state.consented) {
1768
+ skipCookiePersistRef.current = false;
1769
+ }
1770
+ }
1771
+ React4.useEffect(resetSkipCookiePersistOnConsentRevoked, [state.consented]);
1496
1772
  React4.useEffect(() => {
1497
1773
  if (isHydrated) {
1498
1774
  pushConsentInitializedEvent(state.preferences);
@@ -1502,7 +1778,9 @@ function ConsentProvider({
1502
1778
  }
1503
1779
  }, [isHydrated]);
1504
1780
  React4.useEffect(() => {
1505
- if (state.consented) writeConsentCookie(state, finalCategoriesConfig, cookie);
1781
+ if (!state.consented) return;
1782
+ if (skipCookiePersistRef.current) return;
1783
+ writeConsentCookie(state, finalCategoriesConfig, cookie);
1506
1784
  }, [state, cookie, finalCategoriesConfig]);
1507
1785
  const prevConsented = React4.useRef(state.consented);
1508
1786
  React4.useEffect(() => {
@@ -1530,15 +1808,22 @@ function ConsentProvider({
1530
1808
  const api = React4.useMemo(() => {
1531
1809
  const acceptAll = () => dispatch({ type: "ACCEPT_ALL", config: finalCategoriesConfig });
1532
1810
  const rejectAll = () => dispatch({ type: "REJECT_ALL", config: finalCategoriesConfig });
1533
- const setPreference = (category, value) => dispatch({ type: "SET_CATEGORY", category, value });
1811
+ const setPreference = (category, value) => {
1812
+ if (category === "necessary") {
1813
+ logger.warn("setPreference: attempt to toggle necessary category ignored.");
1814
+ return;
1815
+ }
1816
+ dispatch({ type: "SET_CATEGORY", category, value });
1817
+ };
1534
1818
  const setPreferences = (preferences) => {
1819
+ const sanitized = ensureNecessaryAlwaysOn(preferences);
1535
1820
  dispatch({
1536
1821
  type: "SET_PREFERENCES",
1537
- preferences,
1822
+ preferences: sanitized,
1538
1823
  config: finalCategoriesConfig
1539
1824
  });
1540
1825
  if (onPreferencesSaved) {
1541
- setTimeout(() => onPreferencesSaved(preferences), 150);
1826
+ setTimeout(() => onPreferencesSaved(sanitized), 150);
1542
1827
  }
1543
1828
  };
1544
1829
  const openPreferences = () => dispatch({ type: "OPEN_MODAL" });
@@ -1694,10 +1979,13 @@ function ConsentGate(props) {
1694
1979
  }
1695
1980
 
1696
1981
  // src/utils/scriptLoader.ts
1982
+ var LOADING_SCRIPTS = /* @__PURE__ */ new Map();
1697
1983
  function loadScript(id, src, category = null, attrs = {}) {
1698
1984
  if (typeof document === "undefined") return Promise.resolve();
1699
1985
  if (document.getElementById(id)) return Promise.resolve();
1700
- return new Promise((resolve, reject) => {
1986
+ const existingPromise = LOADING_SCRIPTS.get(id);
1987
+ if (existingPromise) return existingPromise;
1988
+ const promise = new Promise((resolve, reject) => {
1701
1989
  const checkConsent = () => {
1702
1990
  const consentCookie = document.cookie.split("; ").find((row) => row.startsWith("cookieConsent="))?.split("=")[1];
1703
1991
  if (!consentCookie) {
@@ -1711,7 +1999,7 @@ function loadScript(id, src, category = null, attrs = {}) {
1711
1999
  return;
1712
2000
  }
1713
2001
  if (category && !consent.preferences[category]) {
1714
- reject(new Error(`Consent not given for ${category} scripts`));
2002
+ setTimeout(checkConsent, 100);
1715
2003
  return;
1716
2004
  }
1717
2005
  const s = document.createElement("script");
@@ -1719,8 +2007,14 @@ function loadScript(id, src, category = null, attrs = {}) {
1719
2007
  s.src = src;
1720
2008
  s.async = true;
1721
2009
  for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
1722
- s.onload = () => resolve();
1723
- s.onerror = () => reject(new Error(`Failed to load script: ${src}`));
2010
+ s.onload = () => {
2011
+ LOADING_SCRIPTS.delete(id);
2012
+ resolve();
2013
+ };
2014
+ s.onerror = () => {
2015
+ LOADING_SCRIPTS.delete(id);
2016
+ reject(new Error(`Failed to load script: ${src}`));
2017
+ };
1724
2018
  document.body.appendChild(s);
1725
2019
  } catch {
1726
2020
  setTimeout(checkConsent, 100);
@@ -1728,6 +2022,8 @@ function loadScript(id, src, category = null, attrs = {}) {
1728
2022
  };
1729
2023
  checkConsent();
1730
2024
  });
2025
+ LOADING_SCRIPTS.set(id, promise);
2026
+ return promise;
1731
2027
  }
1732
2028
 
1733
2029
  // src/utils/autoConfigureCategories.ts
@@ -1955,21 +2251,29 @@ function ConsentScriptLoader({
1955
2251
  }, [integrations, categories]);
1956
2252
  React4.useEffect(() => {
1957
2253
  if (!consented) return;
1958
- integrations.forEach(async (integration) => {
1959
- const shouldLoad = preferences[integration.category];
1960
- const alreadyLoaded = loadedScripts.current.has(integration.id);
1961
- if (shouldLoad && (!alreadyLoaded || reloadOnChange)) {
1962
- try {
1963
- await loadScript(integration.id, integration.src, integration.category, integration.attrs);
1964
- if (integration.init) {
1965
- integration.init();
2254
+ const timeoutId = setTimeout(async () => {
2255
+ for (const integration of integrations) {
2256
+ const shouldLoad = preferences[integration.category];
2257
+ const alreadyLoaded = loadedScripts.current.has(integration.id);
2258
+ if (shouldLoad && (!alreadyLoaded || reloadOnChange)) {
2259
+ try {
2260
+ await loadScript(
2261
+ integration.id,
2262
+ integration.src,
2263
+ integration.category,
2264
+ integration.attrs
2265
+ );
2266
+ if (integration.init) {
2267
+ integration.init();
2268
+ }
2269
+ loadedScripts.current.add(integration.id);
2270
+ } catch (error) {
2271
+ logger.error(`\u274C Failed to load script: ${integration.id}`, error);
1966
2272
  }
1967
- loadedScripts.current.add(integration.id);
1968
- } catch (error) {
1969
- logger.error(`\u274C Failed to load script: ${integration.id}`, error);
1970
2273
  }
1971
2274
  }
1972
- });
2275
+ }, 0);
2276
+ return () => clearTimeout(timeoutId);
1973
2277
  }, [preferences, consented, integrations, reloadOnChange]);
1974
2278
  return null;
1975
2279
  }
@@ -2539,4 +2843,4 @@ var TEXT_TEMPLATES = {
2539
2843
  }
2540
2844
  };
2541
2845
 
2542
- export { COMMON_INTEGRATIONS, ConsentGate, ConsentProvider, ConsentScriptLoader, DEFAULT_PROJECT_CATEGORIES, DesignProvider, EXPANDED_DEFAULT_TEXTS, GUIDANCE_PRESETS, INTEGRATION_TEMPLATES, LogLevel, TEXT_TEMPLATES, analyzeDeveloperConfiguration, analyzeIntegrationCategories, autoConfigureCategories, categorizeDiscoveredCookies, createClarityIntegration, createCorporateIntegrations, createECommerceIntegrations, createFacebookPixelIntegration, createGoogleAnalyticsIntegration, createGoogleTagManagerIntegration, createHotjarIntegration, createIntercomIntegration, createMixpanelIntegration, createProjectPreferences, createSaaSIntegrations, createUserWayIntegration, createZendeskChatIntegration, defaultTexts, detectConsentCookieName, discoverRuntimeCookies, extractCategoriesFromIntegrations, getAllProjectCategories, getCookiesInfoForCategory, loadScript, logDeveloperGuidance, logger, openPreferencesModal, pushConsentInitializedEvent, pushConsentUpdatedEvent, resolveTexts, setCookieCatalogOverrides, setCookieCategoryOverrides, setDebugLogging, suggestCategoryForScript, useCategories, useCategoryStatus, useConsent, useConsentHydration, useConsentScriptLoader, useConsentTexts, useDataLayerEvents, useDesignTokens, useDeveloperGuidance, useOpenPreferencesModal, validateIntegrationCategories, validateNecessaryClassification, validateProjectPreferences };
2846
+ export { COMMON_INTEGRATIONS, ConsentGate, ConsentProvider, ConsentScriptLoader, DEFAULT_PROJECT_CATEGORIES, DesignProvider, EXPANDED_DEFAULT_TEXTS, GUIDANCE_PRESETS, INTEGRATION_TEMPLATES, LogLevel, TEXT_TEMPLATES, analyzeDeveloperConfiguration, analyzeIntegrationCategories, autoConfigureCategories, buildConsentStorageKey, categorizeDiscoveredCookies, checkPeerDeps, createClarityIntegration, createCorporateIntegrations, createECommerceIntegrations, createFacebookPixelIntegration, createGoogleAnalyticsIntegration, createGoogleTagManagerIntegration, createHotjarIntegration, createIntercomIntegration, createMixpanelIntegration, createProjectPreferences, createSaaSIntegrations, createUserWayIntegration, createZendeskChatIntegration, defaultTexts, detectConsentCookieName, discoverRuntimeCookies, ensureNecessaryAlwaysOn, extractCategoriesFromIntegrations, getAllProjectCategories, getCookiesInfoForCategory, loadScript, logDeveloperGuidance, logger, openPreferencesModal, pushConsentInitializedEvent, pushConsentUpdatedEvent, resolveTexts, runPeerDepsCheck, setCookieCatalogOverrides, setCookieCategoryOverrides, setDebugLogging, suggestCategoryForScript, useCategories, useCategoryStatus, useConsent, useConsentHydration, useConsentScriptLoader, useConsentTexts, useDataLayerEvents, useDesignTokens, useDeveloperGuidance, useOpenPreferencesModal, validateIntegrationCategories, validateNecessaryClassification, validateProjectPreferences };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-lgpd-consent/core",
3
- "version": "0.5.1",
3
+ "version": "0.6.2",
4
4
  "description": "Núcleo da biblioteca de consentimento LGPD para React - Estado, hooks e utilitários sem dependências de UI",
5
5
  "keywords": [
6
6
  "lgpd",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "js-cookie": "^3.0.5",
56
- "zod": "^4.1.12"
56
+ "zod": "^4.1.13"
57
57
  },
58
58
  "repository": {
59
59
  "type": "git",