@silverassist/leadcapture-form 0.1.2 → 0.1.3

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
@@ -2,7 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [0.1.2] - Unreleased
5
+ ## [0.1.3] - Unreleased
6
+
7
+ ### Changed
8
+
9
+ - Bumped `@silverassist/next-script-loader` to `^0.1.1` and removed the
10
+ package-local generation map (`bumpGeneration`/`currentGeneration`) and
11
+ the 0.1.2 `hasHandledRemountRef` guard, now that both are handled at the
12
+ source: `ScriptLoader.reload()` itself shares the in-flight promise for
13
+ a same-variant reload already in progress (the actual fix for the
14
+ React Strict Mode double-render), and `ScriptLoader.getGeneration()` /
15
+ `unload(atGeneration?)` replace the hand-rolled generation tracking this
16
+ package carried since 0.1.0. No behavior change for consumers — same
17
+ fix, now enforced one layer down so every `ScriptLoader` consumer gets
18
+ it, not just this one.
19
+
20
+ ## [0.1.2] - 2026-08-31
6
21
 
7
22
  ### Fixed
8
23
 
package/dist/index.js CHANGED
@@ -24,26 +24,6 @@ let react_jsx_runtime = require("react/jsx-runtime");
24
24
  * the README for the workaround.
25
25
  */
26
26
  const leadCaptureLoader = new _silverassist_next_script_loader.ScriptLoader();
27
- /**
28
- * Generation counter per variant, incremented on every {@link ScriptLoader.load}/
29
- * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the
30
- * generation at mount time and skips its `unload()` call if the generation has
31
- * since advanced — i.e. a new mount already reloaded the script before the old
32
- * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).
33
- * `ScriptLoader`'s own ref-counting handles the common case; this guards the
34
- * one case it doesn't: `reload()` doesn't change the reference count, so a
35
- * stale `unload()` after a `reload()` could drop the count to zero and tear
36
- * down a script a fresh mount is now depending on.
37
- */
38
- const generationByVariant = /* @__PURE__ */ new Map();
39
- function bumpGeneration(variant) {
40
- const next = (generationByVariant.get(variant) ?? 0) + 1;
41
- generationByVariant.set(variant, next);
42
- return next;
43
- }
44
- function currentGeneration(variant) {
45
- return generationByVariant.get(variant) ?? 0;
46
- }
47
27
  const DEFAULT_LEADCAPTURE_SCRIPT_URL = "https://api.useleadbot.com/lead-bots/get-pixel-script.js";
48
28
  /**
49
29
  * LeadCaptureForm — renders a LeadCapture IO form with support for
@@ -78,7 +58,6 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
78
58
  const formRef = (0, react.useRef)(null);
79
59
  const mountGenRef = (0, react.useRef)(0);
80
60
  const isMountedRef = (0, react.useRef)(true);
81
- const hasHandledRemountRef = (0, react.useRef)(false);
82
61
  const containerId = `leadcapture-container-${formVariant}-${usageContext}`;
83
62
  (0, react.useEffect)(() => {
84
63
  isMountedRef.current = true;
@@ -118,7 +97,6 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
118
97
  if (!(usageContext === "modal" ? isModalOpen === true : isInViewport)) return;
119
98
  const load = () => {
120
99
  window.form_token = formTokens[formVariant];
121
- bumpGeneration(formVariant);
122
100
  leadCaptureLoader.load(formVariant).then(() => {
123
101
  if (!isMountedRef.current) return;
124
102
  leadCaptureLoader.forceSetOwner(containerId);
@@ -163,27 +141,20 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
163
141
  * that case immediately, using this variant's own load history instead
164
142
  * of the interaction gate above.
165
143
  *
166
- * Guarded by `hasHandledRemountRef` so this can only ever act once per
167
- * real component instance: `reload()` tears down and recreates the
168
- * `<script>` element, and removing a `<script>` doesn't reliably cancel
169
- * its in-flight network request, so calling it twice in a row for the
170
- * same mount (e.g. React Strict Mode's dev-only effect-cleanup-effect
171
- * replay, which reuses this same ref) can let both the superseded and
172
- * the current script execute and each populate the container, rendering
173
- * the widget twice. Claiming ownership isn't a substitute for this guard
174
- * -- `setOwner` is idempotent for the same id, so a second call from the
175
- * same instance succeeds too.
144
+ * Safe to call on every render this condition holds, including twice in
145
+ * a row for the same mount under React Strict Mode's dev-only effect
146
+ * replay: `ScriptLoader.reload()` (0.1.1+) shares the in-flight promise
147
+ * for a same-variant reload already in progress instead of tearing the
148
+ * script down again, so a second call here is a no-op rather than a
149
+ * second script execution.
176
150
  */
177
151
  (0, react.useEffect)(() => {
178
152
  if (usageContext !== "onPage") return;
179
- if (hasHandledRemountRef.current) return;
180
- if (currentGeneration(formVariant) === 0) return;
153
+ if (leadCaptureLoader.getGeneration() === 0) return;
181
154
  if (!leadCaptureLoader.setOwner(containerId)) return;
182
155
  const container = formRef.current?.querySelector(".leadforms-embd-form");
183
156
  if (!container || container.children.length > 0) return;
184
- hasHandledRemountRef.current = true;
185
157
  window.form_token = formTokens[formVariant];
186
- bumpGeneration(formVariant);
187
158
  leadCaptureLoader.reload(formVariant).then(() => {
188
159
  if (!isMountedRef.current) return;
189
160
  leadCaptureLoader.forceSetOwner(containerId);
@@ -199,7 +170,7 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
199
170
  * already reloaded) is a no-op.
200
171
  */
201
172
  (0, react.useEffect)(() => {
202
- mountGenRef.current = currentGeneration(formVariant);
173
+ mountGenRef.current = leadCaptureLoader.getGeneration();
203
174
  }, [formVariant]);
204
175
  /**
205
176
  * Cleanup on unmount only.
@@ -213,8 +184,7 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
213
184
  if (usageContext === "onPage") {
214
185
  const gen = mountGenRef.current;
215
186
  setTimeout(() => {
216
- if (gen < currentGeneration(formVariant)) return;
217
- leadCaptureLoader.unload();
187
+ leadCaptureLoader.unload(gen);
218
188
  }, 100);
219
189
  }
220
190
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ScriptLoader","useState","useRef"],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\n/**\n * Generation counter per variant, incremented on every {@link ScriptLoader.load}/\n * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the\n * generation at mount time and skips its `unload()` call if the generation has\n * since advanced — i.e. a new mount already reloaded the script before the old\n * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).\n * `ScriptLoader`'s own ref-counting handles the common case; this guards the\n * one case it doesn't: `reload()` doesn't change the reference count, so a\n * stale `unload()` after a `reload()` could drop the count to zero and tear\n * down a script a fresh mount is now depending on.\n */\nconst generationByVariant = new Map<string, number>();\n\nfunction bumpGeneration(variant: string): number {\n const next = (generationByVariant.get(variant) ?? 0) + 1;\n generationByVariant.set(variant, next);\n return next;\n}\n\nfunction currentGeneration(variant: string): number {\n return generationByVariant.get(variant) ?? 0;\n}\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const hasHandledRemountRef = useRef(false);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n bumpGeneration(formVariant);\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Handles a client-side navigation remount. The vendor script only scans\n * the DOM for `.leadforms-embd-form` divs once, when it first loads (see\n * the container comment below) -- it never repopulates a div added by a\n * later mount. Without this, a second page's form waits forever for a\n * *fresh* minimal-interaction event, which the click that triggered the\n * navigation doesn't itself produce (no new focus/mousemove/scroll/\n * touchstart fires on the new page unless the user moves again). Detect\n * that case immediately, using this variant's own load history instead\n * of the interaction gate above.\n *\n * Guarded by `hasHandledRemountRef` so this can only ever act once per\n * real component instance: `reload()` tears down and recreates the\n * `<script>` element, and removing a `<script>` doesn't reliably cancel\n * its in-flight network request, so calling it twice in a row for the\n * same mount (e.g. React Strict Mode's dev-only effect-cleanup-effect\n * replay, which reuses this same ref) can let both the superseded and\n * the current script execute and each populate the container, rendering\n * the widget twice. Claiming ownership isn't a substitute for this guard\n * -- `setOwner` is idempotent for the same id, so a second call from the\n * same instance succeeds too.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\") return;\n if (hasHandledRemountRef.current) return;\n if (currentGeneration(formVariant) === 0) return;\n if (!leadCaptureLoader.setOwner(containerId)) return;\n\n const container = formRef.current?.querySelector(\".leadforms-embd-form\");\n if (!container || container.children.length > 0) return;\n\n hasHandledRemountRef.current = true;\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n bumpGeneration(formVariant);\n leadCaptureLoader\n .reload(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade -- the surrounding page stays usable without the embed.\n });\n }, [formVariant, containerId, usageContext]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = currentGeneration(formVariant);\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n if (gen < currentGeneration(formVariant)) return;\n leadCaptureLoader.unload();\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,oBAAoB,IAAIA,8CAAa;;;;;;;;;;;;AAalD,MAAM,sCAAsB,IAAI,IAAoB;AAEpD,SAAS,eAAe,SAAyB;CAC/C,MAAM,QAAQ,oBAAoB,IAAI,OAAO,KAAK,KAAK;CACvD,oBAAoB,IAAI,SAAS,IAAI;CACrC,OAAO;AACT;AAEA,SAAS,kBAAkB,SAAyB;CAClD,OAAO,oBAAoB,IAAI,OAAO,KAAK;AAC7C;AAoCA,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BvC,SAAwB,gBAAgB,EACtC,aACA,YACA,WACA,cACA,aACA,YAAY,IACZ,iBACuB;CACvB,MAAM,CAAC,cAAc,uBAAmBC,gBAAS,KAAK;CACtD,MAAM,cAAUC,cAAuB,IAAI;CAC3C,MAAM,kBAAcA,cAAe,CAAC;CACpC,MAAM,mBAAeA,cAAgB,IAAI;CACzC,MAAM,2BAAuBA,cAAO,KAAK;CACzC,MAAM,cAAc,yBAAyB,YAAY,GAAG;CAE5D,2BAAgB;EACd,aAAa,UAAU;EACvB,aAAa;GACX,aAAa,UAAU;EACzB;CACF,GAAG,CAAC,CAAC;CAEL,2BAAgB;EACd,kBAAkB,UAAU,EAC1B,MAAM,GAAG,cAAc,aAAa,+BAA+B,EACrE,CAAC;CACH,GAAG,CAAC,aAAa,SAAS,CAAC;;;;CAK3B,2BAAgB;EACd,IAAI,iBAAiB,YAAY,CAAC,QAAQ,SAAS;EAEnD,MAAM,WAAW,IAAI,sBAClB,YAAY;GACX,QAAQ,SAAS,UAAU;IACzB,IAAI,MAAM,gBAAgB;KACxB,gBAAgB,IAAI;KACpB,SAAS,WAAW;IACtB;GACF,CAAC;EACH,GACA;GAAE,YAAY;GAAS,WAAW;EAAI,CACxC;EAEA,SAAS,QAAQ,QAAQ,OAAO;EAChC,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,YAAY,CAAC;;;;;;;CAQjB,2BAAgB;EAGd,IAAI,EAFe,iBAAiB,UAAU,gBAAgB,OAAO,eAEpD;EAEjB,MAAM,aAAa;GAIjB,AAAC,OAA4C,aAAa,WAAW;GAErE,eAAe,WAAW;GAC1B,kBACG,KAAK,WAAW,CAAC,CACjB,WAAW;IACV,IAAI,CAAC,aAAa,SAAS;IAC3B,kBAAkB,cAAc,WAAW;GAC7C,CAAC,CAAC,CACD,YAAY,CAGb,CAAC;EACL;EAEA,IAAI,iBAAiB,SAAS;GAC5B,KAAK;GACL;EACF;EAEA,MAAM,SAAS;GAAC;GAAS;GAAa;GAAU;EAAY;EAC5D,MAAM,iBAAiB;GACrB,KAAK;GACL,OAAO,SAAS,UAAU,SAAS,oBAAoB,OAAO,QAAQ,CAAC;EACzE;EACA,OAAO,SAAS,UAAU;GACxB,SAAS,iBAAiB,OAAO,UAAU,EAAE,MAAM,KAAK,CAAC;EAC3D,CAAC;EAED,aAAa;GACX,OAAO,SAAS,UAAU,SAAS,oBAAoB,OAAO,QAAQ,CAAC;EACzE;CACF,GAAG;EAAC;EAAa;EAAc;EAAa;EAAc;EAAa;CAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAwBlF,2BAAgB;EACd,IAAI,iBAAiB,UAAU;EAC/B,IAAI,qBAAqB,SAAS;EAClC,IAAI,kBAAkB,WAAW,MAAM,GAAG;EAC1C,IAAI,CAAC,kBAAkB,SAAS,WAAW,GAAG;EAE9C,MAAM,YAAY,QAAQ,SAAS,cAAc,sBAAsB;EACvE,IAAI,CAAC,aAAa,UAAU,SAAS,SAAS,GAAG;EAEjD,qBAAqB,UAAU;EAC/B,AAAC,OAA4C,aAAa,WAAW;EACrE,eAAe,WAAW;EAC1B,kBACG,OAAO,WAAW,CAAC,CACnB,WAAW;GACV,IAAI,CAAC,aAAa,SAAS;GAC3B,kBAAkB,cAAc,WAAW;EAC7C,CAAC,CAAC,CACD,YAAY,CAEb,CAAC;CACL,GAAG;EAAC;EAAa;EAAa;CAAY,CAAC;;;;;;CAO3C,2BAAgB;EACd,YAAY,UAAU,kBAAkB,WAAW;CACrD,GAAG,CAAC,WAAW,CAAC;;;;;;;CAQhB,2BAAgB;EACd,aAAa;GACX,kBAAkB,iBAAiB,WAAW;GAE9C,IAAI,iBAAiB,UAAU;IAC7B,MAAM,MAAM,YAAY;IACxB,iBAAiB;KACf,IAAI,MAAM,kBAAkB,WAAW,GAAG;KAC1C,kBAAkB,OAAO;IAC3B,GAAG,GAAG;GACR;EACF;CACF,GAAG,CAAC,CAAC;CAEL,OACE,2CAAC,OAAD;EAAK,KAAK;EAAS,IAAI;EAAwB;YAQ7C,2CAAC,OAAD;GAAK,WAAU;GAAsB,GAAK,gBAAgB,EAAE,IAAI,cAAc,IAAI,CAAC;EAE9E;CACF;AAET"}
1
+ {"version":3,"file":"index.js","names":["ScriptLoader","useState","useRef"],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Handles a client-side navigation remount. The vendor script only scans\n * the DOM for `.leadforms-embd-form` divs once, when it first loads (see\n * the container comment below) -- it never repopulates a div added by a\n * later mount. Without this, a second page's form waits forever for a\n * *fresh* minimal-interaction event, which the click that triggered the\n * navigation doesn't itself produce (no new focus/mousemove/scroll/\n * touchstart fires on the new page unless the user moves again). Detect\n * that case immediately, using this variant's own load history instead\n * of the interaction gate above.\n *\n * Safe to call on every render this condition holds, including twice in\n * a row for the same mount under React Strict Mode's dev-only effect\n * replay: `ScriptLoader.reload()` (0.1.1+) shares the in-flight promise\n * for a same-variant reload already in progress instead of tearing the\n * script down again, so a second call here is a no-op rather than a\n * second script execution.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\") return;\n if (leadCaptureLoader.getGeneration() === 0) return;\n if (!leadCaptureLoader.setOwner(containerId)) return;\n\n const container = formRef.current?.querySelector(\".leadforms-embd-form\");\n if (!container || container.children.length > 0) return;\n\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n leadCaptureLoader\n .reload(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade -- the surrounding page stays usable without the embed.\n });\n }, [formVariant, containerId, usageContext]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = leadCaptureLoader.getGeneration();\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n leadCaptureLoader.unload(gen);\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -23,26 +23,6 @@ import { jsx } from "react/jsx-runtime";
23
23
  * the README for the workaround.
24
24
  */
25
25
  const leadCaptureLoader = new ScriptLoader();
26
- /**
27
- * Generation counter per variant, incremented on every {@link ScriptLoader.load}/
28
- * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the
29
- * generation at mount time and skips its `unload()` call if the generation has
30
- * since advanced — i.e. a new mount already reloaded the script before the old
31
- * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).
32
- * `ScriptLoader`'s own ref-counting handles the common case; this guards the
33
- * one case it doesn't: `reload()` doesn't change the reference count, so a
34
- * stale `unload()` after a `reload()` could drop the count to zero and tear
35
- * down a script a fresh mount is now depending on.
36
- */
37
- const generationByVariant = /* @__PURE__ */ new Map();
38
- function bumpGeneration(variant) {
39
- const next = (generationByVariant.get(variant) ?? 0) + 1;
40
- generationByVariant.set(variant, next);
41
- return next;
42
- }
43
- function currentGeneration(variant) {
44
- return generationByVariant.get(variant) ?? 0;
45
- }
46
26
  const DEFAULT_LEADCAPTURE_SCRIPT_URL = "https://api.useleadbot.com/lead-bots/get-pixel-script.js";
47
27
  /**
48
28
  * LeadCaptureForm — renders a LeadCapture IO form with support for
@@ -77,7 +57,6 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
77
57
  const formRef = useRef(null);
78
58
  const mountGenRef = useRef(0);
79
59
  const isMountedRef = useRef(true);
80
- const hasHandledRemountRef = useRef(false);
81
60
  const containerId = `leadcapture-container-${formVariant}-${usageContext}`;
82
61
  useEffect(() => {
83
62
  isMountedRef.current = true;
@@ -117,7 +96,6 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
117
96
  if (!(usageContext === "modal" ? isModalOpen === true : isInViewport)) return;
118
97
  const load = () => {
119
98
  window.form_token = formTokens[formVariant];
120
- bumpGeneration(formVariant);
121
99
  leadCaptureLoader.load(formVariant).then(() => {
122
100
  if (!isMountedRef.current) return;
123
101
  leadCaptureLoader.forceSetOwner(containerId);
@@ -162,27 +140,20 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
162
140
  * that case immediately, using this variant's own load history instead
163
141
  * of the interaction gate above.
164
142
  *
165
- * Guarded by `hasHandledRemountRef` so this can only ever act once per
166
- * real component instance: `reload()` tears down and recreates the
167
- * `<script>` element, and removing a `<script>` doesn't reliably cancel
168
- * its in-flight network request, so calling it twice in a row for the
169
- * same mount (e.g. React Strict Mode's dev-only effect-cleanup-effect
170
- * replay, which reuses this same ref) can let both the superseded and
171
- * the current script execute and each populate the container, rendering
172
- * the widget twice. Claiming ownership isn't a substitute for this guard
173
- * -- `setOwner` is idempotent for the same id, so a second call from the
174
- * same instance succeeds too.
143
+ * Safe to call on every render this condition holds, including twice in
144
+ * a row for the same mount under React Strict Mode's dev-only effect
145
+ * replay: `ScriptLoader.reload()` (0.1.1+) shares the in-flight promise
146
+ * for a same-variant reload already in progress instead of tearing the
147
+ * script down again, so a second call here is a no-op rather than a
148
+ * second script execution.
175
149
  */
176
150
  useEffect(() => {
177
151
  if (usageContext !== "onPage") return;
178
- if (hasHandledRemountRef.current) return;
179
- if (currentGeneration(formVariant) === 0) return;
152
+ if (leadCaptureLoader.getGeneration() === 0) return;
180
153
  if (!leadCaptureLoader.setOwner(containerId)) return;
181
154
  const container = formRef.current?.querySelector(".leadforms-embd-form");
182
155
  if (!container || container.children.length > 0) return;
183
- hasHandledRemountRef.current = true;
184
156
  window.form_token = formTokens[formVariant];
185
- bumpGeneration(formVariant);
186
157
  leadCaptureLoader.reload(formVariant).then(() => {
187
158
  if (!isMountedRef.current) return;
188
159
  leadCaptureLoader.forceSetOwner(containerId);
@@ -198,7 +169,7 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
198
169
  * already reloaded) is a no-op.
199
170
  */
200
171
  useEffect(() => {
201
- mountGenRef.current = currentGeneration(formVariant);
172
+ mountGenRef.current = leadCaptureLoader.getGeneration();
202
173
  }, [formVariant]);
203
174
  /**
204
175
  * Cleanup on unmount only.
@@ -212,8 +183,7 @@ function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isM
212
183
  if (usageContext === "onPage") {
213
184
  const gen = mountGenRef.current;
214
185
  setTimeout(() => {
215
- if (gen < currentGeneration(formVariant)) return;
216
- leadCaptureLoader.unload();
186
+ leadCaptureLoader.unload(gen);
217
187
  }, 100);
218
188
  }
219
189
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\n/**\n * Generation counter per variant, incremented on every {@link ScriptLoader.load}/\n * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the\n * generation at mount time and skips its `unload()` call if the generation has\n * since advanced — i.e. a new mount already reloaded the script before the old\n * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).\n * `ScriptLoader`'s own ref-counting handles the common case; this guards the\n * one case it doesn't: `reload()` doesn't change the reference count, so a\n * stale `unload()` after a `reload()` could drop the count to zero and tear\n * down a script a fresh mount is now depending on.\n */\nconst generationByVariant = new Map<string, number>();\n\nfunction bumpGeneration(variant: string): number {\n const next = (generationByVariant.get(variant) ?? 0) + 1;\n generationByVariant.set(variant, next);\n return next;\n}\n\nfunction currentGeneration(variant: string): number {\n return generationByVariant.get(variant) ?? 0;\n}\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const hasHandledRemountRef = useRef(false);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n bumpGeneration(formVariant);\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Handles a client-side navigation remount. The vendor script only scans\n * the DOM for `.leadforms-embd-form` divs once, when it first loads (see\n * the container comment below) -- it never repopulates a div added by a\n * later mount. Without this, a second page's form waits forever for a\n * *fresh* minimal-interaction event, which the click that triggered the\n * navigation doesn't itself produce (no new focus/mousemove/scroll/\n * touchstart fires on the new page unless the user moves again). Detect\n * that case immediately, using this variant's own load history instead\n * of the interaction gate above.\n *\n * Guarded by `hasHandledRemountRef` so this can only ever act once per\n * real component instance: `reload()` tears down and recreates the\n * `<script>` element, and removing a `<script>` doesn't reliably cancel\n * its in-flight network request, so calling it twice in a row for the\n * same mount (e.g. React Strict Mode's dev-only effect-cleanup-effect\n * replay, which reuses this same ref) can let both the superseded and\n * the current script execute and each populate the container, rendering\n * the widget twice. Claiming ownership isn't a substitute for this guard\n * -- `setOwner` is idempotent for the same id, so a second call from the\n * same instance succeeds too.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\") return;\n if (hasHandledRemountRef.current) return;\n if (currentGeneration(formVariant) === 0) return;\n if (!leadCaptureLoader.setOwner(containerId)) return;\n\n const container = formRef.current?.querySelector(\".leadforms-embd-form\");\n if (!container || container.children.length > 0) return;\n\n hasHandledRemountRef.current = true;\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n bumpGeneration(formVariant);\n leadCaptureLoader\n .reload(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade -- the surrounding page stays usable without the embed.\n });\n }, [formVariant, containerId, usageContext]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = currentGeneration(formVariant);\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n if (gen < currentGeneration(formVariant)) return;\n leadCaptureLoader.unload();\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Handles a client-side navigation remount. The vendor script only scans\n * the DOM for `.leadforms-embd-form` divs once, when it first loads (see\n * the container comment below) -- it never repopulates a div added by a\n * later mount. Without this, a second page's form waits forever for a\n * *fresh* minimal-interaction event, which the click that triggered the\n * navigation doesn't itself produce (no new focus/mousemove/scroll/\n * touchstart fires on the new page unless the user moves again). Detect\n * that case immediately, using this variant's own load history instead\n * of the interaction gate above.\n *\n * Safe to call on every render this condition holds, including twice in\n * a row for the same mount under React Strict Mode's dev-only effect\n * replay: `ScriptLoader.reload()` (0.1.1+) shares the in-flight promise\n * for a same-variant reload already in progress instead of tearing the\n * script down again, so a second call here is a no-op rather than a\n * second script execution.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\") return;\n if (leadCaptureLoader.getGeneration() === 0) return;\n if (!leadCaptureLoader.setOwner(containerId)) return;\n\n const container = formRef.current?.querySelector(\".leadforms-embd-form\");\n if (!container || container.children.length > 0) return;\n\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n leadCaptureLoader\n .reload(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade -- the surrounding page stays usable without the embed.\n });\n }, [formVariant, containerId, usageContext]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = leadCaptureLoader.getGeneration();\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n leadCaptureLoader.unload(gen);\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverassist/leadcapture-form",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "LeadCapture IO form integration for Next.js — variant switching, ownership arbitration, and ref-counted script lifecycle via @silverassist/next-script-loader",
5
5
  "author": "Miguel Colmenares <me@miguelcolmenares.com>",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -71,7 +71,7 @@
71
71
  "react-dom": ">=18.0.0"
72
72
  },
73
73
  "dependencies": {
74
- "@silverassist/next-script-loader": "^0.1.0"
74
+ "@silverassist/next-script-loader": "^0.1.1"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@playwright/test": "^1.62.1",