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

156 lines
3.4 KiB
TypeScript

/**
* useSignupFunnel - Multi-step signup flow tracking
*
* Tracks a typical signup funnel:
* 1. email_entered - User enters their email
* 2. email_verified - User verifies email (magic link, code, etc.)
* 3. profile_created - User fills out profile info
* 4. completed - Signup complete
*/
import { useCallback, useEffect, useRef } from 'react';
import {
startFunnel,
trackFunnelStep,
completeFunnel,
abandonFunnel,
isFunnelActive,
} from './funnel-tracker';
const FUNNEL_ID = 'signup';
export type SignupStep =
| 'email_entered'
| 'email_verified'
| 'profile_created'
| 'plan_selected'
| 'completed';
interface UseSignupFunnelOptions {
/**
* Which plan the user is signing up for (if applicable).
*/
plan?: string;
/**
* Source of the signup (hero, pricing page, etc.).
*/
source?: string;
}
interface UseSignupFunnelReturn {
/**
* Start the signup funnel. Call when user lands on signup page.
*/
startSignup: () => void;
/**
* Track a step completion.
*/
trackStep: (step: SignupStep, metadata?: Record<string, unknown>) => void;
/**
* Complete the funnel (successful signup).
*/
completeSignup: (userId?: string) => void;
/**
* User explicitly cancelled signup.
*/
cancelSignup: (reason?: string) => void;
/**
* Check if funnel is active.
*/
isActive: boolean;
}
/**
* Hook for tracking signup funnel progression.
*
* @example
* ```tsx
* function SignupPage() {
* const { startSignup, trackStep, completeSignup } = useSignupFunnel({
* plan: 'pro',
* source: 'pricing-page',
* });
*
* useEffect(() => {
* startSignup();
* }, [startSignup]);
*
* const handleEmailSubmit = async (email: string) => {
* await sendVerificationEmail(email);
* trackStep('email_entered', { email_domain: email.split('@')[1] });
* };
*
* const handleVerified = () => {
* trackStep('email_verified');
* };
*
* const handleSignupComplete = (user: User) => {
* completeSignup(user.id);
* };
* }
* ```
*/
export function useSignupFunnel(
options: UseSignupFunnelOptions = {},
): UseSignupFunnelReturn {
const { plan, source } = options;
const startedRef = useRef(false);
const startSignup = useCallback(() => {
if (startedRef.current || isFunnelActive(FUNNEL_ID)) {
return; // Already started
}
startedRef.current = true;
startFunnel(FUNNEL_ID, {
plan,
source,
startedAt: new Date().toISOString(),
});
}, [plan, source]);
const trackStep = useCallback(
(step: SignupStep, metadata?: Record<string, unknown>) => {
trackFunnelStep(FUNNEL_ID, step, metadata);
},
[],
);
const completeSignup = useCallback((userId?: string) => {
completeFunnel(FUNNEL_ID, {
userId,
completedAt: new Date().toISOString(),
});
startedRef.current = false;
}, []);
const cancelSignup = useCallback((reason?: string) => {
abandonFunnel(FUNNEL_ID, {
reason: reason || 'user_cancelled',
cancelledAt: new Date().toISOString(),
});
startedRef.current = false;
}, []);
// Clean up on unmount
useEffect(() => {
return () => {
if (isFunnelActive(FUNNEL_ID)) {
abandonFunnel(FUNNEL_ID, { reason: 'component_unmount' });
}
};
}, []);
return {
startSignup,
trackStep,
completeSignup,
cancelSignup,
isActive: isFunnelActive(FUNNEL_ID),
};
}