special-offer

add setting to admin
This commit is contained in:
gofnnp 2025-10-11 20:34:04 +04:00
parent 90126bbf3b
commit f8305e193a
10 changed files with 131 additions and 1 deletions

View File

@ -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() {
</select>
</label>
)}
{/* Экран при попытке перехода назад */}
<label className="flex flex-col gap-2 mt-3">
<span className="text-sm font-medium text-muted-foreground">
Экран при попытке перехода назад
</span>
<select
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
value={selectedScreen.navigation?.onBackScreenId ?? ""}
onChange={(e) =>
updateNavigation(selectedScreen, {
...(selectedScreen.navigation ?? {}),
onBackScreenId: e.target.value || undefined,
})
}
>
<option value=""></option>
{screenOptions
.filter((s) => s.id !== selectedScreen.id)
.map((s) => (
<option key={s.id} value={s.id}>
{s.title}
</option>
))}
</select>
</label>
</Section>
{selectedScreenIsListType &&

View File

@ -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) {
</select>
</label>
)}
{/* Экран при попытке перехода назад */}
{/* {!screen.navigation?.isEndScreen && ( */}
<label className="flex flex-col gap-2 mt-3">
<span className="text-sm font-medium text-muted-foreground">
Экран при попытке перехода назад
</span>
<select
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
value={screen.navigation?.onBackScreenId ?? ""}
onChange={(e) =>
updateNavigation(screen, {
...(screen.navigation ?? {}),
onBackScreenId: e.target.value || undefined,
})
}
>
<option value=""></option>
{screenOptions
.filter((s) => s.id !== screen.id)
.map((s) => (
<option key={s.id} value={s.id}>
{s.title}
</option>
))}
</select>
</label>
{/* )} */}
</Section>
{selectedScreenIsListType && !screen.navigation?.isEndScreen && (

View File

@ -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<string, unknown>);
}
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({

View File

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

View File

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

View File

@ -26,6 +26,7 @@ export type BuilderAction =
defaultNextScreenId?: string | null;
rules?: NavigationRuleDefinition[];
isEndScreen?: boolean;
onBackScreenId?: string | null;
};
};
}

View File

@ -87,6 +87,9 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderSc
navigation: screen.navigation
? {
defaultNextScreenId: screen.navigation.defaultNextScreenId,
...(screen.navigation.onBackScreenId
? { onBackScreenId: screen.navigation.onBackScreenId }
: {}),
rules: screen.navigation.rules?.map((rule) => ({
nextScreenId: rule.nextScreenId,
conditions: rule.conditions.map((condition) => ({

View File

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

View File

@ -106,6 +106,8 @@ export interface NavigationDefinition {
defaultNextScreenId?: string;
rules?: NavigationRuleDefinition[];
isEndScreen?: boolean; // Указывает что это финальный экран воронки
/** Экран, на который нужно перейти при попытке возврата назад (UI/браузер) */
onBackScreenId?: string;
}
// Рекурсивный Partial для глубоких вложенных объектов

View File

@ -145,6 +145,7 @@ const NavigationDefinitionSchema = new Schema(
rules: [NavigationRuleSchema],
defaultNextScreenId: String,
isEndScreen: { type: Boolean, default: false },
onBackScreenId: String,
},
{ _id: false }
);