// Cart sheet — РЕАЛЬНЫЙ чекаут: по каждому подарку POST /purchase с // pay_with_balance (списание с внутреннего ₽-баланса) → поллинг всех заказов // до success/failed. Дизайн-экраны прежние + прогресс и partial-fail. // NB: внешняя платёжка (invoice_url/_openPayUrl) здесь НЕ используется — чекаут // балансовый; оплата картой при нехватке баланса была бы отдельным путём. function CartRow({ item, onRemove }) { return (
{item.name}
{item.num != null &&
#{item.num.toLocaleString('ru-RU')}
}
{fmtRub(item.rub)}
); } function CartSheet({ cart, balance, onClose, onRemove, onCheckout, onBrowseMore, onSuccess }) { const [stage, setStage] = React.useState('list'); // list | processing | success | error const [note, setNote] = React.useState(''); const [okItems, setOkItems] = React.useState([]); const [failItems, setFailItems] = React.useState([]); const total = cart.reduce((s, i) => s + i.rub, 0); const stopRef = React.useRef(false); React.useEffect(() => () => { stopRef.current = true; }, []); // Snapshot the cart at the moment we begin processing so the processing / // success cards keep showing the items even after the parent clears the cart. const purchasedRef = React.useRef([]); const purchasedItems = stage === 'list' ? cart : purchasedRef.current; const purchasedTotal = purchasedItems.reduce((s, i) => s + i.rub, 0); const startCheckout = async () => { const DS = window.DataSource; const items = cart.slice(); purchasedRef.current = items; setOkItems([]); setFailItems([]); setStage('processing'); if (!DS || !DS.purchase) { // Прод в Telegram без DataSource — НЕ имитируем успех (иначе «куплено» без покупки). if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) { setFailItems(items.map(it => ({ item: it, err: 'сервис недоступен' }))); setStage('error'); return; } setTimeout(() => { setOkItems(items); setStage('success'); }, 1900); // дизайн-симулятор return; } // 1) Создаём счета ПОСЛЕДОВАТЕЛЬНО (Portals душит параллель) и оплачиваем. const orders = []; const failed = []; for (let i = 0; i < items.length; i++) { if (stopRef.current) return; const item = items[i]; setNote(`Оплата ${i + 1} из ${items.length}: ${item.name}…`); try { // Списание с ₽-баланса — заказ сразу в processing, поллим ниже. const inv = await DS.purchase(item, true); orders.push({ item, inv }); } catch (e) { failed.push({ item, err: window.friendlyError ? window.friendlyError(e, 'cart') : 'не получилось' }); } } // 2) Поллим все заказы до терминального статуса (до 10 минут суммарно). const done = []; let pending = orders.slice(); const deadline = Date.now() + 10 * 60 * 1000; while (pending.length && !stopRef.current && Date.now() < deadline) { await new Promise(r => setTimeout(r, 4000)); if (stopRef.current) return; setNote(`Оплачено ${done.length} из ${orders.length}, ждём подтверждения…`); const still = []; for (const o of pending) { let st = null; try { st = await DS.getOrder(o.inv.order_id); } catch (_) { still.push(o); continue; } if (st.status === 'success') done.push(o.item); else if (st.status === 'failed') { try { console.warn('[cart] order failed:', st.error_msg); } catch (_) {} failed.push({ item: o.item, err: 'оплата не прошла' }); } else still.push(o); } pending = still; } // По таймауту оставшиеся считаем «в обработке» — они доедут в «Мои подарки». pending.forEach(o => done.push(o.item)); if (stopRef.current) return; setOkItems(done); setFailItems(failed); if (done.length > 0) setStage('success'); else setStage('error'); }; // Commit the purchase the instant we transition into 'success' — clearing // the parent's cart and hiding bought listings from the market. React.useEffect(() => { if (stage === 'success' && typeof onSuccess === 'function' && okItems.length) { onSuccess(okItems); } if (stage === 'success' && window.SFX) window.SFX.success(); }, [stage]); if (cart.length === 0 && stage === 'list') { return (
Корзина пуста
Добавь подарки из маркета — здесь они подождут оплаты одной транзакцией.
{close => ( )}
); } return ( {stage === 'list' && ( <>
Корзина ·{cart.length}
Баланс: {fmtRub(balance)}
{cart.map(item => ( ))}
Подарков
{cart.length}
Итого
{fmtRub(total)}
{balance < total && (
Пополни ещё на{' '} {fmtRub(total - balance)} {' '}в разделе Кошелёк
)}
)} {stage === 'processing' && (
{purchasedItems.slice(0, 6).map((item, i) => (
))}
Оплачиваем покупку…
{note || `${purchasedItems.length} подарк${purchasedItems.length === 1 ? '' : purchasedItems.length < 5 ? 'а' : 'ов'} на ${fmtRub(purchasedTotal)}`}
)} {stage === 'error' && (
Не получилось
К сожалению, оплатить сейчас не получилось. Подарки остались в корзине — попробуй через минуту.
)} {stage === 'success' && (
{okItems.slice(0, 6).map((item, i) => (
))}
Готово! 🎉
{okItems.length} подарк{okItems.length === 1 ? '' : okItems.length < 5 ? 'а' : 'ов'} добавлено в твою коллекцию
{failItems.length > 0 && (
Не удалось: {failItems.map(f => f.item.name).join(', ')}
)}
{close => ( )} {close => ( )}
)} ); } window.CartSheet = CartSheet;