w-funnel/src/components/funnel/FunnelUnleashWrapper.tsx
dev.daminik00 ea381ea399 ab test
2025-10-30 01:56:59 +01:00

175 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
)}
</>
);
}