fix
This commit is contained in:
parent
aa956adebb
commit
c4ba2ff541
@ -81,6 +81,11 @@
|
||||
"align": "left",
|
||||
"color": "default"
|
||||
},
|
||||
"bottomActionButton": {
|
||||
"show": false,
|
||||
"cornerRadius": "3xl",
|
||||
"showPrivacyTermsConsent": false
|
||||
},
|
||||
"navigation": {
|
||||
"rules": [],
|
||||
"defaultNextScreenId": "relationship-status",
|
||||
|
||||
@ -4,12 +4,19 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
import type { FunnelAnswers } from "./types";
|
||||
import {
|
||||
saveFunnelState,
|
||||
loadFunnelState,
|
||||
clearExpiredFunnelStates,
|
||||
clearFunnelState,
|
||||
} from "./storage";
|
||||
|
||||
interface FunnelRuntimeState {
|
||||
answers: FunnelAnswers;
|
||||
@ -61,10 +68,35 @@ interface FunnelProviderProps {
|
||||
|
||||
export function FunnelProvider({ children }: FunnelProviderProps) {
|
||||
const [state, setState] = useState<Record<string, FunnelRuntimeState>>({});
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
|
||||
// Восстанавливаем состояние из localStorage при монтировании
|
||||
useEffect(() => {
|
||||
// Очищаем устаревшие состояния
|
||||
clearExpiredFunnelStates();
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
|
||||
// Автоматически сохраняем состояние при изменениях
|
||||
useEffect(() => {
|
||||
if (!isHydrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем состояние каждой воронки
|
||||
for (const [funnelId, funnelState] of Object.entries(state)) {
|
||||
saveFunnelState(funnelId, funnelState);
|
||||
}
|
||||
}, [state, isHydrated]);
|
||||
|
||||
const registerScreenVisit = useCallback((funnelId: string, screenId: string) => {
|
||||
setState((prev) => {
|
||||
const previousState = prev[funnelId] ?? createInitialState();
|
||||
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
||||
let previousState = prev[funnelId];
|
||||
if (!previousState) {
|
||||
const loaded = loadFunnelState(funnelId);
|
||||
previousState = loaded ?? createInitialState();
|
||||
}
|
||||
const history = previousState.history ?? [];
|
||||
|
||||
let nextHistory = history;
|
||||
@ -96,7 +128,12 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
|
||||
const updateScreenAnswers = useCallback(
|
||||
(funnelId: string, screenId: string, answers: string[]) => {
|
||||
setState((prev) => {
|
||||
const previousState = prev[funnelId] ?? createInitialState();
|
||||
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
||||
let previousState = prev[funnelId];
|
||||
if (!previousState) {
|
||||
const loaded = loadFunnelState(funnelId);
|
||||
previousState = loaded ?? createInitialState();
|
||||
}
|
||||
const previousAnswers = previousState.answers ?? {};
|
||||
const existingAnswers = previousAnswers[screenId];
|
||||
|
||||
@ -150,6 +187,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Очищаем состояние из localStorage
|
||||
clearFunnelState(funnelId);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[funnelId]: {
|
||||
|
||||
@ -89,6 +89,11 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
||||
"align": "left",
|
||||
"color": "default"
|
||||
},
|
||||
"bottomActionButton": {
|
||||
"show": false,
|
||||
"cornerRadius": "3xl",
|
||||
"showPrivacyTermsConsent": false
|
||||
},
|
||||
"navigation": {
|
||||
"rules": [],
|
||||
"defaultNextScreenId": "relationship-status",
|
||||
|
||||
157
src/lib/funnel/storage.ts
Normal file
157
src/lib/funnel/storage.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import type { FunnelAnswers } from "./types";
|
||||
|
||||
/**
|
||||
* Структура данных для хранения состояния воронки
|
||||
*/
|
||||
export interface FunnelRuntimeState {
|
||||
answers: FunnelAnswers;
|
||||
history: string[];
|
||||
version: number;
|
||||
timestamp?: number; // Для отслеживания времени последнего обновления
|
||||
}
|
||||
|
||||
/**
|
||||
* Префикс для ключей в localStorage
|
||||
*/
|
||||
const STORAGE_PREFIX = "funnel_state_";
|
||||
|
||||
/**
|
||||
* Время жизни данных в localStorage (7 дней)
|
||||
*/
|
||||
const STORAGE_TTL = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Проверяет доступность localStorage
|
||||
*/
|
||||
function isLocalStorageAvailable(): boolean {
|
||||
try {
|
||||
const testKey = "__storage_test__";
|
||||
localStorage.setItem(testKey, "test");
|
||||
localStorage.removeItem(testKey);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает ключ для localStorage по ID воронки
|
||||
*/
|
||||
function getStorageKey(funnelId: string): string {
|
||||
return `${STORAGE_PREFIX}${funnelId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет состояние воронки в localStorage
|
||||
*/
|
||||
export function saveFunnelState(
|
||||
funnelId: string,
|
||||
state: FunnelRuntimeState
|
||||
): void {
|
||||
if (!isLocalStorageAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataToSave: FunnelRuntimeState = {
|
||||
...state,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
localStorage.setItem(getStorageKey(funnelId), JSON.stringify(dataToSave));
|
||||
} catch (error) {
|
||||
console.warn("Failed to save funnel state to localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает состояние воронки из localStorage
|
||||
*/
|
||||
export function loadFunnelState(funnelId: string): FunnelRuntimeState | null {
|
||||
if (!isLocalStorageAvailable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(getStorageKey(funnelId));
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(stored) as FunnelRuntimeState;
|
||||
|
||||
// Проверяем TTL
|
||||
if (data.timestamp) {
|
||||
const age = Date.now() - data.timestamp;
|
||||
if (age > STORAGE_TTL) {
|
||||
// Данные устарели, удаляем
|
||||
clearFunnelState(funnelId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.warn("Failed to load funnel state from localStorage:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает состояние воронки из localStorage
|
||||
*/
|
||||
export function clearFunnelState(funnelId: string): void {
|
||||
if (!isLocalStorageAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(getStorageKey(funnelId));
|
||||
} catch (error) {
|
||||
console.warn("Failed to clear funnel state from localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все устаревшие состояния воронок
|
||||
*/
|
||||
export function clearExpiredFunnelStates(): void {
|
||||
if (!isLocalStorageAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
// Проходим по всем ключам в localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!key?.startsWith(STORAGE_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = localStorage.getItem(key);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(value) as FunnelRuntimeState;
|
||||
if (data.timestamp && now - data.timestamp > STORAGE_TTL) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
} catch {
|
||||
// Если не удалось распарсить - удаляем
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем устаревшие ключи
|
||||
for (const key of keysToRemove) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to clear expired funnel states:", error);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user