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

@ -142,7 +142,9 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
currentScreen.dateInput?.registrationFieldKey; currentScreen.dateInput?.registrationFieldKey;
updateSession({ updateSession({
...(shouldSkipAnswers ? {} : { ...(shouldSkipAnswers
? {}
: {
answers: { answers: {
[currentScreen.id]: answers[currentScreen.id], [currentScreen.id]: answers[currentScreen.id],
}, },
@ -159,9 +161,10 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
goToScreen(nextScreenId); goToScreen(nextScreenId);
}; };
const handleSelectionChange = (ids: string[]) => { const handleSelectionChange = (ids: string[], skipCheckChanges = false) => {
const prevSelectedIds = selectedOptionIds; const prevSelectedIds = selectedOptionIds;
const hasChanged = const hasChanged =
skipCheckChanges ||
prevSelectedIds.length !== ids.length || prevSelectedIds.length !== ids.length ||
prevSelectedIds.some((value, index) => value !== ids[index]); 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 { TemplateLayout } from "../layouts/TemplateLayout";
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers"; import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
interface ListTemplateProps { export interface ListTemplateProps {
screen: ListScreenDefinition; screen: ListScreenDefinition;
selectedOptionIds: string[]; selectedOptionIds: string[];
onSelectionChange: (selectedIds: string[]) => void; onSelectionChange: (
selectedIds: string[],
skipCheckChanges?: boolean
) => void;
actionButtonProps?: ActionButtonProps; actionButtonProps?: ActionButtonProps;
canGoBack: boolean; canGoBack: boolean;
onBack: () => void; onBack: () => void;
@ -39,7 +42,8 @@ export function ListTemplate({
screenProgress, screenProgress,
}: ListTemplateProps) { }: ListTemplateProps) {
const buttons = useMemo( const buttons = useMemo(
() => mapListOptionsToButtons(screen.list.options, screen.list.selectionType), () =>
mapListOptionsToButtons(screen.list.options, screen.list.selectionType),
[screen.list.options, screen.list.selectionType] [screen.list.options, screen.list.selectionType]
); );
@ -70,9 +74,15 @@ export function ListTemplate({
onSelectionChange(id ? [id] : []); onSelectionChange(id ? [id] : []);
}; };
const handleSelectChange: SelectAnswersListProps["onChangeSelectedAnswers"] = ( const handleRadioAnswerClick: RadioAnswersListProps["onAnswerClick"] = (
answers answer
) => { ) => {
const id = stringId(answer?.id);
onSelectionChange(id ? [id] : [], true);
};
const handleSelectChange: SelectAnswersListProps["onChangeSelectedAnswers"] =
(answers) => {
const ids = answers const ids = answers
?.map((answer) => stringId(answer.id)) ?.map((answer) => stringId(answer.id))
.filter((value): value is string => Boolean(value)); .filter((value): value is string => Boolean(value));
@ -84,6 +94,7 @@ export function ListTemplate({
answers: buttons, answers: buttons,
activeAnswer, activeAnswer,
onChangeSelectedAnswer: handleRadioChange, onChangeSelectedAnswer: handleRadioChange,
onAnswerClick: handleRadioAnswerClick,
}; };
const selectContent: SelectAnswersListProps = { const selectContent: SelectAnswersListProps = {
@ -92,16 +103,20 @@ export function ListTemplate({
onChangeSelectedAnswers: handleSelectChange, onChangeSelectedAnswers: handleSelectChange,
}; };
const actionButtonOptions = actionButtonProps ? { const actionButtonOptions = actionButtonProps
defaultText: actionButtonProps.children as string || "Next", ? {
defaultText: (actionButtonProps.children as string) || "Next",
// Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано // Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано
disabled: actionButtonProps.disabled || selectedOptionIds.length === 0, disabled: actionButtonProps.disabled || selectedOptionIds.length === 0,
onClick: () => { onClick: () => {
if (actionButtonProps.onClick) { if (actionButtonProps.onClick) {
actionButtonProps.onClick({} as React.MouseEvent<HTMLButtonElement>); actionButtonProps.onClick(
{} as React.MouseEvent<HTMLButtonElement>
);
} }
}, },
} : undefined; }
: undefined;
const layoutProps = createTemplateLayoutProps( const layoutProps = createTemplateLayoutProps(
screen, screen,

View File

@ -8,7 +8,7 @@ import type {
import { TemplateLayout } from "../layouts/TemplateLayout"; import { TemplateLayout } from "../layouts/TemplateLayout";
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers"; import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useRef } from "react"; import { useRef, useState } from "react";
import { import {
Header, Header,
JoinedToday, JoinedToday,
@ -109,12 +109,16 @@ export function TrialPaymentTemplate({
const currency = placement?.currency || Currency.USD; const currency = placement?.currency || Currency.USD;
const paymentUrl = placement?.paymentUrl || ""; 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=${( const redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${productId}&jwtToken=${token}&price=${(
(trialPrice || 100) / 100 (trialPrice || 100) / 100
).toFixed(2)}&currency=${currency}&${getTrackingCookiesForRedirect()}`; ).toFixed(2)}&currency=${currency}&${getTrackingCookiesForRedirect()}`;
console.log("redirectUrl", redirectUrl);
return window.location.replace(redirectUrl); return window.location.replace(redirectUrl);
}; };
@ -574,7 +578,7 @@ export function TrialPaymentTemplate({
<div ref={paymentSectionRef} className="w-full"> <div ref={paymentSectionRef} className="w-full">
<PaymentButtons <PaymentButtons
className="mt-[46px]" className="mt-[46px]"
buttons={screen.paymentButtons.buttons.map((b) => { buttons={screen.paymentButtons.buttons.map((b, index) => {
const icon = const icon =
b.icon === "pay" ? ( b.icon === "pay" ? (
<svg <svg
@ -650,10 +654,15 @@ export function TrialPaymentTemplate({
const className = b.primary ? "bg-primary" : undefined; const className = b.primary ? "bg-primary" : undefined;
return { return {
children: b.text, children:
icon, index === loadingButtonIndex ? (
<Spinner className="size-6" />
) : (
b.text
),
icon: loadingButtonIndex === index ? undefined : icon,
className, className,
onClick: handlePayClick, onClick: () => handlePayClick(index),
}; };
})} })}
/> />

View File

@ -89,7 +89,8 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
} }
}, [state, isHydrated]); }, [state, isHydrated]);
const registerScreenVisit = useCallback((funnelId: string, screenId: string) => { const registerScreenVisit = useCallback(
(funnelId: string, screenId: string) => {
setState((prev) => { setState((prev) => {
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
let previousState = prev[funnelId]; let previousState = prev[funnelId];
@ -123,7 +124,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
}, },
}; };
}); });
}, []); },
[]
);
const updateScreenAnswers = useCallback( const updateScreenAnswers = useCallback(
(funnelId: string, screenId: string, answers: string[]) => { (funnelId: string, screenId: string, answers: string[]) => {
@ -205,7 +208,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
[state, registerScreenVisit, updateScreenAnswers, resetFunnel] [state, registerScreenVisit, updateScreenAnswers, resetFunnel]
); );
return <FunnelContext.Provider value={value}>{children}</FunnelContext.Provider>; return (
<FunnelContext.Provider value={value}>{children}</FunnelContext.Provider>
);
} }
function useFunnelContext() { function useFunnelContext() {
@ -220,7 +225,19 @@ export function useFunnelRuntime(funnelId: string) {
const { state, registerScreenVisit, updateScreenAnswers, resetFunnel } = const { state, registerScreenVisit, updateScreenAnswers, resetFunnel } =
useFunnelContext(); 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( const setAnswers = useCallback(
(screenId: string, answers: string[]) => { (screenId: string, answers: string[]) => {

View File

@ -33,7 +33,7 @@ export interface ScreenRenderProps {
funnel: FunnelDefinition; funnel: FunnelDefinition;
screen: ScreenDefinition; screen: ScreenDefinition;
selectedOptionIds: string[]; selectedOptionIds: string[];
onSelectionChange: (ids: string[]) => void; onSelectionChange: (ids: string[], skipCheckChanges?: boolean) => void;
onContinue: () => void; onContinue: () => void;
canGoBack: boolean; canGoBack: boolean;
onBack: () => void; onBack: () => void;