// 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' : '')}>
{d.name}
{d.badge}
{d.sub}
); })}
Сумма
Без комиссии — на баланс зачислится вся сумма
{QUICK_AMOUNTS.map(a => (
setAmount(a)}> {fmtRubShort(a)}
))}
Открыть в 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} заказов`}
{DEPOSIT_METHODS.map(d => { const Logo = d.Logo; return (
{d.name}
{d.badge}
{d.sub}
); })}
{refreshing || meState.loading ? (
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
))}
) : pageItems.length === 0 ? (
Пока пусто — купи первый подарок в маркете
) : (
{pageItems.map((tx, i) => (
{i < pageItems.length - 1 &&
} ))}
)} {totalPages > 1 && (
{safePage} / {totalPages}
)}
Всего {txAll.length} операций
); } window.WalletScreen = WalletScreen; window.DepositSheet = DepositSheet;