/* ============================================================ 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 (
{/* ---- left: fields ---- */}
{/* ---- выбор тарифа ---- */}
{options.map((op, i) => { const isCh = !!op.isChannel; const c = isCh ? channelCopy : (tierCopy[i] || {}); const sub = (op.from ? t("sales.pricing.from") + " " : "") + price(op.price) + (isCh && channelCopy.priceNote ? " " + channelCopy.priceNote : "") + (c.tag ? " · " + c.tag : ""); return ( setTariff(op.id)} title={c.name || op.id} sub={sub} /> ); })}
setPay("card")} title={t("enroll.payCard")} sub={t("enroll.payCardSub")} /> setPay("sbp")} title={t("enroll.paySbp")} sub={t("enroll.paySbpSub")} /> setPay("transfer")} title={t("enroll.payTransfer")} sub={t("enroll.payTransferSub")} /> setPay("cny")} title={t("enroll.payCny")} sub={t("enroll.payCnySub")} />
{/* ---- авторизация покупателя (телефон + пароль + 2FA) ---- */}
{authPhone ? {t("auth.loggedInAs")} {(user && (user.name || user.email)) || authPhone} : t("auth.authHint")}
{authPhone ? : setShowAuth(true)} style={{ height: 40 }}>{t("auth.loginRegister")}}
{/* ---- договор оферты: прочитать → «Согласен» ---- */}
{/* ---- right: order summary (dark) ---- */}
setShowAuth(false)} initialMode="login" onAuthed={() => setShowAuth(false)} /> setShowOffer(false)} onAccept={() => setOfferAccepted(true)} />
); } Object.assign(window, { EnrollForm });