@datlv-trustshop/shopify-inapp-components 0.1.24 → 0.1.26
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/dist/components/FloatingCard.d.ts +2 -0
- package/dist/components/FloatingCard.js +31 -3
- package/dist/core/engine.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useCampaignTracking.d.ts +44 -0
- package/dist/hooks/useCampaignTracking.js +116 -0
- package/dist/hooks/useFloatingCards.js +24 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/types/dashboard.d.ts +1 -0
- package/dist/utils/campaignTracking.d.ts +21 -0
- package/dist/utils/campaignTracking.js +130 -0
- package/dist/utils/sessionManager.d.ts +16 -0
- package/dist/utils/sessionManager.js +62 -0
- package/package.json +1 -1
|
@@ -30,9 +30,11 @@ export interface FloatingCardData {
|
|
|
30
30
|
auto_close_after?: number | null;
|
|
31
31
|
reappear_interval?: number | null;
|
|
32
32
|
display_pages?: string[];
|
|
33
|
+
campaign_id?: string | number;
|
|
33
34
|
}
|
|
34
35
|
interface FloatingCardProps {
|
|
35
36
|
data: FloatingCardData;
|
|
37
|
+
shopId?: string;
|
|
36
38
|
onDismiss?: () => void;
|
|
37
39
|
onPrimaryAction?: (data: FloatingCardData) => void;
|
|
38
40
|
onSecondaryAction?: (data: FloatingCardData) => void;
|
|
@@ -2,8 +2,29 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Card, BlockStack, InlineStack, Text, Badge, Button, Box, } from "@shopify/polaris";
|
|
4
4
|
import { XIcon } from "@shopify/polaris-icons";
|
|
5
|
-
|
|
5
|
+
import { useCampaignTracking } from "../hooks/useCampaignTracking";
|
|
6
|
+
import { DashboardContext } from "../provider/DashboardProvider";
|
|
7
|
+
export const FloatingCard = ({ data, shopId: propsShopId, onDismiss, onPrimaryAction, onSecondaryAction, position = 'bottom-right', showCloseButton = true, }) => {
|
|
8
|
+
// Get shopId from context if not provided via props
|
|
9
|
+
const dashboardContext = React.useContext(DashboardContext);
|
|
10
|
+
const contextShopId = dashboardContext?.engine?.config?.shopId;
|
|
11
|
+
const shopId = propsShopId || contextShopId;
|
|
12
|
+
// Use the campaign ID from data or fallback to regular id
|
|
13
|
+
const campaignId = data.campaign_id || data.id;
|
|
14
|
+
// Set up campaign tracking
|
|
15
|
+
const tracking = shopId && campaignId ? useCampaignTracking({
|
|
16
|
+
shopId,
|
|
17
|
+
campaignId,
|
|
18
|
+
threshold: 0.5, // Track when 50% visible
|
|
19
|
+
visibilityDuration: 1000, // Track view after 1 second
|
|
20
|
+
metadata: {
|
|
21
|
+
card_type: data.type,
|
|
22
|
+
card_key: data.key,
|
|
23
|
+
},
|
|
24
|
+
}) : null;
|
|
6
25
|
const handlePrimaryAction = () => {
|
|
26
|
+
// Track the click
|
|
27
|
+
tracking?.trackPrimaryAction();
|
|
7
28
|
if (data.primary_action) {
|
|
8
29
|
const url = data.primary_action.url;
|
|
9
30
|
// Check if it's an external URL
|
|
@@ -27,6 +48,8 @@ export const FloatingCard = ({ data, onDismiss, onPrimaryAction, onSecondaryActi
|
|
|
27
48
|
}
|
|
28
49
|
};
|
|
29
50
|
const handleSecondaryAction = () => {
|
|
51
|
+
// Track the click
|
|
52
|
+
tracking?.trackSecondaryAction();
|
|
30
53
|
if (data.secondary_action) {
|
|
31
54
|
const url = data.secondary_action.url;
|
|
32
55
|
if (data.secondary_action.external) {
|
|
@@ -41,6 +64,11 @@ export const FloatingCard = ({ data, onDismiss, onPrimaryAction, onSecondaryActi
|
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
66
|
};
|
|
67
|
+
const handleDismiss = () => {
|
|
68
|
+
// Track the dismiss
|
|
69
|
+
tracking?.trackDismiss();
|
|
70
|
+
onDismiss?.();
|
|
71
|
+
};
|
|
44
72
|
// CSS-in-JS styles - no external CSS file needed
|
|
45
73
|
const floatingCardStyles = {
|
|
46
74
|
position: 'fixed',
|
|
@@ -111,11 +139,11 @@ export const FloatingCard = ({ data, onDismiss, onPrimaryAction, onSecondaryActi
|
|
|
111
139
|
document.head.appendChild(style);
|
|
112
140
|
}
|
|
113
141
|
}, []);
|
|
114
|
-
return (_jsx("div", { style: floatingCardStyles, children: _jsxs(Card, { padding: "0", roundedAbove: "sm", children: [(showCloseButton && data.dismissible !== false) && (_jsx("button", { style: closeButtonStyles, onMouseEnter: (e) => {
|
|
142
|
+
return (_jsx("div", { ref: tracking?.elementRef, style: floatingCardStyles, children: _jsxs(Card, { padding: "0", roundedAbove: "sm", children: [(showCloseButton && data.dismissible !== false) && (_jsx("button", { style: closeButtonStyles, onMouseEnter: (e) => {
|
|
115
143
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.30)';
|
|
116
144
|
}, onMouseLeave: (e) => {
|
|
117
145
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.20)';
|
|
118
|
-
}, onClick:
|
|
146
|
+
}, onClick: handleDismiss, "aria-label": "Close", children: _jsx(XIcon, { style: { width: '20px', height: '20px', color: 'rgba(255, 255, 255, 0.9)' } }) })), _jsxs(BlockStack, { gap: "0", children: [data.img_url && (_jsx("div", { style: imageContainerStyles, children: _jsx("img", { src: data.img_url, alt: data.image_alt || data.title, style: {
|
|
119
147
|
width: "100%",
|
|
120
148
|
height: "100%",
|
|
121
149
|
objectFit: "cover",
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { DashboardData, DashboardConfig, BannerItem, AppItem, AppGroup, ArticleI
|
|
|
2
2
|
export declare class DashboardEngine {
|
|
3
3
|
private static instance;
|
|
4
4
|
private data;
|
|
5
|
-
|
|
5
|
+
config: DashboardConfig;
|
|
6
6
|
private initPromise;
|
|
7
7
|
private lastFetchTime;
|
|
8
8
|
private listeners;
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface UseCampaignVisibilityOptions {
|
|
2
|
+
shopId: string;
|
|
3
|
+
campaignId: string | number;
|
|
4
|
+
threshold?: number;
|
|
5
|
+
visibilityDuration?: number;
|
|
6
|
+
metadata?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Hook to track campaign visibility using IntersectionObserver
|
|
10
|
+
* Automatically tracks views when element is visible for specified duration
|
|
11
|
+
*/
|
|
12
|
+
export declare function useCampaignVisibility(options: UseCampaignVisibilityOptions): {
|
|
13
|
+
elementRef: import("react").RefObject<HTMLDivElement>;
|
|
14
|
+
isVisible: boolean;
|
|
15
|
+
hasTrackedView: boolean;
|
|
16
|
+
getTimeToAction: () => number | undefined;
|
|
17
|
+
};
|
|
18
|
+
interface UseCampaignTrackingOptions {
|
|
19
|
+
shopId: string;
|
|
20
|
+
campaignId: string | number;
|
|
21
|
+
threshold?: number;
|
|
22
|
+
visibilityDuration?: number;
|
|
23
|
+
metadata?: Record<string, any>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Main hook for campaign tracking
|
|
27
|
+
* Combines visibility tracking with click tracking utilities
|
|
28
|
+
*/
|
|
29
|
+
export declare function useCampaignTracking(options: UseCampaignTrackingOptions): {
|
|
30
|
+
trackClick: (clickOptions?: {
|
|
31
|
+
actionType?: string;
|
|
32
|
+
ctaId?: string;
|
|
33
|
+
ctaKey?: string;
|
|
34
|
+
metadata?: Record<string, any>;
|
|
35
|
+
}) => void;
|
|
36
|
+
trackPrimaryAction: () => void;
|
|
37
|
+
trackSecondaryAction: () => void;
|
|
38
|
+
trackDismiss: () => void;
|
|
39
|
+
elementRef: import("react").RefObject<HTMLDivElement>;
|
|
40
|
+
isVisible: boolean;
|
|
41
|
+
hasTrackedView: boolean;
|
|
42
|
+
getTimeToAction: () => number | undefined;
|
|
43
|
+
};
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { trackCampaignView, trackCampaignClick } from '../utils/campaignTracking';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to track campaign visibility using IntersectionObserver
|
|
5
|
+
* Automatically tracks views when element is visible for specified duration
|
|
6
|
+
*/
|
|
7
|
+
export function useCampaignVisibility(options) {
|
|
8
|
+
const { shopId, campaignId, threshold = 0.5, // Default: 50% visible
|
|
9
|
+
visibilityDuration = 1000, // Default: 1 second
|
|
10
|
+
metadata = {}, } = options;
|
|
11
|
+
const elementRef = useRef(null);
|
|
12
|
+
const visibilityTimerRef = useRef(null);
|
|
13
|
+
const hasTrackedViewRef = useRef(false);
|
|
14
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
15
|
+
const firstVisibleTimeRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!elementRef.current || typeof window === 'undefined')
|
|
18
|
+
return;
|
|
19
|
+
const observer = new IntersectionObserver((entries) => {
|
|
20
|
+
entries.forEach((entry) => {
|
|
21
|
+
const nowVisible = entry.isIntersecting && entry.intersectionRatio >= threshold;
|
|
22
|
+
if (nowVisible && !hasTrackedViewRef.current) {
|
|
23
|
+
// Element became visible
|
|
24
|
+
setIsVisible(true);
|
|
25
|
+
// Record first visible time if not already set
|
|
26
|
+
if (!firstVisibleTimeRef.current) {
|
|
27
|
+
firstVisibleTimeRef.current = Date.now();
|
|
28
|
+
}
|
|
29
|
+
// Start timer for view tracking
|
|
30
|
+
if (visibilityTimerRef.current) {
|
|
31
|
+
clearTimeout(visibilityTimerRef.current);
|
|
32
|
+
}
|
|
33
|
+
visibilityTimerRef.current = setTimeout(() => {
|
|
34
|
+
// Track view after visibility duration
|
|
35
|
+
if (!hasTrackedViewRef.current) {
|
|
36
|
+
trackCampaignView(shopId, campaignId, metadata);
|
|
37
|
+
hasTrackedViewRef.current = true;
|
|
38
|
+
}
|
|
39
|
+
}, visibilityDuration);
|
|
40
|
+
}
|
|
41
|
+
else if (!nowVisible) {
|
|
42
|
+
// Element became hidden
|
|
43
|
+
setIsVisible(false);
|
|
44
|
+
// Clear timer if element becomes hidden before tracking
|
|
45
|
+
if (visibilityTimerRef.current && !hasTrackedViewRef.current) {
|
|
46
|
+
clearTimeout(visibilityTimerRef.current);
|
|
47
|
+
visibilityTimerRef.current = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}, {
|
|
52
|
+
threshold: [0, threshold], // Watch for both becoming visible and hidden
|
|
53
|
+
rootMargin: '0px',
|
|
54
|
+
});
|
|
55
|
+
observer.observe(elementRef.current);
|
|
56
|
+
return () => {
|
|
57
|
+
// Cleanup
|
|
58
|
+
if (visibilityTimerRef.current) {
|
|
59
|
+
clearTimeout(visibilityTimerRef.current);
|
|
60
|
+
}
|
|
61
|
+
observer.disconnect();
|
|
62
|
+
};
|
|
63
|
+
}, [shopId, campaignId, threshold, visibilityDuration, metadata]);
|
|
64
|
+
// Calculate time to action for clicks
|
|
65
|
+
const getTimeToAction = useCallback(() => {
|
|
66
|
+
if (firstVisibleTimeRef.current) {
|
|
67
|
+
return Date.now() - firstVisibleTimeRef.current;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}, []);
|
|
71
|
+
return {
|
|
72
|
+
elementRef,
|
|
73
|
+
isVisible,
|
|
74
|
+
hasTrackedView: hasTrackedViewRef.current,
|
|
75
|
+
getTimeToAction,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Main hook for campaign tracking
|
|
80
|
+
* Combines visibility tracking with click tracking utilities
|
|
81
|
+
*/
|
|
82
|
+
export function useCampaignTracking(options) {
|
|
83
|
+
const { shopId, campaignId, ...visibilityOptions } = options;
|
|
84
|
+
const visibility = useCampaignVisibility({
|
|
85
|
+
shopId,
|
|
86
|
+
campaignId,
|
|
87
|
+
...visibilityOptions,
|
|
88
|
+
});
|
|
89
|
+
const trackClick = useCallback((clickOptions) => {
|
|
90
|
+
const timeToAction = visibility.getTimeToAction();
|
|
91
|
+
trackCampaignClick(shopId, campaignId, {
|
|
92
|
+
...clickOptions,
|
|
93
|
+
timeToAction,
|
|
94
|
+
metadata: {
|
|
95
|
+
...options.metadata,
|
|
96
|
+
...clickOptions?.metadata,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}, [shopId, campaignId, visibility, options.metadata]);
|
|
100
|
+
const trackPrimaryAction = useCallback(() => {
|
|
101
|
+
trackClick({ actionType: 'primary' });
|
|
102
|
+
}, [trackClick]);
|
|
103
|
+
const trackSecondaryAction = useCallback(() => {
|
|
104
|
+
trackClick({ actionType: 'secondary' });
|
|
105
|
+
}, [trackClick]);
|
|
106
|
+
const trackDismiss = useCallback(() => {
|
|
107
|
+
trackClick({ actionType: 'dismiss' });
|
|
108
|
+
}, [trackClick]);
|
|
109
|
+
return {
|
|
110
|
+
...visibility,
|
|
111
|
+
trackClick,
|
|
112
|
+
trackPrimaryAction,
|
|
113
|
+
trackSecondaryAction,
|
|
114
|
+
trackDismiss,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -9,7 +9,7 @@ export function useFloatingCards(options = {}) {
|
|
|
9
9
|
const dashboardContext = useDashboardContext();
|
|
10
10
|
const contextLocale = dashboardContext?.locale;
|
|
11
11
|
// Use props locale if provided, otherwise use context locale, fallback to 'en'
|
|
12
|
-
const locale = propsLocale || contextLocale ||
|
|
12
|
+
const locale = propsLocale || contextLocale || "en";
|
|
13
13
|
const [cards, setCards] = useState([]);
|
|
14
14
|
const [loading, setLoading] = useState(false);
|
|
15
15
|
const [error, setError] = useState(null);
|
|
@@ -23,7 +23,6 @@ export function useFloatingCards(options = {}) {
|
|
|
23
23
|
setError(null);
|
|
24
24
|
try {
|
|
25
25
|
const id = shopId;
|
|
26
|
-
// Use proxy in development
|
|
27
26
|
const apiUrl = typeof window !== "undefined" &&
|
|
28
27
|
window.location.hostname === "localhost"
|
|
29
28
|
? `/api/campaigns?shop_id=${id}&locale=${locale}`
|
|
@@ -41,7 +40,6 @@ export function useFloatingCards(options = {}) {
|
|
|
41
40
|
}
|
|
42
41
|
const data = await response.json();
|
|
43
42
|
if (data.success && Array.isArray(data.data)) {
|
|
44
|
-
// Transform the API response to match our FloatingCardData interface
|
|
45
43
|
const transformedCards = data.data.map((card) => ({
|
|
46
44
|
...card,
|
|
47
45
|
// Ensure date format
|
|
@@ -138,19 +136,36 @@ export function useVisibleFloatingCards(options = {}) {
|
|
|
138
136
|
return false;
|
|
139
137
|
}
|
|
140
138
|
// Check display_pages logic
|
|
141
|
-
if (card.display_pages !== undefined) {
|
|
139
|
+
if (card.display_pages !== undefined && card.display_pages !== null) {
|
|
142
140
|
// If display_pages is empty array, show on all pages
|
|
143
141
|
if (card.display_pages.length === 0) {
|
|
144
142
|
return true;
|
|
145
143
|
}
|
|
146
144
|
// If display_pages has values, check if current route matches
|
|
147
|
-
if (currentRoute) {
|
|
148
|
-
// Extract the page key from the route
|
|
149
|
-
|
|
145
|
+
if (currentRoute !== undefined) {
|
|
146
|
+
// Extract the page key from the route
|
|
147
|
+
// Special handling: "/" or "" should map to "dashboard"
|
|
148
|
+
let currentPage = currentRoute.replace(/^\//, "").split("/")[0];
|
|
149
|
+
// Map root route to dashboard (in-app convention)
|
|
150
|
+
if (!currentPage || currentPage === "" || currentRoute === "/") {
|
|
151
|
+
currentPage = "dashboard";
|
|
152
|
+
}
|
|
153
|
+
// Debug log for development
|
|
154
|
+
if (typeof window !== "undefined" &&
|
|
155
|
+
window.location.hostname === "localhost") {
|
|
156
|
+
console.log("[FloatingCard] Route matching:", {
|
|
157
|
+
cardId: card.id,
|
|
158
|
+
currentRoute,
|
|
159
|
+
currentPage,
|
|
160
|
+
display_pages: card.display_pages,
|
|
161
|
+
matches: card.display_pages.includes(currentPage),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Check if current page is in display_pages
|
|
150
165
|
return card.display_pages.includes(currentPage);
|
|
151
166
|
}
|
|
152
|
-
// If no current route provided,
|
|
153
|
-
return
|
|
167
|
+
// If no current route provided, assume dashboard
|
|
168
|
+
return card.display_pages.includes("dashboard");
|
|
154
169
|
}
|
|
155
170
|
// If display_pages is not defined, show on all pages (backward compatibility)
|
|
156
171
|
return true;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from "./components";
|
|
|
10
10
|
export type { SDKTranslations } from './types/translations';
|
|
11
11
|
export { defaultTranslations } from './translations/default';
|
|
12
12
|
export { useTranslations, useTranslation } from './hooks/useTranslations';
|
|
13
|
+
export { trackCampaignView, trackCampaignClick, clearViewTrackingCache } from './utils/campaignTracking';
|
|
14
|
+
export { getSessionId, clearSession, hasSession } from './utils/sessionManager';
|
|
15
|
+
export { useCampaignTracking, useCampaignVisibility } from './hooks/useCampaignTracking';
|
|
13
16
|
import { DashboardEngine } from "./core/engine";
|
|
14
17
|
declare const _default: {
|
|
15
18
|
DashboardEngine: typeof DashboardEngine;
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ export * from "./hooks";
|
|
|
8
8
|
export * from "./components";
|
|
9
9
|
export { defaultTranslations } from './translations/default';
|
|
10
10
|
export { useTranslations, useTranslation } from './hooks/useTranslations';
|
|
11
|
+
// Campaign Tracking
|
|
12
|
+
export { trackCampaignView, trackCampaignClick, clearViewTrackingCache } from './utils/campaignTracking';
|
|
13
|
+
export { getSessionId, clearSession, hasSession } from './utils/sessionManager';
|
|
14
|
+
export { useCampaignTracking, useCampaignVisibility } from './hooks/useCampaignTracking';
|
|
11
15
|
import { DashboardEngine } from "./core/engine";
|
|
12
16
|
import { DashboardProvider } from "./provider/DashboardProvider";
|
|
13
17
|
export default {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track campaign view
|
|
3
|
+
* Handles deduplication and fire-and-forget requests
|
|
4
|
+
*/
|
|
5
|
+
export declare function trackCampaignView(shopId: string, campaignId: string | number, metadata?: Record<string, any>): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Track campaign click
|
|
8
|
+
* Tracks immediately without debouncing
|
|
9
|
+
*/
|
|
10
|
+
export declare function trackCampaignClick(shopId: string, campaignId: string | number, options?: {
|
|
11
|
+
actionType?: string;
|
|
12
|
+
ctaId?: string;
|
|
13
|
+
ctaKey?: string;
|
|
14
|
+
timeToAction?: number;
|
|
15
|
+
metadata?: Record<string, any>;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Clear view tracking deduplication cache
|
|
19
|
+
* Useful for testing or when session changes
|
|
20
|
+
*/
|
|
21
|
+
export declare function clearViewTrackingCache(): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getSessionId } from './sessionManager';
|
|
2
|
+
const TRACKING_BASE_URL = 'https://ops.trustshop.io';
|
|
3
|
+
/**
|
|
4
|
+
* View tracking deduplication map
|
|
5
|
+
* Key: ${sessionId}-${campaignId}
|
|
6
|
+
* Value: timestamp in milliseconds
|
|
7
|
+
*/
|
|
8
|
+
const viewTrackingMap = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Deduplication threshold for view tracking (5 seconds)
|
|
11
|
+
*/
|
|
12
|
+
const VIEW_DEDUP_THRESHOLD_MS = 5000;
|
|
13
|
+
/**
|
|
14
|
+
* Get current page context from URL
|
|
15
|
+
*/
|
|
16
|
+
function getPageContext() {
|
|
17
|
+
if (typeof window === 'undefined') {
|
|
18
|
+
return '/';
|
|
19
|
+
}
|
|
20
|
+
// Get the current pathname
|
|
21
|
+
const pathname = window.location.pathname;
|
|
22
|
+
// In Shopify admin apps, the actual route might be in hash
|
|
23
|
+
const hash = window.location.hash;
|
|
24
|
+
if (hash && hash.startsWith('#/')) {
|
|
25
|
+
return hash.substring(1); // Remove the '#'
|
|
26
|
+
}
|
|
27
|
+
return pathname;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Track campaign view
|
|
31
|
+
* Handles deduplication and fire-and-forget requests
|
|
32
|
+
*/
|
|
33
|
+
export async function trackCampaignView(shopId, campaignId, metadata) {
|
|
34
|
+
const sessionId = getSessionId();
|
|
35
|
+
const dedupKey = `${sessionId}-${campaignId}`;
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
// Check for deduplication
|
|
38
|
+
const lastTracked = viewTrackingMap.get(dedupKey);
|
|
39
|
+
if (lastTracked && (now - lastTracked) < VIEW_DEDUP_THRESHOLD_MS) {
|
|
40
|
+
// Skip tracking - still within dedup threshold
|
|
41
|
+
console.log(`⏭️ [Campaign Tracking] View skipped (dedup) for campaign ${campaignId} - last tracked ${now - lastTracked}ms ago`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Update tracking map
|
|
45
|
+
viewTrackingMap.set(dedupKey, now);
|
|
46
|
+
const payload = {
|
|
47
|
+
session_id: sessionId,
|
|
48
|
+
page_context: getPageContext(),
|
|
49
|
+
metadata: {
|
|
50
|
+
source: 'in-app-sdk',
|
|
51
|
+
placement: 'floating-card',
|
|
52
|
+
...metadata,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const url = `${TRACKING_BASE_URL}/api/shops/${shopId}/campaigns/${campaignId}/track/view`;
|
|
56
|
+
// Log tracking attempt in development
|
|
57
|
+
console.log(`👁️ [Campaign Tracking] VIEW tracked for campaign ${campaignId}`);
|
|
58
|
+
console.log(`📍 URL: ${url}`);
|
|
59
|
+
console.log(`📦 Payload:`, payload);
|
|
60
|
+
// Fire and forget - no await, no error handling that would break UI
|
|
61
|
+
void fetch(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
'Accept': 'application/json',
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
credentials: 'include',
|
|
69
|
+
}).catch(error => {
|
|
70
|
+
// Log error in development
|
|
71
|
+
console.error('[Campaign Tracking] View tracking failed:', error);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Track campaign click
|
|
76
|
+
* Tracks immediately without debouncing
|
|
77
|
+
*/
|
|
78
|
+
export async function trackCampaignClick(shopId, campaignId, options = {}) {
|
|
79
|
+
const { actionType, ctaId, ctaKey, timeToAction, metadata } = options;
|
|
80
|
+
// Validate that at least one required field is present
|
|
81
|
+
if (!actionType && !ctaId && !ctaKey) {
|
|
82
|
+
console.warn('[Campaign Tracking] Click tracking requires at least one of: action_type, cta_id, cta_key');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const sessionId = getSessionId();
|
|
86
|
+
const payload = {
|
|
87
|
+
session_id: sessionId,
|
|
88
|
+
page_context: getPageContext(),
|
|
89
|
+
metadata: {
|
|
90
|
+
source: 'in-app-sdk',
|
|
91
|
+
placement: 'floating-card',
|
|
92
|
+
...metadata,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
// Add optional fields if provided
|
|
96
|
+
if (actionType)
|
|
97
|
+
payload.action_type = actionType;
|
|
98
|
+
if (ctaId)
|
|
99
|
+
payload.cta_id = ctaId;
|
|
100
|
+
if (ctaKey)
|
|
101
|
+
payload.cta_key = ctaKey;
|
|
102
|
+
if (timeToAction !== undefined)
|
|
103
|
+
payload.time_to_action = timeToAction;
|
|
104
|
+
const url = `${TRACKING_BASE_URL}/api/shops/${shopId}/campaigns/${campaignId}/track/click`;
|
|
105
|
+
// Log tracking attempt in development
|
|
106
|
+
console.log(`🖱️ [Campaign Tracking] CLICK tracked for campaign ${campaignId}`);
|
|
107
|
+
console.log(`📍 URL: ${url}`);
|
|
108
|
+
console.log(`📦 Payload:`, payload);
|
|
109
|
+
console.log(`⏱️ Time to action: ${timeToAction}ms`);
|
|
110
|
+
// Fire and forget - no await, no error handling that would break UI
|
|
111
|
+
void fetch(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'Accept': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(payload),
|
|
118
|
+
credentials: 'include',
|
|
119
|
+
}).catch(error => {
|
|
120
|
+
// Log error in development
|
|
121
|
+
console.error('[Campaign Tracking] Click tracking failed:', error);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clear view tracking deduplication cache
|
|
126
|
+
* Useful for testing or when session changes
|
|
127
|
+
*/
|
|
128
|
+
export function clearViewTrackingCache() {
|
|
129
|
+
viewTrackingMap.clear();
|
|
130
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management utility for campaign tracking
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get or create session ID
|
|
6
|
+
* Session persists for the browser session (using sessionStorage)
|
|
7
|
+
*/
|
|
8
|
+
export declare function getSessionId(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Clear current session
|
|
11
|
+
*/
|
|
12
|
+
export declare function clearSession(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a session exists
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasSession(): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management utility for campaign tracking
|
|
3
|
+
*/
|
|
4
|
+
const SESSION_ID_KEY = 'trustshop_inapp_session_id';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique session ID
|
|
7
|
+
*/
|
|
8
|
+
function generateSessionId() {
|
|
9
|
+
const timestamp = Date.now();
|
|
10
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
11
|
+
return `sess_${timestamp}_${random}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get or create session ID
|
|
15
|
+
* Session persists for the browser session (using sessionStorage)
|
|
16
|
+
*/
|
|
17
|
+
export function getSessionId() {
|
|
18
|
+
if (typeof window === 'undefined') {
|
|
19
|
+
return generateSessionId();
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
// Check if session ID exists in sessionStorage
|
|
23
|
+
let sessionId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
24
|
+
if (!sessionId) {
|
|
25
|
+
// Generate new session ID
|
|
26
|
+
sessionId = generateSessionId();
|
|
27
|
+
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
|
28
|
+
}
|
|
29
|
+
return sessionId;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// Fallback if sessionStorage is not available
|
|
33
|
+
console.warn('SessionStorage not available, using temporary session ID', error);
|
|
34
|
+
return generateSessionId();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Clear current session
|
|
39
|
+
*/
|
|
40
|
+
export function clearSession() {
|
|
41
|
+
if (typeof window === 'undefined')
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
sessionStorage.removeItem(SESSION_ID_KEY);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.warn('Failed to clear session', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a session exists
|
|
52
|
+
*/
|
|
53
|
+
export function hasSession() {
|
|
54
|
+
if (typeof window === 'undefined')
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
return sessionStorage.getItem(SESSION_ID_KEY) !== null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|