fragment-headless-sdk 2.1.8 → 2.2.0

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.
@@ -1,28 +1,23 @@
1
1
  import React from "react";
2
- import { ButtonType } from "../../constants";
3
- import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, resolveTokenByCategory, } from "../../utils";
2
+ import { ButtonType, SectionType } from "../../constants";
3
+ import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
4
4
  export default function AnnouncementButton({ content, buttonHref, clickHref, }) {
5
5
  // Don’t render if no button or explicitly None
6
6
  if (!content?.buttonText || content.buttonType === ButtonType.None)
7
7
  return null;
8
- // If we werent given a usable href, dont render a broken link
8
+ // If we weren't given a usable href, don't render a broken link
9
9
  if (!buttonHref)
10
10
  return null;
11
11
  const styling = content.styling;
12
- const baseTextColor = resolveTokenByCategory(styling, "colors", "text") ||
13
- resolveToken(styling, "textColor");
14
- const buttonBgColor = resolveTokenByCategory(styling, "colors", "button") ||
15
- resolveToken(styling, "buttonColor");
16
- const buttonTextColor = resolveTokenByCategory(styling, "colors", "buttonText") ||
17
- resolveToken(styling, "buttonTextColor");
12
+ const colors = resolveAnnouncementColors(content);
18
13
  const baseStyle = content.buttonType === ButtonType.Text
19
14
  ? {
20
15
  textDecoration: "underline",
21
- color: baseTextColor,
16
+ color: colors.text,
22
17
  }
23
18
  : {
24
- backgroundColor: buttonBgColor,
25
- color: buttonTextColor,
19
+ backgroundColor: colors.button,
20
+ color: colors.buttonText,
26
21
  };
27
22
  const style = mergeSlotStyles(baseStyle, styling, "announcementButton", "button");
28
23
  const className = mergeSlotClasses("whitespace-nowrap rounded-md px-3 py-2 text-sm font-semibold no-underline hover:cursor-pointer hover:opacity-70", styling, "announcementButton", "button");
@@ -36,7 +31,7 @@ export default function AnnouncementButton({ content, buttonHref, clickHref, })
36
31
  const handleClick = React.useCallback(() => {
37
32
  if (!clickHref)
38
33
  return;
39
- fireClickMetric(clickHref);
40
- }, [clickHref]);
34
+ fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
35
+ }, [clickHref, content.measurementId, content.sectionType, content.sectionId]);
41
36
  return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
42
37
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState } from "react";
2
- import { mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, } from "../../utils";
2
+ import { mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveCountdownColors, } from "../../utils";
3
3
  export default function CountdownTimer({ content, }) {
4
4
  const [timeLeft, setTimeLeft] = useState({
5
5
  days: "00",
@@ -33,28 +33,25 @@ export default function CountdownTimer({ content, }) {
33
33
  const blockClass = mergeSlotClasses("flex flex-col items-center", styling, "countdownBlock");
34
34
  const blockStyle = mergeSlotStyles(undefined, styling, "countdownBlock");
35
35
  const blockAttributes = mergeSlotAttributes(styling, "countdownBlock");
36
- const digitClass = mergeSlotClasses("text-white text-xl font-bold flex items-center justify-center leading-none rounded px-1", styling, "countdownDigit");
37
- const digitTextColor = resolveToken(styling, "counterDigitColor", "#FFFFFF") || "#FFFFFF";
38
- const digitBackgroundColor = resolveToken(styling, "counterDigitBackgroundColor", "#000000") ||
39
- "#000000";
40
- const counterTextColor = resolveToken(styling, "counterTextColor", digitTextColor);
36
+ const digitClass = mergeSlotClasses("text-lg font-bold flex items-center justify-center leading-tight rounded px-1", styling, "countdownDigit");
37
+ const colors = resolveCountdownColors(content);
41
38
  const digitStyle = mergeSlotStyles({
42
- color: digitTextColor,
43
- backgroundColor: digitBackgroundColor,
39
+ color: colors.digitColor,
40
+ backgroundColor: colors.digitBackgroundColor,
44
41
  }, styling, "countdownDigit");
45
42
  const digitAttributes = mergeSlotAttributes(styling, "countdownDigit");
46
43
  const labelClass = mergeSlotClasses("mt-0.5 text-[10px] leading-tight", styling, "countdownLabel");
47
44
  const labelStyle = mergeSlotStyles({
48
- color: counterTextColor,
45
+ color: colors.textColor,
49
46
  }, styling, "countdownLabel");
50
47
  const labelAttributes = mergeSlotAttributes(styling, "countdownLabel");
51
48
  const separatorClass = mergeSlotClasses("text-xl font-semibold -mt-4", styling, "countdownSeparator");
52
49
  const separatorStyle = mergeSlotStyles({
53
- color: counterTextColor,
50
+ color: colors.textColor,
54
51
  }, styling, "countdownSeparator");
55
52
  const separatorAttributes = mergeSlotAttributes(styling, "countdownSeparator");
56
53
  const renderBlock = (value, label) => (React.createElement("div", { className: blockClass, style: blockStyle, ...(blockAttributes ?? {}) },
57
- React.createElement("div", { className: "flex" }, value.split("").map((digit, i) => (React.createElement("span", { key: i, className: digitClass, style: digitStyle, ...(digitAttributes ?? {}) }, digit)))),
54
+ React.createElement("div", { className: "flex gap-1" }, value.split("").map((digit, i) => (React.createElement("span", { key: i, className: digitClass, style: digitStyle, ...(digitAttributes ?? {}) }, digit)))),
58
55
  React.createElement("span", { className: labelClass, style: labelStyle, ...(labelAttributes ?? {}) }, label)));
59
56
  return (React.createElement("div", { className: containerClass, style: containerStyle, ...(containerAttributes ?? {}) },
60
57
  renderBlock(timeLeft.days, "Days"),
@@ -1,7 +1,7 @@
1
1
  import { XMarkIcon } from "@heroicons/react/24/outline";
2
2
  import React, { useEffect, useRef } from "react";
3
- import { AnnouncementType, ButtonType } from "../../constants";
4
- import { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, resolveTokenByCategory, } from "../../utils";
3
+ import { AnnouncementType, ButtonType, SectionType } from "../../constants";
4
+ import { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
5
5
  import AnnouncementButton from "./AnnouncementButton";
6
6
  import { AnnouncementStyles } from "./AnnouncementStyles";
7
7
  import CountdownTimer from "./CountdownTimer";
@@ -9,9 +9,9 @@ export default function Announcement({ content, type, handleClose, }) {
9
9
  const ref = useRef(null);
10
10
  useEffect(() => {
11
11
  if (ref.current && content.impressionUrl) {
12
- fireImpressionWhenVisible(ref.current, content.impressionUrl);
12
+ fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
13
13
  }
14
- }, [content?.impressionUrl]);
14
+ }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
15
15
  const buttonHref = content?.buttonLink || undefined;
16
16
  const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
17
17
  ? buildClickUrl(content.clickUrlBase, content.buttonLink)
@@ -19,15 +19,12 @@ export default function Announcement({ content, type, handleClose, }) {
19
19
  if (!content)
20
20
  return null;
21
21
  const styling = content.styling;
22
- const backgroundColor = resolveTokenByCategory(styling, "colors", "background") ||
23
- resolveToken(styling, "bgColor");
24
- const textColor = resolveTokenByCategory(styling, "colors", "text") ||
25
- resolveToken(styling, "textColor");
22
+ const colors = resolveAnnouncementColors(content);
26
23
  const themeClasses = getThemeClasses(styling);
27
24
  const rootClass = mergeSlotClasses(`relative w-full ${themeClasses}`, styling, "root");
28
25
  const rootStyle = mergeSlotStyles({
29
- backgroundColor,
30
- color: textColor,
26
+ backgroundColor: colors.background,
27
+ color: colors.text,
31
28
  }, styling, "root");
32
29
  const rootAttributes = mergeSlotAttributes(styling, "root");
33
30
  const innerClass = mergeSlotClasses("relative mx-auto flex max-w-screen-xl flex-col md:flex-row items-center justify-center gap-4 px-10 md:px-4 py-2 md:py-0 text-center md:text-left min-h-[50px]", styling, "inner", "wrapper");
@@ -48,13 +45,11 @@ export default function Announcement({ content, type, handleClose, }) {
48
45
  const contentRowClass = mergeSlotClasses("flex flex-col md:flex-row items-center justify-center gap-2 md:gap-4 w-full", styling, "contentRow");
49
46
  const contentRowStyle = mergeSlotStyles(undefined, styling, "contentRow");
50
47
  const contentRowAttributes = mergeSlotAttributes(styling, "contentRow");
51
- const announcementTextClass = mergeSlotClasses("max-w-none text-base font-semibold", styling, "announcementText");
48
+ const announcementTextClass = mergeSlotClasses("max-w-none text-based", styling, "announcementText");
52
49
  const announcementTextStyle = mergeSlotStyles(undefined, styling, "announcementText");
53
50
  const announcementTextAttributes = mergeSlotAttributes(styling, "announcementText");
54
51
  const closeButtonClass = mergeSlotClasses("absolute right-4 top-1/2 -translate-y-1/2 cursor-pointer", styling, "closeButton");
55
- const closeButtonColor = resolveTokenByCategory(styling, "colors", "closeButton") ||
56
- (resolveToken(styling, "closeButtonColor", textColor ?? "#000") ?? "#000");
57
- const closeButtonStyle = mergeSlotStyles({ color: closeButtonColor }, styling, "closeButton");
52
+ const closeButtonStyle = mergeSlotStyles({ color: colors.closeButton }, styling, "closeButton");
58
53
  const closeButtonAttributes = mergeSlotAttributes(styling, "closeButton");
59
54
  return (React.createElement("div", { ref: ref, className: rootClass, style: rootStyle, ...(rootAttributes ?? {}) },
60
55
  React.createElement(AnnouncementStyles, null),
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { SectionType } from "../../constants";
2
3
  import { fireClickMetric } from "../../utils";
3
4
  import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
4
5
  export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }) {
@@ -16,8 +17,13 @@ export default function DesktopHero({ buttonHref, clickHref, content, colors, co
16
17
  const handleClick = React.useCallback(() => {
17
18
  if (!clickHref)
18
19
  return;
19
- fireClickMetric(clickHref);
20
- }, [clickHref]);
20
+ fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
21
+ }, [
22
+ clickHref,
23
+ content.measurementId,
24
+ content.sectionType,
25
+ content.sectionId,
26
+ ]);
21
27
  return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
22
28
  content?.videoUrl ? (React.createElement("video", { src: content.videoUrl, autoPlay: true, muted: true, loop: true, playsInline: true, className: "absolute inset-0 z-0 object-cover w-full h-full" })) : (
23
29
  /* Image Background */
@@ -1,12 +1,18 @@
1
1
  import React from "react";
2
+ import { SectionType } from "../../constants";
2
3
  import { fireClickMetric } from "../../utils";
3
4
  import { renderText, } from "../../utils/hero-resolvers";
4
5
  export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }) {
5
6
  const handleClick = React.useCallback(() => {
6
7
  if (!clickHref)
7
8
  return;
8
- fireClickMetric(clickHref);
9
- }, [clickHref]);
9
+ fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
10
+ }, [
11
+ clickHref,
12
+ content.measurementId,
13
+ content.sectionType,
14
+ content.sectionId,
15
+ ]);
10
16
  return (React.createElement("div", { className: "relative z-10 mx-auto gap-4 flex max-w-screen-md flex-col items-center justify-center py-6 text-center", style: { backgroundColor: colors.background } },
11
17
  renderText({
12
18
  fontSize: typography.title.fontSize,
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from "react";
2
+ import { SectionType } from "../../constants";
2
3
  import { buildClickUrl, fireImpressionWhenVisible, } from "../../utils";
3
4
  import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
4
5
  import DesktopHero from "./DesktopHero";
@@ -7,9 +8,9 @@ export default function Hero({ content }) {
7
8
  const ref = useRef(null);
8
9
  useEffect(() => {
9
10
  if (ref.current && content.impressionUrl) {
10
- fireImpressionWhenVisible(ref.current, content.impressionUrl);
11
+ fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
11
12
  }
12
- }, [content?.impressionUrl]);
13
+ }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
13
14
  const buttonHref = content?.buttonLink || undefined;
14
15
  const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
15
16
  ? buildClickUrl(content.clickUrlBase, content.buttonLink)
@@ -1,2 +1,3 @@
1
1
  export * from "./announcement";
2
2
  export * from "./hero";
3
+ export * from "./section";
@@ -1,2 +1,3 @@
1
1
  export * from "./announcement";
2
2
  export * from "./hero";
3
+ export * from "./section";
@@ -0,0 +1,4 @@
1
+ export declare enum SectionType {
2
+ Announcement = "announcement",
3
+ HeroBanner = "hero_banner"
4
+ }
@@ -0,0 +1,5 @@
1
+ export var SectionType;
2
+ (function (SectionType) {
3
+ SectionType["Announcement"] = "announcement";
4
+ SectionType["HeroBanner"] = "hero_banner";
5
+ })(SectionType || (SectionType = {}));
@@ -1,4 +1,4 @@
1
- import { AnnouncementStatus, AnnouncementType, ButtonType } from "../constants";
1
+ import { AnnouncementStatus, AnnouncementType, ButtonType, SectionType } from "../constants";
2
2
  import { IAnnouncementStyling } from "./styling";
3
3
  export declare const buttonTypes: {
4
4
  label: string;
@@ -12,6 +12,9 @@ export interface IAnnouncementContent {
12
12
  counterEndDate?: string;
13
13
  impressionUrl: string;
14
14
  clickUrlBase: string;
15
+ measurementId?: string;
16
+ sectionId?: string;
17
+ sectionType?: SectionType;
15
18
  styling?: IAnnouncementStyling;
16
19
  }
17
20
  export interface IAnnouncement {
@@ -21,8 +24,11 @@ export interface IAnnouncement {
21
24
  name: string;
22
25
  status: AnnouncementStatus;
23
26
  content: IAnnouncementContent | null;
27
+ active_duration_seconds: number | null;
24
28
  active_start_date: string | null;
25
29
  active_end_date: string | null;
30
+ last_activated_at: string | null;
31
+ last_deactivated_at: string | null;
26
32
  created_at: string | null;
27
33
  updated_at: string | null;
28
34
  views_count: number;
@@ -1,4 +1,4 @@
1
- import { HeroStatus, HeroType } from "../constants";
1
+ import { HeroStatus, HeroType, SectionType } from "../constants";
2
2
  import { IHeroStyling } from "./styling";
3
3
  export type ShopPage = {
4
4
  id: string;
@@ -15,6 +15,9 @@ export interface IHeroContent {
15
15
  videoUrl?: string;
16
16
  impressionUrl: string;
17
17
  clickUrlBase: string;
18
+ measurementId?: string;
19
+ sectionId?: string;
20
+ sectionType?: SectionType;
18
21
  styling?: IHeroStyling;
19
22
  }
20
23
  export interface IHero {
@@ -25,7 +28,10 @@ export interface IHero {
25
28
  page: ShopPage["handle"] | null;
26
29
  status: HeroStatus;
27
30
  content: IHeroContent | null;
31
+ active_duration_seconds: number | null;
28
32
  active_start_date: string | null;
33
+ last_activated_at: string | null;
34
+ last_deactivated_at: string | null;
29
35
  active_end_date: string | null;
30
36
  created_at: string | null;
31
37
  updated_at: string | null;
@@ -0,0 +1,15 @@
1
+ import { IAnnouncementContent } from "../types";
2
+ export interface AnnouncementResolvedColors {
3
+ background: string;
4
+ text: string;
5
+ button: string;
6
+ buttonText: string;
7
+ closeButton: string;
8
+ }
9
+ export interface CountdownResolvedColors {
10
+ digitColor: string;
11
+ digitBackgroundColor: string;
12
+ textColor: string;
13
+ }
14
+ export declare function resolveAnnouncementColors(content: IAnnouncementContent): AnnouncementResolvedColors;
15
+ export declare function resolveCountdownColors(content: IAnnouncementContent): CountdownResolvedColors;
@@ -0,0 +1,45 @@
1
+ import { resolveToken, resolveTokenByCategory } from "./styling";
2
+ const DEFAULT_ANNOUNCEMENT_COLORS = {
3
+ background: "#000000",
4
+ text: "#ffffff",
5
+ button: "#ffffff",
6
+ buttonText: "#000000",
7
+ closeButton: "#ffffff",
8
+ };
9
+ const DEFAULT_COUNTDOWN_COLORS = {
10
+ digitColor: "#ffffff",
11
+ digitBackgroundColor: "#000000",
12
+ textColor: "#ffffff",
13
+ };
14
+ const fallbackColor = (value, fallback) => typeof value === "string" && value.trim().length > 0 ? value : fallback;
15
+ export function resolveAnnouncementColors(content) {
16
+ const styling = content.styling;
17
+ const background = resolveTokenByCategory(styling, "colors", "background") ||
18
+ resolveToken(styling, "bgColor");
19
+ const text = resolveTokenByCategory(styling, "colors", "text") ||
20
+ resolveToken(styling, "textColor");
21
+ const button = resolveTokenByCategory(styling, "colors", "button") ||
22
+ resolveToken(styling, "buttonColor");
23
+ const buttonText = resolveTokenByCategory(styling, "colors", "buttonText") ||
24
+ resolveToken(styling, "buttonTextColor");
25
+ const closeButton = resolveTokenByCategory(styling, "colors", "closeButton") ||
26
+ resolveToken(styling, "closeButtonColor");
27
+ return {
28
+ background: fallbackColor(background, DEFAULT_ANNOUNCEMENT_COLORS.background),
29
+ text: fallbackColor(text, DEFAULT_ANNOUNCEMENT_COLORS.text),
30
+ button: fallbackColor(button, DEFAULT_ANNOUNCEMENT_COLORS.button),
31
+ buttonText: fallbackColor(buttonText, DEFAULT_ANNOUNCEMENT_COLORS.buttonText),
32
+ closeButton: fallbackColor(closeButton, fallbackColor(text, DEFAULT_ANNOUNCEMENT_COLORS.closeButton)),
33
+ };
34
+ }
35
+ export function resolveCountdownColors(content) {
36
+ const styling = content.styling;
37
+ const digitColor = resolveToken(styling, "counterDigitColor");
38
+ const digitBackgroundColor = resolveToken(styling, "counterDigitBackgroundColor");
39
+ const textColor = resolveToken(styling, "counterTextColor", digitColor);
40
+ return {
41
+ digitColor: fallbackColor(digitColor, DEFAULT_COUNTDOWN_COLORS.digitColor),
42
+ digitBackgroundColor: fallbackColor(digitBackgroundColor, DEFAULT_COUNTDOWN_COLORS.digitBackgroundColor),
43
+ textColor: fallbackColor(textColor, DEFAULT_COUNTDOWN_COLORS.textColor),
44
+ };
45
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./announcement-resolvers";
1
2
  export * from "./cache";
2
3
  export * from "./fetch-resource";
3
4
  export * from "./hero-resolvers";
@@ -1,3 +1,4 @@
1
+ export * from "./announcement-resolvers";
1
2
  export * from "./cache";
2
3
  export * from "./fetch-resource";
3
4
  export * from "./hero-resolvers";
@@ -1,4 +1,11 @@
1
+ import { SectionType } from "../constants";
1
2
  export declare function toBase64Url(input: string): string;
2
3
  export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
3
- export declare function fireClickMetric(clickUrl: string): void;
4
- export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string): void;
4
+ export declare function fireClickMetric(clickUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
5
+ declare global {
6
+ interface Window {
7
+ gtag?: (command: string, targetId: string | Date, config?: Record<string, unknown>) => void;
8
+ dataLayer?: unknown[];
9
+ }
10
+ }
11
+ export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
@@ -16,11 +16,15 @@ export function buildClickUrl(clickUrlBase, targetHref) {
16
16
  }
17
17
  // Fire the click tracking URL without relying on a redirect
18
18
  // Default to GET so legacy tracking endpoints continue to accept the request
19
- export function fireClickMetric(clickUrl) {
19
+ export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId) {
20
20
  if (typeof window === "undefined")
21
21
  return;
22
22
  if (!clickUrl)
23
23
  return;
24
+ // Send to GA4 first (if available)
25
+ if (measurementId && sectionType && sectionId) {
26
+ sendGA4Event("section_click", measurementId, sectionType, sectionId);
27
+ }
24
28
  try {
25
29
  if (typeof fetch === "function") {
26
30
  fetch(clickUrl, {
@@ -45,9 +49,32 @@ export function fireClickMetric(clickUrl) {
45
49
  // nothing else we can do
46
50
  }
47
51
  }
52
+ /**
53
+ * Sends a GA4 event if gtag is available and measurementId is provided.
54
+ * This is a fire-and-forget operation that won't throw errors.
55
+ */
56
+ function sendGA4Event(eventName, measurementId, sectionType, sectionId) {
57
+ if (typeof window === "undefined")
58
+ return;
59
+ if (!measurementId)
60
+ return;
61
+ if (!window.gtag)
62
+ return;
63
+ try {
64
+ window.gtag("event", eventName, {
65
+ section_type: sectionType,
66
+ section_id: sectionId,
67
+ event_category: "Fragment Sections",
68
+ event_label: `${sectionType}_${sectionId}`,
69
+ });
70
+ }
71
+ catch {
72
+ // Silently fail - don't break tracking if GA4 fails
73
+ }
74
+ }
48
75
  // --- View tracking (once per element) ---
49
76
  const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
50
- export function fireImpressionWhenVisible(el, pixelUrl) {
77
+ export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionType, sectionId) {
51
78
  if (typeof window === "undefined")
52
79
  return; // SSR guard
53
80
  if (!el || !pixelUrl)
@@ -64,6 +91,10 @@ export function fireImpressionWhenVisible(el, pixelUrl) {
64
91
  seenEls.add(el);
65
92
  img.referrerPolicy = "strict-origin-when-cross-origin";
66
93
  img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
94
+ // Also send to GA4 if available
95
+ if (measurementId && sectionType && sectionId) {
96
+ sendGA4Event("section_view", measurementId, sectionType, sectionId);
97
+ }
67
98
  };
68
99
  // Fallback if IntersectionObserver is missing
69
100
  const fallback = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -4,7 +4,7 @@ The official SDK for integrating with Fragment-Shopify CMS. Provides React compo
4
4
 
5
5
  ## ✨ What's New
6
6
 
7
- **v2.1.8** - Enhanced countdown timer styling with customizable color tokens
7
+ **v2.2.0** - Google Analytics 4 (GA4) integration with automatic section tracking
8
8
 
9
9
  > See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
10
10
 
@@ -71,6 +71,15 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
71
71
  - 🌐 **Cross-Browser**: Works across all modern browsers with appropriate fallbacks
72
72
  - 🔄 **Graceful Degradation**: Tracking fails silently without affecting user experience
73
73
 
74
+ ### Google Analytics 4 (GA4) Integration (v2.2.0+)
75
+
76
+ - 📊 **Automatic Event Tracking**: Automatic `section_view` and `section_click` events
77
+ - 🎯 **Type-Safe Section Types**: `SectionType` enum for consistent section identification
78
+ - 🔧 **Configurable Tracking**: Control tracking via `measurementId`, `sectionId`, and `sectionType` fields
79
+ - ⚡ **Dual Tracking**: Maintains existing pixel tracking while adding GA4 support
80
+ - 🛡️ **Graceful Fallback**: Works even if GA4 is not configured (doesn't break functionality)
81
+ - 📦 **Exported Types**: `SectionType` enum available for consumer use
82
+
74
83
  ---
75
84
 
76
85
  ## 📦 Installation