219 lines
4.8 KiB
TypeScript
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,
|
|
};
|
|
}
|