← Back
import { useEffect, useState } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { useT } from '../../i18n/LanguageContext';
import { readPusdBalance } from '../../utils/dwApprovals';

interface MarketTrade {
  yes_price: number;
  no_price: number;
  clob_token_ids: string[];
}

interface Props {
  marketId: string;
  question: string;
  side: 'yes' | 'no';
  onClose: () => void;
}

const PRESETS = [5, 10, 25, 50];

// Custody "Войти": places a manual BUY from the user's deposit wallet (same pUSD
// pool as copy-trading) — no wallet signature. Posts an intent to
// /api/copy/manual-order under the Privy session; the daemon executes it (~8s).
export default function EnterTradeModal({ marketId, question, side, onClose }: Props) {
  const { t } = useT();
  const { authenticated, getAccessToken } = usePrivy();
  const [data, setData] = useState<MarketTrade | null>(null);
  const [loadErr, setLoadErr] = useState(false);
  const [loading, setLoading] = useState(true);
  const [amount, setAmount] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null);
  const [pusdBalance, setPusdBalance] = useState<number>(-1); // -1 = unknown (don't block)

  useEffect(() => {
    if (!authenticated) return;
    let alive = true;
    (async () => {
      try {
        const token = await getAccessToken();
        const r = await fetch('/api/copy/wallet', { credentials: 'include', headers: token ? { authorization: `Bearer ${token}` } : {} });
        const d = await r.json();
        const dw = d?.data?.depositWallet;
        if (alive && dw) setPusdBalance(Number(await readPusdBalance(dw)) / 1e6);
      } catch { /* leave unknown → don't block submit */ }
    })();
    return () => { alive = false; };
  }, [authenticated, getAccessToken]);

  useEffect(() => {
    let alive = true;
    fetch(`/api/screener/market/${encodeURIComponent(marketId)}`)
      .then(r => r.json())
      .then(d => { if (alive) { d.success ? setData(d.data) : setLoadErr(true); } })
      .catch(() => { if (alive) setLoadErr(true); })
      .finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [marketId]);

  const ids = data?.clob_token_ids ?? [];
  const tokenId = side === 'yes' ? ids[0] : ids[1];
  const price = data ? (side === 'yes' ? data.yes_price : data.no_price) : 0;
  const outcome = side === 'yes' ? 'Yes' : 'No';
  const amountNum = parseFloat(amount) || 0;
  const shares = price > 0 ? amountNum / price : 0;
  const insufficient = pusdBalance >= 0 && amountNum > pusdBalance;
  const canSubmit = authenticated && amountNum >= 1 && price > 0 && price < 1 && !!tokenId && !submitting && !insufficient;

  const submit = async () => {
    if (!canSubmit || !tokenId) return;
    setSubmitting(true);
    try {
      const token = await getAccessToken();
      const res = await fetch('/api/copy/manual-order', {
        method: 'POST',
        credentials: 'include',
        headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
        body: JSON.stringify({
          market_id: marketId,
          token_id: tokenId,
          outcome,
          amount_usd: amountNum,
          limit_price: price,
        }),
      });
      const d = await res.json();
      if (d.success) {
        setToast({ msg: d.noop ? t('flow.alreadyQueued') : t('flow.queued'), ok: true });
        setTimeout(onClose, 1600);
      } else {
        setToast({ msg: d.error || t('flow.orderFailed'), ok: false });
      }
    } catch {
      setToast({ msg: t('flow.orderFailed'), ok: false });
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="flow-modal-overlay" onClick={onClose}>
      <div className="flow-modal" onClick={e => e.stopPropagation()}>
        <div className="flow-modal-head">
          <span className="flow-modal-q">{question}</span>
          <button className="flow-modal-close" onClick={onClose} aria-label="close">✕</button>
        </div>

        {loading && <div className="flow-modal-body">{t('flow.loading')}</div>}
        {!loading && (loadErr || !data || !tokenId) && (
          <div className="flow-modal-body">{t('flow.tradeUnavailable')}</div>
        )}
        {!loading && data && tokenId && !authenticated && (
          <div className="flow-modal-body">{t('flow.needLogin')}</div>
        )}

        {!loading && data && tokenId && authenticated && (
          <div className="flow-buy">
            <div className="flow-buy-side">
              <span className={`wf-bet ${side === 'yes' ? 'wf-bet-yes' : 'wf-bet-no'}`}>{outcome}</span>
              <span className="flow-buy-price">{Math.round(price * 100)}¢</span>
            </div>

            <div className="order-field">
              <label className="order-label">{t('flow.amount')}</label>
              <div className="order-input-wrap">
                <span className="order-input-prefix">$</span>
                <input type="number" className="order-input" placeholder="0.00" min="1" step="1"
                  value={amount} onChange={e => setAmount(e.target.value)} />
              </div>
              <div className="order-presets">
                {PRESETS.map(p => (
                  <button key={p} className="preset-btn" onClick={() => setAmount(String(p))}>${p}</button>
                ))}
                {pusdBalance >= 0 && (
                  <span className="preset-balance">${pusdBalance.toFixed(2)}</span>
                )}
              </div>
              {insufficient && (
                <div className="flow-buy-est" style={{ color: 'var(--loss)' }}>{t('flow.insufficient')}</div>
              )}
            </div>

            {amountNum > 0 && price > 0 && (
              <div className="flow-buy-est">{t('flow.estShares', { n: shares.toFixed(1) })}</div>
            )}

            <button className="flow-enter-btn" onClick={submit} disabled={!canSubmit}>
              {submitting ? t('flow.submitting') : t('flow.buyConfirm', { x: amountNum.toFixed(2) })}
            </button>

            <p className="order-disclaimer">{t('flow.custodyNote')}</p>
          </div>
        )}

        {toast && (
          <div className={`order-toast ${toast.ok ? 'order-toast-success' : 'order-toast-error'}`}>{toast.msg}</div>
        )}
      </div>
    </div>
  );
}

📜 Git History

b9c19bfchore(poli): reconcile local Flow/Insider/manual-trade work with deployed state3 days ago
cecbd3afeat(poli): Flow «Войти» custody modal (chunk 3) — manual buy from deposit, no wallet sign4 days ago
eb6fff8feat(poli): Flow chunk 3 — green «Войти» button + trade modal4 days ago
Show last diff
Loading...