import React, { useMemo, useRef, useEffect, useCallback, Suspense, lazy, useState } from "react"; import { useTranslation } from 'react-i18next'; import { Steps } from "@/components/ui/Steps"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { CheckoutWizardProvider, useCheckoutWizard } from "./WizardContext"; import type { CheckoutPackage, CheckoutStepId, OAuthProfilePrefill } from "./types"; import { PackageStep } from "./steps/PackageStep"; import { AuthStep } from "./steps/AuthStep"; import { ConfirmationStep } from "./steps/ConfirmationStep"; import { useAnalytics } from '@/hooks/useAnalytics'; import { cn } from "@/lib/utils"; import toast from 'react-hot-toast'; const PaymentStep = lazy(() => import('./steps/PaymentStep').then((module) => ({ default: module.PaymentStep }))); interface CheckoutWizardProps { initialPackage: CheckoutPackage; packageOptions: CheckoutPackage[]; privacyHtml: string; initialAuthUser?: { id: number; email: string; name?: string; pending_purchase?: boolean; } | null; initialStep?: CheckoutStepId; googleProfile?: OAuthProfilePrefill | null; facebookProfile?: OAuthProfilePrefill | null; paddle?: { environment?: string | null; client_token?: string | null; } | null; } const baseStepConfig: { id: CheckoutStepId; titleKey: string; descriptionKey: string; detailsKey: string }[] = [ { id: "package", titleKey: 'checkout.package_step.title', descriptionKey: 'checkout.package_step.subtitle', detailsKey: 'checkout.package_step.description' }, { id: "auth", titleKey: 'checkout.auth_step.title', descriptionKey: 'checkout.auth_step.subtitle', detailsKey: 'checkout.auth_step.description' }, { id: "payment", titleKey: 'checkout.payment_step.title', descriptionKey: 'checkout.payment_step.subtitle', detailsKey: 'checkout.payment_step.description' }, { id: "confirmation", titleKey: 'checkout.confirmation_step.title', descriptionKey: 'checkout.confirmation_step.subtitle', detailsKey: 'checkout.confirmation_step.description' }, ]; const PaymentStepFallback: React.FC = () => (
); const WizardBody: React.FC<{ privacyHtml: string; prefillProfile?: OAuthProfilePrefill | null; onClearPrefill?: () => void; }> = ({ privacyHtml, prefillProfile, onClearPrefill }) => { const primaryCtaClassName = "min-w-[160px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground"; const { t } = useTranslation('marketing'); const { currentStep, nextStep, previousStep, selectedPackage, authUser, isAuthenticated, paymentCompleted, goToStep, } = useCheckoutWizard(); const progressRef = useRef(null); const hasMountedRef = useRef(false); const { trackEvent } = useAnalytics(); const stepConfig = useMemo(() => baseStepConfig.map(step => ({ id: step.id, title: t(step.titleKey), description: t(step.descriptionKey), details: t(step.detailsKey), })), [t] ); const currentIndex = useMemo( () => stepConfig.findIndex((step) => step.id === currentStep), [currentStep, stepConfig] ); const progress = useMemo(() => { if (currentIndex < 0) { return 0; } return (currentIndex / (stepConfig.length - 1)) * 100; }, [currentIndex, stepConfig]); useEffect(() => { trackEvent({ category: 'marketing_checkout', action: 'step_view', name: currentStep, }); }, [currentStep, trackEvent]); useEffect(() => { if (currentStep === 'payment' && !isAuthenticated) { toast.error(t('checkout.payment_step.auth_required')); goToStep('auth'); } }, [currentStep, goToStep, isAuthenticated, t]); useEffect(() => { if (typeof window === 'undefined' || !progressRef.current) { return; } if (!hasMountedRef.current) { hasMountedRef.current = true; return; } const element = progressRef.current; const rect = element.getBoundingClientRect(); const scrollTop = window.scrollY + rect.top - 16; // slightly above the progress bar window.scrollTo({ top: Math.max(scrollTop, 0), behavior: 'smooth', }); }, [currentStep]); const atLastStep = currentIndex >= stepConfig.length - 1; const canProceedToNextStep = useMemo(() => { if (atLastStep) { return false; } if (currentStep === 'package') { return Boolean(selectedPackage); } if (currentStep === 'auth') { return Boolean(isAuthenticated && authUser); } if (currentStep === 'payment') { return paymentCompleted; } return true; }, [atLastStep, authUser, currentStep, isAuthenticated, paymentCompleted, selectedPackage]); const shouldShowNextButton = useMemo(() => currentStep !== 'confirmation', [currentStep]); const highlightNextCta = currentStep === 'payment' && paymentCompleted; const handleNext = useCallback(() => { if (!canProceedToNextStep) { return; } const targetStep = stepConfig[currentIndex + 1]?.id ?? 'end'; trackEvent({ category: 'marketing_checkout', action: 'step_next', name: `${currentStep}->${targetStep}`, }); nextStep(); }, [canProceedToNextStep, currentIndex, currentStep, nextStep, stepConfig, trackEvent]); const handlePrevious = useCallback(() => { const targetStep = stepConfig[currentIndex - 1]?.id ?? 'start'; trackEvent({ category: 'marketing_checkout', action: 'step_previous', name: `${currentStep}->${targetStep}`, }); previousStep(); }, [currentIndex, currentStep, previousStep, stepConfig, trackEvent]); const handleViewProfile = useCallback(() => { window.location.href = '/settings/profile'; }, []); const handleGoToAdmin = useCallback(() => { window.location.href = '/event-admin'; }, []); const primaryCta = useMemo(() => { if (currentStep === 'confirmation') { return { label: t('checkout.confirmation_step.to_admin'), onClick: handleGoToAdmin, disabled: false, }; } if (!shouldShowNextButton) { return null; } return { label: t('checkout.next'), onClick: handleNext, disabled: !canProceedToNextStep, }; }, [currentStep, handleGoToAdmin, handleNext, shouldShowNextButton, t, canProceedToNextStep]); const ctaClassName = cn(primaryCtaClassName, highlightNextCta && 'animate-pulse ring-2 ring-primary/50 ring-offset-2 ring-offset-background'); return (
{primaryCta && (
)}
= 0 ? currentIndex : 0} />
{currentStep === "package" && } {currentStep === "auth" && ( )} {currentStep === "payment" && ( }> )} {currentStep === "confirmation" && ( )}
{primaryCta ? ( ) : (
); }; export const CheckoutWizard: React.FC = ({ initialPackage, packageOptions, privacyHtml, initialAuthUser, initialStep, googleProfile, facebookProfile, paddle, }) => { const [storedGoogleProfile, setStoredGoogleProfile] = useState(() => { if (typeof window === 'undefined') { return null; } const raw = window.localStorage.getItem('checkout-google-profile'); if (!raw) { return null; } try { return JSON.parse(raw) as OAuthProfilePrefill; } catch (error) { console.warn('Failed to parse checkout google profile from storage', error); window.localStorage.removeItem('checkout-google-profile'); return null; } }); useEffect(() => { if (!googleProfile) { return; } setStoredGoogleProfile(googleProfile); if (typeof window !== 'undefined') { window.localStorage.setItem('checkout-google-profile', JSON.stringify(googleProfile)); } }, [googleProfile]); const clearStoredGoogleProfile = useCallback(() => { setStoredGoogleProfile(null); if (typeof window !== 'undefined') { window.localStorage.removeItem('checkout-google-profile'); } }, []); const [storedFacebookProfile, setStoredFacebookProfile] = useState(() => { if (typeof window === 'undefined') { return null; } const raw = window.localStorage.getItem('checkout-facebook-profile'); if (!raw) { return null; } try { return JSON.parse(raw) as OAuthProfilePrefill; } catch (error) { console.warn('Failed to parse checkout facebook profile from storage', error); window.localStorage.removeItem('checkout-facebook-profile'); return null; } }); useEffect(() => { if (!facebookProfile) { return; } setStoredFacebookProfile(facebookProfile); if (typeof window !== 'undefined') { window.localStorage.setItem('checkout-facebook-profile', JSON.stringify(facebookProfile)); } }, [facebookProfile]); const clearStoredFacebookProfile = useCallback(() => { setStoredFacebookProfile(null); if (typeof window !== 'undefined') { window.localStorage.removeItem('checkout-facebook-profile'); } }, []); const clearStoredProfiles = useCallback(() => { clearStoredGoogleProfile(); clearStoredFacebookProfile(); }, [clearStoredFacebookProfile, clearStoredGoogleProfile]); const effectiveProfile = facebookProfile ?? storedFacebookProfile ?? googleProfile ?? storedGoogleProfile; return ( ); };