/* ============================================================
EnrollForm — enrollment + payment for the author's course
Monochrome, hairline-driven. Mock payment flow (prototype):
on submit it transitions to a confirmation state.
NOTE: price is a placeholder — confirm the real number.
============================================================ */
/* Тарифы и цены живут в SalesSections.jsx (SALES_TARIFFS) —
этот файл только читает их. Правьте цены там. */
const CNY_RATE = 10.5831; // ₽ за 1 ¥ — курс ЦБ РФ на 10.06.2026
const CNY_FEE = 0.02; // +2% комиссия за оплату в юанях
const toCny = (rub) => Math.ceil((rub / CNY_RATE) * (1 + CNY_FEE));
const fmt = (n) => n.toLocaleString("ru-RU");
const fmtCny = (n) => "¥" + n.toLocaleString("zh-CN");
/* labelled text input */
function Field({ label, type = "text", value, onChange, placeholder, required, half }) {
const [focus, setFocus] = React.useState(false);
return (
);
}
/* selectable option tile (payment method) */
function OptionTile({ active, onClick, title, sub }) {
const [h, setH] = React.useState(false);
return (
);
}
function SummaryRow({ label, value, strong, muted }) {
return (
{label}
{value}
);
}
function EnrollForm() {
const { t } = useT();
const tariffs = window.SALES_TARIFFS || [];
// подписка на закрытый Telegram-канал — как отдельный оформляемый пункт
const channelOpt = { id: "channel", price: window.CHANNEL_PRICE || 4990, isChannel: true };
const options = [...tariffs, channelOpt];
const initialTariff = () => {
try {
const p = new URLSearchParams(window.location.search).get("tariff");
if (p && options.some((x) => x.id === p)) return p;
} catch (e) {}
return "flagship";
};
const [tariff, setTariff] = React.useState(initialTariff);
const [form, setForm] = React.useState({ name: "", phone: "", email: "", tg: "", comment: "" });
const [pay, setPay] = React.useState(() => (getLang() === "zh" ? "cny" : "card"));
const [promo, setPromo] = React.useState("");
const [offerAccepted, setOfferAccepted] = React.useState(false);
const [showAuth, setShowAuth] = React.useState(false);
const [showOffer, setShowOffer] = React.useState(false);
const [sent, setSent] = React.useState(false);
const [paying, setPaying] = React.useState(false);
const [payErr, setPayErr] = React.useState("");
const { phone: authPhone, user, logout } = useAuth();
const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v }));
// подставляем имя из аккаунта, если поле ещё пустое
React.useEffect(() => {
if (user && user.name) setForm((f) => (f.name ? f : { ...f, name: user.name }));
}, [authPhone]);
// оплата доступна только вошедшему пользователю, принявшему оферту
const valid = form.name.trim() && authPhone && offerAccepted;
const submit = async (e) => {
e.preventDefault();
if (!form.name.trim()) return;
// цель «форма отправлена» — до всех гейтов: считаем намерение, а не только успех
if (typeof alGoal === "function") alGoal("form_submit", { tariff: cur.id });
// шаг 1 — авторизация: нужен РЕАЛЬНЫЙ токен бэкенда, а не кэш-сессия (иначе 401)
if (!authPhone || (typeof getToken === "function" && !getToken())) { setShowAuth(true); return; }
if (!offerAccepted) { setShowOffer(true); return; } // шаг 2 — договор оферты
// шаг 3 — реальная оплата через ЮKassa (чек 54-ФЗ → нужен e-mail)
const email = (form.email || "").trim() || (user && user.email) || "";
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
setPayErr(t("enroll.needEmail", "Укажите e-mail — на него придёт чек")); return;
}
setPayErr(""); setPaying(true);
try {
const r = await authApi.startPayment(cur.id, email);
if (r && r.confirmation_url) { window.location.href = r.confirmation_url; return; }
setPayErr(t("enroll.payErr", "Не удалось создать платёж, попробуйте позже."));
} catch (err) {
if (err && err.status === 401) { // токен истёк/сессия невалидна — на вход
setShowAuth(true);
setPayErr(t("enroll.reauth", "Войдите в аккаунт заново, чтобы оплатить."));
} else if (err && err.code === "bad_email") {
setPayErr(t("enroll.needEmail", "Укажите корректный e-mail для чека"));
} else {
setPayErr(t("enroll.payErr", "Оплата недоступна, попробуйте позже."));
}
} finally { setPaying(false); }
};
React.useEffect(() => { if (window.lucide) lucide.createIcons(); });
const inCny = pay === "cny";
const price = (rub) => (inCny ? fmtCny(toCny(rub)) : fmt(rub) + " ₽");
const tierCopy = t("sales.pricing.tiers");
const channelCopy = t("sales.pricing.channel");
const curIdx = Math.max(0, options.findIndex((x) => x.id === tariff));
const cur = options[curIdx] || options[0] || { id: "flagship", price: 0 };
const isChannel = !!cur.isChannel;
const curCopy = isChannel ? { name: channelCopy.name, tag: channelCopy.tag } : (tierCopy[curIdx] || {});
const fromPfx = cur.from ? t("sales.pricing.from") + " " : "";
if (sent) {
return (
{t("enroll.sentTitle")}
{(isChannel ? t("enroll.sentChannelBody") : t("enroll.sentBody")).replace("{name}", form.name.split(" ")[0] || t("enroll.trader"))}
{isChannel
?
{
if (typeof alGoal === "function") alGoal("click_bot", { place: "enroll_done" });
const u = window.TG_SALES_BOT_URL || "https://t.me/Aleksey_Lobov_bot";
window.open((typeof alBotUrl === "function") ? alBotUrl(u) : u, "_blank", "noopener");
}}
style={{ background: "var(--paper-on-ink)", color: "var(--ink)", boxShadow: "none" }}>{t("enroll.openChannel")}
:
{ window.location.href = "learn.html"; }}
style={{ background: "var(--paper-on-ink)", color: "var(--ink)", boxShadow: "none" }}>{t("learn.title")}}
setSent(false)}
style={{ color: "var(--paper-on-ink)", borderColor: "rgba(250,250,248,0.32)" }}>{t("enroll.editBtn")}
);
}
return (
setShowAuth(false)} initialMode="login"
onAuthed={() => setShowAuth(false)} />
setShowOffer(false)}
onAccept={() => setOfferAccepted(true)} />
);
}
Object.assign(window, { EnrollForm });