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

219 lines
4.8 KiB
TypeScript

/**
* useCheckoutFunnel - E-commerce checkout flow tracking
*
* Tracks a typical checkout funnel:
* 1. cart_viewed - User views their cart
* 2. checkout_started - User begins checkout
* 3. shipping_entered - User enters shipping info
* 4. payment_entered - User enters payment info
* 5. order_placed - Order successfully placed
*/
import { useCallback, useEffect, useRef } from 'react';
import {
startFunnel,
trackFunnelStep,
completeFunnel,
abandonFunnel,
isFunnelActive,
getFunnelState,
} from './funnel-tracker';
const FUNNEL_ID = 'checkout';
export type CheckoutStep =
| 'cart_viewed'
| 'checkout_started'
| 'shipping_entered'
| 'payment_entered'
| 'order_placed';
interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
}
interface UseCheckoutFunnelOptions {
/**
* Cart contents when funnel starts.
*/
cart?: CartItem[];
/**
* Currency code (USD, EUR, etc.)
*/
currency?: string;
}
interface UseCheckoutFunnelReturn {
/**
* Start tracking when user views cart.
*/
viewCart: (cart: CartItem[]) => void;
/**
* User clicks "Checkout" button.
*/
startCheckout: () => void;
/**
* User completes shipping form.
*/
enterShipping: (shippingMethod?: string) => void;
/**
* User completes payment form.
*/
enterPayment: (paymentMethod?: string) => void;
/**
* Order placed successfully.
*/
placeOrder: (orderId: string, total: number) => void;
/**
* User abandons checkout.
*/
abandonCheckout: (reason?: string) => void;
/**
* Check if funnel is active.
*/
isActive: boolean;
/**
* Get current step index.
*/
currentStepIndex: number;
}
/**
* Hook for tracking checkout funnel progression.
*
* @example
* ```tsx
* function CartPage() {
* const cart = useCart();
* const {
* viewCart,
* startCheckout,
* enterShipping,
* enterPayment,
* placeOrder,
* } = useCheckoutFunnel({ currency: 'USD' });
*
* useEffect(() => {
* viewCart(cart.items);
* }, [viewCart, cart.items]);
*
* const handleCheckout = () => {
* startCheckout();
* navigate('/checkout/shipping');
* };
*
* const handleShippingSubmit = (data: ShippingData) => {
* enterShipping(data.method);
* navigate('/checkout/payment');
* };
*
* const handlePaymentSubmit = async (data: PaymentData) => {
* enterPayment(data.method);
* const order = await submitOrder();
* placeOrder(order.id, order.total);
* };
* }
* ```
*/
export function useCheckoutFunnel(
options: UseCheckoutFunnelOptions = {},
): UseCheckoutFunnelReturn {
const { currency = 'USD' } = options;
const startedRef = useRef(false);
const calculateCartValue = (cart: CartItem[]): number => {
return cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
};
const viewCart = useCallback(
(cart: CartItem[]) => {
if (startedRef.current || isFunnelActive(FUNNEL_ID)) {
return;
}
startedRef.current = true;
const cartValue = calculateCartValue(cart);
startFunnel(FUNNEL_ID, {
cartValue,
currency,
itemCount: cart.length,
products: cart.map((item) => ({
productId: item.productId,
name: item.name,
price: item.price,
quantity: item.quantity,
})),
});
trackFunnelStep(FUNNEL_ID, 'cart_viewed', { cartValue, itemCount: cart.length });
},
[currency],
);
const startCheckout = useCallback(() => {
trackFunnelStep(FUNNEL_ID, 'checkout_started');
}, []);
const enterShipping = useCallback((shippingMethod?: string) => {
trackFunnelStep(FUNNEL_ID, 'shipping_entered', { shippingMethod });
}, []);
const enterPayment = useCallback((paymentMethod?: string) => {
trackFunnelStep(FUNNEL_ID, 'payment_entered', { paymentMethod });
}, []);
const placeOrder = useCallback(
(orderId: string, total: number) => {
completeFunnel(FUNNEL_ID, {
orderId,
orderTotal: total,
currency,
completedAt: new Date().toISOString(),
});
startedRef.current = false;
},
[currency],
);
const abandonCheckout = useCallback((reason?: string) => {
abandonFunnel(FUNNEL_ID, {
reason: reason || 'user_abandoned',
abandonedAt: new Date().toISOString(),
});
startedRef.current = false;
}, []);
// Clean up on unmount
useEffect(() => {
return () => {
if (isFunnelActive(FUNNEL_ID)) {
abandonFunnel(FUNNEL_ID, { reason: 'component_unmount' });
}
};
}, []);
const state = getFunnelState(FUNNEL_ID);
return {
viewCart,
startCheckout,
enterShipping,
enterPayment,
placeOrder,
abandonCheckout,
isActive: isFunnelActive(FUNNEL_ID),
currentStepIndex: state?.steps.length ?? 0,
};
}