/* MBEX — Home screen */
function getHomeRateMeta(dir, rates) {
const buying = dir === "buy";
const rate = Number((buying ? rates.buy : rates.sell) || 0);
const marketRate = Number(
buying
? (rates.marketSell || rates.cbr || rates.buy || 0)
: (rates.marketBuy || rates.cbr || rates.sell || 0),
);
const enabled = buying
? rates.tradeAvailability?.buy_enabled !== false
: rates.tradeAvailability?.sell_enabled !== false;
const action = buying ? "Вы покупаете USDT" : "Вы продаёте USDT";
const toneClass = buying ? "rate-hero-buy" : "rate-hero-sell";
return { buying, rate, marketRate, enabled, action, toneClass };
}
function formatRateTimestamp(value) {
if (!value) {
return "Обновляется";
}
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) {
return "Обновляется";
}
return `Актуально ${new Date(parsed).toLocaleString("ru-RU", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).replace(", ", " в ")}`;
}
function getRateRange(data, fallbackRate) {
const values = (Array.isArray(data) ? data : [])
.map((item) => Number(typeof item === "object" ? item.value : item))
.filter((item) => Number.isFinite(item) && item > 0);
const fallback = Number(fallbackRate) || 0;
return {
min: values.length ? Math.min(...values) : fallback,
max: values.length ? Math.max(...values) : fallback,
};
}
function formatRateTitle(value) {
if (!value || !Number.isFinite(Date.parse(value))) {
return "Курс обновляется";
}
return `Обновлено ${new Date(Date.parse(value)).toLocaleString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}`;
}
function RateHero({ dir, app }) {
const [isRateInfoOpen, setIsRateInfoOpen] = React.useState(false);
const rates = app.publicState.rates;
const meta = getHomeRateMeta(dir, rates);
const marketDiff = meta.marketRate > 0 && meta.rate > 0
? meta.rate - meta.marketRate
: 0;
const marketDeltaPct = meta.marketRate > 0 && meta.rate > 0
? (marketDiff / meta.marketRate) * 100
: 0;
const chartData = meta.buying ? rates.buyPoints : rates.sellPoints;
const updatedLabel = formatRateTimestamp(rates.updatedAt);
const rateRange = getRateRange(chartData, meta.rate);
const rateFixingInfo = meta.buying
? "Курс фиксируется после пересчёта средств."
: "Курс фиксируется после поступления и проверки USDT на одноразовый кошелёк.";
React.useEffect(() => {
setIsRateInfoOpen(false);
}, [dir]);
return (
USDT / RUB
{window.MBEX.fmtRub2(meta.rate)}
₽
= 0 ? "is-up" : "is-down"}>
{marketDeltaPct >= 0 ? "+" : ""}{marketDeltaPct.toFixed(1)}%
{updatedLabel}
Мин.
{window.MBEX.fmtRub2(rateRange.min)} ₽
Макс.
{window.MBEX.fmtRub2(rateRange.max)} ₽
{meta.action}
setIsRateInfoOpen(true)}
>
Как фиксируется курс?
{rateFixingInfo}
setIsRateInfoOpen(false)}
>
{!meta.enabled ? (
{meta.buying ? "Покупка временно недоступна" : "Продажа временно недоступна"}
) : null}
);
}
function DirectionSwitcher({ dir, setDir, app }) {
const rates = app.publicState.rates;
return (
setDir("buy")}
>
Купить
{rates.tradeAvailability?.buy_enabled === false ? off : null}
setDir("sell")}
>
Продать
{rates.tradeAvailability?.sell_enabled === false ? off : null}
);
}
const CALCULATOR_RECEIVE_NOTICE = "Обратите внимание: сумма в калькуляторе является предварительной и не включает комиссию сервиса и комиссию сети TRC20. Итоговая сумма к получению с учетом всех комиссий указана ниже в расчете";
function CalculatorReceiveNotice({ onClose }) {
React.useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "Escape") {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return ReactDOM.createPortal(
event.stopPropagation()}
>
Предварительный расчёт
{CALCULATOR_RECEIVE_NOTICE}
,
document.body,
);
}
function CalcCard({ dir, setDir, payInput, setPayInput, app, onCreate }) {
const rates = app.publicState.rates;
const profile = app.privateState.profile || {};
const referral = app.privateState.referral || {};
const [activeInput, setActiveInput] = React.useState("pay");
const [receiveInput, setReceiveInput] = React.useState("");
const [isReceiveNoticeOpen, setIsReceiveNoticeOpen] = React.useState(false);
const meta = getHomeRateMeta(dir, rates);
const buying = meta.buying;
const rate = meta.rate;
const networkFeeUsdt = Number(rates.commissionSettings?.network_fee_usdt || 0);
const rawPayInput = normalizeCalcInput(payInput);
const rawReceiveInput = normalizeCalcInput(receiveInput);
const payInputNum = parseCalcInput(rawPayInput);
const receiveInputNum = parseCalcInput(rawReceiveInput);
const reversePayNum = activeInput === "receive"
? (
buying
? window.MBEX.calculateBuyRubFromGrossUsdt(receiveInputNum, rate)
: window.MBEX.calculateSellUsdtFromPayoutRub(receiveInputNum, rate, profile)
)
: 0;
const rawPayNum = activeInput === "receive" ? reversePayNum : payInputNum;
const payNum = buying
? window.MBEX.snapRubStepAmount(rawPayNum, 1000)
: rawPayNum;
const draftPayNum = roundCalcAmount(payNum, buying ? 0 : 2);
const buyEstimate = buying
? window.MBEX.calculateBuyEstimate(payNum, rate, profile, referral, networkFeeUsdt)
: null;
const sellEstimate = !buying
? window.MBEX.calculateSellEstimate(payNum, rate, profile)
: null;
const receiveValue = activeInput === "receive"
? receiveInput
: formatCalcInput(
buying ? buyEstimate?.grossUsdt : sellEstimate?.payoutRub,
buying ? 2 : 0,
);
const payValue = activeInput === "receive"
? formatCalcInput(payNum, buying ? 0 : 2)
: payInput;
const minSell = Number(rates.sellLimits?.min_usdt || 65);
const maxSell = Number(rates.sellLimits?.max_usdt || 100000);
const buyEnabled = rates.tradeAvailability?.buy_enabled !== false;
const sellEnabled = rates.tradeAvailability?.sell_enabled !== false;
const currentEnabled = buying ? buyEnabled : sellEnabled;
const sumOk = buying
? rawPayNum > 0 && payNum >= 1000
: payNum >= minSell && payNum <= maxSell;
const canContinue = currentEnabled && sumOk && rawPayNum > 0;
function onInput(event) {
setActiveInput("pay");
setReceiveInput("");
setPayInput(event.target.value.replace(/[^\d.,]/g, ""));
}
function onReceiveInput(event) {
setActiveInput("receive");
setReceiveInput(event.target.value.replace(/[^\d.,]/g, ""));
}
function snapPayInput() {
if (!buying || activeInput !== "pay" || payInputNum <= 0) {
return;
}
setPayInput(formatCalcInput(window.MBEX.snapRubStepAmount(payInputNum, 1000), 0));
}
function onSwap() {
if (activeInput === "receive") {
setPayInput(formatCalcInput(payNum, buying ? 0 : 2));
setReceiveInput("");
setActiveInput("pay");
}
setDir(buying ? "sell" : "buy");
}
function disabledText() {
if (buying && !buyEnabled) {
return "Покупка USDT временно отключена в настройках Mini App.";
}
if (!buying && !sellEnabled) {
return "Продажа USDT временно отключена в настройках Mini App.";
}
return "";
}
return (
{!currentEnabled ? (
{disabledText()}
) : null}
Вы платите
{buying ? "Мин. 1 000 ₽ · шаг 500 ₽" : `Мин. ${minSell} USDT`}
Вы получаете
setIsReceiveNoticeOpen(true)}
aria-label="Подробнее о предварительном расчёте"
aria-haspopup="dialog"
>
1 USDT = {window.MBEX.fmtRub2(rate)} ₽
{buying ? (
<>
≈ Предварительно
{window.MBEX.fmtUsdt(buyEstimate?.walletUsdt || 0)} USDT
Комиссия
{window.MBEX.fmtUsdt(buyEstimate?.commissionUsdt || 0)} USDT / {window.MBEX.fmtPct(buyEstimate?.commissionPct || 0)}%
Комиссия сети TRC-20
{window.MBEX.fmtUsdt(buyEstimate?.networkFeeUsdt || 0)} USDT
>
) : (
<>
≈ Предварительно
{window.MBEX.fmtRub(sellEstimate?.payoutRub || 0)} ₽
Комиссия
{window.MBEX.fmtRub(sellEstimate?.commissionRub || 0)} ₽ / {window.MBEX.fmtPct(sellEstimate?.commissionPct || 0)}%
>
)}
{
if (activeInput === "receive") {
setPayInput(formatCalcInput(payNum, buying ? 0 : 2));
}
onCreate({
dir,
inputValue: draftPayNum,
rate,
buyEstimate,
sellEstimate,
});
}}
>
{!currentEnabled
? "Направление недоступно"
: (!sumOk && payNum > 0)
? (buying ? "Сумма ниже минимума" : "Сумма вне лимитов")
: "Создать заявку"}
{isReceiveNoticeOpen ? (
setIsReceiveNoticeOpen(false)} />
) : null}
);
}
function normalizeCalcInput(value) {
return String(value || "").replace(/\s/g, "").replace(",", ".");
}
function parseCalcInput(value) {
const parsed = Number(normalizeCalcInput(value) || 0);
return Number.isFinite(parsed) ? parsed : 0;
}
function formatCalcInput(value, maximumFractionDigits) {
const num = Number(value || 0);
if (!Number.isFinite(num) || num <= 0) {
return "";
}
return num.toLocaleString("ru-RU", {
maximumFractionDigits,
});
}
function roundCalcAmount(value, fractionDigits) {
const num = Number(value || 0);
if (!Number.isFinite(num) || num <= 0) {
return 0;
}
const factor = 10 ** fractionDigits;
return Math.round(num * factor) / factor;
}
function ActiveOrderCard({ order, nowMs, onOpen }) {
const statusMeta = window.MBEX.getStatusMeta(order, nowMs);
const secondsLeft = window.MBEX.getOrderSecondsLeft(order, nowMs);
const transfer = order.typeKey === "transfer";
const buying = order.typeKey === "buy";
const title = transfer ? "Перестановка RUB" : buying ? "Покупка USDT" : "Продажа USDT";
const route = transfer ? [order.fromCity, order.toCity].filter(Boolean).join(" → ") : "";
return (
{statusMeta.title}
{secondsLeft > 0 ? window.MBEX.formatDuration(secondsLeft) : "00:00"}
{title}
{window.MBEX.orderDisplayCode(order)} · {route || statusMeta.subtitle}
{window.MBEX.orderDisplayAmount(order)}
{transfer
? "Маршрут"
: (order.sumUsdt || order.depositAmountReceived) > 0
? `${window.MBEX.fmtUsdt(order.sumUsdt || order.depositAmountReceived)} USDT`
: "Ожидаем сумму"}
Открыть статус
);
}
function formatCbrDate(nowMs, offsetDays) {
const baseMs = Number(nowMs) || Date.now();
return new Date(baseMs + offsetDays * 24 * 60 * 60 * 1000).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "Europe/Moscow",
});
}
function CbrRateCard({ app }) {
const rates = app.publicState.rates;
const currentRate = Number(rates.cbr || 0);
const previousRate = Number(rates.cbrYesterday || 0);
const changePct = currentRate > 0 && previousRate > 0
? ((currentRate - previousRate) / previousRate) * 100
: null;
const changeTone = changePct > 0 ? "is-up" : changePct < 0 ? "is-down" : "is-flat";
return (
Курс ЦБ РФ
Вчера, {formatCbrDate(app.nowMs, -1)}
{previousRate > 0 ? `${window.MBEX.fmtRub2(previousRate)} ₽` : "—"}
Сегодня, {formatCbrDate(app.nowMs, 0)}
{currentRate > 0 ? `${window.MBEX.fmtRub2(currentRate)} ₽` : "—"}
{changePct !== null ? (
{changePct > 0 ? : null}
{changePct < 0 ? : null}
{changePct > 0 ? "+" : ""}{changePct.toFixed(2)}%
) : null}
);
}
function ChartCard({ app }) {
const rates = app.publicState.rates;
const [timeframe, setTimeframe] = React.useState("MBEX");
const data = timeframe === "Рынок" ? rates.marketSeries : rates.buySeries;
const current = timeframe === "Рынок" ? Number(rates.marketSell || 0) : Number(rates.buy || 0);
const first = data.length ? data[0] : current;
const last = data.length ? data[data.length - 1] : current;
const change = first > 0 ? ((last - first) / first) * 100 : 0;
const up = change >= 0;
return (
Курс USDT / RUB
{window.MBEX.fmtRub2(current)} ₽
{up ? : }
{Math.abs(change).toFixed(2)}%
{["MBEX", "Рынок"].map((key) => (
setTimeframe(key)}
style={{
flex: 1,
textAlign: "center",
padding: "7px 0",
borderRadius: 9,
fontSize: 12.5,
fontWeight: 700,
cursor: "pointer",
background: timeframe === key ? "var(--accent-soft)" : "transparent",
color: timeframe === key ? "var(--accent)" : "var(--text-3)",
}}
>
{key}
))}
Рынок Rapira
{window.MBEX.fmtRub2(rates.marketSell || 0)} ₽
Курс ЦБ РФ
{window.MBEX.fmtRub2(rates.cbr || 0)} ₽
);
}
function ReferralTeaser({ app, onClick }) {
const referral = app.privateState.referral;
const count = Number(referral?.refCount || 0);
const balance = Number(referral?.refBalance || 0);
const subtitle = count > 0
? `${count} рефералов · ${window.MBEX.fmtUsdt(balance)} USDT доступно`
: "Другу скидка на первую сделку, вам — бонус с комиссии";
return (
Реферальная программа
{subtitle}
);
}
function HomeQuickLinks({ app, nav }) {
const items = [
{
icon: ,
title: "Рефералка",
sub: "Бонусы и ссылка",
target: "referral",
},
{
icon: ,
title: "Офисы",
sub: "Адреса и контакты",
target: "offices",
},
{
icon: ,
title: "Поддержка",
sub: "Связаться с нами",
target: "support",
},
{
icon: ,
title: "AML / KYC",
sub: "Правила проверки",
target: "aml",
},
];
return (
{items.map((item) => (
nav(item.target)}
>
{item.icon}
{item.title}
{item.sub}
))}
);
}
function HomeScreen({ dir, setDir, payInput, setPayInput, app, onCreate, onOpenOrder, nav }) {
const active = app.activeOrders[0] || null;
return (
{!active ?
: null}
{app.authState.lastError ? (
{app.authState.lastError}
Обновить статус
) : null}
{active ? (
Активная заявка
onOpenOrder(active.id)}>Статус
onOpenOrder(active.id)} />
) : null}
{active ?
: null}
Сервисы
Курс и комиссия фиксируются в заявке по текущим правилам MBEX.
);
}
window.HomeScreen = HomeScreen;