add payment buttons loader state
fix click active answer
fix load state
This commit is contained in:
gofnnp 2025-10-08 23:19:36 +04:00
parent fadf12e780
commit 1b13a6b0ab
5 changed files with 115 additions and 71 deletions

View File

@ -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]);

View File

@ -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<HTMLButtonElement>);
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<HTMLButtonElement>
);
}
},
}
},
} : undefined;
: undefined;
const layoutProps = createTemplateLayoutProps(
screen,

View File

@ -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<number>();
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)}&currency=${currency}&${getTrackingCookiesForRedirect()}`;
console.log("redirectUrl", redirectUrl);
return window.location.replace(redirectUrl);
};
@ -574,7 +578,7 @@ export function TrialPaymentTemplate({
<div ref={paymentSectionRef} className="w-full">
<PaymentButtons
className="mt-[46px]"
buttons={screen.paymentButtons.buttons.map((b) => {
buttons={screen.paymentButtons.buttons.map((b, index) => {
const icon =
b.icon === "pay" ? (
<svg
@ -650,10 +654,15 @@ export function TrialPaymentTemplate({
const className = b.primary ? "bg-primary" : undefined;
return {
children: b.text,
icon,
children:
index === loadingButtonIndex ? (
<Spinner className="size-6" />
) : (
b.text
),
icon: loadingButtonIndex === index ? undefined : icon,
className,
onClick: handlePayClick,
onClick: () => handlePayClick(index),
};
})}
/>

View File

@ -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 <FunnelContext.Provider value={value}>{children}</FunnelContext.Provider>;
return (
<FunnelContext.Provider value={value}>{children}</FunnelContext.Provider>
);
}
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<FunnelRuntimeState>(
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[]) => {

View File

@ -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;