/* MBEX — History, Profile, Referral and info screens */ function isHistoryFinal(order) { return ["completed", "cancelled"].includes(order.serverStatus); } function historyOrderSubtitle(order) { if (order.typeKey === "sell") { const status = order.depositStatus || "waiting"; if (status === "aml_pending") { return "AML проверка депозита"; } if (status === "aml_failed") { return "Нужен AML-разбор"; } if (status === "limit_failed") { return "Сумма вне лимитов"; } if (status === "refund_swept") { return "Средства на возвратном кошельке"; } if (order.depositAmountReceived > 0) { return `${window.MBEX.fmtUsdt(order.depositAmountReceived)} USDT получено`; } if (status === "expired") { return "Депозитный адрес истёк"; } return "Ожидаем USDT TRC-20"; } if (order.typeKey === "transfer") { return [order.fromCity, order.toCity].filter(Boolean).join(" → ") || "Маршрут уточняется"; } return order.wallet ? `На ${order.wallet}` : "TRC-20 кошелёк из заявки"; } const HISTORY_TYPE_FILTERS = [ { v: "all", l: "Все операции" }, { v: "buy", l: "Покупка" }, { v: "sell", l: "Продажа" }, ]; const HISTORY_STATUS_FILTERS = [ { v: "all", l: "Все статусы" }, { v: "active", l: "В работе" }, { v: "done", l: "Завершённые" }, ]; function historyMatchesFilters(order, typeFilter, statusFilter) { const typeMatches = typeFilter === "all" || order.typeKey === typeFilter; const statusMatches = statusFilter === "all" || (statusFilter === "active" && !isHistoryFinal(order)) || (statusFilter === "done" && isHistoryFinal(order)); return typeMatches && statusMatches; } function historyFilterLabel(options, value) { return options.find((item) => item.v === value)?.l || options[0].l; } function HistoryFilterChip({ label, active, onClick, ariaLabel }) { return ( ); } function HistoryFilterSheet({ title, options, value, onSelect, 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()} >
{title}
{options.map((item) => { const selected = value === item.v; return ( ); })}
, document.body, ); } function HistoryOrderRow({ order, app, onOpenOrder }) { const buying = order.typeKey === "buy"; const selling = order.typeKey === "sell"; const transfer = order.typeKey === "transfer"; const badge = window.MBEX.getHistoryBadge(order, app.nowMs); const Icon = transfer ? Icons.swap : buying ? Icons.arrowDown : Icons.arrowUp; const iconClass = transfer ? "is-transfer" : buying ? "is-buy" : "is-sell"; return (
onOpenOrder(order.id)}>
{window.MBEX.orderTypeLabel(order)} {window.MBEX.orderDisplayCode(order)}
{historyOrderSubtitle(order)}
{window.MBEX.formatDateTime(order.createdAt)} {selling && order.depositStatus ? {depositStatusText(order.depositStatus)} : null}
{window.MBEX.orderDisplayAmount(order)} {badge.label}
); } function HistoryScreen({ app, onOpenOrder }) { const [typeFilter, setTypeFilter] = React.useState("all"); const [statusFilter, setStatusFilter] = React.useState("all"); const [openFilter, setOpenFilter] = React.useState(null); const orders = app.privateState.orders || []; const list = orders.filter((order) => historyMatchesFilters(order, typeFilter, statusFilter)); const activeOptions = openFilter === "type" ? HISTORY_TYPE_FILTERS : HISTORY_STATUS_FILTERS; const activeValue = openFilter === "type" ? typeFilter : statusFilter; const activeTitle = openFilter === "type" ? "Тип операции" : "Статус"; function selectFilter(value) { if (openFilter === "type") { setTypeFilter(value); } else { setStatusFilter(value); } setOpenFilter(null); } return ( <>
setOpenFilter("type")} ariaLabel={`Тип операции: ${historyFilterLabel(HISTORY_TYPE_FILTERS, typeFilter)}`} /> setOpenFilter("status")} ariaLabel={`Статус: ${historyFilterLabel(HISTORY_STATUS_FILTERS, statusFilter)}`} />
{list.length ? (
{list.map((order) => ( ))}
) : (
} title={orders.length ? "Ничего не найдено" : "Заявок пока нет"} text={orders.length ? "Измените тип операции или статус." : "После первой покупки или продажи история появится здесь."} />
)}
{openFilter ? ( setOpenFilter(null)} /> ) : null} ); } function formatProfilePct(value) { const number = Number(value || 0); return `${number.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}%`; } function formatLoyaltyPct(value) { const number = Number(value || 0); return `${number.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 3, })}%`; } function formatLoyaltyUsdt(value) { return Number(value || 0).toLocaleString("ru-RU", { minimumFractionDigits: 0, maximumFractionDigits: 2, }); } function ProfileInfoRow({ label, value, tone }) { const color = tone === "pos" ? "var(--pos)" : tone === "warn" ? "var(--warn)" : "var(--text)"; return (
{label} {value}
); } const MINIAPP_TRC20_RE = /^T[a-zA-Z0-9]{33}$/; function referralDeepLink(referral, botUrl) { return referral.refCode ? `https://t.me/mbex_exchange_bot?start=${referral.refCode}` : botUrl; } async function shareReferralLink(app, deepLink) { const ok = await window.MBEX.shareText({ title: "MBEX", text: `Обменивай USDT в MBEX: ${deepLink}`, url: deepLink, }); if (!ok) { const copied = await window.MBEX.copyText(deepLink); app.pushToast(copied ? "Ссылка скопирована" : "Не удалось поделиться", copied ? "success" : "error"); } } function ProfileMenuRow({ title, detail, onClick, muted = false }) { return ( ); } function ProfileScreen({ app, nav }) { const profile = app.privateState.profile || {}; const referral = app.privateState.referral || {}; const vip = referral.vip || {}; const telegramUser = window.MBEX.getTelegramUser() || {}; const canLogoutWeb = Boolean(app.authState && app.authState.isWebSession); const displayName = profile.fio || [telegramUser.first_name, telegramUser.last_name].filter(Boolean).join(" ") || "Клиент MBEX"; const shortName = displayName.trim().split(/\s+/).slice(0, 2).join(" "); const avatarLetter = shortName.charAt(0).toUpperCase() || "M"; const wallets = app.privateState.wallets || []; const couriers = app.privateState.couriers || []; const totalUsdt = Number(vip.total_usdt ?? profile.vip_total_usdt ?? profile.total_usdt ?? 0); const vipLevel = Number(vip.level ?? profile.vip_level ?? 0); const vipName = vip.name || profile.vip_name || (vipLevel > 0 ? `VIP ${vipLevel}` : "Standard"); const vipDiscountPct = Number(vip.commission_discount_pct ?? profile.commission_discount_pct ?? 0); const hasReferralDiscount = Boolean(referral.hasFirstOrderDiscount || referral.has_first_order_discount); const buyBasePct = Number(profile.commission_pct || 0); const buyAfterVipPct = buyBasePct * Math.max(0, 1 - vipDiscountPct / 100); const buyEffectivePct = hasReferralDiscount ? buyAfterVipPct * 0.5 : buyAfterVipPct; const nextTier = Number(vip.next_tier ?? profile.next_tier ?? 0); const remaining = nextTier > 0 ? Math.max(0, nextTier - totalUsdt) : 0; const progress = nextTier > 0 ? Math.max(0, Math.min(100, (totalUsdt / nextTier) * 100)) : 100; const webLogin = profile.web_login || telegramUser.username || ""; const loginDisplay = webLogin ? `@${webLogin}` : "client"; const deepLink = referralDeepLink(referral, app.botUrl); const referralDisplayLink = referral.refCode ? `mbex.pro/r/${referral.refCode}` : "Ссылка появится после загрузки"; const referralSharePct = Number(referral.commissionSharePct || 35); const globalBuyPct = Number(app.publicState?.config?.commission_settings?.buy_pct ?? 0.1); const referralTurnoverPct = globalBuyPct * referralSharePct / 100; const referralTurnoverText = referralTurnoverPct.toLocaleString("ru-RU", { minimumFractionDigits: 3, maximumFractionDigits: 3, }); const [editing, setEditing] = React.useState(false); const [saving, setSaving] = React.useState(false); const [form, setForm] = React.useState({ fio: profile.fio || "", phone: profile.phone || "", email: profile.email || "", }); React.useEffect(() => { if (!editing) { setForm({ fio: profile.fio || "", phone: profile.phone || "", email: profile.email || "", }); } }, [profile.fio, profile.phone, profile.email, editing]); function startEdit() { setForm({ fio: profile.fio || "", phone: profile.phone || "", email: profile.email || "", }); setEditing(true); } function updateField(key, value) { setForm((prev) => ({ ...prev, [key]: value })); } async function saveProfile() { setSaving(true); try { await app.updateProfile({ fio: form.fio.trim(), phone: form.phone.trim(), email: form.email.trim(), }); setEditing(false); } catch (error) { // toast handled by bridge } finally { setSaving(false); } } async function handleShare() { await shareReferralLink(app, deepLink); } return (
Ваш уровень {vipName}
Комиссия {formatProfilePct(buyEffectivePct)}
Объём за 30 дней {window.MBEX.fmtUsdt(totalUsdt)}{nextTier > 0 ? ` / ${window.MBEX.fmtUsdt(nextTier)} USDT` : " USDT"}
{progress > 0 ? : null}

{nextTier > 0 ? `Ещё ${window.MBEX.fmtUsdt(remaining)} USDT за 30 дней — и комиссия станет ниже на уровне VIP` : "У вас максимальный доступный уровень и минимальная комиссия"}

Друзья

Приглашено {referral.refCount || 0}
Заработано, USDT {window.MBEX.fmtUsdt(referral.refEarned || 0)}

Вам — {referralTurnoverText}% с оборота каждого приглашённого, пожизненно.

{referralDisplayLink}
{editing ? (
Редактировать профиль
updateField("fio", event.target.value)} />
updateField("phone", event.target.value)} />
updateField("email", event.target.value)} />
) : null}
nav("wallets")} /> nav("couriers")} /> nav("support")} /> nav("offices")} /> nav("aml")} /> {canLogoutWeb ? ( ) : null}
); } function WalletsScreen({ app }) { const wallets = app.privateState.wallets || []; const [showForm, setShowForm] = React.useState(false); const [name, setName] = React.useState(""); const [address, setAddress] = React.useState(""); const [saving, setSaving] = React.useState(false); const [menuId, setMenuId] = React.useState(null); const [confirmingId, setConfirmingId] = React.useState(null); const [renamingId, setRenamingId] = React.useState(null); const [renameValue, setRenameValue] = React.useState(""); const addressValue = address.trim(); const nameValue = name.trim(); const validAddress = MINIAPP_TRC20_RE.test(addressValue); const canAdd = validAddress && !saving && wallets.length < 20; function formatWalletUse(value) { if (!value) { return "ещё не использован"; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return "Использован ранее"; } const date = parsed.toLocaleDateString("ru-RU", { day: "numeric", month: "long" }); return `использован ${date}`; } function openForm() { setMenuId(null); setConfirmingId(null); setShowForm(true); } function closeForm() { setName(""); setAddress(""); setShowForm(false); } async function pasteAddress() { if (!navigator.clipboard || !navigator.clipboard.readText) { app.pushToast("Вставьте адрес вручную", "error"); return; } try { const text = (await navigator.clipboard.readText()).trim(); if (!text) { app.pushToast("Буфер обмена пуст", "error"); return; } setAddress(text.slice(0, 34)); } catch (error) { app.pushToast("Не удалось прочитать буфер. Вставьте адрес вручную.", "error"); } } async function addWallet() { if (!validAddress) { app.pushToast("Адрес TRC-20 начинается с T и состоит из 34 символов", "error"); return; } setSaving(true); try { await app.addWallet(nameValue || "Кошелёк", addressValue); closeForm(); } catch (error) { // toast handled by bridge } finally { setSaving(false); } } async function deleteWallet(walletId) { if (confirmingId !== walletId) { setConfirmingId(walletId); return; } setConfirmingId(null); try { await app.deleteWallet(walletId); } catch (error) { // toast handled by bridge } } async function setDefault(walletId) { setMenuId(null); try { await app.updateWallet(walletId, { is_default: true }); } catch (error) { // toast handled by bridge } } function startRename(wallet) { setRenameValue(wallet.name || "Кошелёк"); setRenamingId(wallet.id); setMenuId(null); } async function saveRename(walletId) { const nextName = renameValue.trim(); if (!nextName) { app.pushToast("Укажите название кошелька", "error"); return; } try { await app.updateWallet(walletId, { name: nextName }); setRenamingId(null); setRenameValue(""); } catch (error) { // toast handled by bridge } } return (
{!wallets.length && !showForm ? (

Сохраните адрес — и он будет подставляться в заявку

При покупке USDT адрес не придётся вводить и сверять заново: выберете его из списка одним касанием. До 20 адресов.

) : null} {showForm ? (

Куда получать USDT

{addressValue ? ( validAddress ? (
✓ Формат верный — сеть TRON
) : (
Адрес TRC-20 начинается с T и состоит из 34 символов
) ) : null}
) : null} {wallets.length ? (
Адреса для получения USDT {wallets.length} из 20
{wallets.map((wallet) => (
{renamingId === wallet.id ? (
setRenameValue(event.target.value)} />
) : (
{wallet.name || "Кошелёк"} {wallet.is_default ? По умолчанию : null}
{formatWalletUse(wallet.last_used_at)}
{menuId === wallet.id ? (
) : null}
)}
))}
{!showForm && wallets.length < 20 ? ( ) : null}
) : showForm ? (

Сохранённых адресов пока нет — первый появится здесь после сохранения. Всего можно хранить до 20.

) : (

Сохранённый адрес используется только для получения USDT по вашим заявкам. Изменить или удалить его можно в любой момент.

)} {wallets.length ? (

Адрес по умолчанию подставляется в заявку на покупку — его можно сменить в один тап при оформлении.

) : null}
); } function CouriersScreen({ app }) { const couriers = app.privateState.couriers || []; const [fio, setFio] = React.useState(""); const [saving, setSaving] = React.useState(false); const [confirmingId, setConfirmingId] = React.useState(null); const fioValue = fio.trim(); const canAdd = fioValue.length > 2 && !saving; async function addCourier() { if (fioValue.length <= 2) { app.pushToast("Укажите ФИО получателя", "error"); return; } setSaving(true); try { await app.addCourier(fioValue); setFio(""); } catch (error) { // toast handled by bridge } finally { setSaving(false); } } async function deleteCourier(courierId) { if (confirmingId !== courierId) { setConfirmingId(courierId); return; } setConfirmingId(null); try { await app.deleteCourier(courierId); } catch (error) { // toast handled by bridge } } return (
Новое ФИО {couriers.length}/20
setFio(event.target.value)} />
Сохранённые получатели
{couriers.length ? (
{couriers.map((courier) => (
{courier.fio || "Получатель"} Для заявок с другим ФИО
))}
) : (
} title="Получателей пока нет" text="Сохранённые ФИО будут доступны в формах покупки и продажи." />
)}
); } function parseReferralAmount(value) { const parsed = Number(String(value || "").replace(/\s/g, "").replace(",", ".") || 0); return Number.isFinite(parsed) ? parsed : 0; } function referralStatusText(status) { const key = String(status || "pending").toLowerCase(); const labels = { pending: "На проверке", approved: "Одобрено", paid: "Выплачено", completed: "Выплачено", rejected: "Отклонено", cancelled: "Отменено", }; return labels[key] || status || "На проверке"; } function referralPersonName(item) { return item?.referred_fio || item?.fio || item?.tg_username || `ID ${item?.referred_tg_id || item?.tg_id || "—"}`; } function ReferralScreen({ app, nav }) { const referral = app.privateState.referral || {}; const profile = app.privateState.profile || {}; const wallets = app.privateState.wallets || []; const accruals = app.privateState.referralLog || []; const withdrawals = app.privateState.referralWithdrawals || []; const referrals = referral.referrals || []; const vip = referral.vip || {}; const [code, setCode] = React.useState(""); const [withdrawAmount, setWithdrawAmount] = React.useState(""); const [withdrawWallet, setWithdrawWallet] = React.useState( wallets[0] ? wallets[0].address : "" ); const [selectedWalletId, setSelectedWalletId] = React.useState(wallets[0] ? wallets[0].id : null); const [historyTab, setHistoryTab] = React.useState("accruals"); const [showWithdrawForm, setShowWithdrawForm] = React.useState(false); const [showApplyForm, setShowApplyForm] = React.useState(false); const [applying, setApplying] = React.useState(false); const [withdrawing, setWithdrawing] = React.useState(false); const deepLink = referralDeepLink(referral, app.botUrl); const refBalance = Number(referral.refBalance || 0); const refEarned = Number(referral.refEarned || 0); const withdrawMin = 100; const withdrawMissing = Math.max(0, withdrawMin - refBalance); const withdrawProgress = Math.max(0, Math.min(100, (refBalance / withdrawMin) * 100)); const withdrawAmountNum = parseReferralAmount(withdrawAmount); const withdrawWalletValue = withdrawWallet.trim(); const withdrawWalletOk = MINIAPP_TRC20_RE.test(withdrawWalletValue); const canWithdraw = refBalance >= withdrawMin && withdrawAmountNum >= withdrawMin && withdrawAmountNum <= refBalance && withdrawWalletOk && !withdrawing; const buyBasePct = Number(profile.commission_pct || app.publicState?.config?.commission_settings?.buy_pct || 0); const vipDiscountPct = Number(vip.commission_discount_pct ?? profile.commission_discount_pct ?? 0); const buyAfterVipPct = buyBasePct * Math.max(0, 1 - vipDiscountPct / 100); const buyEffectivePct = referral.hasFirstOrderDiscount ? buyAfterVipPct * 0.5 : buyAfterVipPct; const vipName = vip.name || profile.vip_name || (Number(vip.level || profile.vip_level || 0) > 0 ? `VIP ${vip.level || profile.vip_level}` : "Standard"); const withdrawHint = refBalance < withdrawMin ? `Вывод откроется после накопления ${window.MBEX.fmtUsdt(withdrawMin)} USDT.` : withdrawAmountNum > refBalance ? "Сумма больше доступного баланса." : withdrawAmountNum > 0 && withdrawAmountNum < withdrawMin ? "Минимальная сумма вывода — 100 USDT." : withdrawWalletValue && !withdrawWalletOk ? "Укажите корректный TRC-20 адрес: T + 33 символа." : "Заявка уйдёт оператору, статус появится в истории выводов."; const historyTabs = [ { id: "accruals", label: "Начисления", count: accruals.length }, { id: "friends", label: "Друзья", count: referrals.length }, { id: "withdrawals", label: "Выводы", count: withdrawals.length }, ]; React.useEffect(() => { if (!withdrawWallet && wallets[0]) { setWithdrawWallet(wallets[0].address); setSelectedWalletId(wallets[0].id); } }, [wallets, withdrawWallet]); async function handleShare() { await shareReferralLink(app, deepLink); } async function handleCopyCode() { if (!referral.refCode) { app.pushToast("Код ещё не загрузился", "error"); return; } const copied = await window.MBEX.copyText(referral.refCode); app.pushToast(copied ? "Код скопирован" : "Не удалось скопировать", copied ? "success" : "error"); } async function handleApply() { if (!code.trim()) { app.pushToast("Введите реферальный код", "error"); return; } setApplying(true); try { await app.applyReferralCode(code.trim().toUpperCase()); setCode(""); } catch (error) { // toast handled by bridge } finally { setApplying(false); } } async function handleWithdraw() { if (withdrawAmountNum < withdrawMin) { app.pushToast("Минимальная сумма вывода — 100 USDT", "error"); return; } if (withdrawAmountNum > refBalance) { app.pushToast("Недостаточно реферального баланса", "error"); return; } if (!withdrawWalletOk) { app.pushToast("Укажите корректный TRC-20 адрес", "error"); return; } setWithdrawing(true); try { await app.withdrawReferral(withdrawAmountNum, withdrawWalletValue); setWithdrawAmount(""); } catch (error) { // toast handled by bridge } finally { setWithdrawing(false); } } function selectWallet(wallet) { setSelectedWalletId(wallet.id); setWithdrawWallet(wallet.address || ""); } function historyContent() { if (historyTab === "accruals" && accruals.length) { return (
{accruals.slice(0, 8).map((item) => (
{referralPersonName(item)} {window.MBEX.formatDateTime(item.created_at)}
+{window.MBEX.fmtUsdt(item.bonus_usdt)} USDT
))}
); } if (historyTab === "friends" && referrals.length) { return (
{referrals.slice(0, 8).map((item) => (
{referralPersonName(item)} {Number(item.order_count || 0)} заявок · {window.MBEX.formatDateTime(item.created_at)}
{window.MBEX.fmtUsdt(item.earned_usdt)} USDT
))}
); } if (historyTab === "withdrawals" && withdrawals.length) { return (
{withdrawals.map((item) => (
#{item.id} · {referralStatusText(item.status)} {item.wallet || item.address || ""} · {window.MBEX.formatDateTime(item.created_at)}
{window.MBEX.fmtUsdt(item.amount || item.amount_usdt)} USDT
))}
); } const empty = { accruals: ["Начислений пока нет", "Появятся после первого обмена приглашённого друга."], friends: ["Друзей пока нет", "Поделитесь ссылкой — приглашённые появятся здесь."], withdrawals: ["Выводов пока нет", "История появится после первого запроса на вывод."], }[historyTab]; return (
{empty[0]}

{empty[1]}

); } return (

Другу — −{referral.firstOrderDiscountPct || 50}% на первую
сделку, вам — {referral.commissionSharePct || 35}% с
его комиссии

Скидка срабатывает на первом обмене друга.
Ваши {referral.commissionSharePct || 35}% начисляются в USDT после каждого
его обмена.

Код {referral.refCode || "—"}
Доступно к выводу {window.MBEX.fmtUsdt(refBalance)} USDT
Всего заработано {window.MBEX.fmtUsdt(refEarned)} USDT
{withdrawProgress > 0 ? : null}

{refBalance < withdrawMin ? ( <>Вывод открывается с {window.MBEX.fmtUsdt(withdrawMin)} USDT — не хватает {window.MBEX.fmtUsdt(withdrawMissing)} USDT. ) : "Вывод доступен — выберите сумму и кошелёк."}

{refBalance >= withdrawMin ? ( ) : null}
{showWithdrawForm ? (
Вывод USDT {window.MBEX.fmtUsdt(refBalance)} USDT
setWithdrawAmount(event.target.value.replace(/[^\d.,]/g, ""))} /> { setWithdrawWallet(event.target.value); setSelectedWalletId(null); }} /> {wallets.length ? (
{wallets.slice(0, 5).map((wallet) => ( ))}
) : (
Сохранённых кошельков нет. Можно ввести адрес вручную или добавить его в профиле.
)} {nav ? ( ) : null}
{withdrawHint}
) : null}
Приглашено {referral.refCount || 0}
Активных за 30 дней {referral.activeReferrals || 0}
{historyTabs.map((tab) => ( ))}
{historyContent()}
{referral.canApplyReferral ? (
{showApplyForm ? (
setCode(event.target.value.toUpperCase())} />
) : null}
) : null}
); } function LoyaltyScreen({ app }) { const profile = app.privateState.profile || {}; const referral = app.privateState.referral || {}; const vip = referral.vip || {}; const totalUsdt = Number(vip.total_usdt ?? profile.vip_total_usdt ?? profile.total_usdt ?? 0); const nextTier = Number(vip.next_tier ?? profile.next_tier ?? 0); const vipLevel = Number(vip.level ?? profile.vip_level ?? 0); const vipName = vip.name || profile.vip_name || (vipLevel > 0 ? `VIP ${vipLevel}` : "Standard"); const vipDiscountPct = Number(vip.commission_discount_pct ?? profile.commission_discount_pct ?? 0); const configBuyPct = Number(app.publicState?.config?.commission_settings?.buy_pct || 0); const buyBasePct = Number(profile.commission_pct || configBuyPct || Number(vip.commission_rate || 0) * 100); const buyAfterVipPct = buyBasePct * Math.max(0, 1 - vipDiscountPct / 100); const hasReferralDiscount = Boolean(referral.hasFirstOrderDiscount); const buyEffectivePct = hasReferralDiscount ? buyAfterVipPct * 0.5 : buyAfterVipPct; const sellEffectivePct = Number( profile.sell_commission_pct || app.publicState?.config?.commission_settings?.sell_pct || 0 ); const remaining = nextTier > 0 ? Math.max(0, nextTier - totalUsdt) : 0; const progress = nextTier > 0 ? Math.max(0, Math.min(100, (totalUsdt / nextTier) * 100)) : 100; const vipTiers = Array.isArray(referral.vipTiers) ? referral.vipTiers : []; const currentTierIndex = vipTiers.findIndex((tier) => Number(tier.level) === vipLevel); const levelPosition = currentTierIndex >= 0 ? currentTierIndex + 1 : Math.max(1, vipLevel + 1); const levelCount = Math.max(vipTiers.length, levelPosition); const nextTierData = vipTiers.find((tier) => Number(tier.minUsdt) === nextTier) || vipTiers.find((tier) => Number(tier.minUsdt) > totalUsdt) || null; const nextTierName = nextTierData?.name || "следующего уровня"; const nextTierCommissionPct = nextTierData ? buyBasePct * Math.max(0, 1 - Number(nextTierData.commissionDiscountPct || 0) / 100) : buyAfterVipPct; const deepLink = referralDeepLink(referral, app.botUrl); const displayLink = referral.refCode ? `mbex.pro/r/${referral.refCode}` : "Ссылка появится после загрузки"; const commissionSharePct = Number(referral.commissionSharePct || 35); const firstOrderDiscountPct = Number(referral.firstOrderDiscountPct || 50); const [showAllTiers, setShowAllTiers] = React.useState(false); const previewStart = currentTierIndex < 2 ? 0 : Math.min(Math.max(0, currentTierIndex - 1), Math.max(0, vipTiers.length - 3)); const visibleTiers = showAllTiers ? vipTiers : vipTiers.slice(previewStart, previewStart + 3); async function handleShare() { await shareReferralLink(app, deepLink); } function tierSubtitle(tier, index) { const next = vipTiers[index + 1]; if (Number(tier.level) === 0 && next) { return `оборот до ${formatLoyaltyUsdt(next.minUsdt)} USDT`; } return `от ${formatLoyaltyUsdt(tier.minUsdt)} USDT за 30 дней`; } return (
Ваш уровень {vipName}
{levelPosition} из {levelCount}
Комиссия покупки {formatLoyaltyPct(buyEffectivePct)}
Комиссия продажи {formatLoyaltyPct(sellEffectivePct)}

{vipDiscountPct > 0 ? `На уровне ${vipName} комиссия покупки ниже базовой на ${vipDiscountPct}%.` : `На Standard скидки нет: комиссия покупки равна базовой ${formatLoyaltyPct(buyBasePct)}.`} {hasReferralDiscount ? ` Для первой заявки действует дополнительная скидка ${firstOrderDiscountPct}%.` : ""}

{nextTier > 0 ? `До уровня ${nextTierName}` : "Максимальный уровень"} {formatLoyaltyUsdt(totalUsdt)}{nextTier > 0 ? ` / ${formatLoyaltyUsdt(nextTier)} USDT` : " USDT"}

{nextTier > 0 ? ( <> Ещё {formatLoyaltyUsdt(remaining)} USDT оборота за 30 дней — и комиссия покупки станет {formatLoyaltyPct(nextTierCommissionPct)}. ) : "У вас максимальный уровень и минимальная доступная комиссия покупки."}

Уровни и комиссии

{vipTiers.length > 3 ? ( ) : null}
{visibleTiers.length ? visibleTiers.map((tier) => { const tierIndex = vipTiers.indexOf(tier); const isCurrent = Number(tier.level) === vipLevel; const tierCommissionPct = buyBasePct * Math.max( 0, 1 - Number(tier.commissionDiscountPct || 0) / 100 ); return (
{tier.name || `Уровень ${Number(tier.level) + 1}`} {tierSubtitle(tier, tierIndex)}
{isCurrent ? Сейчас : null} {formatLoyaltyPct(tierCommissionPct)}
); }) : (
{vipName} Текущий уровень
Сейчас {formatLoyaltyPct(buyAfterVipPct)}
)}

Приводите друзей — вам {commissionSharePct}% с комиссии каждого обмена

Начисляем в USDT после каждой завершённой заявки друга. Друг получает скидку {firstOrderDiscountPct}% на первую комиссию.

{displayLink}

Скидка уровня и реферальные начисления не уменьшают комиссию продажи — она считается по правилам сервера.

); } function SupportScreen({ app }) { const links = app.supportLinks || []; const openSupport = () => window.MBEX.openTelegramUrl(app.supportUrl); const openLink = (item) => { if (item.kind === "telegram") { window.MBEX.openTelegramUrl(item.url); return; } window.MBEX.openExternalUrl(item.url); }; return (
Support
Поддержка MBEX

Напишите оператору, если нужна помощь по заявке, депозиту, кошельку, верификации или возврату.

По активной заявке приложите код заявки и коротко опишите ситуацию. Полные seed-фразы, приватные ключи и пароли поддержка не запрашивает.
{links.map((item) => ( } accent={item.kind === "telegram"} title={item.label} sub={item.sub || item.url.replace(/^https?:\/\//, "")} onClick={() => openLink(item)} /> ))}
Что подготовить
} title="Заявка" text="Номер заявки, направление и сумма ускоряют проверку." /> } title="Кошелёк" text="Для TRC-20 вопросов укажите только публичный адрес или tx hash." /> } title="Возвраты" text="Если депозит попал на ручную проверку, оператор подскажет следующий шаг." />
); } function OfficesScreen({ app }) { const offices = app.offices || []; const openSupport = () => window.MBEX.openTelegramUrl(app.supportUrl); return (
Адреса и материалы взяты из текущего Mini App. Перед визитом уточните детали и доступность оператора в поддержке.
{offices.map((office) => (
Office
{office.city}

{office.addr}

{office.details ?

{office.details}

: null}
Режим работы {office.hours}
Визит По согласованию
{office.photos && office.photos.length ? (
{office.photos.map((photo) => ( {photo.alt} ))}
) : null}
{office.videoUrl ? ( ) : null}
))}
); } function AmlScreen({ app }) { const profile = app.privateState.profile || {}; const kycVerified = Boolean(profile.verified); const items = [ { icon: , title: "KYC — идентификация клиента", text: "Верификация может потребоваться для лимитов, спорных операций и соблюдения правил обмена.", }, { icon: , title: "Продажа: проверка депозита", text: "Когда USDT поступают на депозитный адрес, сервер проверяет TRC-20 транзакцию до выдачи RUB.", }, { icon: , title: "Покупка: проверка кошелька", text: "Перед отправкой USDT операторский сервер проверяет адрес получателя. При ожидании или отказе отправка блокируется до решения.", }, { icon: , title: "AML / KYT статусы", text: "Ожидание означает, что результат ещё не получен; отказ или ошибка требуют ручного разбора. Для проблемного депозита продажи доступно обращение на возврат.", }, ]; return (
Compliance
AML / KYC / KYT

Кратко о проверках, которые защищают клиента и обменный процесс.

{kycVerified ? "KYC пройден" : "KYC не пройден"}
{items.map((item) => ( ))}
Мы не показываем клиенту внутренние provider details и raw-проверки. Если заявка ушла на ручной разбор, решение и дальнейшие действия подтверждает оператор.
); } function InfoBullet({ icon, title, text }) { return (
{icon}
{title} {text}
); } window.HistoryScreen = HistoryScreen; window.ProfileScreen = ProfileScreen; window.WalletsScreen = WalletsScreen; window.CouriersScreen = CouriersScreen; window.ReferralScreen = ReferralScreen; window.LoyaltyScreen = LoyaltyScreen; window.SupportScreen = SupportScreen; window.OfficesScreen = OfficesScreen; window.AmlScreen = AmlScreen;