@react-lgpd-consent/core 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog - @react-lgpd-consent/core
2
2
 
3
+ ## 0.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#113](https://github.com/lucianoedipo/react-lgpd-consent/pull/113) [`b51e1f8`](https://github.com/lucianoedipo/react-lgpd-consent/commit/b51e1f8dcac35907e3a30471135da14160c95213) Thanks [@lucianoedipo](https://github.com/lucianoedipo)! - feat: Compatibilidade completa com React 19 StrictMode
8
+ - Implementado registro global `LOADING_SCRIPTS` em `scriptLoader.ts` para prevenir injeções duplicadas de scripts durante double-invoking de efeitos
9
+ - Adicionado `setTimeout` com cleanup adequado em `ConsentScriptLoader.tsx` para prevenir race conditions
10
+ - Scripts agora carregam apenas uma vez mesmo em desenvolvimento com StrictMode ativo
11
+ - Função `loadScript` é idempotente: múltiplas chamadas simultâneas retornam a mesma Promise
12
+ - **Correção crítica**: `loadScript` agora aguarda dinamicamente o consentimento em vez de rejeitar imediatamente, permitindo que scripts carreguem quando preferências mudarem
13
+ - Cleanup automático do registro ao completar/falhar carregamento
14
+ - Adicionados testes extensivos: `ConsentScriptLoader.strictmode.test.tsx` e `scriptLoader.strictmode.test.ts`
15
+ - Documentação completa em `docs/REACT19-STRICTMODE.md`
16
+ - Todos os 302 testes passando, incluindo 5 novos testes de StrictMode
17
+
18
+ **Breaking Changes:** Nenhuma - totalmente retrocompatível
19
+
3
20
  ## 0.6.1
4
21
 
5
22
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -436,7 +436,7 @@ function removeConsentCookie(opts) {
436
436
  }
437
437
 
438
438
  // src/utils/dataLayerEvents.ts
439
- var LIBRARY_VERSION = "0.6.1";
439
+ var LIBRARY_VERSION = "0.6.2";
440
440
  function ensureDataLayer() {
441
441
  if (typeof window === "undefined") return;
442
442
  if (!window.dataLayer) {
@@ -2003,10 +2003,13 @@ function ConsentGate(props) {
2003
2003
  }
2004
2004
 
2005
2005
  // src/utils/scriptLoader.ts
2006
+ var LOADING_SCRIPTS = /* @__PURE__ */ new Map();
2006
2007
  function loadScript(id, src, category = null, attrs = {}) {
2007
2008
  if (typeof document === "undefined") return Promise.resolve();
2008
2009
  if (document.getElementById(id)) return Promise.resolve();
2009
- return new Promise((resolve, reject) => {
2010
+ const existingPromise = LOADING_SCRIPTS.get(id);
2011
+ if (existingPromise) return existingPromise;
2012
+ const promise = new Promise((resolve, reject) => {
2010
2013
  const checkConsent = () => {
2011
2014
  const consentCookie = document.cookie.split("; ").find((row) => row.startsWith("cookieConsent="))?.split("=")[1];
2012
2015
  if (!consentCookie) {
@@ -2020,7 +2023,7 @@ function loadScript(id, src, category = null, attrs = {}) {
2020
2023
  return;
2021
2024
  }
2022
2025
  if (category && !consent.preferences[category]) {
2023
- reject(new Error(`Consent not given for ${category} scripts`));
2026
+ setTimeout(checkConsent, 100);
2024
2027
  return;
2025
2028
  }
2026
2029
  const s = document.createElement("script");
@@ -2028,8 +2031,14 @@ function loadScript(id, src, category = null, attrs = {}) {
2028
2031
  s.src = src;
2029
2032
  s.async = true;
2030
2033
  for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
2031
- s.onload = () => resolve();
2032
- s.onerror = () => reject(new Error(`Failed to load script: ${src}`));
2034
+ s.onload = () => {
2035
+ LOADING_SCRIPTS.delete(id);
2036
+ resolve();
2037
+ };
2038
+ s.onerror = () => {
2039
+ LOADING_SCRIPTS.delete(id);
2040
+ reject(new Error(`Failed to load script: ${src}`));
2041
+ };
2033
2042
  document.body.appendChild(s);
2034
2043
  } catch {
2035
2044
  setTimeout(checkConsent, 100);
@@ -2037,6 +2046,8 @@ function loadScript(id, src, category = null, attrs = {}) {
2037
2046
  };
2038
2047
  checkConsent();
2039
2048
  });
2049
+ LOADING_SCRIPTS.set(id, promise);
2050
+ return promise;
2040
2051
  }
2041
2052
 
2042
2053
  // src/utils/autoConfigureCategories.ts
@@ -2264,21 +2275,29 @@ function ConsentScriptLoader({
2264
2275
  }, [integrations, categories]);
2265
2276
  React4__namespace.useEffect(() => {
2266
2277
  if (!consented) return;
2267
- integrations.forEach(async (integration) => {
2268
- const shouldLoad = preferences[integration.category];
2269
- const alreadyLoaded = loadedScripts.current.has(integration.id);
2270
- if (shouldLoad && (!alreadyLoaded || reloadOnChange)) {
2271
- try {
2272
- await loadScript(integration.id, integration.src, integration.category, integration.attrs);
2273
- if (integration.init) {
2274
- integration.init();
2278
+ const timeoutId = setTimeout(async () => {
2279
+ for (const integration of integrations) {
2280
+ const shouldLoad = preferences[integration.category];
2281
+ const alreadyLoaded = loadedScripts.current.has(integration.id);
2282
+ if (shouldLoad && (!alreadyLoaded || reloadOnChange)) {
2283
+ try {
2284
+ await loadScript(
2285
+ integration.id,
2286
+ integration.src,
2287
+ integration.category,
2288
+ integration.attrs
2289
+ );
2290
+ if (integration.init) {
2291
+ integration.init();
2292
+ }
2293
+ loadedScripts.current.add(integration.id);
2294
+ } catch (error) {
2295
+ logger.error(`\u274C Failed to load script: ${integration.id}`, error);
2275
2296
  }
2276
- loadedScripts.current.add(integration.id);
2277
- } catch (error) {
2278
- logger.error(`\u274C Failed to load script: ${integration.id}`, error);
2279
2297
  }
2280
2298
  }
2281
- });
2299
+ }, 0);
2300
+ return () => clearTimeout(timeoutId);
2282
2301
  }, [preferences, consented, integrations, reloadOnChange]);
2283
2302
  return null;
2284
2303
  }
package/dist/index.d.cts CHANGED
@@ -784,7 +784,8 @@ type BackdropConfig = boolean | string | {
784
784
  *
785
785
  * @category Types
786
786
  * @since 0.1.3
787
- * @version 0.4.1 - Expandido substancialmente com novos tokens
787
+ * @remarks
788
+ * **Histórico**: v0.4.1 - Expandido substancialmente com novos tokens
788
789
  * @public
789
790
  *
790
791
  * @example Configuração básica
@@ -1475,8 +1476,8 @@ interface ConsentContextValue {
1475
1476
  * @param cat - ID da categoria (predefinida ou customizada)
1476
1477
  * @param value - Valor do consentimento para a categoria
1477
1478
  *
1478
- * @breakingChange
1479
- * **v0.4.1**: Parâmetro `cat` mudou de `Category` para `string` para suportar
1479
+ * @remarks
1480
+ * **Breaking Change (v0.4.1)**: Parâmetro `cat` mudou de `Category` para `string` para suportar
1480
1481
  * categorias customizadas. O uso com strings literais continua funcionando.
1481
1482
  *
1482
1483
  * @example
@@ -2252,6 +2253,7 @@ declare function useCategoryStatus(categoryId: string): {
2252
2253
  * }
2253
2254
  * ```
2254
2255
  *
2256
+ *
2255
2257
  * @component
2256
2258
  * @category Context
2257
2259
  * @since 0.1.0
@@ -2361,6 +2363,9 @@ declare function buildConsentStorageKey(options?: {
2361
2363
  * após o consentimento do usuário. Ela garante que o script só seja inserido na página
2362
2364
  * se o consentimento for dado e o contexto estiver disponível.
2363
2365
  *
2366
+ * **React 19 StrictMode**: A função é idempotente e mantém um registro global de scripts
2367
+ * em carregamento para evitar duplicações durante double-invoking de efeitos em desenvolvimento.
2368
+ *
2364
2369
  * @param {string} id Um identificador único para o elemento `<script>` a ser criado.
2365
2370
  * @param {string} src A URL do script externo a ser carregado.
2366
2371
  * @param {string | null} [category=null] A categoria de consentimento exigida para o script. Suporta tanto categorias predefinidas quanto customizadas. Se o consentimento para esta categoria não for dado, o script não será carregado.
@@ -3321,7 +3326,8 @@ type LanguageTexts = Partial<Omit<ConsentTexts, 'i18n' | 'variants' | 'contexts'
3321
3326
  *
3322
3327
  * @category Types
3323
3328
  * @since 0.4.1
3324
- * @version 0.4.1 - Nova interface com suporte avançado a i18n e contextos
3329
+ * @remarks
3330
+ * **Histórico**: v0.4.1 - Nova interface com suporte avançado a i18n e contextos
3325
3331
  *
3326
3332
  * @example Configuração multilíngue
3327
3333
  * ```typescript
package/dist/index.d.ts CHANGED
@@ -784,7 +784,8 @@ type BackdropConfig = boolean | string | {
784
784
  *
785
785
  * @category Types
786
786
  * @since 0.1.3
787
- * @version 0.4.1 - Expandido substancialmente com novos tokens
787
+ * @remarks
788
+ * **Histórico**: v0.4.1 - Expandido substancialmente com novos tokens
788
789
  * @public
789
790
  *
790
791
  * @example Configuração básica
@@ -1475,8 +1476,8 @@ interface ConsentContextValue {
1475
1476
  * @param cat - ID da categoria (predefinida ou customizada)
1476
1477
  * @param value - Valor do consentimento para a categoria
1477
1478
  *
1478
- * @breakingChange
1479
- * **v0.4.1**: Parâmetro `cat` mudou de `Category` para `string` para suportar
1479
+ * @remarks
1480
+ * **Breaking Change (v0.4.1)**: Parâmetro `cat` mudou de `Category` para `string` para suportar
1480
1481
  * categorias customizadas. O uso com strings literais continua funcionando.
1481
1482
  *
1482
1483
  * @example
@@ -2252,6 +2253,7 @@ declare function useCategoryStatus(categoryId: string): {
2252
2253
  * }
2253
2254
  * ```
2254
2255
  *
2256
+ *
2255
2257
  * @component
2256
2258
  * @category Context
2257
2259
  * @since 0.1.0
@@ -2361,6 +2363,9 @@ declare function buildConsentStorageKey(options?: {
2361
2363
  * após o consentimento do usuário. Ela garante que o script só seja inserido na página
2362
2364
  * se o consentimento for dado e o contexto estiver disponível.
2363
2365
  *
2366
+ * **React 19 StrictMode**: A função é idempotente e mantém um registro global de scripts
2367
+ * em carregamento para evitar duplicações durante double-invoking de efeitos em desenvolvimento.
2368
+ *
2364
2369
  * @param {string} id Um identificador único para o elemento `<script>` a ser criado.
2365
2370
  * @param {string} src A URL do script externo a ser carregado.
2366
2371
  * @param {string | null} [category=null] A categoria de consentimento exigida para o script. Suporta tanto categorias predefinidas quanto customizadas. Se o consentimento para esta categoria não for dado, o script não será carregado.
@@ -3321,7 +3326,8 @@ type LanguageTexts = Partial<Omit<ConsentTexts, 'i18n' | 'variants' | 'contexts'
3321
3326
  *
3322
3327
  * @category Types
3323
3328
  * @since 0.4.1
3324
- * @version 0.4.1 - Nova interface com suporte avançado a i18n e contextos
3329
+ * @remarks
3330
+ * **Histórico**: v0.4.1 - Nova interface com suporte avançado a i18n e contextos
3325
3331
  *
3326
3332
  * @example Configuração multilíngue
3327
3333
  * ```typescript
package/dist/index.js CHANGED
@@ -412,7 +412,7 @@ function removeConsentCookie(opts) {
412
412
  }
413
413
 
414
414
  // src/utils/dataLayerEvents.ts
415
- var LIBRARY_VERSION = "0.6.1";
415
+ var LIBRARY_VERSION = "0.6.2";
416
416
  function ensureDataLayer() {
417
417
  if (typeof window === "undefined") return;
418
418
  if (!window.dataLayer) {
@@ -1979,10 +1979,13 @@ function ConsentGate(props) {
1979
1979
  }
1980
1980
 
1981
1981
  // src/utils/scriptLoader.ts
1982
+ var LOADING_SCRIPTS = /* @__PURE__ */ new Map();
1982
1983
  function loadScript(id, src, category = null, attrs = {}) {
1983
1984
  if (typeof document === "undefined") return Promise.resolve();
1984
1985
  if (document.getElementById(id)) return Promise.resolve();
1985
- return new Promise((resolve, reject) => {
1986
+ const existingPromise = LOADING_SCRIPTS.get(id);
1987
+ if (existingPromise) return existingPromise;
1988
+ const promise = new Promise((resolve, reject) => {
1986
1989
  const checkConsent = () => {
1987
1990
  const consentCookie = document.cookie.split("; ").find((row) => row.startsWith("cookieConsent="))?.split("=")[1];
1988
1991
  if (!consentCookie) {
@@ -1996,7 +1999,7 @@ function loadScript(id, src, category = null, attrs = {}) {
1996
1999
  return;
1997
2000
  }
1998
2001
  if (category && !consent.preferences[category]) {
1999
- reject(new Error(`Consent not given for ${category} scripts`));
2002
+ setTimeout(checkConsent, 100);
2000
2003
  return;
2001
2004
  }
2002
2005
  const s = document.createElement("script");
@@ -2004,8 +2007,14 @@ function loadScript(id, src, category = null, attrs = {}) {
2004
2007
  s.src = src;
2005
2008
  s.async = true;
2006
2009
  for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
2007
- s.onload = () => resolve();
2008
- 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
+ };
2009
2018
  document.body.appendChild(s);
2010
2019
  } catch {
2011
2020
  setTimeout(checkConsent, 100);
@@ -2013,6 +2022,8 @@ function loadScript(id, src, category = null, attrs = {}) {
2013
2022
  };
2014
2023
  checkConsent();
2015
2024
  });
2025
+ LOADING_SCRIPTS.set(id, promise);
2026
+ return promise;
2016
2027
  }
2017
2028
 
2018
2029
  // src/utils/autoConfigureCategories.ts
@@ -2240,21 +2251,29 @@ function ConsentScriptLoader({
2240
2251
  }, [integrations, categories]);
2241
2252
  React4.useEffect(() => {
2242
2253
  if (!consented) return;
2243
- integrations.forEach(async (integration) => {
2244
- const shouldLoad = preferences[integration.category];
2245
- const alreadyLoaded = loadedScripts.current.has(integration.id);
2246
- if (shouldLoad && (!alreadyLoaded || reloadOnChange)) {
2247
- try {
2248
- await loadScript(integration.id, integration.src, integration.category, integration.attrs);
2249
- if (integration.init) {
2250
- 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);
2251
2272
  }
2252
- loadedScripts.current.add(integration.id);
2253
- } catch (error) {
2254
- logger.error(`\u274C Failed to load script: ${integration.id}`, error);
2255
2273
  }
2256
2274
  }
2257
- });
2275
+ }, 0);
2276
+ return () => clearTimeout(timeoutId);
2258
2277
  }, [preferences, consented, integrations, reloadOnChange]);
2259
2278
  return null;
2260
2279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-lgpd-consent/core",
3
- "version": "0.6.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",