edits
add payment buttons loader state fix click active answer fix load state
This commit is contained in:
parent
fadf12e780
commit
1b13a6b0ab
@ -136,17 +136,19 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Для date экранов с registrationFieldKey НЕ отправляем answers
|
// Для date экранов с registrationFieldKey НЕ отправляем answers
|
||||||
const shouldSkipAnswers =
|
const shouldSkipAnswers =
|
||||||
currentScreen.template === "date" &&
|
currentScreen.template === "date" &&
|
||||||
"dateInput" in currentScreen &&
|
"dateInput" in currentScreen &&
|
||||||
currentScreen.dateInput?.registrationFieldKey;
|
currentScreen.dateInput?.registrationFieldKey;
|
||||||
|
|
||||||
updateSession({
|
updateSession({
|
||||||
...(shouldSkipAnswers ? {} : {
|
...(shouldSkipAnswers
|
||||||
answers: {
|
? {}
|
||||||
[currentScreen.id]: answers[currentScreen.id],
|
: {
|
||||||
},
|
answers: {
|
||||||
}),
|
[currentScreen.id]: answers[currentScreen.id],
|
||||||
|
},
|
||||||
|
}),
|
||||||
// Добавляем данные с registrationFieldKey
|
// Добавляем данные с registrationFieldKey
|
||||||
...sessionData,
|
...sessionData,
|
||||||
});
|
});
|
||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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,20 +74,27 @@ export function ListTemplate({
|
|||||||
onSelectionChange(id ? [id] : []);
|
onSelectionChange(id ? [id] : []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange: SelectAnswersListProps["onChangeSelectedAnswers"] = (
|
const handleRadioAnswerClick: RadioAnswersListProps["onAnswerClick"] = (
|
||||||
answers
|
answer
|
||||||
) => {
|
) => {
|
||||||
const ids = answers
|
const id = stringId(answer?.id);
|
||||||
?.map((answer) => stringId(answer.id))
|
onSelectionChange(id ? [id] : [], true);
|
||||||
.filter((value): value is string => Boolean(value));
|
|
||||||
|
|
||||||
onSelectionChange(ids ?? []);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = {
|
const radioContent: RadioAnswersListProps = {
|
||||||
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",
|
? {
|
||||||
// Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано
|
defaultText: (actionButtonProps.children as string) || "Next",
|
||||||
disabled: actionButtonProps.disabled || selectedOptionIds.length === 0,
|
// Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано
|
||||||
onClick: () => {
|
disabled: actionButtonProps.disabled || selectedOptionIds.length === 0,
|
||||||
if (actionButtonProps.onClick) {
|
onClick: () => {
|
||||||
actionButtonProps.onClick({} as React.MouseEvent<HTMLButtonElement>);
|
if (actionButtonProps.onClick) {
|
||||||
|
actionButtonProps.onClick(
|
||||||
|
{} as React.MouseEvent<HTMLButtonElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
: undefined;
|
||||||
} : undefined;
|
|
||||||
|
|
||||||
const layoutProps = createTemplateLayoutProps(
|
const layoutProps = createTemplateLayoutProps(
|
||||||
screen,
|
screen,
|
||||||
|
|||||||
@ -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)}¤cy=${currency}&${getTrackingCookiesForRedirect()}`;
|
).toFixed(2)}¤cy=${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),
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -89,41 +89,44 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
|
|||||||
}
|
}
|
||||||
}, [state, isHydrated]);
|
}, [state, isHydrated]);
|
||||||
|
|
||||||
const registerScreenVisit = useCallback((funnelId: string, screenId: string) => {
|
const registerScreenVisit = useCallback(
|
||||||
setState((prev) => {
|
(funnelId: string, screenId: string) => {
|
||||||
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
setState((prev) => {
|
||||||
let previousState = prev[funnelId];
|
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
||||||
if (!previousState) {
|
let previousState = prev[funnelId];
|
||||||
const loaded = loadFunnelState(funnelId);
|
if (!previousState) {
|
||||||
previousState = loaded ?? createInitialState();
|
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 history = previousState.history ?? [];
|
||||||
|
|
||||||
if (nextHistory === history) {
|
let nextHistory = history;
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (history.length === 0 || history[history.length - 1] !== screenId) {
|
||||||
...prev,
|
const existingIndex = history.indexOf(screenId);
|
||||||
[funnelId]: {
|
|
||||||
...previousState,
|
if (existingIndex === -1) {
|
||||||
history: nextHistory,
|
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(
|
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[]) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user