diff --git a/micopay/frontend/src/App.tsx b/micopay/frontend/src/App.tsx index a82fee0..3893727 100644 --- a/micopay/frontend/src/App.tsx +++ b/micopay/frontend/src/App.tsx @@ -21,6 +21,7 @@ import DepositQR from "./pages/DepositQR"; import SuccessScreen from "./pages/SuccessScreen"; import Explore from "./pages/Explore"; import History from "./pages/History"; +import TradeDetail from "./pages/TradeDetail"; import CETESScreen from "./pages/CETESScreen"; import BlendScreen from "./pages/BlendScreen"; import MerchantInbox from "./pages/MerchantInbox"; @@ -43,9 +44,6 @@ const USERS_STORAGE_KEY = "micopay_users"; interface StoredUsers { buyer: UserData; seller: UserData } -interface AppProps { - initialTradeId?: string | null; -} type Flow = 'cashout' | 'deposit' | null; @@ -96,12 +94,24 @@ function HistoryRoute() { return ( navigate('/')} - onSelectTrade={() => { /* deep-link a /trade/:id pendiente */ }} + onSelectTrade={(trade) => navigate(`/trade/${trade.id}`)} token={buyerUser?.token ?? null} /> ); } +function TradeDetailRoute() { + const navigate = useNavigate(); + const { buyerUser, sellerUser } = useAppCtx(); + return ( + navigate('/history')} + /> + ); +} + function InboxRoute() { const navigate = useNavigate(); const { sellerUser } = useAppCtx(); @@ -343,12 +353,18 @@ const HIDE_BOTTOMNAV_ROUTES = new Set([ '/terms', ]); +const shouldHideBottomNav = (pathname: string): boolean => { + if (HIDE_BOTTOMNAV_ROUTES.has(pathname)) return true; + if (pathname.startsWith('/trade/')) return true; + return false; +}; + function BottomNavAdapter() { const navigate = useNavigate(); const location = useLocation(); const { sellerUser } = useAppCtx(); - if (HIDE_BOTTOMNAV_ROUTES.has(location.pathname)) return null; + if (shouldHideBottomNav(location.pathname)) return null; const navMap: Record = { home: '/', @@ -369,7 +385,7 @@ function BottomNavAdapter() { // ── Root App ──────────────────────────────────────────────────────────────── -function App({ initialTradeId: _initialTradeId = null }: AppProps) { +function App() { const [flow, setFlow] = useState(null); const [buyerUser, setBuyerUser] = useState(null); const [sellerUser, setSellerUser] = useState(null); @@ -479,6 +495,7 @@ function App({ initialTradeId: _initialTradeId = null }: AppProps) { } /> } /> + } /> } /> } /> } /> diff --git a/micopay/frontend/src/main.tsx b/micopay/frontend/src/main.tsx index 234a123..0bf54d5 100644 --- a/micopay/frontend/src/main.tsx +++ b/micopay/frontend/src/main.tsx @@ -28,14 +28,13 @@ if (Capacitor.isNativePlatform()) { // External claim links: /claim/:requestId // Any AI agent (Claude, GPT, WhatsApp bot...) sends users here to show the QR const claimMatch = window.location.pathname.match(/^\/claim\/([a-zA-Z0-9_-]+)$/) -const tradeDetailMatch = window.location.pathname.match(/^\/trade\/([a-f0-9-]{36})$/i) ReactDOM.createRoot(document.getElementById('root')!).render( {claimMatch ? ( ) : ( - + )} , ) diff --git a/micopay/frontend/src/pages/TradeDetail.tsx b/micopay/frontend/src/pages/TradeDetail.tsx index e0cf805..c37ae65 100644 --- a/micopay/frontend/src/pages/TradeDetail.tsx +++ b/micopay/frontend/src/pages/TradeDetail.tsx @@ -6,6 +6,7 @@ import { cancelTradeRequest, TradeDetailResponse, } from '../services/api'; +import { readJSON } from '../services/secureStorage'; type TradeDetailData = TradeDetailResponse['trade'] & { platform_fee_mxn?: number; @@ -13,12 +14,16 @@ type TradeDetailData = TradeDetailResponse['trade'] & { completed_at?: string | null; }; -function getToken(): string | null { +interface TradeDetailProps { + buyerToken: string | null; + sellerToken: string | null; + onBack: () => void; +} + +async function getStoredToken(): Promise { try { - const raw = localStorage.getItem('micopay_users'); - if (!raw) return null; - const parsed = JSON.parse(raw); - return parsed?.buyer?.token ?? parsed?.seller?.token ?? null; + const stored = await readJSON<{ buyer?: { token: string }; seller?: { token: string } }>('micopay_users'); + return stored?.buyer?.token ?? stored?.seller?.token ?? null; } catch { return null; } @@ -199,16 +204,16 @@ function RevealingView({ trade }: { trade: TradeDetailData }) { ); } -function RevealedView({ trade, onComplete }: { trade: TradeDetailData; onComplete: () => void }) { +function RevealedView({ trade, onComplete, token }: { trade: TradeDetailData; onComplete: () => void; token: string | null }) { const [isConfirming, setIsConfirming] = useState(false); const handleConfirm = async () => { if (isConfirming) return; setIsConfirming(true); try { - const token = getToken(); - if (token) { - await completeTrade(trade.id, token); + const effectiveToken = token ?? (await getStoredToken()); + if (effectiveToken) { + await completeTrade(trade.id, effectiveToken); } } catch (e) { console.warn('Could not complete trade on backend', e); @@ -425,25 +430,30 @@ function NetworkError({ onRetry }: { onRetry: () => void }) { // ── Main component ────────────────────────────────────────────────────────── -export default function TradeDetail() { +function TradeDetailContent({ buyerToken, sellerToken, onBack }: TradeDetailProps) { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [trade, setTrade] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<'not_found' | 'forbidden' | 'network' | null>(null); + const [token, setToken] = useState(buyerToken ?? sellerToken ?? null); + + useEffect(() => { + if (token) return; + getStoredToken().then(t => setToken(t ?? null)); + }, [token]); const fetchTrade = useCallback(async () => { if (!id) return; - const token = getToken(); - if (!token) { - localStorage.setItem('pendingTradeRedirect', `/trade/${id}`); - navigate('/login'); + const effectiveToken = token ?? (await getStoredToken()); + if (!effectiveToken) { + navigate('/'); return; } try { - const data = await fetchTradeDetail(id, token); + const data = await fetchTradeDetail(id, effectiveToken); setTrade(data.trade as TradeDetailData); setError(null); } catch (e: any) { @@ -458,7 +468,7 @@ export default function TradeDetail() { } finally { setLoading(false); } - }, [id, navigate]); + }, [id, navigate, token]); // Fetch on mount useEffect(() => { @@ -477,11 +487,11 @@ export default function TradeDetail() { const handleCancel = async () => { if (!trade) return; - const token = getToken(); - if (!token) return; + const effectiveToken = token ?? (await getStoredToken()); + if (!effectiveToken) return; try { - await cancelTradeRequest(trade.id, token); + await cancelTradeRequest(trade.id, effectiveToken); fetchTrade(); // Refresh trade state } catch (e) { console.error('Failed to cancel trade', e); @@ -544,7 +554,7 @@ export default function TradeDetail() { case 'revealing': return ; case 'revealed': - return ; + return ; case 'completed': return ; case 'cancelled': @@ -563,7 +573,7 @@ export default function TradeDetail() {
); } + +export default function TradeDetail(props: TradeDetailProps) { + return ; +}