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 }
);