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 ?? [],
|
rules: navigationUpdates.rules ?? screen.navigation?.rules ?? [],
|
||||||
isEndScreen:
|
isEndScreen:
|
||||||
navigationUpdates.isEndScreen ?? screen.navigation?.isEndScreen,
|
navigationUpdates.isEndScreen ?? screen.navigation?.isEndScreen,
|
||||||
|
onBackScreenId:
|
||||||
|
navigationUpdates.onBackScreenId ?? screen.navigation?.onBackScreenId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -627,6 +629,32 @@ export function BuilderSidebar() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
</Section>
|
||||||
|
|
||||||
{selectedScreenIsListType &&
|
{selectedScreenIsListType &&
|
||||||
|
|||||||
@ -50,6 +50,9 @@ export function NavigationPanel({ screen }: NavigationPanelProps) {
|
|||||||
isEndScreen:
|
isEndScreen:
|
||||||
navigationUpdates.isEndScreen ??
|
navigationUpdates.isEndScreen ??
|
||||||
targetScreen.navigation?.isEndScreen,
|
targetScreen.navigation?.isEndScreen,
|
||||||
|
onBackScreenId:
|
||||||
|
navigationUpdates.onBackScreenId ??
|
||||||
|
targetScreen.navigation?.onBackScreenId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -152,6 +155,34 @@ export function NavigationPanel({ screen }: NavigationPanelProps) {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
</Section>
|
||||||
|
|
||||||
{selectedScreenIsListType && !screen.navigation?.isEndScreen && (
|
{selectedScreenIsListType && !screen.navigation?.isEndScreen && (
|
||||||
|
|||||||
@ -257,6 +257,14 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBack = () => {
|
const onBack = () => {
|
||||||
|
const backTarget: string | undefined = currentScreen.navigation?.onBackScreenId;
|
||||||
|
|
||||||
|
if (backTarget) {
|
||||||
|
// Переназначаем назад на конкретный экран без роста истории
|
||||||
|
router.replace(`/${funnel.meta.id}/${backTarget}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentIndex = historyWithCurrent.lastIndexOf(currentScreen.id);
|
const currentIndex = historyWithCurrent.lastIndexOf(currentScreen.id);
|
||||||
|
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
@ -272,6 +280,36 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) {
|
|||||||
router.back();
|
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;
|
const canGoBack = historyWithCurrent.lastIndexOf(currentScreen.id) > 0;
|
||||||
|
|
||||||
return renderScreen({
|
return renderScreen({
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function SpecialOfferTemplate({
|
|||||||
defaultTexts,
|
defaultTexts,
|
||||||
}: SpecialOfferProps) {
|
}: SpecialOfferProps) {
|
||||||
const token = useClientToken();
|
const token = useClientToken();
|
||||||
const paymentId = "main";
|
const paymentId = "main_secret_discount";
|
||||||
const { placement, isLoading } = usePaymentPlacement({ funnel, paymentId });
|
const { placement, isLoading } = usePaymentPlacement({ funnel, paymentId });
|
||||||
const [isLoadingRedirect, setIsLoadingRedirect] = useState(false);
|
const [isLoadingRedirect, setIsLoadingRedirect] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@ -370,6 +370,9 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
|||||||
defaultNextScreenId: navigation.defaultNextScreenId ?? undefined,
|
defaultNextScreenId: navigation.defaultNextScreenId ?? undefined,
|
||||||
rules: navigation.rules ?? [],
|
rules: navigation.rules ?? [],
|
||||||
isEndScreen: navigation.isEndScreen,
|
isEndScreen: navigation.isEndScreen,
|
||||||
|
...("onBackScreenId" in navigation
|
||||||
|
? { onBackScreenId: navigation.onBackScreenId ?? undefined }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: screen
|
: screen
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export type BuilderAction =
|
|||||||
defaultNextScreenId?: string | null;
|
defaultNextScreenId?: string | null;
|
||||||
rules?: NavigationRuleDefinition[];
|
rules?: NavigationRuleDefinition[];
|
||||||
isEndScreen?: boolean;
|
isEndScreen?: boolean;
|
||||||
|
onBackScreenId?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,9 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderSc
|
|||||||
navigation: screen.navigation
|
navigation: screen.navigation
|
||||||
? {
|
? {
|
||||||
defaultNextScreenId: screen.navigation.defaultNextScreenId,
|
defaultNextScreenId: screen.navigation.defaultNextScreenId,
|
||||||
|
...(screen.navigation.onBackScreenId
|
||||||
|
? { onBackScreenId: screen.navigation.onBackScreenId }
|
||||||
|
: {}),
|
||||||
rules: screen.navigation.rules?.map((rule) => ({
|
rules: screen.navigation.rules?.map((rule) => ({
|
||||||
nextScreenId: rule.nextScreenId,
|
nextScreenId: rule.nextScreenId,
|
||||||
conditions: rule.conditions.map((condition) => ({
|
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()) {
|
for (const [ruleIndex, rule] of (navigation.rules ?? []).entries()) {
|
||||||
if (!screenIds.has(rule.nextScreenId)) {
|
if (!screenIds.has(rule.nextScreenId)) {
|
||||||
issues.push(
|
issues.push(
|
||||||
|
|||||||
@ -106,6 +106,8 @@ export interface NavigationDefinition {
|
|||||||
defaultNextScreenId?: string;
|
defaultNextScreenId?: string;
|
||||||
rules?: NavigationRuleDefinition[];
|
rules?: NavigationRuleDefinition[];
|
||||||
isEndScreen?: boolean; // Указывает что это финальный экран воронки
|
isEndScreen?: boolean; // Указывает что это финальный экран воронки
|
||||||
|
/** Экран, на который нужно перейти при попытке возврата назад (UI/браузер) */
|
||||||
|
onBackScreenId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Рекурсивный Partial для глубоких вложенных объектов
|
// Рекурсивный Partial для глубоких вложенных объектов
|
||||||
|
|||||||
@ -145,6 +145,7 @@ const NavigationDefinitionSchema = new Schema(
|
|||||||
rules: [NavigationRuleSchema],
|
rules: [NavigationRuleSchema],
|
||||||
defaultNextScreenId: String,
|
defaultNextScreenId: String,
|
||||||
isEndScreen: { type: Boolean, default: false },
|
isEndScreen: { type: Boolean, default: false },
|
||||||
|
onBackScreenId: String,
|
||||||
},
|
},
|
||||||
{ _id: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user