@datlv-trustshop/shopify-inapp-components 0.1.25 → 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/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
|
+
}
|
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
|
+
}
|