@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.
@@ -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
- export const FloatingCard = ({ data, onDismiss, onPrimaryAction, onSecondaryAction, position = 'bottom-right', showCloseButton = true, }) => {
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: onDismiss, "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: {
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",
@@ -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
- private config;
5
+ config: DashboardConfig;
6
6
  private initPromise;
7
7
  private lastFetchTime;
8
8
  private listeners;
@@ -6,3 +6,4 @@ export * from "./useWhatsNew";
6
6
  export * from "./useGrowApps";
7
7
  export * from "./usePartnerIntegration";
8
8
  export * from "./useFloatingCards";
9
+ export * from "./useCampaignTracking";
@@ -6,3 +6,4 @@ export * from "./useWhatsNew";
6
6
  export * from "./useGrowApps";
7
7
  export * from "./usePartnerIntegration";
8
8
  export * from "./useFloatingCards";
9
+ export * from "./useCampaignTracking";
@@ -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 {
@@ -29,6 +29,7 @@ export interface DashboardData {
29
29
  }
30
30
  export interface DashboardConfig {
31
31
  apiUrl: string;
32
+ shopId?: string;
32
33
  locale?: string;
33
34
  cacheTime?: number;
34
35
  retryAttempts?: number;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datlv-trustshop/shopify-inapp-components",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "private": false,
5
5
  "description": "React TypeScript components for Shopify in-app dashboard content",
6
6
  "main": "dist/index.js",