/* 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 (
{label}
);
}
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 (
onSelect(item.v)}
role="radio"
aria-checked={selected}
>
{item.l}
{selected ? : null}
);
})}
,
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 (
{title}
{detail ? {detail} : null}
);
}
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 (
{avatarLetter}
{shortName}
{loginDisplay}
{profile.verified ? "KYC ✓" : "Без KYC"}
Ваш уровень
{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`
: "У вас максимальный доступный уровень и минимальная комиссия"}
nav("loyalty")}>
Все уровни и комиссии
Друзья
nav("referral")}>Подробнее ›
Приглашено
{referral.refCount || 0}
Заработано, USDT
{window.MBEX.fmtUsdt(referral.refEarned || 0)}
Вам — {referralTurnoverText}% с оборота каждого приглашённого, пожизненно.
{referralDisplayLink}
Отправить
{editing ? (
) : 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 ? (
) : null}
{wallets.length ? (
Адреса для получения USDT
{wallets.length} из 20
{wallets.map((wallet) => (
{renamingId === wallet.id ? (
) : (
!wallet.is_default && setDefault(wallet.id)}
>
{wallet.name || "Кошелёк"}
{wallet.is_default ? По умолчанию : null}
window.MBEX.copyText(wallet.address)}
>
{wallet.address}
{formatWalletUse(wallet.last_used_at)}
{
setConfirmingId(null);
setMenuId(menuId === wallet.id ? null : wallet.id);
}}
>
{menuId === wallet.id ? (
startRename(wallet)}>Переименовать
deleteWallet(wallet.id)}
>
{confirmingId === 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 ? (
{couriers.map((courier) => (
{courier.fio || "Получатель"}
Для заявок с другим ФИО
deleteCourier(courier.id)}
>
{confirmingId === courier.id ? "Подтвердить" : }
))}
) : (
}
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 (
);
}
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 ? (
setShowWithdrawForm((value) => !value)}>
{showWithdrawForm ? "Скрыть" : "Вывести"}
) : null}
{showWithdrawForm ? (
) : null}
Приглашено
{referral.refCount || 0}
Активных за 30 дней
{referral.activeReferrals || 0}
{historyTabs.map((tab) => (
setHistoryTab(tab.id)}
>
{tab.label} {tab.count}
))}
{historyContent()}
nav && nav("loyalty")}>
Уровень лояльности
{vipName} · {formatLoyaltyPct(buyEffectivePct)}
nav && nav("wallets")}>
Кошельки TRC-20
{wallets.length}
{referral.canApplyReferral ? (
) : 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 ? (
setShowAllTiers((value) => !value)}>
{showAllTiers ? "Свернуть" : `Все ${vipTiers.length}`}
) : 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) => (
))}
) : null}
window.MBEX.openExternalUrl(office.mapUrl)}>
Карта
{office.videoUrl ? (
window.MBEX.openExternalUrl(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-проверки.
Если заявка ушла на ручной разбор, решение и дальнейшие действия
подтверждает оператор.
window.MBEX.openTelegramUrl(app.supportUrl)}>
Задать вопрос по проверке
);
}
function InfoBullet({ icon, title, text }) {
return (
);
}
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;