special-offer
add setting to admin
This commit is contained in:
parent
90126bbf3b
commit
f8305e193a
@ -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 &&
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -26,6 +26,7 @@ export type BuilderAction =
|
||||
defaultNextScreenId?: string | null;
|
||||
rules?: NavigationRuleDefinition[];
|
||||
isEndScreen?: boolean;
|
||||
onBackScreenId?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -106,6 +106,8 @@ export interface NavigationDefinition {
|
||||
defaultNextScreenId?: string;
|
||||
rules?: NavigationRuleDefinition[];
|
||||
isEndScreen?: boolean; // Указывает что это финальный экран воронки
|
||||
/** Экран, на который нужно перейти при попытке возврата назад (UI/браузер) */
|
||||
onBackScreenId?: string;
|
||||
}
|
||||
|
||||
// Рекурсивный Partial для глубоких вложенных объектов
|
||||
|
||||
@ -145,6 +145,7 @@ const NavigationDefinitionSchema = new Schema(
|
||||
rules: [NavigationRuleSchema],
|
||||
defaultNextScreenId: String,
|
||||
isEndScreen: { type: Boolean, default: false },
|
||||
onBackScreenId: String,
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user