/* MBEX — app shell for the new Mini App route */ const { useEffect, useMemo, useState } = React; const SUB_TITLES = { create: "Создание заявки", status: "Статус заявки", referral: "Реферальная программа", loyalty: "Лояльность", wallets: "Кошельки", couriers: "Получатели", support: "Поддержка", offices: "Офисы MBEX", aml: "AML / KYC / KYT", }; function MbexLogo({ className = "" }) { return ( ); } function MbexHeaderLogo() { const legacyLogoPath = "M703 276H772.5L829 190H830L886.324 276H956.5V275L865 136V135L953.808 0H883L830 81H829L776 0H705.5L776 107.5H556.5V52.5H703V0H486.5V34L481.5 26L476.5 20.5L470 14L466 11L460 7L454 4L444 0H193L144 182.5L94.7868 0H0V276H61V79H62L115 276H172.5L175.5 267L180 252.5L185.5 232L190.5 214L196 193.5L201.5 174.5L205.5 160.5L209 148.5L216 122.5L219 110L224.5 88L226 79V276.5H419L430 275.5L442 273L454 269.5L466 264L473 259.5L481 253L486 248H487V276H703ZM699 273L772.5 161H553.5V215H699V273ZM483 97V141L473.5 133L464.5 127.5L456 124L451.5 122.5V120.5L460 117L469 112L475.5 106L479 102L483 97ZM286 48H400L411.5 53.5L417.86 58.0299L421.668 63.0448L425 69.7313V82.2687L422.144 89.791L419.764 93.1343L415.955 96.8955L412.623 98.9851L406.911 101.493L398.342 104H286V48ZM286 151H394.5L403 152L408.5 153L417 157L424 162.5L428.5 169L430 172L432 177V192L431 195.5L429 201L426.5 205L422.5 209.5L419 212L413 215L404 218H286V151Z"; return (
Crypto Exchange
); } function TabBar({ tab, setTab }) { const tabs = [ { id: "home", label: "Главная", icon: Icons.home }, { id: "history", label: "История", icon: Icons.history }, { id: "profile", label: "Профиль", icon: Icons.user }, ]; return (
{tabs.map((item) => { const Icon = item.icon; const active = tab === item.id; return (
setTab(item.id)}> {item.label}
); })}
); } function AuthOverlay({ app }) { const [login, setLogin] = useState(""); const [password, setPassword] = useState(""); const [mode, setMode] = useState("login"); const [registerMethod, setRegisterMethod] = useState("telegram"); const [email, setEmail] = useState(""); const [registerLogin, setRegisterLogin] = useState(""); const [registerPassword, setRegisterPassword] = useState(""); const [emailCode, setEmailCode] = useState(""); const [emailCodeSent, setEmailCodeSent] = useState(false); const [localStatus, setLocalStatus] = useState(null); if (!app.authState.requiresLogin) { return null; } function showLocalStatus(text, tone) { setLocalStatus({ text, tone: tone || "info" }); } function switchMode(nextMode) { setMode(nextMode); setLocalStatus(null); } function switchRegisterMethod(nextMethod) { setRegisterMethod(nextMethod); setLocalStatus(null); } async function handleLogin() { if (!login.trim() || !password.trim()) { app.pushToast("Введите логин и пароль", "error"); showLocalStatus("Введите логин и пароль", "error"); return; } try { await app.login(login.trim(), password); } catch (error) { // handled by bridge } } async function handleTelegram() { try { await app.startTelegramLogin(); showLocalStatus("Подтвердите вход в Telegram-боте и вернитесь на эту страницу", "ok"); } catch (error) { // handled by bridge } } async function handleEmailStart() { const emailValue = email.trim().toLowerCase(); const loginValue = registerLogin.trim().toLowerCase(); if (!emailValue || !loginValue || !registerPassword) { showLocalStatus("Заполните почту, логин и пароль", "error"); return; } if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(emailValue)) { showLocalStatus("Введите корректную почту", "error"); return; } if (!/^[a-z0-9_.-]{3,64}$/.test(loginValue)) { showLocalStatus("Логин: латиница, цифры, точка, дефис или подчёркивание", "error"); return; } if (registerPassword.length < 8) { showLocalStatus("Пароль должен быть минимум 8 символов", "error"); return; } try { await app.startEmailRegistration(emailValue, loginValue, registerPassword); setEmailCodeSent(true); showLocalStatus("Код отправлен на почту", "ok"); } catch (error) { // handled by bridge } } async function handleEmailConfirm() { const emailValue = email.trim().toLowerCase(); const codeValue = emailCode.trim().replace(/\s/g, ""); if (!emailValue || !codeValue) { showLocalStatus("Введите код из письма", "error"); return; } try { await app.confirmEmailRegistration(emailValue, codeValue); } catch (error) { // handled by bridge } } const status = localStatus || (app.authState.lastError ? { text: app.authState.lastError, tone: "error" } : null); const pending = app.authState.pending; return (
Быстрый обмен USDT с фиксацией курса и статусом заявки внутри Mini App.
{mode === "login" ? (
) : (
{registerMethod === "telegram" ? (
Регистрация через Telegram Откроем бота MBEX. После подтверждения web-сессия появится автоматически.
) : (
{emailCodeSent ? ( ) : null} {emailCodeSent ? ( ) : null}
)}
)} {app.authState.telegramWaiting ? (
Подтвердите вход в боте и вернитесь в окно. После подтверждения приложение само подтянет web-сессию.
) : null} {status ? (
{status.text}
) : null}
В Telegram Mini App вход идёт через Telegram. Web-версия использует отдельную клиентскую сессию.
); } function AppToast({ toast }) { if (!toast || !toast.message) { return null; } return (
{toast.message}
); } function App() { const app = window.useMBEXApp(); const [tab, setTabState] = useState("home"); const [stack, setStack] = useState([]); const [viewMotion, setViewMotion] = useState({ phase: "idle", direction: "forward" }); const [draft, setDraft] = useState(null); const [currentOrderId, setCurrentOrderId] = useState(null); const [dir, setDir] = useState("buy"); const [payInput, setPayInput] = useState(""); const transitionBusyRef = React.useRef(false); const transitionTimersRef = React.useRef([]); const currentOrder = useMemo( () => (app.privateState.orders || []).find((item) => item.id === currentOrderId) || null, [app.privateState.orders, currentOrderId], ); function toggleTheme() { app.setTheme(app.theme === "dark" ? "light" : "dark"); } function clearTransitionTimers() { transitionTimersRef.current.forEach((timer) => window.clearTimeout(timer)); transitionTimersRef.current = []; } function transitionTo(direction, updateView) { const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduceMotion) { updateView(); return; } if (transitionBusyRef.current) { return; } transitionBusyRef.current = true; setViewMotion({ phase: "leaving", direction }); const leaveTimer = window.setTimeout(() => { const enterView = () => { updateView(); setViewMotion({ phase: "entering", direction }); }; if (ReactDOM.flushSync) { ReactDOM.flushSync(enterView); } else { enterView(); } const enterTimer = window.setTimeout(() => { setViewMotion({ phase: "idle", direction }); transitionBusyRef.current = false; transitionTimersRef.current = []; }, 280); transitionTimersRef.current = [enterTimer]; }, 110); transitionTimersRef.current = [leaveTimer]; } function push(screenId) { transitionTo("forward", () => { setStack((prev) => [...prev, screenId]); }); } function pop() { transitionTo("back", () => { setStack((prev) => prev.slice(0, -1)); }); } function closeWebApp() { try { if (window.Telegram && Telegram.WebApp && Telegram.WebApp.close) { Telegram.WebApp.close(); } } catch (error) { // ignore } } function handleBackClose() { if (stack.length) { pop(); return; } closeWebApp(); } function setTab(nextTab) { if (nextTab === tab) { return; } const tabs = ["home", "history", "profile"]; const direction = tabs.indexOf(nextTab) > tabs.indexOf(tab) ? "forward" : "back"; transitionTo(direction, () => { setStack([]); setTabState(nextTab); }); } function openOrder(orderId) { transitionTo("forward", () => { setCurrentOrderId(orderId); setStack(["status"]); }); } function openCreate(nextDraft) { transitionTo("forward", () => { setDraft(nextDraft); setStack((prev) => [...prev, "create"]); }); } async function submitCreate(payload) { if (!draft) { return; } try { let order = null; if (draft.dir === "buy") { order = await app.createBuyOrder({ sumRub: payload.sumRub ?? draft.inputValue, fio: payload.fio, wallet: payload.wallet, delivery: payload.delivery, courierFio: payload.courierFio, }); } else if (draft.dir === "sell") { order = await app.createSellOrder({ fio: payload.fio, delivery: payload.delivery, courierFio: payload.courierFio, sumUsdt: payload.sumUsdt, }); } else { return; } transitionTo("forward", () => { setCurrentOrderId(order.id); setStack(["status"]); }); } catch (error) { // toast handled in bridge } } async function cancelCurrentOrder() { if (!currentOrder) { return; } try { await app.cancelOrder(currentOrder.id); } catch (error) { // toast handled in bridge } } function repeatCurrentOrder() { if (!currentOrder) { return; } if (currentOrder.typeKey === "transfer") { return; } const nextDir = currentOrder.typeKey === "sell" ? "sell" : "buy"; const nextInput = nextDir === "sell" ? Number(currentOrder.sumUsdt || currentOrder.depositAmountReceived || 0) : Number(currentOrder.sumRub || 0); transitionTo("forward", () => { setDir(nextDir); setPayInput(nextInput > 0 ? String(nextInput) : ""); setDraft({ dir: nextDir, inputValue: nextInput, rate: Number(currentOrder.rate || (nextDir === "sell" ? app.publicState.rates.sell : app.publicState.rates.buy) || 0), }); setStack(["create"]); }); } const top = stack[stack.length - 1]; const view = top || tab; const onTabRoot = ["home", "history", "profile"].includes(view); const headerTitle = onTabRoot ? null : (view === "status" && currentOrder ? `Заявка · ${window.MBEX.orderDisplayCode(currentOrder)}` : SUB_TITLES[view]); React.useLayoutEffect(() => { window.scrollTo(0, 0); const scroller = document.querySelector(".app-scroll"); if (scroller) { scroller.scrollTo({ top: 0, left: 0, behavior: "auto" }); } }, [view, currentOrderId]); useEffect(() => () => { clearTransitionTimers(); transitionBusyRef.current = false; }, []); useEffect(() => { let backButton = null; let handleNativeBack = null; try { if (window.Telegram && Telegram.WebApp && Telegram.WebApp.BackButton) { backButton = Telegram.WebApp.BackButton; handleNativeBack = handleBackClose; backButton.onClick(handleNativeBack); if (stack.length) { backButton.show(); } else if (backButton.hide) { backButton.hide(); } } } catch (error) { backButton = null; handleNativeBack = null; } return () => { if (!backButton || !handleNativeBack) { return; } try { if (backButton.offClick) { backButton.offClick(handleNativeBack); } if (backButton.hide) { backButton.hide(); } } catch (error) { // ignore } }; }, [stack.length, tab]); let content = null; if (view === "home") { content = ( ); } else if (view === "history") { content = ; } else if (view === "profile") { content = ; } else if (view === "create") { content = draft ? : null; } else if (view === "status") { content = ; } else if (view === "referral") { content = ; } else if (view === "loyalty") { content = ; } else if (view === "wallets") { content = ; } else if (view === "couriers") { content = ; } else if (view === "support") { content = ; } else if (view === "offices") { content = ; } else if (view === "aml") { content = ; } return (
21:03
{onTabRoot ? ( view === "profile" ? (
Профиль
) : (
) ) : ( <>
{headerTitle}
)}
{app.theme === "dark" ? : }
{content}
{onTabRoot ? : null}
@mbex_exchange_bot
); } ReactDOM.createRoot(document.getElementById("root")).render();