fragment-headless-sdk 2.3.0 β†’ 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
1
  import React from "react";
2
2
  import { IAnnouncementContent } from "../../types";
3
- export default function AnnouncementButton({ content, buttonHref, clickHref, }: {
3
+ export default function AnnouncementButton({ content, buttonHref, }: {
4
4
  content: IAnnouncementContent;
5
5
  buttonHref?: string;
6
- clickHref?: string;
7
6
  }): React.JSX.Element | null;
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { ButtonType, SectionType } from "../../constants";
3
3
  import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
4
- export default function AnnouncementButton({ content, buttonHref, clickHref, }) {
4
+ export default function AnnouncementButton({ content, buttonHref, }) {
5
5
  // Don’t render if no button or explicitly None
6
6
  if (!content?.buttonText || content.buttonType === ButtonType.None)
7
7
  return null;
@@ -29,9 +29,7 @@ export default function AnnouncementButton({ content, buttonHref, clickHref, })
29
29
  delete attributes["aria-label"];
30
30
  }
31
31
  const handleClick = React.useCallback(() => {
32
- if (!clickHref)
33
- return;
34
- fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
35
- }, [clickHref, content.measurementId, content.sectionType, content.sectionId]);
32
+ fireClickMetric(content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
33
+ }, [content.measurementId, content.sectionType, content.sectionId]);
36
34
  return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
37
35
  }
@@ -1,21 +1,19 @@
1
+ "use client";
1
2
  import { XMarkIcon } from "@heroicons/react/24/outline";
2
3
  import React, { useEffect, useRef } from "react";
3
4
  import { AnnouncementType, ButtonType, SectionType } from "../../constants";
4
- import { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
5
+ import { fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
5
6
  import AnnouncementButton from "./AnnouncementButton";
6
7
  import { AnnouncementStyles } from "./AnnouncementStyles";
7
8
  import CountdownTimer from "./CountdownTimer";
8
9
  export default function Announcement({ content, type, handleClose, }) {
9
10
  const ref = useRef(null);
10
11
  useEffect(() => {
11
- if (ref.current && content.impressionUrl) {
12
- fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
12
+ if (ref.current) {
13
+ fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
13
14
  }
14
- }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
15
+ }, [content?.measurementId, content?.sectionType, content?.sectionId]);
15
16
  const buttonHref = content?.buttonLink || undefined;
16
- const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
17
- ? buildClickUrl(content.clickUrlBase, content.buttonLink)
18
- : undefined;
19
17
  if (!content)
20
18
  return null;
21
19
  const styling = content.styling;
@@ -60,13 +58,13 @@ export default function Announcement({ content, type, handleClose, }) {
60
58
  React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
61
59
  __html: content.announcementHtml || "",
62
60
  } }))),
63
- content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
61
+ content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
64
62
  React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
65
63
  React.createElement("div", { dangerouslySetInnerHTML: {
66
64
  __html: content.announcementHtml || "",
67
65
  } })),
68
66
  type === AnnouncementType.Countdown && (React.createElement(CountdownTimer, { content: content })),
69
- content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref })))),
67
+ content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))),
70
68
  React.createElement("div", { onClick: handleClose, className: closeButtonClass, style: closeButtonStyle, ...(closeButtonAttributes ?? {}) },
71
69
  React.createElement(XMarkIcon, { className: "w-5 h-5" })))));
72
70
  }
@@ -3,7 +3,6 @@ import { IHeroContent } from "../../types";
3
3
  import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
4
4
  interface HeroViewProps {
5
5
  buttonHref?: string;
6
- clickHref?: string;
7
6
  content: IHeroContent;
8
7
  colors: ReturnType<typeof resolveHeroColors>;
9
8
  contentWidthClass: string;
@@ -11,5 +10,5 @@ interface HeroViewProps {
11
10
  position: "left" | "center" | "right";
12
11
  height: string;
13
12
  }
14
- export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
13
+ export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
15
14
  export {};
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { SectionType } from "../../constants";
3
3
  import { fireClickMetric } from "../../utils";
4
4
  import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
5
- export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }) {
5
+ export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }) {
6
6
  const getPositionClasses = () => {
7
7
  switch (position) {
8
8
  case "center":
@@ -15,15 +15,8 @@ export default function DesktopHero({ buttonHref, clickHref, content, colors, co
15
15
  }
16
16
  };
17
17
  const handleClick = React.useCallback(() => {
18
- if (!clickHref)
19
- return;
20
- fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
21
- }, [
22
- clickHref,
23
- content.measurementId,
24
- content.sectionType,
25
- content.sectionId,
26
- ]);
18
+ fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
19
+ }, [content.measurementId, content.sectionType, content.sectionId]);
27
20
  return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
28
21
  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" })) : (
29
22
  /* Image Background */
@@ -3,10 +3,9 @@ import { IHeroContent } from "../../types";
3
3
  import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
4
4
  interface MobileHeroProps {
5
5
  buttonHref?: string;
6
- clickHref?: string;
7
6
  content: IHeroContent;
8
7
  colors: ReturnType<typeof resolveHeroColors>;
9
8
  typography: ReturnType<typeof resolveHeroTypography>;
10
9
  }
11
- export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
10
+ export default function MobileHero({ buttonHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
12
11
  export {};
@@ -2,17 +2,10 @@ import React from "react";
2
2
  import { SectionType } from "../../constants";
3
3
  import { fireClickMetric } from "../../utils";
4
4
  import { renderText, } from "../../utils/hero-resolvers";
5
- export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }) {
5
+ export default function MobileHero({ buttonHref, content, colors, typography, }) {
6
6
  const handleClick = React.useCallback(() => {
7
- if (!clickHref)
8
- return;
9
- fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
10
- }, [
11
- clickHref,
12
- content.measurementId,
13
- content.sectionType,
14
- content.sectionId,
15
- ]);
7
+ fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
8
+ }, [content.measurementId, content.sectionType, content.sectionId]);
16
9
  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 } },
17
10
  renderText({
18
11
  fontSize: typography.title.fontSize,
@@ -1,20 +1,18 @@
1
+ "use client";
1
2
  import React, { useEffect, useRef } from "react";
2
3
  import { SectionType } from "../../constants";
3
- import { buildClickUrl, fireImpressionWhenVisible, } from "../../utils";
4
+ import { fireImpressionWhenVisible, } from "../../utils";
4
5
  import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
5
6
  import DesktopHero from "./DesktopHero";
6
7
  import MobileHero from "./MobileHero";
7
8
  export default function Hero({ content }) {
8
9
  const ref = useRef(null);
9
10
  useEffect(() => {
10
- if (ref.current && content.impressionUrl) {
11
- fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
11
+ if (ref.current) {
12
+ fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
12
13
  }
13
- }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
14
+ }, [content?.measurementId, content?.sectionType, content?.sectionId]);
14
15
  const buttonHref = content?.buttonLink || undefined;
15
- const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
16
- ? buildClickUrl(content.clickUrlBase, content.buttonLink)
17
- : undefined;
18
16
  if (!content)
19
17
  return null;
20
18
  const colors = resolveHeroColors(content);
@@ -24,7 +22,7 @@ export default function Hero({ content }) {
24
22
  const height = resolveHeight(content);
25
23
  return (React.createElement("div", { className: "bg-black", ref: ref, style: { backgroundColor: colors.background } },
26
24
  React.createElement("div", { className: "hidden lg:block" },
27
- React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
25
+ React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
28
26
  React.createElement("div", { className: "block lg:hidden" },
29
- React.createElement(MobileHero, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref, colors: colors, typography: typography }))));
27
+ React.createElement(MobileHero, { content: content, buttonHref: buttonHref, colors: colors, typography: typography }))));
30
28
  }
@@ -10,8 +10,6 @@ export interface IAnnouncementContent {
10
10
  buttonLink: string;
11
11
  announcementHtml: string;
12
12
  counterEndDate?: string;
13
- impressionUrl: string;
14
- clickUrlBase: string;
15
13
  measurementId?: string;
16
14
  sectionId?: string;
17
15
  sectionType?: SectionType;
@@ -13,8 +13,6 @@ export interface IHeroContent {
13
13
  imageUrl: string;
14
14
  mobileImageUrl: string;
15
15
  videoUrl?: string;
16
- impressionUrl: string;
17
- clickUrlBase: string;
18
16
  measurementId?: string;
19
17
  sectionId?: string;
20
18
  sectionType?: SectionType;
@@ -1,6 +1,7 @@
1
+ import { SectionType } from '../constants';
1
2
  export interface FragmentAttribution {
2
3
  sectionId: string;
3
- sectionType: 'announcement' | 'hero_banner';
4
+ sectionType: SectionType;
4
5
  timestamp: number;
5
6
  }
6
7
  /**
@@ -9,10 +10,10 @@ export interface FragmentAttribution {
9
10
  * @param sectionId - The UUID of the section
10
11
  * @param sectionType - The type of section (announcement or hero_banner)
11
12
  */
12
- export declare function setAttribution(sectionId: string, sectionType: string): void;
13
+ export declare function setAttribution(sectionId: string, sectionType: SectionType): void;
13
14
  /**
14
15
  * Retrieve stored attribution data
15
- * @returns The stored attribution or null if none exists
16
+ * @returns The stored attribution or null if none exists or data is invalid
16
17
  */
17
18
  export declare function getAttribution(): FragmentAttribution | null;
18
19
  /**
@@ -20,3 +21,12 @@ export declare function getAttribution(): FragmentAttribution | null;
20
21
  * Called after successful conversion tracking
21
22
  */
22
23
  export declare function clearAttribution(): void;
24
+ declare global {
25
+ interface Window {
26
+ fragmentAttribution?: {
27
+ get: () => FragmentAttribution | null;
28
+ set: (sectionId: string, sectionType: SectionType) => void;
29
+ clear: () => void;
30
+ };
31
+ }
32
+ }
@@ -1,3 +1,4 @@
1
+ import { SectionType } from '../constants';
1
2
  const STORAGE_KEY = 'fragment_attribution';
2
3
  /**
3
4
  * Store attribution data for a section click
@@ -10,7 +11,7 @@ export function setAttribution(sectionId, sectionType) {
10
11
  return;
11
12
  const attribution = {
12
13
  sectionId,
13
- sectionType: sectionType,
14
+ sectionType,
14
15
  timestamp: Date.now()
15
16
  };
16
17
  try {
@@ -21,20 +22,46 @@ export function setAttribution(sectionId, sectionType) {
21
22
  console.warn('Fragment: Failed to set attribution', e);
22
23
  }
23
24
  }
25
+ /**
26
+ * Validates that parsed data matches the FragmentAttribution interface
27
+ */
28
+ function isValidAttribution(data) {
29
+ if (!data || typeof data !== 'object')
30
+ return false;
31
+ const obj = data;
32
+ return (typeof obj.sectionId === 'string' &&
33
+ (obj.sectionType === SectionType.Announcement || obj.sectionType === SectionType.HeroBanner) &&
34
+ typeof obj.timestamp === 'number');
35
+ }
24
36
  /**
25
37
  * Retrieve stored attribution data
26
- * @returns The stored attribution or null if none exists
38
+ * @returns The stored attribution or null if none exists or data is invalid
27
39
  */
28
40
  export function getAttribution() {
29
41
  if (typeof sessionStorage === 'undefined')
30
42
  return null;
31
43
  try {
32
44
  const stored = sessionStorage.getItem(STORAGE_KEY);
33
- return stored ? JSON.parse(stored) : null;
45
+ if (!stored)
46
+ return null;
47
+ const parsed = JSON.parse(stored);
48
+ if (!isValidAttribution(parsed)) {
49
+ // Invalid data - clear it and return null
50
+ sessionStorage.removeItem(STORAGE_KEY);
51
+ return null;
52
+ }
53
+ return parsed;
34
54
  }
35
55
  catch (e) {
36
56
  // Handle parse errors or other issues
37
57
  console.warn('Fragment: Failed to get attribution', e);
58
+ // Clear potentially corrupted data
59
+ try {
60
+ sessionStorage.removeItem(STORAGE_KEY);
61
+ }
62
+ catch {
63
+ // Ignore errors when clearing
64
+ }
38
65
  return null;
39
66
  }
40
67
  }
@@ -15,12 +15,14 @@ export async function revalidateFragmentCache(tags) {
15
15
  try {
16
16
  // Dynamic import to avoid issues in non-Next.js environments
17
17
  // @ts-ignore - next/cache may not be available in all environments
18
- const { revalidateTag } = await import("next/cache");
18
+ const mod = await import("next/cache");
19
+ // Cast: Next.js types vary by version (1 arg vs 2 arg with profile). Pass tag + "max" for Next 16+.
20
+ const revalidateTag = mod.revalidateTag;
19
21
  if (tags && tags.length > 0) {
20
22
  // Revalidate specific tags
21
23
  tags.forEach((tag) => {
22
24
  try {
23
- revalidateTag(tag);
25
+ revalidateTag(tag, "max");
24
26
  }
25
27
  catch (err) {
26
28
  console.warn(`Failed to revalidate tag ${tag}:`, err);
@@ -31,7 +33,7 @@ export async function revalidateFragmentCache(tags) {
31
33
  // Revalidate all Fragment resource types
32
34
  Object.values(ResourceType).forEach((type) => {
33
35
  try {
34
- revalidateTag(`fragment-${type}`);
36
+ revalidateTag(`fragment-${type}`, "max");
35
37
  }
36
38
  catch (err) {
37
39
  console.warn(`Failed to revalidate fragment-${type}:`, err);
@@ -1,7 +1,5 @@
1
1
  import { SectionType } from "../constants";
2
- export declare function toBase64Url(input: string): string;
3
- export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
4
- export declare function fireClickMetric(clickUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
2
+ export declare function fireClickMetric(measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
5
3
  declare global {
6
4
  interface Window {
7
5
  gtag?: (command: string, targetId: string | Date, config?: Record<string, unknown>) => void;
@@ -12,4 +10,4 @@ declare global {
12
10
  * Fire a scroll past metric when user scrolls past a section
13
11
  */
14
12
  export declare function fireScrollPastMetric(measurementId: string | undefined, sectionType: SectionType, sectionId: string): void;
15
- export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
13
+ export declare function fireImpressionWhenVisible(el: HTMLElement, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
@@ -1,68 +1,22 @@
1
1
  import { SectionType } from "../constants";
2
2
  import { setAttribution } from "./attribution";
3
- // --- Unicode-safe Base64URL ---
4
- export function toBase64Url(input) {
5
- // Handles emojis & non-ASCII reliably:
6
- const b64 = btoa(encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
7
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
8
- }
9
- // --- Robust query appender ---
10
- function appendQuery(url, key, value) {
11
- const sep = url.includes("?") ? "&" : "?";
12
- return `${url}${sep}${key}=${value}`;
13
- }
14
- // Build the tracking URL that encodes the final destination for metrics
15
- export function buildClickUrl(clickUrlBase, targetHref) {
16
- const u = encodeURIComponent(toBase64Url(targetHref));
17
- return appendQuery(clickUrlBase, "u", u);
18
- }
19
- // Fire the click tracking URL without relying on a redirect
20
- // Default to GET so legacy tracking endpoints continue to accept the request
21
- export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId) {
3
+ export function fireClickMetric(measurementId, sectionType, sectionId) {
22
4
  if (typeof window === "undefined")
23
5
  return;
24
- if (!clickUrl)
25
- return;
26
- // Store attribution for potential purchase/add-to-cart tracking
6
+ // Store attribution for potential purchase/add-to-cart
27
7
  if (sectionType && sectionId) {
28
8
  setAttribution(sectionId, sectionType);
29
9
  }
30
- // Send to GA4 first (if available)
31
10
  if (measurementId && sectionType && sectionId) {
32
11
  sendGA4Event("click", measurementId, sectionType, sectionId);
33
12
  }
34
- try {
35
- if (typeof fetch === "function") {
36
- fetch(clickUrl, {
37
- method: "GET",
38
- mode: "no-cors",
39
- keepalive: true,
40
- }).catch(() => {
41
- /* no-op */
42
- });
43
- return;
44
- }
45
- }
46
- catch {
47
- // swallow and fall back to <img>
48
- }
49
- try {
50
- const img = new Image();
51
- img.referrerPolicy = "strict-origin-when-cross-origin";
52
- img.src = clickUrl;
53
- }
54
- catch {
55
- // nothing else we can do
56
- }
57
13
  }
58
14
  /**
59
15
  * Generates a section-specific event name for GA4 tracking.
60
16
  * This allows filtering by event name in GA4 without requiring custom dimensions.
61
17
  */
62
18
  function getSectionEventName(baseEventName, sectionType) {
63
- const sectionPrefix = sectionType === SectionType.Announcement
64
- ? "fragment_announcement"
65
- : "fragment_hero_banner";
19
+ const sectionPrefix = sectionType === SectionType.Announcement ? "fragment_announcement" : "fragment_hero_banner";
66
20
  return `${sectionPrefix}_${baseEventName}`;
67
21
  }
68
22
  /**
@@ -104,25 +58,21 @@ export function fireScrollPastMetric(measurementId, sectionType, sectionId) {
104
58
  }
105
59
  // --- View tracking (once per element) ---
106
60
  const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
107
- export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionType, sectionId) {
61
+ export function fireImpressionWhenVisible(el, measurementId, sectionType, sectionId) {
108
62
  if (typeof window === "undefined")
109
63
  return; // SSR guard
110
- if (!el || !pixelUrl)
64
+ if (!el)
111
65
  return;
112
66
  if (seenEls && seenEls.has(el))
113
67
  return; // de-dupe by element
114
68
  let fired = false;
115
69
  let hasScrolledPast = false;
116
- const img = new Image();
117
70
  const fire = () => {
118
71
  if (fired)
119
72
  return;
120
73
  fired = true;
121
74
  if (seenEls)
122
75
  seenEls.add(el);
123
- img.referrerPolicy = "strict-origin-when-cross-origin";
124
- img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
125
- // Also send to GA4 if available
126
76
  if (measurementId && sectionType && sectionId) {
127
77
  sendGA4Event("view", measurementId, sectionType, sectionId);
128
78
  }
@@ -140,18 +90,18 @@ export function fireImpressionWhenVisible(el, pixelUrl, measurementId, sectionTy
140
90
  for (const e of entries) {
141
91
  if (e.isIntersecting && e.intersectionRatio >= 0.3) {
142
92
  fire();
143
- // Don't disconnect - keep observing for scroll past
144
93
  }
145
94
  else if (!e.isIntersecting &&
146
95
  !hasScrolledPast &&
147
96
  e.boundingClientRect.top < 0 &&
148
97
  fired) {
149
- // User has scrolled past the section (it was visible, now it's above viewport)
98
+ // User scrolled past the section (it's above viewport and was previously visible)
150
99
  hasScrolledPast = true;
151
100
  if (measurementId && sectionType && sectionId) {
152
101
  fireScrollPastMetric(measurementId, sectionType, sectionId);
153
102
  }
154
103
  io.disconnect();
104
+ break;
155
105
  }
156
106
  }
157
107
  }, { threshold: [0, 0.3] });
@@ -0,0 +1,555 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ### [Unreleased]
9
+
10
+ #### Removed
11
+
12
+ ### [2.3.2] - 2026-02-27
13
+
14
+ #### Removed
15
+
16
+ - **Legacy server-side tracking** – The previous pixel/click tracking system has been fully replaced by Google Analytics 4 (GA4):
17
+ - `impressionUrl` and `clickUrlBase` on content objects are no longer used; the API now provides `measurementId`, `sectionId`, and `sectionType` for GA4 events.
18
+ - The `makeSignedMetricUrls` helper and server-side `/api/v1/t` endpoints (impression/click) have been removed from the Fragment app; the SDK sends view/click/scroll_past via `gtag` only.
19
+ - See [2.2.0] for GA4 integration details. Historical entries [1.0.5] and earlier described the old pixel-based flow.
20
+
21
+ ### [2.3.1] - 2026-01-25
22
+
23
+ ### πŸ”§ Technical Improvements
24
+
25
+ - **Enhanced Error Handling** – Improved error handling in attribution functions
26
+ - Better error messages and logging
27
+ - Automatic cleanup of corrupted data
28
+ - Graceful degradation when sessionStorage is unavailable
29
+
30
+ ## [2.3.0] - 2026-01-25
31
+
32
+ ### ✨ Attribution Tracking System
33
+
34
+ - **Session-Based Attribution** – New attribution tracking system for purchase/add-to-cart attribution
35
+ - Automatically stores attribution data when users click section buttons
36
+ - Uses sessionStorage for session-scoped attribution (last-click wins)
37
+ - Tracks section ID, section type, and timestamp for each click
38
+
39
+ ### πŸ“š Documentation
40
+
41
+ - **New Attribution Section** – Added comprehensive documentation for attribution tracking
42
+ - Usage examples for Shopify theme integrations
43
+ - Programmatic access examples
44
+ - Use cases for purchase and add-to-cart attribution
45
+
46
+ ## [2.2.0] - 2025-01-18
47
+
48
+ ### ✨ Google Analytics 4 (GA4) Integration
49
+
50
+ - **GA4 Event Tracking** – Added comprehensive Google Analytics 4 integration for section tracking
51
+ - Automatic `section_view` events when sections become visible
52
+ - Automatic `section_click` events when users interact with buttons
53
+ - Configurable via `measurementId`, `sectionId`, and `sectionType` fields
54
+ - Graceful fallback if GA4 is not available (doesn't break functionality)
55
+
56
+ ## [2.1.9] - 2025-12-26
57
+
58
+ ### ✨ Announcement Resolvers System
59
+
60
+ - **New Announcement Resolvers Utility** – Added centralized color resolution system for Announcement components
61
+ - `resolveAnnouncementColors()` – Intelligent color resolution with guaranteed fallback values
62
+ - `resolveCountdownColors()` – Dedicated color resolver for countdown timer styling
63
+ - Supports both new format (`colors.background`) and legacy format (`bgColor`) for backward compatibility
64
+
65
+ ### πŸ”§ Component Refactoring
66
+
67
+ - **Centralized Color Resolution** – Refactored all Announcement components to use the new resolver system
68
+ - Improved code maintainability and consistency across all announcement types
69
+
70
+ ### 🎨 Countdown Timer Styling Improvements
71
+
72
+ - **Enhanced Layout** – Improved visual spacing and sizing
73
+ - Added `gap-1` between countdown digits
74
+ - Reduced digit font size from `text-xl` to `text-lg`
75
+ - Changed line height from `leading-none` to `leading-tight`
76
+
77
+ ### πŸ”§ Type System Updates
78
+
79
+ - **Enhanced Type Definitions** – Updated `IAnnouncement` and `IHero` interfaces
80
+ - Added `active_duration_seconds`, `last_activated_at`, and `last_deactivated_at` fields
81
+
82
+ ### πŸ“š Documentation
83
+
84
+ - **New Announcement Resolvers Documentation** – Added comprehensive guide for the new resolver system
85
+
86
+ ## [2.1.8] - 2025-12-25
87
+
88
+ ### ✨ Countdown Timer Styling Enhancements
89
+
90
+ - **Enhanced Countdown Timer Color Tokens** – Added comprehensive color customization for countdown timers
91
+ - `counterDigitColor` – Customize the color of countdown digits (default: `#FFFFFF`)
92
+ - `counterDigitBackgroundColor` – Customize the background color of countdown digits (default: `#000000`)
93
+ - `counterTextColor` – Customize the color of labels and separators (default: uses `counterDigitColor`)
94
+ - **Fixed Token Resolution** – Corrected `counterTextColor` token resolution to use proper fallback handling
95
+ - **Documentation Updates** – Added comprehensive countdown timer styling documentation to README
96
+
97
+ ## [2.1.5] - 2025-12-05
98
+
99
+ ### πŸ”§ Type System Updates
100
+
101
+ - **Enhanced Type Definitions** – Updated `IAnnouncement` and `IHero` interfaces to match Supabase schema
102
+ - Added `active_start_date: string | null` field
103
+ - Added `active_end_date: string | null` field
104
+ - Made `created_at` and `updated_at` nullable
105
+ - Made `page` field nullable in `IHero`
106
+
107
+ ### πŸ—‘οΈ Breaking Changes
108
+
109
+ - **Removed Theme & Variant System** – Simplified styling system by removing unused theme/variant abstractions
110
+ - Removed `theme` property from `IFragmentStyling`
111
+ - Removed `variant` property from `IFragmentStyling`
112
+ - Focus now exclusively on design tokens, slots, responsive design, and state-based styling
113
+ - Updated all documentation to reflect the simplified system
114
+
115
+ ## [2.1.4] - 2025-11-18
116
+
117
+ ### ✨ UI/UX Improvements
118
+
119
+ - **Countdown Timer Enhancements** – Improved countdown timer visual design
120
+ - Removed background color from countdown digits for cleaner appearance
121
+ - Increased digit font size for better visibility
122
+ - Shortened label text: "Minutes" β†’ "Mins", "Seconds" β†’ "Secs"
123
+
124
+ - **Announcement Component Updates** – Enhanced announcement functionality
125
+ - Countdown timers can now display alongside buttons when both are enabled
126
+ - Improved layout flexibility for countdown announcements with CTAs
127
+
128
+ ## [2.1.2] - 2025-10-30
129
+
130
+ ### πŸ“š Documentation Updates
131
+
132
+ - **Enhanced README** – Updated documentation to highlight new click tracking features
133
+ - **Feature Highlights** – Added comprehensive documentation for the enhanced click tracking system
134
+ - **Usage Examples** – Improved examples showing the separation of button destinations and tracking
135
+
136
+ ## [2.1.1] - 2025-10-30
137
+
138
+ ### 🎯 Enhanced Click Tracking System
139
+
140
+ - **Improved Click Tracking Architecture** – Separated button destinations from click tracking for better user experience
141
+ - `buttonHref` now contains the actual destination URL (no redirect)
142
+ - `clickHref` contains the tracking URL for metrics collection
143
+ - Users go directly to intended destinations instead of through redirects
144
+ - **New `fireClickMetric()` Function** – Advanced click tracking without relying on redirects
145
+ - Uses `fetch()` with `no-cors` mode and `keepalive` for reliable tracking
146
+ - Falls back to Image pixel tracking for maximum compatibility
147
+ - Handles server-side rendering gracefully
148
+ - **Removed Automatic New Tab Behavior** – Links no longer force `target="_blank"`
149
+ - Provides more natural user experience
150
+ - Allows developers to control link behavior explicitly
151
+ - Maintains accessibility with proper ARIA labels
152
+
153
+ ### πŸ›  Technical Improvements
154
+
155
+ - **Enhanced Component Props** – Both Hero and Announcement components now accept separate tracking parameters
156
+ - `buttonHref` for the actual destination
157
+ - `clickHref` for tracking metrics
158
+ - **Better Error Handling** – Click tracking fails gracefully without affecting user experience
159
+ - **Performance Optimized** – Non-blocking click tracking that doesn't delay navigation
160
+ - **Cross-Browser Compatible** – Works across all modern browsers with appropriate fallbacks
161
+
162
+ ### πŸ“ Usage Examples
163
+
164
+ ```typescript
165
+ // The SDK automatically handles the separation of concerns
166
+ const heroContent = {
167
+ title: "Shop Now",
168
+ buttonText: "Get Started",
169
+ buttonLink: "https://example.com/products", // Direct destination
170
+ clickUrlBase: "https://tracking.example.com/click", // Tracking base
171
+ // SDK automatically creates:
172
+ // - buttonHref: "https://example.com/products" (direct link)
173
+ // - clickHref: "https://tracking.example.com/click?u=..." (tracking)
174
+ };
175
+
176
+ <Hero content={heroContent} />;
177
+ ```
178
+
179
+ ### ⚑ Performance Benefits
180
+
181
+ - **Faster Navigation** – Users go directly to destinations without redirect delays
182
+ - **Reliable Tracking** – Click metrics are captured even if users navigate away quickly
183
+ - **Better SEO** – Direct links improve search engine crawling and indexing
184
+ - **Enhanced UX** – More predictable link behavior for better user experience
185
+
186
+ ### πŸ”„ Backward Compatibility
187
+
188
+ - **No Breaking Changes** – All existing implementations continue to work
189
+ - **Automatic Upgrade** – New tracking system activates automatically when `clickUrlBase` is present
190
+ - **Legacy Support** – Existing tracking URLs continue to function as before
191
+
192
+ ## [2.1.0] - 2025-10-27
193
+
194
+ ### 🎨 Enhanced Hero Styling System
195
+
196
+ - **New Hero Resolvers Utility** – Comprehensive utility system for advanced Hero component customization
197
+ - `resolveHeroColors()` – Intelligent color resolution with fallback handling
198
+ - `resolveHeroTypography()` – Typography settings with font family, size, and line height control
199
+ - `resolveContentWidthClass()` – Dynamic content width management
200
+ - `resolvePosition()` – Content positioning (left, center, right alignment)
201
+ - `resolveHeight()` – Flexible height configuration
202
+ - `renderText()` – Unified text rendering with typography and styling support
203
+
204
+ ### ✨ Advanced Typography Features
205
+
206
+ - **Font Family Support** – Built-in support for popular font families:
207
+ - Roboto, Open Sans, Lato, Montserrat, Poppins, Inter, Nunito Sans, Source Sans Pro
208
+ - Custom font family support through `FontKey` type system
209
+ - **Responsive Typography** – Granular control over font sizes and line heights
210
+ - Separate title and description typography settings
211
+ - Tailwind CSS class integration for responsive design
212
+ - **Typography Tokens** – New styling tokens for enhanced typography control:
213
+ - `titleFontSize`, `titleLineHeight`, `titleFont`
214
+ - `descriptionFontSize`, `descriptionLineHeight`, `descriptionFont`
215
+
216
+ ### πŸ—οΈ Layout & Positioning Enhancements
217
+
218
+ - **Content Positioning** – New positioning system for Hero content alignment
219
+ - Left, center, and right alignment options
220
+ - Responsive positioning with proper text alignment
221
+ - **Content Width Control** – Dynamic content width management
222
+ - Configurable content container widths
223
+ - Responsive design integration
224
+ - **Height Management** – Flexible height configuration system
225
+ - Custom height classes support
226
+ - Default height fallbacks
227
+
228
+ ### 🎯 Developer Experience Improvements
229
+
230
+ - **Type Safety** – Enhanced TypeScript interfaces for all new features
231
+ - `HeroResolvedColors` interface for color resolution
232
+ - `HeroTypographySettings` interface for typography configuration
233
+ - `FontKey` type for font family validation
234
+ - **Utility Functions** – New helper functions for common operations
235
+ - `joinClassNames()` – Safe CSS class concatenation
236
+ - `fallbackColor()` – Color value validation with fallbacks
237
+ - **Better Defaults** – Comprehensive default values for all styling options
238
+ - `DEFAULT_COLORS` for color fallbacks
239
+ - `DEFAULT_TYPOGRAPHY` for typography defaults
240
+ - `FONT_FAMILY_MAP` for font family mappings
241
+
242
+ ### πŸ”„ Backward Compatibility
243
+
244
+ - **Seamless Migration** – All existing Hero components continue to work without changes
245
+ - **Progressive Enhancement** – New features are opt-in and don't affect existing implementations
246
+ - **Legacy Support** – Existing styling approaches remain fully supported
247
+
248
+ ### πŸ“ Usage Examples
249
+
250
+ ```typescript
251
+ // Enhanced Hero with new typography and positioning
252
+ const heroContent = {
253
+ title: "Welcome to Our Store",
254
+ description: "Discover amazing products",
255
+ buttonText: "Shop Now",
256
+ buttonLink: "/products",
257
+ imageUrl: "https://example.com/hero.jpg",
258
+
259
+ styling: {
260
+ tokens: {
261
+ colors: {
262
+ title: "#ffffff",
263
+ text: "#f0f0f0",
264
+ button: "#007bff",
265
+ buttonText: "#ffffff",
266
+ background: "#1a1a1a",
267
+ },
268
+ typography: {
269
+ titleFont: "montserrat",
270
+ titleFontSize: "text-6xl",
271
+ titleLineHeight: "leading-tight",
272
+ descriptionFont: "inter",
273
+ descriptionFontSize: "text-xl",
274
+ descriptionLineHeight: "leading-relaxed",
275
+ },
276
+ layout: {
277
+ contentWidth: "max-w-4xl",
278
+ position: "center",
279
+ height: "min-h-screen",
280
+ },
281
+ },
282
+ },
283
+ };
284
+ ```
285
+
286
+ ### πŸ›  Technical Improvements
287
+
288
+ - **Performance Optimized** – Efficient color and typography resolution
289
+ - **Memory Efficient** – Optimized utility functions with minimal overhead
290
+ - **Tree Shakeable** – Individual utility functions can be imported separately
291
+ - **CSS-in-JS Ready** – Full compatibility with styled-components and emotion
292
+
293
+ ## [1.0.6] - 2025-10-16
294
+
295
+ ### πŸš€ Next.js Caching Fix
296
+
297
+ - **Fixed Vercel/Next.js Caching Issues** – Resolved aggressive caching that prevented fresh data from appearing in production deployments
298
+ - Added `cache: 'no-store'` by default for all `fetchResource()` calls
299
+ - Added Next.js-specific `revalidate: 0` configuration
300
+ - Added cache-busting headers (`Cache-Control`, `Pragma`) to prevent CDN caching
301
+ - Smart environment detection for Next.js vs other frameworks
302
+
303
+ ### ✨ New Cache Management Features
304
+
305
+ - **Cache Configuration Options** – Added optional `cacheOptions` parameter to `fetchResource()`
306
+ - `cache`: Control request cache mode (default: 'no-store' for fresh data)
307
+ - `revalidate`: Next.js revalidation time in seconds (default: 0)
308
+ - `tags`: Next.js cache tags for selective invalidation
309
+ - **Cache Invalidation Utilities** – New helper functions for cache management
310
+ - `revalidateFragmentCache()` – Invalidate all or specific Fragment caches
311
+ - `revalidateResourceType()` – Invalidate cache for specific resource type
312
+ - `revalidateAllFragmentCaches()` – Clear all Fragment-related caches
313
+ - `createCacheTag()` / `createCacheTags()` – Generate cache tags
314
+
315
+ ### πŸ›  Technical Improvements
316
+
317
+ - **Environment Detection** – Automatic Next.js environment detection for optimal cache settings
318
+ - **Backward Compatibility** – All existing code continues to work without changes
319
+ - **TypeScript Support** – Full type definitions for new cache options
320
+
321
+ ### πŸ“ Usage Examples
322
+
323
+ ```typescript
324
+ // Default behavior - always fresh data (recommended)
325
+ const announcements = await fetchResource({
326
+ baseUrl: process.env.FRAGMENT_BASE_URL,
327
+ apiKey: process.env.FRAGMENT_API_KEY,
328
+ type: ResourceType.Announcements,
329
+ });
330
+
331
+ // Optional: Enable caching for performance
332
+ const cachedHeroes = await fetchResource({
333
+ baseUrl: process.env.FRAGMENT_BASE_URL,
334
+ apiKey: process.env.FRAGMENT_API_KEY,
335
+ type: ResourceType.HeroBanners,
336
+ cacheOptions: {
337
+ cache: "default",
338
+ revalidate: 300, // 5 minutes
339
+ },
340
+ });
341
+
342
+ // Cache invalidation (server-side only)
343
+ import { revalidateResourceType } from "fragment-headless-sdk";
344
+ await revalidateResourceType(ResourceType.Announcements);
345
+ ```
346
+
347
+ ### ⚠️ Migration Notes
348
+
349
+ - **No breaking changes** – Existing code works without modification
350
+ - **Fresh data by default** – Your database updates will now appear immediately in production
351
+ - **Opt-in caching** – Use `cacheOptions` if you want to enable caching for performance
352
+
353
+ ## [1.0.5] - 2025-09-28
354
+
355
+ ### ✨ New Features
356
+
357
+ - **Metrics Tracking** – Added built-in view and click tracking for both **Hero** and **Announcement** sections.
358
+ - Each resource’s `content` object now includes two server-generated fields:
359
+ - `impressionUrl` – 1Γ—1 pixel URL automatically fired when the component enters the viewport.
360
+ - `clickUrlBase` – base redirect URL used to record button clicks before sending the user to the final destination.
361
+ - The SDK’s `<Hero>` and `<Announcement>` components now automatically:
362
+ - trigger a view pixel when visible, and
363
+ - wrap their CTA buttons with a signed click-tracking redirect.
364
+
365
+ ### πŸ›  Technical Notes
366
+
367
+ - The `makeSignedMetricUrls` helper was refactored to attach `impressionUrl` and `clickUrlBase` **inside the `content` object** for each item returned by the API.
368
+ - New client-side utilities exported from `utils`:
369
+ - `buildClickUrl()` – safely appends the final destination (`&u=...`) to a signed `clickUrlBase`.
370
+ - `fireImpressionWhenVisible()` – fires a pixel only once when an element is at least 30 % visible.
371
+
372
+ ### ⚠️ Migration Notes
373
+
374
+ - **No breaking changes.**
375
+ Existing components continue to work; the new tracking is automatic when you upgrade to v1.0.5.
376
+ - If you build custom CTAs outside the provided components, use the new helpers to track clicks and views manually.
377
+
378
+ ## [1.0.4] - 2025-09-27
379
+
380
+ - **Types:** `IHero` now includes `views_count: number` and `clicks_count: number`.
381
+
382
+ ### πŸ“ Notes
383
+
384
+ - No breaking changes..
385
+
386
+ ## [1.0.3] - 2025-09-21
387
+
388
+ ### 🎨 UI/UX Improvements
389
+
390
+ - **Announcement Type Rename** - Changed `AnnouncementType.Announcement` to `AnnouncementType.Static` for better clarity
391
+ - **Countdown Timer Styling** - Removed white background from countdown timer for cleaner appearance
392
+ - **Layout Optimization** - Improved announcement banner layout with:
393
+ - Removed top/bottom padding (`py-3`) for more compact design
394
+ - Added 50px minimum height for consistent banner sizing
395
+ - Enhanced vertical centering of all content elements
396
+ - **Timer Digit Sizing** - Made countdown timer digits smaller and more compact:
397
+ - Reduced digit size from 24Γ—28px to 20Γ—24px
398
+ - Changed font size from `text-xl` to `text-base`
399
+
400
+ ### πŸ”§ Technical Changes
401
+
402
+ - Updated `announcementTypes` array to reflect new "Static" label
403
+ - Improved flexbox layout for better vertical alignment
404
+ - Maintained responsive design across all screen sizes
405
+
406
+ ## [1.0.2] - 2025-09-20
407
+
408
+ ### πŸ”„ Breaking Changes
409
+
410
+ - **Component Naming** - Renamed `Banner` component and all related types to `Announcement`
411
+ - `Banner` β†’ `Announcement`
412
+ - `IBannerContent` β†’ `IAnnouncementContent`
413
+ - `IBanner` β†’ `IAnnouncement`
414
+ - `BannerType` β†’ `AnnouncementType`
415
+ - `BannerStatus` β†’ `AnnouncementStatus`
416
+ - `BannerButton` β†’ `AnnouncementButton`
417
+ - `BannerStyles` β†’ `AnnouncementStyles`
418
+ - `bannerHtml` property β†’ `announcementHtml`
419
+
420
+ - **Resource Type Updates** - Updated resource type enums for consistency
421
+ - `ResourceType.HeroSections` β†’ `ResourceType.HeroBanners`
422
+ - `ResourceType.Banners` β†’ `ResourceType.Announcements`
423
+
424
+ ### πŸ”— API Endpoint Changes
425
+
426
+ - Updated API endpoints to match new naming:
427
+ - `/api/v1/hero-sections` β†’ `/api/v1/hero-banners`
428
+ - `/api/v1/banners` β†’ `/api/v1/announcements`
429
+
430
+ ### πŸ“š Documentation
431
+
432
+ - Updated all documentation to reflect new component and type names
433
+ - Updated README.md examples with new ResourceType values
434
+ - Updated code examples throughout
435
+
436
+ ### πŸ› οΈ Migration Guide
437
+
438
+ To update your existing code:
439
+
440
+ ```typescript
441
+ // Before (v1.0.1)
442
+ import {
443
+ Banner,
444
+ BannerType,
445
+ IBannerContent,
446
+ ResourceType,
447
+ } from "fragment-headless-sdk";
448
+
449
+ const banners = await fetchResource({
450
+ type: ResourceType.Banners,
451
+ });
452
+
453
+ <Banner content={bannerContent} type={BannerType.Standard} />;
454
+
455
+ // After (v1.0.2)
456
+ import {
457
+ Announcement,
458
+ AnnouncementType,
459
+ IAnnouncementContent,
460
+ ResourceType,
461
+ } from "fragment-headless-sdk";
462
+
463
+ const announcements = await fetchResource({
464
+ type: ResourceType.Announcements,
465
+ });
466
+
467
+ <Announcement
468
+ content={announcementContent}
469
+ type={AnnouncementType.Announcement}
470
+ />;
471
+ ```
472
+
473
+ ## [1.0.1] - 2025-09-07
474
+
475
+ ### πŸŽ‰ Initial Release
476
+
477
+ The official SDK for integrating with fragment-shopify CMS. Production-ready with full API key authentication support.
478
+
479
+ ### Features
480
+
481
+ - **Complete TypeScript Support** - Full type definitions for all components and API responses
482
+ - **React Components** - Pre-built Hero and Announcement components with responsive design
483
+ - **API Integration** - Built-in utilities for fetching sections from fragment-shopify app
484
+ - **Production Ready** - Full API key authentication with v1 endpoints
485
+ - **Tailwind CSS** - Styled components with customizable design system
486
+
487
+ ### Components
488
+
489
+ - **Hero Component** - Responsive hero sections with desktop/mobile layouts
490
+ - Support for images, videos, and call-to-action buttons
491
+ - Customizable content and styling
492
+ - **Announcement Component** - Flexible announcement bars with multiple display types
493
+ - Standard, marquee, and countdown announcement types
494
+ - `AnnouncementButton` and `CountdownTimer` sub-components
495
+
496
+ ### API Integration
497
+
498
+ - **`fetchResource()` Function** - Simple API for fetching sections
499
+ - **API Key Authentication** - Secure authentication using `keyId:secret` format
500
+ - **v1 Endpoints** - Production endpoints (`/api/v1/announcements`, `/api/v1/hero-banners`)
501
+ - **Error Handling** - Comprehensive error handling and logging
502
+ - **Type Safety** - Full TypeScript support for all API responses
503
+
504
+ ### Usage
505
+
506
+ ```tsx
507
+ import {
508
+ fetchResource,
509
+ ResourceType,
510
+ Hero,
511
+ Announcement,
512
+ } from "fragment-headless-sdk";
513
+
514
+ // Fetch data
515
+ const heroes = await fetchResource({
516
+ baseUrl: process.env.EXTERNAL_API_URL,
517
+ apiKey: process.env.FRAGMENT_API_KEY,
518
+ type: ResourceType.HeroBanners,
519
+ });
520
+
521
+ // Render components
522
+ <Hero content={heroes[0]?.content} />;
523
+ ```
524
+
525
+ ### Environment Variables
526
+
527
+ ```bash
528
+ EXTERNAL_API_URL=https://your-fragment-app.vercel.app
529
+ FRAGMENT_API_KEY=bh_a1b2c3d4e5f6:your-64-char-secret
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Upcoming Changes
535
+
536
+ ### v1.1.0 (Planned)
537
+
538
+ - Enhanced error handling with specific error types
539
+ - Support for additional section types
540
+ - Caching and performance optimizations
541
+
542
+ ### v1.2.0 (Planned)
543
+
544
+ - Real-time updates via webhooks
545
+ - Advanced filtering and sorting options
546
+ - Batch operations support
547
+ - TypeScript strict mode compatibility
548
+
549
+ ---
550
+
551
+ ## Support
552
+
553
+ - **Documentation**: [README.md](./README.md)
554
+ - **NPM Package**: https://www.npmjs.com/package/fragment-shopify-sdk
555
+ - **Issues**: Please report issues in the GitHub repository
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
+ "description": "Headless SDK for Fragment Shopify storefronts",
4
5
  "license": "MIT",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
@@ -12,14 +13,37 @@
12
13
  "./styles": "./dist/styles/fragment-sdk.css"
13
14
  },
14
15
  "files": [
15
- "dist"
16
+ "dist",
17
+ "docs/CHANGELOG.md"
16
18
  ],
19
+ "sideEffects": false,
17
20
  "scripts": {
18
- "build": "tsc"
21
+ "build": "tsc && mkdir -p dist/styles && cp src/styles/*.css dist/styles/",
22
+ "dev": "tsc --watch",
23
+ "type-check": "tsc --noEmit",
24
+ "clean": "rm -rf dist .turbo"
19
25
  },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "registry": "https://registry.npmjs.org/"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/sevenbrand/fragment-monorepo",
33
+ "directory": "packages/sdk"
34
+ },
35
+ "keywords": [
36
+ "shopify",
37
+ "headless",
38
+ "fragment",
39
+ "sdk",
40
+ "storefront"
41
+ ],
20
42
  "dependencies": {
21
- "@heroicons/react": "^2.2.0",
22
- "react": "^19.1.0"
43
+ "@heroicons/react": "^2.2.0"
44
+ },
45
+ "peerDependencies": {
46
+ "react": "^18.0.0 || ^19.0.0"
23
47
  },
24
48
  "devDependencies": {
25
49
  "@types/node": "^24.7.2",
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.3.0** - Attribution tracking system and scroll-past engagement metrics
7
+ **v2.3.1** - Enhanced attribution tracking with session storage and global API
8
8
 
9
9
  > See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
10
10
 
@@ -73,21 +73,21 @@ Fragment-Shopify App (CMS) β†’ API Endpoint β†’ fragment-headless-sdk (Consumer)
73
73
 
74
74
  ### Google Analytics 4 (GA4) Integration (v2.2.0+)
75
75
 
76
- - πŸ“Š **Automatic Event Tracking**: Automatic section-specific events (`fragment_announcement_view`, `fragment_hero_banner_click`, etc.)
76
+ - πŸ“Š **Automatic Event Tracking**: Automatic `section_view` and `section_click` events
77
77
  - 🎯 **Type-Safe Section Types**: `SectionType` enum for consistent section identification
78
78
  - πŸ”§ **Configurable Tracking**: Control tracking via `measurementId`, `sectionId`, and `sectionType` fields
79
79
  - ⚑ **Dual Tracking**: Maintains existing pixel tracking while adding GA4 support
80
80
  - πŸ›‘οΈ **Graceful Fallback**: Works even if GA4 is not configured (doesn't break functionality)
81
81
  - πŸ“¦ **Exported Types**: `SectionType` enum available for consumer use
82
- - πŸ“ˆ **Scroll Past Tracking**: Automatic `scroll_past` events when users scroll past sections (v2.3.0+)
83
82
 
84
- ### Attribution Tracking System (v2.3.0+)
83
+ ### Attribution Tracking System (v2.4.0+)
85
84
 
86
- - 🎯 **Last-Click Attribution**: Session-scoped attribution tracking for section interactions
87
- - πŸ’Ύ **SessionStorage Integration**: Stores attribution data in browser sessionStorage
88
- - πŸ”— **Shopify Theme Integration**: Accessible via `window.fragmentAttribution` for purchase tracking
89
- - πŸ“Š **Conversion Tracking**: Enables tracking which sections lead to conversions
90
- - 🧹 **Automatic Cleanup**: Attribution data can be cleared after successful conversion tracking
85
+ - 🎯 **Session-Based Attribution**: Tracks which section was clicked for purchase/add-to-cart attribution
86
+ - πŸ’Ύ **SessionStorage Integration**: Uses sessionStorage for session-scoped attribution (last-click wins)
87
+ - 🌐 **Global API Access**: Exposed as `window.fragmentAttribution` for Shopify theme integrations
88
+ - πŸ”’ **Type-Safe**: Full TypeScript support with proper type definitions
89
+ - βœ… **Data Validation**: Automatic validation and cleanup of corrupted attribution data
90
+ - 🧹 **Auto-Cleanup**: Invalid or corrupted data is automatically cleared from storage
91
91
 
92
92
  ---
93
93
 
@@ -126,7 +126,7 @@ module.exports = {
126
126
  "./components/**/*.{js,ts,jsx,tsx,mdx}",
127
127
  path.join(
128
128
  __dirname,
129
- "node_modules/fragment-headless-sdk/dist/**/*.{js,ts,jsx,tsx}"
129
+ "node_modules/fragment-headless-sdk/dist/**/*.{js,ts,jsx,tsx}",
130
130
  ),
131
131
  ],
132
132
  theme: {
@@ -377,8 +377,9 @@ interface IHeroContent {
377
377
  imageUrl: string;
378
378
  mobileImageUrl: string;
379
379
  videoUrl?: string;
380
- impressionUrl: string;
381
- clickUrlBase: string;
380
+ measurementId?: string; // GA4 measurement ID (from app config)
381
+ sectionId?: string; // Section ID for GA4 event params
382
+ sectionType?: SectionType;
382
383
  styling?: IHeroStyling; // v2.0+ enhanced styling
383
384
  }
384
385
  ```
@@ -434,8 +435,9 @@ interface IAnnouncementContent {
434
435
  buttonLink: string;
435
436
  announcementHtml: string;
436
437
  counterEndDate?: string;
437
- impressionUrl: string;
438
- clickUrlBase: string;
438
+ measurementId?: string; // GA4 measurement ID (from app config)
439
+ sectionId?: string; // Section ID for GA4 event params
440
+ sectionType?: SectionType;
439
441
  styling?: IAnnouncementStyling; // v2.0+ enhanced styling
440
442
  }
441
443
  ```
@@ -490,6 +492,105 @@ const announcementContent = {
490
492
  };
491
493
  ```
492
494
 
495
+ ## 🎯 Attribution Tracking
496
+
497
+ The SDK automatically tracks which section was clicked for attribution purposes. This is useful for tracking conversions (purchases, add-to-cart) back to the original section interaction.
498
+
499
+ ### Automatic Attribution
500
+
501
+ When a user clicks a button in a Hero or Announcement section, the SDK automatically stores attribution data in `sessionStorage`:
502
+
503
+ ```typescript
504
+ // Attribution is automatically set when clicks occur
505
+ // No additional code needed - it happens automatically!
506
+ ```
507
+
508
+ ### Global API (Shopify Theme Integration)
509
+
510
+ For Shopify theme integrations, attribution data is exposed globally via `window.fragmentAttribution`:
511
+
512
+ ```javascript
513
+ // Get current attribution data
514
+ const attribution = window.fragmentAttribution.get();
515
+ // Returns: { sectionId: "uuid", sectionType: "announcement" | "hero_banner", timestamp: 1234567890 }
516
+ // Or: null if no attribution exists
517
+
518
+ // Manually set attribution (usually not needed - SDK handles this automatically)
519
+ window.fragmentAttribution.set("section-uuid", "announcement");
520
+
521
+ // Clear attribution (e.g., after successful conversion tracking)
522
+ window.fragmentAttribution.clear();
523
+ ```
524
+
525
+ ### Programmatic Access
526
+
527
+ You can also import and use attribution functions directly:
528
+
529
+ ```typescript
530
+ import { getAttribution, clearAttribution } from "fragment-headless-sdk";
531
+
532
+ // Get attribution data
533
+ const attribution = getAttribution();
534
+ if (attribution) {
535
+ console.log(`User clicked section: ${attribution.sectionId}`);
536
+ console.log(`Section type: ${attribution.sectionType}`);
537
+ console.log(`Clicked at: ${new Date(attribution.timestamp)}`);
538
+ }
539
+
540
+ // Clear attribution after tracking conversion
541
+ clearAttribution();
542
+ ```
543
+
544
+ ### Use Cases
545
+
546
+ **Purchase Attribution:**
547
+
548
+ ```javascript
549
+ // In your Shopify checkout success page
550
+ if (window.fragmentAttribution) {
551
+ const attribution = window.fragmentAttribution.get();
552
+ if (attribution) {
553
+ // Track purchase back to the section click
554
+ // e.g., send to analytics, update database, etc.
555
+ trackPurchase(attribution.sectionId, attribution.sectionType);
556
+
557
+ // Clear attribution after tracking
558
+ window.fragmentAttribution.clear();
559
+ }
560
+ }
561
+ ```
562
+
563
+ **Add-to-Cart Attribution:**
564
+
565
+ ```javascript
566
+ // In your add-to-cart handler
567
+ if (window.fragmentAttribution) {
568
+ const attribution = window.fragmentAttribution.get();
569
+ if (attribution) {
570
+ // Track add-to-cart back to the section click
571
+ trackAddToCart(attribution.sectionId, attribution.sectionType);
572
+ }
573
+ }
574
+ ```
575
+
576
+ ### Attribution Data Structure
577
+
578
+ ```typescript
579
+ interface FragmentAttribution {
580
+ sectionId: string; // UUID of the section that was clicked
581
+ sectionType: SectionType; // "announcement" | "hero_banner"
582
+ timestamp: number; // Unix timestamp of when the click occurred
583
+ }
584
+ ```
585
+
586
+ ### Features
587
+
588
+ - βœ… **Session-Scoped**: Attribution data persists for the browser session only
589
+ - βœ… **Last-Click Wins**: New clicks overwrite previous attribution data
590
+ - βœ… **Type-Safe**: Full TypeScript support with proper type definitions
591
+ - βœ… **Data Validation**: Automatically validates and cleans corrupted data
592
+ - βœ… **Graceful Degradation**: Works even if sessionStorage is unavailable
593
+
493
594
  ## πŸ”‘ API Key Setup
494
595
 
495
596
  Before using the SDK, you need to generate an API key from your Fragment-Shopify app: