analytics/examples/ecommerce/use-cart-tracking.ts
2026-01-29 08:20:58 -08:00

228 lines
6.6 KiB
TypeScript

/**
* useCartTracking - React hook for cart analytics
*
* Integrates with your cart state to automatically track cart events.
*/
import { useCallback, useEffect, useRef } from 'react';
import { useAnalytics } from '../react-spa/analytics-setup';
import {
trackCartAdd,
trackCartRemove,
trackCartUpdate,
trackCartView,
trackCheckoutStart,
trackCartAbandonment,
trackPromoCodeApply,
type Cart,
} from './cart-analytics';
import type { Product } from './product-analytics';
// ─────────────────────────────────────────────────────────────────────────────
// Hook
// ─────────────────────────────────────────────────────────────────────────────
interface UseCartTrackingOptions {
/** Track cart view automatically when cart changes */
trackViewOnChange?: boolean;
/** Track abandonment on page unload */
trackAbandonmentOnUnload?: boolean;
}
export function useCartTracking(cart: Cart, options: UseCartTrackingOptions = {}) {
const { trackViewOnChange = false, trackAbandonmentOnUnload = true } = options;
const { client } = useAnalytics();
const previousCartRef = useRef<Cart | null>(null);
// Track cart view when cart contents change
useEffect(() => {
if (!trackViewOnChange) return;
const prevItemCount = previousCartRef.current?.items.length ?? 0;
const currItemCount = cart.items.length;
// Only track if items actually changed
if (prevItemCount !== currItemCount) {
trackCartView(client, cart);
}
previousCartRef.current = cart;
}, [client, cart, trackViewOnChange]);
// Track abandonment on page unload
useEffect(() => {
if (!trackAbandonmentOnUnload) return;
if (cart.items.length === 0) return;
const handleUnload = () => {
trackCartAbandonment(client, cart, 'browser_close');
};
window.addEventListener('beforeunload', handleUnload);
return () => window.removeEventListener('beforeunload', handleUnload);
}, [client, cart, trackAbandonmentOnUnload]);
/**
* Track item added to cart.
*/
const trackAdd = useCallback(
(product: Product, quantity: number) => {
trackCartAdd(client, product, quantity, cart);
},
[client, cart],
);
/**
* Track item removed from cart.
*/
const trackRemove = useCallback(
(product: Product, quantity: number) => {
trackCartRemove(client, product, quantity, cart);
},
[client, cart],
);
/**
* Track item quantity update.
*/
const trackUpdate = useCallback(
(product: Product, previousQuantity: number, newQuantity: number) => {
trackCartUpdate(client, product, previousQuantity, newQuantity, cart);
},
[client, cart],
);
/**
* Track cart page view.
*/
const trackView = useCallback(() => {
trackCartView(client, cart);
}, [client, cart]);
/**
* Track checkout initiation.
*/
const trackCheckout = useCallback(() => {
trackCheckoutStart(client, cart);
}, [client, cart]);
/**
* Track cart abandonment.
*/
const trackAbandonment = useCallback(
(reason?: 'navigation' | 'timeout' | 'browser_close') => {
trackCartAbandonment(client, cart, reason);
},
[client, cart],
);
/**
* Track promo code application.
*/
const trackPromoCode = useCallback(
(code: string, success: boolean, discount?: number, errorMessage?: string) => {
trackPromoCodeApply(client, code, success, discount, errorMessage);
},
[client],
);
return {
trackAdd,
trackRemove,
trackUpdate,
trackView,
trackCheckout,
trackAbandonment,
trackPromoCode,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Cart Provider Integration Example
// ─────────────────────────────────────────────────────────────────────────────
/**
* @example
* ```tsx
* // CartProvider.tsx - Integrate tracking with your cart state
*
* import { createContext, useContext, useState, useMemo } from 'react';
* import { useCartTracking } from './use-cart-tracking';
*
* const CartContext = createContext(null);
*
* export function CartProvider({ children }) {
* const [cart, setCart] = useState({ items: [], subtotal: 0, currency: 'USD' });
* const tracking = useCartTracking(cart, {
* trackAbandonmentOnUnload: true,
* });
*
* const addItem = (product, quantity) => {
* setCart(prev => {
* const newCart = addToCart(prev, product, quantity);
* // Track after state update
* tracking.trackAdd(product, quantity);
* return newCart;
* });
* };
*
* const removeItem = (product) => {
* const item = cart.items.find(i => i.id === product.id);
* if (!item) return;
*
* setCart(prev => {
* const newCart = removeFromCart(prev, product.id);
* tracking.trackRemove(product, item.quantity);
* return newCart;
* });
* };
*
* const updateQuantity = (product, newQuantity) => {
* const item = cart.items.find(i => i.id === product.id);
* if (!item) return;
*
* const previousQuantity = item.quantity;
*
* setCart(prev => {
* const newCart = updateCartItem(prev, product.id, newQuantity);
* tracking.trackUpdate(product, previousQuantity, newQuantity);
* return newCart;
* });
* };
*
* const startCheckout = () => {
* tracking.trackCheckout();
* // Navigate to checkout...
* };
*
* const applyPromoCode = async (code) => {
* try {
* const result = await validatePromoCode(code);
* setCart(prev => applyDiscount(prev, result.discount));
* tracking.trackPromoCode(code, true, result.discount);
* } catch (error) {
* tracking.trackPromoCode(code, false, undefined, error.message);
* }
* };
*
* const value = useMemo(() => ({
* cart,
* addItem,
* removeItem,
* updateQuantity,
* startCheckout,
* applyPromoCode,
* }), [cart]);
*
* return (
* <CartContext.Provider value={value}>
* {children}
* </CartContext.Provider>
* );
* }
*
* export const useCart = () => useContext(CartContext);
* ```
*/