175 lines
6.5 KiB
TypeScript
175 lines
6.5 KiB
TypeScript
"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<string>();
|
||
|
||
// Находим текущий экран
|
||
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<Record<string, string>>({});
|
||
|
||
// Колбэк для получения варианта от 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) => (
|
||
<FlagVariantFetcher
|
||
key={flag}
|
||
flag={flag}
|
||
onVariantLoaded={handleVariantLoaded}
|
||
/>
|
||
))}
|
||
|
||
{/*
|
||
✅ Показываем loader пока ВСЕ флаги не загружены
|
||
Это предотвращает flash когда контент меняется с дефолтного на AB вариант
|
||
*/}
|
||
{!allFlagsLoaded ? (
|
||
<FunnelLoadingScreen />
|
||
) : (
|
||
<UnleashContextProvider activeVariants={activeVariants}>
|
||
{children}
|
||
</UnleashContextProvider>
|
||
)}
|
||
</>
|
||
);
|
||
}
|