"use client"; import { useState, useMemo, useCallback, type ReactNode } from "react"; import { useFlagsStatus } from "@unleash/proxy-client-react"; import { UnleashContextProvider } from "@/lib/funnel/unleash"; import { useUnleashAnalytics } from "@/lib/funnel/unleash/useUnleashAnalytics"; import { FunnelLoadingScreen } from "./FunnelLoadingScreen"; import { FlagVariantFetcher } from "./FlagVariantFetcher"; import type { NavigationConditionDefinition } from "@/lib/funnel/types"; interface FunnelUnleashWrapperProps { children: ReactNode; funnel: { screens: Array<{ id: string; variants?: Array<{ conditions: NavigationConditionDefinition[]; }>; navigation?: { rules?: Array<{ conditions: NavigationConditionDefinition[]; }>; }; }>; }; currentScreenId?: string; // ← НОВОЕ: ID текущего экрана } /** * Wrapper который собирает Unleash флаги для ТЕКУЩЕГО экрана * и передает их активные варианты в контекст * * ВАЖНО: Загружает флаги ТОЛЬКО для текущего экрана, чтобы impression события * отправлялись когда пользователь РЕАЛЬНО доходит до экрана (как в aura-webapp) */ export function FunnelUnleashWrapper({ children, funnel, currentScreenId, }: FunnelUnleashWrapperProps) { // ✅ КРИТИЧЕСКИ ВАЖНО: Подписываемся на impression события ДО загрузки флагов // Это гарантирует что события от useVariant() будут пойманы и отправлены в аналитику useUnleashAnalytics(); const { flagsReady } = useFlagsStatus(); // Собираем флаги ТОЛЬКО для текущего экрана (или все, если currentScreenId не передан) const currentScreenFlags = useMemo(() => { const flags = new Set(); // Находим текущий экран const screensToCheck = currentScreenId ? funnel.screens.filter(screen => screen.id === currentScreenId) : funnel.screens; // Fallback: все экраны если currentScreenId не передан screensToCheck.forEach((screen) => { // Флаги из вариантов экрана screen.variants?.forEach((variant) => { variant.conditions.forEach((condition) => { if ( condition.conditionType === "unleash" && condition.unleashFlag ) { flags.add(condition.unleashFlag); } }); }); // Флаги из правил навигации screen.navigation?.rules?.forEach((rule) => { rule.conditions.forEach((condition) => { if ( condition.conditionType === "unleash" && condition.unleashFlag ) { flags.add(condition.unleashFlag); } }); }); }); return Array.from(flags); }, [funnel.screens, currentScreenId]); // Состояние для хранения вариантов флагов const [loadedVariants, setLoadedVariants] = useState>({}); // Колбэк для получения варианта от FlagVariantFetcher компонента const handleVariantLoaded = useCallback((flag: string, variant: string | undefined) => { // ✅ Сохраняем вариант в любом случае (даже если undefined или "disabled") // Это гарантирует что allFlagsLoaded станет true когда все флаги обработаны setLoadedVariants((prev) => { const newVariant = variant || "disabled"; // undefined → "disabled" // Обновляем только если значение изменилось if (prev[flag] !== newVariant) { console.log(`🚩 [FunnelUnleashWrapper] Flag loaded: "${flag}" = "${newVariant}"`); return { ...prev, [flag]: newVariant }; } return prev; }); }, []); // Проверяем что ВСЕ флаги текущего экрана загружены const allFlagsLoaded = useMemo(() => { if (!flagsReady) { console.log("⏳ [FunnelUnleashWrapper] Waiting for Unleash client to be ready..."); return false; } // Если нет флагов на экране - сразу готовы if (currentScreenFlags.length === 0) { console.log("✅ [FunnelUnleashWrapper] No AB test flags on current screen - ready to render"); return true; } // Проверяем что для каждого флага есть вариант const allLoaded = currentScreenFlags.every(flag => { return flag in loadedVariants; }); console.log(`${allLoaded ? '✅' : '⏳'} [FunnelUnleashWrapper] Flags status:`, { currentScreenId, flagsRequired: currentScreenFlags, flagsLoaded: Object.keys(loadedVariants), allReady: allLoaded, variants: loadedVariants, }); return allLoaded; }, [flagsReady, currentScreenFlags, loadedVariants, currentScreenId]); // Создаем объект активных вариантов const activeVariants = useMemo(() => { if (!allFlagsLoaded) { return {}; } if (process.env.NODE_ENV === "development") { console.log("[FunnelUnleashWrapper] Active variants:", loadedVariants); } return loadedVariants; }, [allFlagsLoaded, loadedVariants]); return ( <> {/* ✅ КРИТИЧЕСКИ ВАЖНО: FlagVariantFetcher рендерятся ВСЕГДА Они невидимые (return null), но загружают варианты асинхронно Это позволяет allFlagsLoaded стать true когда все варианты загружены */} {currentScreenFlags.map((flag) => ( ))} {/* ✅ Показываем loader пока ВСЕ флаги не загружены Это предотвращает flash когда контент меняется с дефолтного на AB вариант */} {!allFlagsLoaded ? ( ) : ( {children} )} ); }