diff --git a/src/components/funnel/FunnelRuntime.tsx b/src/components/funnel/FunnelRuntime.tsx index a7a6150..d518d73 100644 --- a/src/components/funnel/FunnelRuntime.tsx +++ b/src/components/funnel/FunnelRuntime.tsx @@ -136,17 +136,19 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { ); // Для date экранов с registrationFieldKey НЕ отправляем answers - const shouldSkipAnswers = - currentScreen.template === "date" && - "dateInput" in currentScreen && + const shouldSkipAnswers = + currentScreen.template === "date" && + "dateInput" in currentScreen && currentScreen.dateInput?.registrationFieldKey; updateSession({ - ...(shouldSkipAnswers ? {} : { - answers: { - [currentScreen.id]: answers[currentScreen.id], - }, - }), + ...(shouldSkipAnswers + ? {} + : { + answers: { + [currentScreen.id]: answers[currentScreen.id], + }, + }), // Добавляем данные с registrationFieldKey ...sessionData, }); @@ -159,9 +161,10 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { goToScreen(nextScreenId); }; - const handleSelectionChange = (ids: string[]) => { + const handleSelectionChange = (ids: string[], skipCheckChanges = false) => { const prevSelectedIds = selectedOptionIds; const hasChanged = + skipCheckChanges || prevSelectedIds.length !== ids.length || prevSelectedIds.some((value, index) => value !== ids[index]); diff --git a/src/components/funnel/templates/ListTemplate/ListTemplate.tsx b/src/components/funnel/templates/ListTemplate/ListTemplate.tsx index 6021fdf..771acc9 100644 --- a/src/components/funnel/templates/ListTemplate/ListTemplate.tsx +++ b/src/components/funnel/templates/ListTemplate/ListTemplate.tsx @@ -12,10 +12,13 @@ import type { ListScreenDefinition } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers"; -interface ListTemplateProps { +export interface ListTemplateProps { screen: ListScreenDefinition; selectedOptionIds: string[]; - onSelectionChange: (selectedIds: string[]) => void; + onSelectionChange: ( + selectedIds: string[], + skipCheckChanges?: boolean + ) => void; actionButtonProps?: ActionButtonProps; canGoBack: boolean; onBack: () => void; @@ -39,7 +42,8 @@ export function ListTemplate({ screenProgress, }: ListTemplateProps) { const buttons = useMemo( - () => mapListOptionsToButtons(screen.list.options, screen.list.selectionType), + () => + mapListOptionsToButtons(screen.list.options, screen.list.selectionType), [screen.list.options, screen.list.selectionType] ); @@ -70,20 +74,27 @@ export function ListTemplate({ onSelectionChange(id ? [id] : []); }; - const handleSelectChange: SelectAnswersListProps["onChangeSelectedAnswers"] = ( - answers + const handleRadioAnswerClick: RadioAnswersListProps["onAnswerClick"] = ( + answer ) => { - const ids = answers - ?.map((answer) => stringId(answer.id)) - .filter((value): value is string => Boolean(value)); - - onSelectionChange(ids ?? []); + const id = stringId(answer?.id); + onSelectionChange(id ? [id] : [], true); }; + const handleSelectChange: SelectAnswersListProps["onChangeSelectedAnswers"] = + (answers) => { + const ids = answers + ?.map((answer) => stringId(answer.id)) + .filter((value): value is string => Boolean(value)); + + onSelectionChange(ids ?? []); + }; + const radioContent: RadioAnswersListProps = { answers: buttons, activeAnswer, onChangeSelectedAnswer: handleRadioChange, + onAnswerClick: handleRadioAnswerClick, }; const selectContent: SelectAnswersListProps = { @@ -92,16 +103,20 @@ export function ListTemplate({ onChangeSelectedAnswers: handleSelectChange, }; - const actionButtonOptions = actionButtonProps ? { - defaultText: actionButtonProps.children as string || "Next", - // Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано - disabled: actionButtonProps.disabled || selectedOptionIds.length === 0, - onClick: () => { - if (actionButtonProps.onClick) { - actionButtonProps.onClick({} as React.MouseEvent); + const actionButtonOptions = actionButtonProps + ? { + defaultText: (actionButtonProps.children as string) || "Next", + // Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано + disabled: actionButtonProps.disabled || selectedOptionIds.length === 0, + onClick: () => { + if (actionButtonProps.onClick) { + actionButtonProps.onClick( + {} as React.MouseEvent + ); + } + }, } - }, - } : undefined; + : undefined; const layoutProps = createTemplateLayoutProps( screen, diff --git a/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx b/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx index 5d1215d..f01bdbf 100644 --- a/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx +++ b/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx @@ -8,7 +8,7 @@ import type { import { TemplateLayout } from "../layouts/TemplateLayout"; import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers"; import { cn } from "@/lib/utils"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import { Header, JoinedToday, @@ -109,12 +109,16 @@ export function TrialPaymentTemplate({ const currency = placement?.currency || Currency.USD; const paymentUrl = placement?.paymentUrl || ""; - const handlePayClick = () => { + const [loadingButtonIndex, setLoadingButtonIndex] = useState(); + + const handlePayClick = (buttonIndex: number) => { + if (!!loadingButtonIndex || loadingButtonIndex === 0) { + return; + } + setLoadingButtonIndex(buttonIndex); const redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${productId}&jwtToken=${token}&price=${( (trialPrice || 100) / 100 ).toFixed(2)}¤cy=${currency}&${getTrackingCookiesForRedirect()}`; - console.log("redirectUrl", redirectUrl); - return window.location.replace(redirectUrl); }; @@ -574,7 +578,7 @@ export function TrialPaymentTemplate({
{ + buttons={screen.paymentButtons.buttons.map((b, index) => { const icon = b.icon === "pay" ? ( + ) : ( + b.text + ), + icon: loadingButtonIndex === index ? undefined : icon, className, - onClick: handlePayClick, + onClick: () => handlePayClick(index), }; })} /> diff --git a/src/lib/funnel/FunnelProvider.tsx b/src/lib/funnel/FunnelProvider.tsx index b28b987..618c950 100644 --- a/src/lib/funnel/FunnelProvider.tsx +++ b/src/lib/funnel/FunnelProvider.tsx @@ -89,41 +89,44 @@ export function FunnelProvider({ children }: FunnelProviderProps) { } }, [state, isHydrated]); - const registerScreenVisit = useCallback((funnelId: string, screenId: string) => { - setState((prev) => { - // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти - let previousState = prev[funnelId]; - if (!previousState) { - const loaded = loadFunnelState(funnelId); - previousState = loaded ?? createInitialState(); - } - const history = previousState.history ?? []; - - let nextHistory = history; - - if (history.length === 0 || history[history.length - 1] !== screenId) { - const existingIndex = history.indexOf(screenId); - - if (existingIndex === -1) { - nextHistory = [...history, screenId]; - } else if (existingIndex !== history.length - 1) { - nextHistory = history.slice(0, existingIndex + 1); + const registerScreenVisit = useCallback( + (funnelId: string, screenId: string) => { + setState((prev) => { + // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти + let previousState = prev[funnelId]; + if (!previousState) { + const loaded = loadFunnelState(funnelId); + previousState = loaded ?? createInitialState(); } - } + const history = previousState.history ?? []; - if (nextHistory === history) { - return prev; - } + let nextHistory = history; - return { - ...prev, - [funnelId]: { - ...previousState, - history: nextHistory, - }, - }; - }); - }, []); + if (history.length === 0 || history[history.length - 1] !== screenId) { + const existingIndex = history.indexOf(screenId); + + if (existingIndex === -1) { + nextHistory = [...history, screenId]; + } else if (existingIndex !== history.length - 1) { + nextHistory = history.slice(0, existingIndex + 1); + } + } + + if (nextHistory === history) { + return prev; + } + + return { + ...prev, + [funnelId]: { + ...previousState, + history: nextHistory, + }, + }; + }); + }, + [] + ); const updateScreenAnswers = useCallback( (funnelId: string, screenId: string, answers: string[]) => { @@ -205,7 +208,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) { [state, registerScreenVisit, updateScreenAnswers, resetFunnel] ); - return {children}; + return ( + {children} + ); } function useFunnelContext() { @@ -220,7 +225,19 @@ export function useFunnelRuntime(funnelId: string) { const { state, registerScreenVisit, updateScreenAnswers, resetFunnel } = useFunnelContext(); - const runtime = state[funnelId] ?? DEFAULT_RUNTIME_STATE; + const [runtime, setRuntime] = useState( + DEFAULT_RUNTIME_STATE + ); + + useEffect(() => { + const inMemory = state[funnelId]; + if (inMemory) { + setRuntime(inMemory); + return; + } + const loaded = loadFunnelState(funnelId); + setRuntime(loaded ?? DEFAULT_RUNTIME_STATE); + }, [state, funnelId]); const setAnswers = useCallback( (screenId: string, answers: string[]) => { diff --git a/src/lib/funnel/screenRenderer.tsx b/src/lib/funnel/screenRenderer.tsx index 9dbe908..9e874ee 100644 --- a/src/lib/funnel/screenRenderer.tsx +++ b/src/lib/funnel/screenRenderer.tsx @@ -33,7 +33,7 @@ export interface ScreenRenderProps { funnel: FunnelDefinition; screen: ScreenDefinition; selectedOptionIds: string[]; - onSelectionChange: (ids: string[]) => void; + onSelectionChange: (ids: string[], skipCheckChanges?: boolean) => void; onContinue: () => void; canGoBack: boolean; onBack: () => void;