@jwiedeman/gtm-kit-react 1.1.6 → 1.2.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/README.md CHANGED
@@ -124,16 +124,65 @@ const client = useGtmClient();
124
124
 
125
125
  ### `useGtmReady()`
126
126
 
127
- Get a function that resolves when GTM is fully loaded.
127
+ Get a function that resolves when GTM is fully loaded. Best for awaiting in event handlers.
128
128
 
129
129
  ```tsx
130
130
  const whenReady = useGtmReady();
131
131
 
132
- useEffect(() => {
133
- whenReady().then(() => {
134
- console.log('GTM is ready!');
135
- });
136
- }, [whenReady]);
132
+ const handleClick = async () => {
133
+ await whenReady();
134
+ console.log('GTM is ready!');
135
+ };
136
+ ```
137
+
138
+ ### `useIsGtmReady()`
139
+
140
+ Get a function for synchronous ready checks. Best for conditionals in callbacks.
141
+
142
+ ```tsx
143
+ const isReady = useIsGtmReady();
144
+
145
+ const handleClick = () => {
146
+ if (isReady()) {
147
+ // GTM is loaded, safe to use advanced features
148
+ }
149
+ };
150
+ ```
151
+
152
+ ### `useGtmInitialized()`
153
+
154
+ Get a reactive boolean that updates when GTM initializes. Best for conditional rendering.
155
+
156
+ ```tsx
157
+ const isInitialized = useGtmInitialized();
158
+
159
+ return isInitialized ? <Analytics /> : <Skeleton />;
160
+ ```
161
+
162
+ ### `useGtmError()`
163
+
164
+ Get error state from the GTM provider.
165
+
166
+ ```tsx
167
+ const { hasError, errorMessage } = useGtmError();
168
+
169
+ if (hasError) {
170
+ console.error('GTM failed:', errorMessage);
171
+ }
172
+ ```
173
+
174
+ ### `useIsGtmProviderPresent()`
175
+
176
+ Check if GtmProvider exists without throwing. Useful for optional GTM integration.
177
+
178
+ ```tsx
179
+ const hasProvider = useIsGtmProviderPresent();
180
+
181
+ // Safe to call - won't throw if provider is missing
182
+ if (hasProvider) {
183
+ const push = useGtmPush();
184
+ push({ event: 'optional_tracking' });
185
+ }
137
186
  ```
138
187
 
139
188
  ---
@@ -142,13 +191,32 @@ useEffect(() => {
142
191
 
143
192
  ### Setting Consent Defaults Before GTM Loads
144
193
 
145
- To set consent defaults before GTM initializes, use `useGtmConsent` in a component that renders early:
194
+ **Recommended:** Use the `onBeforeInit` callback to set consent defaults before GTM loads:
195
+
196
+ ```tsx
197
+ import { GtmProvider } from '@jwiedeman/gtm-kit-react';
198
+ import { consentPresets } from '@jwiedeman/gtm-kit';
199
+
200
+ function App() {
201
+ return (
202
+ <GtmProvider
203
+ config={{ containers: 'GTM-XXXXXX' }}
204
+ onBeforeInit={(client) => {
205
+ client.setConsentDefaults(consentPresets.eeaDefault, { region: ['EEA'] });
206
+ }}
207
+ >
208
+ <YourApp />
209
+ </GtmProvider>
210
+ );
211
+ }
212
+ ```
213
+
214
+ **Alternative:** Use `useGtmConsent` in a child component (child effects run before parent effects in React):
146
215
 
147
216
  ```tsx
148
217
  import { GtmProvider, useGtmConsent } from '@jwiedeman/gtm-kit-react';
149
218
  import { consentPresets } from '@jwiedeman/gtm-kit';
150
219
 
151
- // Component that sets consent defaults on mount
152
220
  function ConsentInitializer({ children }) {
153
221
  const { setConsentDefaults } = useGtmConsent();
154
222
 
@@ -159,7 +227,6 @@ function ConsentInitializer({ children }) {
159
227
  return <>{children}</>;
160
228
  }
161
229
 
162
- // App wrapper
163
230
  function App() {
164
231
  return (
165
232
  <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>
@@ -228,11 +295,25 @@ updateConsent({ ad_storage: 'granted', ad_user_data: 'granted' });
228
295
  host: 'https://custom.host.com', // Optional
229
296
  scriptAttributes: { nonce: '...' } // Optional: CSP
230
297
  }}
298
+ onBeforeInit={(client) => {
299
+ // Called before GTM loads - ideal for consent defaults
300
+ client.setConsentDefaults(consentPresets.eeaDefault, { region: ['EEA'] });
301
+ }}
302
+ onAfterInit={(client) => {
303
+ // Called after GTM loads
304
+ console.log('GTM initialized');
305
+ }}
231
306
  >
232
307
  {children}
233
308
  </GtmProvider>
234
309
  ```
235
310
 
311
+ | Prop | Type | Required | Description |
312
+ | ---------------- | --------------------------- | -------- | ------------------------------------------------- |
313
+ | `config` | `CreateGtmClientOptions` | Yes | GTM client configuration (containers, host, etc.) |
314
+ | `onBeforeInit` | `(client: GtmClient) => void` | No | Called before GTM initialization — use for consent defaults |
315
+ | `onAfterInit` | `(client: GtmClient) => void` | No | Called after GTM initialization |
316
+
236
317
  ---
237
318
 
238
319
  ## React Router Integration
package/dist/index.cjs CHANGED
@@ -4,13 +4,246 @@ var react = require('react');
4
4
  var gtmKit = require('@jwiedeman/gtm-kit');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
- var u=react.createContext(null),d=(t,n)=>{process.env.NODE_ENV!=="production"&&t!==n&&console.warn("[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used.");},G=t=>{let n=react.useRef(),e=react.useRef();return n.current?e.current&&d(e.current,t):(n.current=gtmKit.createGtmClient(t),e.current=t),n.current},f=({config:t,children:n})=>{let e=G(t);react.useEffect(()=>(e.init(),()=>{e.teardown();}),[e]);let p=react.useMemo(()=>({client:e,push:o=>e.push(o),setConsentDefaults:(o,r)=>e.setConsentDefaults(o,r),updateConsent:(o,r)=>e.updateConsent(o,r),whenReady:()=>e.whenReady(),onReady:o=>e.onReady(o)}),[e]);return jsxRuntime.jsx(u.Provider,{value:p,children:n})},s=()=>{let t=react.useContext(u);if(!t)throw new Error("useGtm hook must be used within a GtmProvider instance.");return t},v=s,x=()=>s().client,y=()=>s().push,h=()=>{let{setConsentDefaults:t,updateConsent:n}=s();return react.useMemo(()=>({setConsentDefaults:t,updateConsent:n}),[t,n])},P=()=>{let{whenReady:t}=s();return t};
7
+ // src/provider.tsx
8
+ var isSsr = () => typeof window === "undefined";
9
+ var useHydrated = () => {
10
+ return react.useSyncExternalStore(
11
+ // Subscribe function (no-op, state never changes after hydration)
12
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
13
+ () => () => {
14
+ },
15
+ // getSnapshot (client): always true after first render
16
+ () => true,
17
+ // getServerSnapshot (server): always false
18
+ () => false
19
+ );
20
+ };
21
+ var GtmContext = react.createContext(null);
22
+ var warnOnNestedProvider = () => {
23
+ if (process.env.NODE_ENV !== "production") {
24
+ console.warn(
25
+ "[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. The nested provider will be ignored."
26
+ );
27
+ }
28
+ };
29
+ var warnOnConfigChange = (initialConfig, nextConfig) => {
30
+ if (process.env.NODE_ENV !== "production" && initialConfig !== nextConfig) {
31
+ console.warn(
32
+ "[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. The initial configuration will continue to be used."
33
+ );
34
+ }
35
+ };
36
+ var extractContainerIds = (config) => {
37
+ const containers = config.containers;
38
+ const containerArray = Array.isArray(containers) ? containers : [containers];
39
+ return containerArray.map((container) => {
40
+ if (typeof container === "string") {
41
+ return container;
42
+ }
43
+ return container.id;
44
+ });
45
+ };
46
+ var warnOnOrphanedSsrScripts = (configuredContainers) => {
47
+ if (process.env.NODE_ENV !== "production" && typeof document !== "undefined") {
48
+ const gtmScripts = document.querySelectorAll('script[src*="googletagmanager.com/gtm.js"]');
49
+ gtmScripts.forEach((script) => {
50
+ const src = script.src;
51
+ const idMatch = src.match(/[?&]id=([^&]+)/);
52
+ const scriptContainerId = idMatch == null ? void 0 : idMatch[1];
53
+ if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {
54
+ console.warn(
55
+ `[gtm-kit/react] Found pre-rendered GTM script for container "${scriptContainerId}" that is not configured in GtmProvider. This may indicate a hydration mismatch between SSR and client-side rendering. Configure GtmProvider with containers: "${scriptContainerId}" to properly hydrate.`
56
+ );
57
+ }
58
+ });
59
+ const gtmNoscripts = document.querySelectorAll('noscript iframe[src*="googletagmanager.com/ns.html"]');
60
+ gtmNoscripts.forEach((iframe) => {
61
+ const src = iframe.src;
62
+ const idMatch = src.match(/[?&]id=([^&]+)/);
63
+ const iframeContainerId = idMatch == null ? void 0 : idMatch[1];
64
+ if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {
65
+ console.warn(
66
+ `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container "${iframeContainerId}" that is not configured in GtmProvider. If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.`
67
+ );
68
+ }
69
+ });
70
+ }
71
+ };
72
+ var useStableClient = (config) => {
73
+ const clientRef = react.useRef();
74
+ const configRef = react.useRef();
75
+ if (!clientRef.current) {
76
+ clientRef.current = gtmKit.createGtmClient(config);
77
+ configRef.current = config;
78
+ } else if (configRef.current) {
79
+ warnOnConfigChange(configRef.current, config);
80
+ }
81
+ return clientRef.current;
82
+ };
83
+ var GtmProvider = ({ config, children, onBeforeInit, onAfterInit }) => {
84
+ const existingContext = react.useContext(GtmContext);
85
+ react.useEffect(() => {
86
+ if (existingContext) {
87
+ warnOnNestedProvider();
88
+ }
89
+ }, [existingContext]);
90
+ if (existingContext) {
91
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
92
+ }
93
+ return /* @__PURE__ */ jsxRuntime.jsx(GtmProviderInner, { config, onBeforeInit, onAfterInit, children });
94
+ };
95
+ var GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }) => {
96
+ const client = useStableClient(config);
97
+ react.useEffect(() => {
98
+ warnOnOrphanedSsrScripts(extractContainerIds(config));
99
+ if (onBeforeInit) {
100
+ onBeforeInit(client);
101
+ }
102
+ client.init();
103
+ if (onAfterInit) {
104
+ onAfterInit(client);
105
+ }
106
+ return () => {
107
+ client.teardown();
108
+ };
109
+ }, [client, config, onBeforeInit, onAfterInit]);
110
+ const value = react.useMemo(
111
+ () => ({
112
+ client,
113
+ push: (value2) => client.push(value2),
114
+ setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),
115
+ updateConsent: (state, options) => client.updateConsent(state, options),
116
+ isReady: () => client.isReady(),
117
+ whenReady: () => client.whenReady(),
118
+ onReady: (callback) => client.onReady(callback)
119
+ }),
120
+ [client]
121
+ );
122
+ return /* @__PURE__ */ jsxRuntime.jsx(GtmContext.Provider, { value, children });
123
+ };
124
+ var useGtmContext = () => {
125
+ const context = react.useContext(GtmContext);
126
+ if (!context) {
127
+ throw new Error(
128
+ '[gtm-kit/react] useGtm() was called outside of a GtmProvider. Make sure to wrap your app with <GtmProvider config={{ containers: "GTM-XXXXXX" }}>.'
129
+ );
130
+ }
131
+ return context;
132
+ };
133
+ var useGtm = useGtmContext;
134
+ var useIsGtmProviderPresent = () => {
135
+ const context = react.useContext(GtmContext);
136
+ return context !== null;
137
+ };
138
+ var useGtmClient = () => {
139
+ return useGtmContext().client;
140
+ };
141
+ var useGtmPush = () => {
142
+ return useGtmContext().push;
143
+ };
144
+ var useGtmConsent = () => {
145
+ const { setConsentDefaults, updateConsent } = useGtmContext();
146
+ return react.useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);
147
+ };
148
+ var useGtmReady = () => {
149
+ const { whenReady } = useGtmContext();
150
+ return whenReady;
151
+ };
152
+ var useIsGtmReady = () => {
153
+ const { isReady } = useGtmContext();
154
+ return isReady;
155
+ };
156
+ var useGtmInitialized = () => {
157
+ const { isReady, onReady } = useGtmContext();
158
+ const [initialized, setInitialized] = react.useState(() => isReady());
159
+ react.useEffect(() => {
160
+ if (isReady()) {
161
+ setInitialized(true);
162
+ return;
163
+ }
164
+ const unsubscribe = onReady(() => {
165
+ setInitialized(true);
166
+ });
167
+ return unsubscribe;
168
+ }, [isReady, onReady]);
169
+ return initialized;
170
+ };
171
+ var useGtmError = () => {
172
+ const { onReady } = useGtmContext();
173
+ const [errorState, setErrorState] = react.useState({
174
+ hasError: false,
175
+ failedScripts: [],
176
+ errorMessage: null
177
+ });
178
+ react.useEffect(() => {
179
+ const unsubscribe = onReady((states) => {
180
+ var _a, _b;
181
+ const failedScripts = states.filter((s) => s.status === "failed" || s.status === "partial");
182
+ if (failedScripts.length > 0) {
183
+ const firstError = (_b = (_a = failedScripts.find((s) => s.error)) == null ? void 0 : _a.error) != null ? _b : null;
184
+ setErrorState({
185
+ hasError: true,
186
+ failedScripts,
187
+ errorMessage: firstError
188
+ });
189
+ }
190
+ });
191
+ return unsubscribe;
192
+ }, [onReady]);
193
+ return errorState;
194
+ };
195
+ var GtmErrorBoundary = class extends react.Component {
196
+ constructor(props) {
197
+ super(props);
198
+ this.reset = () => {
199
+ this.setState({ hasError: false, error: null });
200
+ };
201
+ this.state = { hasError: false, error: null };
202
+ }
203
+ static getDerivedStateFromError(error) {
204
+ return { hasError: true, error };
205
+ }
206
+ componentDidCatch(error, errorInfo) {
207
+ const { onError, logErrors = process.env.NODE_ENV !== "production" } = this.props;
208
+ if (logErrors) {
209
+ console.error("[gtm-kit/react] Error caught by GtmErrorBoundary:", error);
210
+ console.error("[gtm-kit/react] Component stack:", errorInfo.componentStack);
211
+ }
212
+ if (onError) {
213
+ try {
214
+ onError(error, errorInfo);
215
+ } catch (e) {
216
+ }
217
+ }
218
+ }
219
+ render() {
220
+ const { hasError, error } = this.state;
221
+ const { children, fallback } = this.props;
222
+ if (hasError && error) {
223
+ if (fallback === void 0) {
224
+ return children;
225
+ }
226
+ if (typeof fallback === "function") {
227
+ return fallback(error, this.reset);
228
+ }
229
+ return fallback;
230
+ }
231
+ return children;
232
+ }
233
+ };
8
234
 
9
- exports.GtmProvider = f;
10
- exports.useGtm = v;
11
- exports.useGtmClient = x;
12
- exports.useGtmConsent = h;
13
- exports.useGtmPush = y;
14
- exports.useGtmReady = P;
235
+ exports.GtmErrorBoundary = GtmErrorBoundary;
236
+ exports.GtmProvider = GtmProvider;
237
+ exports.isSsr = isSsr;
238
+ exports.useGtm = useGtm;
239
+ exports.useGtmClient = useGtmClient;
240
+ exports.useGtmConsent = useGtmConsent;
241
+ exports.useGtmError = useGtmError;
242
+ exports.useGtmInitialized = useGtmInitialized;
243
+ exports.useGtmPush = useGtmPush;
244
+ exports.useGtmReady = useGtmReady;
245
+ exports.useHydrated = useHydrated;
246
+ exports.useIsGtmProviderPresent = useIsGtmProviderPresent;
247
+ exports.useIsGtmReady = useIsGtmReady;
15
248
  //# sourceMappingURL=out.js.map
16
249
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx"],"names":["createContext","useContext","useEffect","useMemo","useRef","createGtmClient","jsx","GtmContext","warnOnConfigChange","initialConfig","nextConfig","useStableClient","config","clientRef","configRef","GtmProvider","children","client","value","state","options","callback","useGtmContext","context","useGtm","useGtmClient","useGtmPush","useGtmConsent","setConsentDefaults","updateConsent","useGtmReady","whenReady"],"mappings":"AAAA,OAAS,iBAAAA,EAAe,cAAAC,EAAY,aAAAC,EAAW,WAAAC,EAAS,UAAAC,MAAgD,QACxG,OACE,mBAAAC,MAOK,qBAmEE,cAAAC,MAAA,oBA/CT,IAAMC,EAAaP,EAAsC,IAAI,EAEvDQ,EAAqB,CAACC,EAAuCC,IAA6C,CAC1G,QAAQ,IAAI,WAAa,cAAgBD,IAAkBC,GAC7D,QAAQ,KACN,2JAEF,CAEJ,EAEMC,EAAmBC,GAA8C,CACrE,IAAMC,EAAYT,EAAkB,EAC9BU,EAAYV,EAA+B,EAEjD,OAAKS,EAAU,QAGJC,EAAU,SACnBN,EAAmBM,EAAU,QAASF,CAAM,GAH5CC,EAAU,QAAUR,EAAgBO,CAAM,EAC1CE,EAAU,QAAUF,GAKfC,EAAU,OACnB,EAEaE,EAAc,CAAC,CAAE,OAAAH,EAAQ,SAAAI,CAAS,IAAqC,CAClF,IAAMC,EAASN,EAAgBC,CAAM,EAErCV,EAAU,KACRe,EAAO,KAAK,EACL,IAAM,CACXA,EAAO,SAAS,CAClB,GACC,CAACA,CAAM,CAAC,EAEX,IAAMC,EAAQf,EACZ,KAAO,CACL,OAAAc,EACA,KAAOC,GAAUD,EAAO,KAAKC,CAAK,EAClC,mBAAoB,CAACC,EAAOC,IAAYH,EAAO,mBAAmBE,EAAOC,CAAO,EAChF,cAAe,CAACD,EAAOC,IAAYH,EAAO,cAAcE,EAAOC,CAAO,EACtE,UAAW,IAAMH,EAAO,UAAU,EAClC,QAAUI,GAAaJ,EAAO,QAAQI,CAAQ,CAChD,GACA,CAACJ,CAAM,CACT,EAEA,OAAOX,EAACC,EAAW,SAAX,CAAoB,MAAOW,EAAQ,SAAAF,EAAS,CACtD,EAEMM,EAAgB,IAAuB,CAC3C,IAAMC,EAAUtB,EAAWM,CAAU,EACrC,GAAI,CAACgB,EACH,MAAM,IAAI,MAAM,yDAAyD,EAE3E,OAAOA,CACT,EAEaC,EAASF,EAETG,EAAe,IACnBH,EAAc,EAAE,OAGZI,EAAa,IACjBJ,EAAc,EAAE,KAGZK,EAAgB,IAAqB,CAChD,GAAM,CAAE,mBAAAC,EAAoB,cAAAC,CAAc,EAAIP,EAAc,EAC5D,OAAOnB,EAAQ,KAAO,CAAE,mBAAAyB,EAAoB,cAAAC,CAAc,GAAI,CAACD,EAAoBC,CAAa,CAAC,CACnG,EAEaC,EAAc,IAA0C,CACnE,GAAM,CAAE,UAAAC,CAAU,EAAIT,EAAc,EACpC,OAAOS,CACT","sourcesContent":["import { createContext, useContext, useEffect, useMemo, useRef, type PropsWithChildren, type JSX } from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[react-gtm-kit] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\nexport const GtmProvider = ({ config, children }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n client.init();\n return () => {\n client.teardown();\n };\n }, [client]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error('useGtm hook must be used within a GtmProvider instance.');\n }\n return context;\n};\n\nexport const useGtm = useGtmContext;\n\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n"]}
1
+ {"version":3,"sources":["../src/provider.tsx"],"names":["value"],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ErrorInfo,\n type PropsWithChildren,\n type ReactNode,\n type JSX\n} from 'react';\nimport {\n createGtmClient,\n type ConsentRegionOptions,\n type ConsentState,\n type CreateGtmClientOptions,\n type DataLayerValue,\n type GtmClient,\n type ScriptLoadState\n} from '@jwiedeman/gtm-kit';\n\n/**\n * Check if code is running in a server-side rendering environment.\n * Returns true if window is undefined (Node.js/SSR), false in browser.\n */\nexport const isSsr = (): boolean => typeof window === 'undefined';\n\n/**\n * Hook that returns false during SSR and initial hydration, then true after hydration completes.\n * Use this to prevent hydration mismatches when rendering GTM-dependent content.\n *\n * @example\n * ```tsx\n * const isHydrated = useHydrated();\n *\n * // Safe: won't cause hydration mismatch\n * return isHydrated ? <DynamicContent /> : <StaticPlaceholder />;\n * ```\n */\nexport const useHydrated = (): boolean => {\n // useSyncExternalStore with getServerSnapshot ensures consistent SSR/hydration\n return useSyncExternalStore(\n // Subscribe function (no-op, state never changes after hydration)\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n () => () => {},\n // getSnapshot (client): always true after first render\n () => true,\n // getServerSnapshot (server): always false\n () => false\n );\n};\n\nexport interface GtmProviderProps extends PropsWithChildren {\n config: CreateGtmClientOptions;\n\n /**\n * Callback executed before GTM initialization.\n * Use this to set consent defaults.\n *\n * @example\n * ```tsx\n * <GtmProvider\n * config={{ containers: 'GTM-XXXXXX' }}\n * onBeforeInit={(client) => {\n * client.setConsentDefaults({\n * ad_storage: 'denied',\n * analytics_storage: 'denied'\n * });\n * }}\n * >\n * ```\n */\n onBeforeInit?: (client: GtmClient) => void;\n\n /**\n * Callback executed after GTM initialization.\n */\n onAfterInit?: (client: GtmClient) => void;\n}\n\nexport interface GtmContextValue {\n client: GtmClient;\n push: (value: DataLayerValue) => void;\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n /** Synchronously check if all GTM scripts have finished loading */\n isReady: () => boolean;\n whenReady: () => Promise<ScriptLoadState[]>;\n onReady: (callback: (state: ScriptLoadState[]) => void) => () => void;\n}\n\nexport interface GtmConsentApi {\n setConsentDefaults: (state: ConsentState, options?: ConsentRegionOptions) => void;\n updateConsent: (state: ConsentState, options?: ConsentRegionOptions) => void;\n}\n\nconst GtmContext = createContext<GtmContextValue | null>(null);\n\nconst warnOnNestedProvider = (): void => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[gtm-kit/react] Nested GtmProvider detected. You should only have one GtmProvider at the root of your app. ' +\n 'The nested provider will be ignored.'\n );\n }\n};\n\nconst warnOnConfigChange = (initialConfig: CreateGtmClientOptions, nextConfig: CreateGtmClientOptions): void => {\n if (process.env.NODE_ENV !== 'production' && initialConfig !== nextConfig) {\n console.warn(\n '[gtm-kit/react] GtmProvider received new configuration; reconfiguration after mount is not supported. ' +\n 'The initial configuration will continue to be used.'\n );\n }\n};\n\n/**\n * Extracts container IDs from the provider config.\n */\nconst extractContainerIds = (config: CreateGtmClientOptions): string[] => {\n const containers = config.containers;\n const containerArray = Array.isArray(containers) ? containers : [containers];\n\n return containerArray.map((container) => {\n if (typeof container === 'string') {\n return container;\n }\n return container.id;\n });\n};\n\n/**\n * Warns in development if there are orphaned SSR-rendered GTM scripts without a matching client.\n * This helps developers identify hydration mismatches where GTM was rendered on the server\n * but the GtmProvider config doesn't match or is missing.\n */\nconst warnOnOrphanedSsrScripts = (configuredContainers: string[]): void => {\n if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') {\n // Find all GTM scripts on the page\n const gtmScripts = document.querySelectorAll('script[src*=\"googletagmanager.com/gtm.js\"]');\n\n gtmScripts.forEach((script) => {\n const src = (script as HTMLScriptElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const scriptContainerId = idMatch?.[1];\n\n if (scriptContainerId && !configuredContainers.includes(scriptContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM script for container \"${scriptContainerId}\" that is not configured in GtmProvider. ` +\n 'This may indicate a hydration mismatch between SSR and client-side rendering. ' +\n `Configure GtmProvider with containers: \"${scriptContainerId}\" to properly hydrate.`\n );\n }\n });\n\n // Also check for noscript iframes\n const gtmNoscripts = document.querySelectorAll('noscript iframe[src*=\"googletagmanager.com/ns.html\"]');\n gtmNoscripts.forEach((iframe) => {\n const src = (iframe as HTMLIFrameElement).src;\n const idMatch = src.match(/[?&]id=([^&]+)/);\n const iframeContainerId = idMatch?.[1];\n\n if (iframeContainerId && !configuredContainers.includes(iframeContainerId)) {\n console.warn(\n `[gtm-kit/react] Found pre-rendered GTM noscript iframe for container \"${iframeContainerId}\" that is not configured in GtmProvider. ` +\n 'If you pre-render noscript fallbacks on the server, ensure your GtmProvider has the same container ID.'\n );\n }\n });\n }\n};\n\nconst useStableClient = (config: CreateGtmClientOptions): GtmClient => {\n const clientRef = useRef<GtmClient>();\n const configRef = useRef<CreateGtmClientOptions>();\n\n if (!clientRef.current) {\n clientRef.current = createGtmClient(config);\n configRef.current = config;\n } else if (configRef.current) {\n warnOnConfigChange(configRef.current, config);\n }\n\n return clientRef.current!;\n};\n\n/**\n * GTM Provider component that initializes Google Tag Manager for your React app.\n *\n * ## SSR/Hydration Behavior\n *\n * This provider is SSR-safe and handles hydration correctly:\n * - **Server**: No GTM initialization occurs (no window/document access)\n * - **Client Hydration**: GTM initializes only after hydration via useEffect\n * - **No Hydration Mismatch**: Provider renders the same on server and client\n *\n * ## Usage with SSR Frameworks\n *\n * ```tsx\n * // Next.js, Remix, etc.\n * export default function App({ children }) {\n * return (\n * <GtmProvider config={{ containers: 'GTM-XXXXXX' }}>\n * {children}\n * </GtmProvider>\n * );\n * }\n * ```\n *\n * ## Preventing Hydration Mismatches\n *\n * If you render different content based on GTM state, use `useHydrated`:\n *\n * ```tsx\n * const isHydrated = useHydrated();\n * const isGtmReady = useGtmInitialized();\n *\n * // Safe: both server and client initially render the placeholder\n * if (!isHydrated) return <Placeholder />;\n * return isGtmReady ? <TrackedContent /> : <LoadingContent />;\n * ```\n */\nexport const GtmProvider = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const existingContext = useContext(GtmContext);\n\n // Warn if we're inside another GtmProvider (nested providers)\n useEffect(() => {\n if (existingContext) {\n warnOnNestedProvider();\n }\n }, [existingContext]);\n\n // If nested, just pass through children without creating a new context\n if (existingContext) {\n return <>{children}</>;\n }\n\n return (\n <GtmProviderInner config={config} onBeforeInit={onBeforeInit} onAfterInit={onAfterInit}>\n {children}\n </GtmProviderInner>\n );\n};\n\nconst GtmProviderInner = ({ config, children, onBeforeInit, onAfterInit }: GtmProviderProps): JSX.Element => {\n const client = useStableClient(config);\n\n useEffect(() => {\n // Check for orphaned SSR scripts before initializing\n warnOnOrphanedSsrScripts(extractContainerIds(config));\n\n // Call onBeforeInit hook for consent defaults\n if (onBeforeInit) {\n onBeforeInit(client);\n }\n\n client.init();\n\n // Call onAfterInit hook\n if (onAfterInit) {\n onAfterInit(client);\n }\n\n return () => {\n client.teardown();\n };\n }, [client, config, onBeforeInit, onAfterInit]);\n\n const value = useMemo<GtmContextValue>(\n () => ({\n client,\n push: (value) => client.push(value),\n setConsentDefaults: (state, options) => client.setConsentDefaults(state, options),\n updateConsent: (state, options) => client.updateConsent(state, options),\n isReady: () => client.isReady(),\n whenReady: () => client.whenReady(),\n onReady: (callback) => client.onReady(callback)\n }),\n [client]\n );\n\n return <GtmContext.Provider value={value}>{children}</GtmContext.Provider>;\n};\n\nconst useGtmContext = (): GtmContextValue => {\n const context = useContext(GtmContext);\n if (!context) {\n throw new Error(\n '[gtm-kit/react] useGtm() was called outside of a GtmProvider. ' +\n 'Make sure to wrap your app with <GtmProvider config={{ containers: \"GTM-XXXXXX\" }}>.'\n );\n }\n return context;\n};\n\n/**\n * Hook to access the full GTM context. Throws if used outside GtmProvider.\n *\n * For most use cases, prefer the specific hooks:\n * - `useGtmPush()` - Push events to dataLayer\n * - `useGtmConsent()` - Manage consent state\n * - `useGtmClient()` - Access the raw GTM client\n *\n * @throws Error if called outside of GtmProvider\n *\n * @example\n * ```tsx\n * const { push, client, isReady, whenReady } = useGtm();\n * ```\n */\nexport const useGtm = useGtmContext;\n\n/**\n * Hook to check if GtmProvider is present without throwing.\n *\n * Useful for components that may be rendered before the provider mounts,\n * or for optional GTM integration.\n *\n * @returns `true` if inside GtmProvider, `false` otherwise\n *\n * @example\n * ```tsx\n * const hasProvider = useIsGtmProviderPresent();\n *\n * const handleClick = () => {\n * if (hasProvider) {\n * // Safe to use GTM hooks\n * }\n * };\n * ```\n */\nexport const useIsGtmProviderPresent = (): boolean => {\n const context = useContext(GtmContext);\n return context !== null;\n};\n\n/**\n * Hook to access the raw GTM client instance.\n *\n * @throws Error if called outside of GtmProvider\n * @returns The GTM client instance\n *\n * @example\n * ```tsx\n * const client = useGtmClient();\n *\n * // Access low-level APIs\n * const diagnostics = client.getDiagnostics();\n * ```\n */\nexport const useGtmClient = (): GtmClient => {\n return useGtmContext().client;\n};\n\n/**\n * Hook to get the push function for sending events to the dataLayer.\n *\n * This is the most commonly used hook for tracking events.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Function to push values to the dataLayer\n *\n * @example\n * ```tsx\n * const push = useGtmPush();\n *\n * const handleAddToCart = (product) => {\n * push({\n * event: 'add_to_cart',\n * ecommerce: {\n * items: [{ item_id: product.id, item_name: product.name }]\n * }\n * });\n * };\n * ```\n */\nexport const useGtmPush = (): ((value: DataLayerValue) => void) => {\n return useGtmContext().push;\n};\n\n/**\n * Hook to access consent management functions.\n *\n * @throws Error if called outside of GtmProvider\n * @returns Object with `setConsentDefaults` and `updateConsent` functions\n *\n * @example\n * ```tsx\n * const { updateConsent } = useGtmConsent();\n *\n * const handleAcceptCookies = () => {\n * updateConsent({\n * ad_storage: 'granted',\n * analytics_storage: 'granted'\n * });\n * };\n * ```\n */\nexport const useGtmConsent = (): GtmConsentApi => {\n const { setConsentDefaults, updateConsent } = useGtmContext();\n return useMemo(() => ({ setConsentDefaults, updateConsent }), [setConsentDefaults, updateConsent]);\n};\n\n/**\n * Hook that returns the `whenReady` promise function.\n *\n * **When to use**: When you need to await GTM script loading before taking an action.\n * The returned function returns a Promise that resolves when scripts finish loading.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns a Promise resolving to script load states\n *\n * @example\n * ```tsx\n * const whenReady = useGtmReady();\n *\n * const handleClick = async () => {\n * const states = await whenReady();\n * if (states.every(s => s.status === 'loaded')) {\n * // Safe to rely on GTM being fully loaded\n * }\n * };\n * ```\n */\nexport const useGtmReady = (): (() => Promise<ScriptLoadState[]>) => {\n const { whenReady } = useGtmContext();\n return whenReady;\n};\n\n/**\n * Hook that returns a function to synchronously check if GTM is ready.\n *\n * **When to use**: When you need to check readiness without triggering re-renders,\n * typically in event handlers or callbacks.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns Function that returns `true` if scripts loaded, `false` if still loading\n *\n * @example\n * ```tsx\n * const checkReady = useIsGtmReady();\n *\n * const handleSubmit = () => {\n * if (checkReady()) {\n * // GTM is ready, proceed with tracking\n * push({ event: 'form_submit' });\n * }\n * };\n * ```\n */\nexport const useIsGtmReady = (): (() => boolean) => {\n const { isReady } = useGtmContext();\n return isReady;\n};\n\n/**\n * Reactive hook that returns `true` when GTM scripts have finished loading.\n *\n * **When to use**: When you need to conditionally render UI based on GTM readiness.\n * This hook triggers a re-render when the state changes.\n *\n * **Comparison of GTM readiness hooks:**\n * | Hook | Returns | Re-renders | Use Case |\n * |------|---------|------------|----------|\n * | `useGtmReady()` | `() => Promise` | No | Await in event handlers |\n * | `useIsGtmReady()` | `() => boolean` | No | Synchronous checks in callbacks |\n * | `useGtmInitialized()` | `boolean` | Yes | Conditional rendering |\n *\n * @returns `true` if GTM is initialized, `false` otherwise (reactive)\n *\n * @example\n * ```tsx\n * const isInitialized = useGtmInitialized();\n *\n * if (!isInitialized) {\n * return <LoadingSpinner />;\n * }\n *\n * return <AnalyticsDashboard />;\n * ```\n */\nexport const useGtmInitialized = (): boolean => {\n const { isReady, onReady } = useGtmContext();\n const [initialized, setInitialized] = useState(() => isReady());\n\n useEffect(() => {\n // Already initialized on mount\n if (isReady()) {\n setInitialized(true);\n return;\n }\n\n // Subscribe to ready event\n const unsubscribe = onReady(() => {\n setInitialized(true);\n });\n\n return unsubscribe;\n }, [isReady, onReady]);\n\n return initialized;\n};\n\n/**\n * Result from the useGtmError hook.\n */\nexport interface GtmErrorState {\n /** Whether any scripts failed to load */\n hasError: boolean;\n /** Array of failed script states (status 'failed' or 'partial') */\n failedScripts: ScriptLoadState[];\n /** Convenience getter for the first error message, if any */\n errorMessage: string | null;\n}\n\n/**\n * Hook to capture GTM script load errors.\n * Returns reactive state that updates when scripts fail to load.\n *\n * @example\n * ```tsx\n * const { hasError, failedScripts, errorMessage } = useGtmError();\n *\n * if (hasError) {\n * console.error('GTM failed to load:', errorMessage);\n * // Optionally show fallback UI or retry logic\n * }\n * ```\n */\nexport const useGtmError = (): GtmErrorState => {\n const { onReady } = useGtmContext();\n const [errorState, setErrorState] = useState<GtmErrorState>({\n hasError: false,\n failedScripts: [],\n errorMessage: null\n });\n\n useEffect(() => {\n const unsubscribe = onReady((states) => {\n const failedScripts = states.filter((s) => s.status === 'failed' || s.status === 'partial');\n\n if (failedScripts.length > 0) {\n const firstError = failedScripts.find((s) => s.error)?.error ?? null;\n setErrorState({\n hasError: true,\n failedScripts,\n errorMessage: firstError\n });\n }\n });\n\n return unsubscribe;\n }, [onReady]);\n\n return errorState;\n};\n\n/**\n * Props for GtmErrorBoundary component.\n */\nexport interface GtmErrorBoundaryProps {\n children: ReactNode;\n /** Fallback UI to render when an error occurs */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Callback invoked when an error is caught */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n /** Whether to log errors to console (default: true in development) */\n logErrors?: boolean;\n}\n\ninterface GtmErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Error boundary component for GTM provider.\n * Catches errors during GTM initialization and renders a fallback UI.\n * Analytics and tracking will be disabled when an error occurs.\n */\nexport class GtmErrorBoundary extends Component<GtmErrorBoundaryProps, GtmErrorBoundaryState> {\n constructor(props: GtmErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): GtmErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const { onError, logErrors = process.env.NODE_ENV !== 'production' } = this.props;\n\n if (logErrors) {\n console.error('[gtm-kit/react] Error caught by GtmErrorBoundary:', error);\n console.error('[gtm-kit/react] Component stack:', errorInfo.componentStack);\n }\n\n if (onError) {\n try {\n onError(error, errorInfo);\n } catch {\n // Ignore callback errors\n }\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n const { hasError, error } = this.state;\n const { children, fallback } = this.props;\n\n if (hasError && error) {\n if (fallback === undefined) {\n // Default: render children without GTM (silent fallback)\n return children;\n }\n\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n\n return fallback;\n }\n\n return children;\n }\n}\n"]}