156 lines
3.4 KiB
TypeScript
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),
|
|
};
|
|
}
|