// Wallet — RUB balance, deposit via CryptoBot/Platega, paginated history.
// Branded logos — drawn inline (instant, no network) to match the UI.
function CryptoBotLogo({ size = 44 }) {
// Bitcoin ₿ — official mark drawn inline on a warm orange tile.
return (
);
}
function PlategaLogo({ size = 44 }) {
// СБП — pay by QR. Custom crisp QR glyph on a dark tile with SBP-gradient eyes.
const u = size / 44;
const Eye = ({ x, y }) => (
);
return (
{/* scattered data modules */}
{/* accent payment modules in SBP gradient */}
);
}
const DEPOSIT_METHODS = [
{
id: 'cryptobot',
name: 'CryptoBot',
sub: 'Биткоин, USDT, TON',
badge: 'Крипта',
Logo: CryptoBotLogo,
},
{
id: 'platega',
name: 'СБП',
sub: 'QR-код или карта РФ',
badge: 'Мгновенно',
Logo: PlategaLogo,
},
];
const QUICK_AMOUNTS = [500, 1000, 2500, 5000, 10000, 25000];
// Format integer with non-breaking thin spaces: 1234567 → "1 234 567"
function fmtAmount(n) {
if (!n && n !== 0) return '';
return Math.abs(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '\u202F');
}
function DepositSheet({ open, onClose, onConfirm }) {
const [method, setMethod] = React.useState('cryptobot');
const [amount, setAmount] = React.useState(1000);
if (!open) return null;
const onAmountChange = (e) => {
const digits = e.target.value.replace(/\D/g, '');
const n = digits ? parseInt(digits, 10) : 0;
setAmount(Math.min(n, 9_999_999));
};
return (
Способ пополнения
{DEPOSIT_METHODS.map(d => {
const Logo = d.Logo;
return (
setMethod(d.id)}
className={'pay-method' + (method === d.id ? ' active' : '')}>
);
})}
Сумма
{QUICK_AMOUNTS.map(a => (
setAmount(a)}>
{fmtRubShort(a)}
))}
onConfirm(amount, method)}>
Перейти к оплате
Открыть в Telegram · оплата займёт ~30 секунд
);
}
// ─── Transaction history — РЕАЛЬНЫЕ заказы из GET /me (последние 20) ───
// Заказ → строка истории: покупка (минус ₽), отправка/вывод (движение подарка).
function _adaptOrderTx(o) {
const kind = o.kind || '';
const slug = String(o.gift_name || '').replace(/[^A-Za-z0-9]/g, '');
// Реальная миниатюра купленного NFT: строим фото по gift_slug (напр.
// 'InstantRamen-10462'). GiftMedia отдаёт приоритет photo и мягко откатывается
// на градиент, если Fragment вернёт 404. Номер — суффикс слага.
const gslug = o.gift_slug || '';
const photo = gslug ? `https://nft.fragment.com/gift/${gslug}.small.jpg` : null;
const numMatch = gslug.match(/-(\d+)$/);
const num = numMatch ? Number(numMatch[1]) : 1;
const t = o.created_at ? Date.parse(o.created_at) : Date.now();
const rub = Number(o.amount_rub) || 0;
const failed = o.status === 'failed';
const inflight = o.status === 'awaiting_payment' || o.status === 'processing' || o.status === 'pending';
let type, title, sub;
if (kind === 'purchase' || kind === 'buy') {
type = 'buy';
title = `Покупка ${o.gift_name || 'подарка'}`;
sub = failed ? 'отменён' : inflight ? 'в обработке' : 'успешно';
} else if (kind === 'withdraw' || kind === 'tg_transfer') {
type = 'withdraw';
title = `Вывод ${o.gift_name || 'подарка'} в Telegram`;
sub = failed ? 'не удался' : 'успешно';
} else if (kind === 'transfer') {
type = 'send';
title = `Отправка ${o.gift_name || 'подарка'}`;
sub = o.recipient ? '@' + String(o.recipient).replace(/^@/, '') : 'другу';
} else {
type = 'other';
title = kind || 'Операция';
sub = o.status || '';
}
return { type, title, sub, slug, photo, num, t, rub, failed, inflight, id: o.id };
}
function TxRow({ tx }) {
const showAmount = tx.type === 'buy' && tx.rub > 0;
const icon = tx.type === 'withdraw' ?
: tx.type === 'send' ?
: ;
return (
{tx.slug ? (
) : (
{React.cloneElement(icon, { size: 18 })}
)}
{tx.title}
{tx.sub} · {fmtRel(tx.t)}
{showAmount && (
−{fmtAmount(Math.round(tx.rub))} ₽
)}
);
}
function WalletScreen({ balance, onDeposit }) {
const [page, setPage] = React.useState(1);
const [refreshing, setRefreshing] = React.useState(false);
const [histKey, setHistKey] = React.useState(0);
const PAGE_SIZE = 6;
// РЕАЛЬНЫЕ данные из GET /me: потрачено всего + история заказов.
const meState = (window.DataSource && window.DataSource.useMe)
? window.DataSource.useMe(histKey)
: { data: null, loading: false };
const me = meState.data || null;
const spent = me ? Math.round(me.total_spent_rub || 0) : 0;
const txAll = React.useMemo(
() => ((me && me.orders) || []).map(_adaptOrderTx).sort((a, b) => b.t - a.t),
[me]
);
const totalPages = Math.max(1, Math.ceil(txAll.length / PAGE_SIZE));
const safePage = Math.min(page, totalPages);
const pageItems = txAll.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
// Refresh: реальный рефетч /me (bump histKey) + shimmer, пока летит запрос.
const refTimer = React.useRef(0);
const doRefresh = () => {
if (refreshing) return;
setRefreshing(true);
setPage(1);
setHistKey(k => k + 1);
clearTimeout(refTimer.current);
refTimer.current = setTimeout(() => setRefreshing(false), 850);
};
React.useEffect(() => () => clearTimeout(refTimer.current), []);
return (
Баланс кошелька
{refreshing ? (
) : (
fmtAmount(Math.round(v))}/>
₽
)}
{refreshing || meState.loading ? 'Синхронизируем с Portals…' : `потрачено ${fmtAmount(spent)} ₽ · ${me ? (me.orders_total || 0) : 0} заказов`}
Пополнить
{refreshing ? 'Обновляем…' : 'Обновить'}
{DEPOSIT_METHODS.map(d => {
const Logo = d.Logo;
return (
);
})}
{refreshing || meState.loading ? (
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
))}
) : pageItems.length === 0 ? (
Пока пусто — купи первый подарок в маркете
) : (
{pageItems.map((tx, i) => (
{i < pageItems.length - 1 &&
}
))}
)}
{totalPages > 1 && (
setPage(p => Math.max(1, p - 1))}
disabled={safePage <= 1}
aria-label="Предыдущая страница"
style={{
width: 52, height: 52, borderRadius: '50%',
border: '1px solid var(--border)', padding: 0,
background: 'var(--bg-input)', color: 'var(--text)',
cursor: safePage <= 1 ? 'default' : 'pointer',
opacity: safePage <= 1 ? 0.3 : 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'transform 0.15s ease, background 0.2s',
flexShrink: 0,
}}
onMouseDown={(e) => { if (safePage > 1) e.currentTarget.style.transform = 'scale(0.94)'; }}
onMouseUp={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}>
{safePage}
/
{totalPages}
setPage(p => Math.min(totalPages, p + 1))}
disabled={safePage >= totalPages}
aria-label="Следующая страница"
style={{
width: 52, height: 52, borderRadius: '50%',
border: '1px solid var(--border)', padding: 0,
background: 'var(--bg-input)', color: 'var(--text)',
cursor: safePage >= totalPages ? 'default' : 'pointer',
opacity: safePage >= totalPages ? 0.3 : 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'transform 0.15s ease, background 0.2s',
flexShrink: 0,
}}
onMouseDown={(e) => { if (safePage < totalPages) e.currentTarget.style.transform = 'scale(0.94)'; }}
onMouseUp={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}>
)}
Всего {txAll.length} операций
);
}
window.WalletScreen = WalletScreen;
window.DepositSheet = DepositSheet;