From f8305e193a4d0003d8006a7145a5bf69c587afd2 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Sat, 11 Oct 2025 20:34:04 +0400 Subject: [PATCH] special-offer add setting to admin --- .../admin/builder/Sidebar/BuilderSidebar.tsx | 28 ++++++++++++++ .../admin/builder/Sidebar/NavigationPanel.tsx | 31 +++++++++++++++ src/components/funnel/FunnelRuntime.tsx | 38 +++++++++++++++++++ .../templates/SpecialOffer/SpecialOffer.tsx | 2 +- src/lib/admin/builder/state/reducer.ts | 3 ++ src/lib/admin/builder/state/types.ts | 1 + src/lib/admin/builder/utils.ts | 3 ++ src/lib/admin/builder/validation.ts | 23 +++++++++++ src/lib/funnel/types.ts | 2 + src/lib/models/Funnel.ts | 1 + 10 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/components/admin/builder/Sidebar/BuilderSidebar.tsx b/src/components/admin/builder/Sidebar/BuilderSidebar.tsx index 98cd925..85df9d7 100644 --- a/src/components/admin/builder/Sidebar/BuilderSidebar.tsx +++ b/src/components/admin/builder/Sidebar/BuilderSidebar.tsx @@ -143,6 +143,8 @@ export function BuilderSidebar() { rules: navigationUpdates.rules ?? screen.navigation?.rules ?? [], isEndScreen: navigationUpdates.isEndScreen ?? screen.navigation?.isEndScreen, + onBackScreenId: + navigationUpdates.onBackScreenId ?? screen.navigation?.onBackScreenId, }, }, }); @@ -627,6 +629,32 @@ export function BuilderSidebar() { )} + + {/* Экран при попытке перехода назад */} + {selectedScreenIsListType && diff --git a/src/components/admin/builder/Sidebar/NavigationPanel.tsx b/src/components/admin/builder/Sidebar/NavigationPanel.tsx index b13e6b0..05ae9b8 100644 --- a/src/components/admin/builder/Sidebar/NavigationPanel.tsx +++ b/src/components/admin/builder/Sidebar/NavigationPanel.tsx @@ -50,6 +50,9 @@ export function NavigationPanel({ screen }: NavigationPanelProps) { isEndScreen: navigationUpdates.isEndScreen ?? targetScreen.navigation?.isEndScreen, + onBackScreenId: + navigationUpdates.onBackScreenId ?? + targetScreen.navigation?.onBackScreenId, }, }, }); @@ -152,6 +155,34 @@ export function NavigationPanel({ screen }: NavigationPanelProps) { )} + + {/* Экран при попытке перехода назад */} + {/* {!screen.navigation?.isEndScreen && ( */} + + {/* )} */} {selectedScreenIsListType && !screen.navigation?.isEndScreen && ( diff --git a/src/components/funnel/FunnelRuntime.tsx b/src/components/funnel/FunnelRuntime.tsx index d518d73..6640939 100644 --- a/src/components/funnel/FunnelRuntime.tsx +++ b/src/components/funnel/FunnelRuntime.tsx @@ -257,6 +257,14 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { }; const onBack = () => { + const backTarget: string | undefined = currentScreen.navigation?.onBackScreenId; + + if (backTarget) { + // Переназначаем назад на конкретный экран без роста истории + router.replace(`/${funnel.meta.id}/${backTarget}`); + return; + } + const currentIndex = historyWithCurrent.lastIndexOf(currentScreen.id); if (currentIndex > 0) { @@ -272,6 +280,36 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { router.back(); }; + // Перехват аппаратной/браузерной кнопки Назад, когда настроен onBackScreenId + useEffect(() => { + const backTarget: string | undefined = currentScreen.navigation?.onBackScreenId; + if (!backTarget) return; + + const pushTrap = () => { + try { + window.history.pushState({ __trap: true }, "", window.location.href); + } catch {} + }; + + pushTrap(); + + function isTrapState(state: unknown): state is { __trap?: boolean } { + return typeof state === "object" && state !== null && "__trap" in (state as Record); + } + + const handlePopState = (e: PopStateEvent) => { + if (isTrapState(e.state) && e.state.__trap) { + pushTrap(); + router.replace(`/${funnel.meta.id}/${backTarget}`); + } + }; + + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, [currentScreen.id, currentScreen.navigation?.onBackScreenId, funnel.meta.id, router]); + const canGoBack = historyWithCurrent.lastIndexOf(currentScreen.id) > 0; return renderScreen({ diff --git a/src/components/funnel/templates/SpecialOffer/SpecialOffer.tsx b/src/components/funnel/templates/SpecialOffer/SpecialOffer.tsx index 17e97b2..c158657 100644 --- a/src/components/funnel/templates/SpecialOffer/SpecialOffer.tsx +++ b/src/components/funnel/templates/SpecialOffer/SpecialOffer.tsx @@ -38,7 +38,7 @@ export function SpecialOfferTemplate({ defaultTexts, }: SpecialOfferProps) { const token = useClientToken(); - const paymentId = "main"; + const paymentId = "main_secret_discount"; const { placement, isLoading } = usePaymentPlacement({ funnel, paymentId }); const [isLoadingRedirect, setIsLoadingRedirect] = useState(false); diff --git a/src/lib/admin/builder/state/reducer.ts b/src/lib/admin/builder/state/reducer.ts index 28f5577..a9c876e 100644 --- a/src/lib/admin/builder/state/reducer.ts +++ b/src/lib/admin/builder/state/reducer.ts @@ -370,6 +370,9 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil defaultNextScreenId: navigation.defaultNextScreenId ?? undefined, rules: navigation.rules ?? [], isEndScreen: navigation.isEndScreen, + ...("onBackScreenId" in navigation + ? { onBackScreenId: navigation.onBackScreenId ?? undefined } + : {}), }, } : screen diff --git a/src/lib/admin/builder/state/types.ts b/src/lib/admin/builder/state/types.ts index 231782c..29da528 100644 --- a/src/lib/admin/builder/state/types.ts +++ b/src/lib/admin/builder/state/types.ts @@ -26,6 +26,7 @@ export type BuilderAction = defaultNextScreenId?: string | null; rules?: NavigationRuleDefinition[]; isEndScreen?: boolean; + onBackScreenId?: string | null; }; }; } diff --git a/src/lib/admin/builder/utils.ts b/src/lib/admin/builder/utils.ts index 699fd24..f674f2d 100644 --- a/src/lib/admin/builder/utils.ts +++ b/src/lib/admin/builder/utils.ts @@ -87,6 +87,9 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial ({ nextScreenId: rule.nextScreenId, conditions: rule.conditions.map((condition) => ({ diff --git a/src/lib/admin/builder/validation.ts b/src/lib/admin/builder/validation.ts index 9174d81..b28803f 100644 --- a/src/lib/admin/builder/validation.ts +++ b/src/lib/admin/builder/validation.ts @@ -99,6 +99,29 @@ function validateNavigation(screen: BuilderScreen, state: BuilderState, issues: ); } + // Проверка onBackScreenId + const onBackScreenId: string | undefined = navigation.onBackScreenId as string | undefined; + if (onBackScreenId) { + if (!screenIds.has(onBackScreenId)) { + issues.push( + createIssue( + "error", + `Экран \`${screen.id}\` ссылается на несуществующий back-экран \`${onBackScreenId}\``, + { screenId: screen.id } + ) + ); + } + if (onBackScreenId === screen.id) { + issues.push( + createIssue( + "warning", + `Экран \`${screen.id}\`: выбран тот же экран для перехода назад (нет смысла)`, + { screenId: screen.id } + ) + ); + } + } + for (const [ruleIndex, rule] of (navigation.rules ?? []).entries()) { if (!screenIds.has(rule.nextScreenId)) { issues.push( diff --git a/src/lib/funnel/types.ts b/src/lib/funnel/types.ts index d46aa4e..9781207 100644 --- a/src/lib/funnel/types.ts +++ b/src/lib/funnel/types.ts @@ -106,6 +106,8 @@ export interface NavigationDefinition { defaultNextScreenId?: string; rules?: NavigationRuleDefinition[]; isEndScreen?: boolean; // Указывает что это финальный экран воронки + /** Экран, на который нужно перейти при попытке возврата назад (UI/браузер) */ + onBackScreenId?: string; } // Рекурсивный Partial для глубоких вложенных объектов diff --git a/src/lib/models/Funnel.ts b/src/lib/models/Funnel.ts index 75e282c..cc46e23 100644 --- a/src/lib/models/Funnel.ts +++ b/src/lib/models/Funnel.ts @@ -145,6 +145,7 @@ const NavigationDefinitionSchema = new Schema( rules: [NavigationRuleSchema], defaultNextScreenId: String, isEndScreen: { type: Boolean, default: false }, + onBackScreenId: String, }, { _id: false } );