Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions micopay/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

Expand Down Expand Up @@ -96,12 +94,24 @@ function HistoryRoute() {
return (
<History
onBack={() => 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 (
<TradeDetail
buyerToken={buyerUser?.token ?? null}
sellerToken={sellerUser?.token ?? null}
onBack={() => navigate('/history')}
/>
);
}

function InboxRoute() {
const navigate = useNavigate();
const { sellerUser } = useAppCtx();
Expand Down Expand Up @@ -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<string, string> = {
home: '/',
Expand All @@ -369,7 +385,7 @@ function BottomNavAdapter() {

// ── Root App ────────────────────────────────────────────────────────────────

function App({ initialTradeId: _initialTradeId = null }: AppProps) {
function App() {
const [flow, setFlow] = useState<Flow>(null);
const [buyerUser, setBuyerUser] = useState<UserData | null>(null);
const [sellerUser, setSellerUser] = useState<UserData | null>(null);
Expand Down Expand Up @@ -479,6 +495,7 @@ function App({ initialTradeId: _initialTradeId = null }: AppProps) {
<Routes>
<Route path="/" element={<HomeRoute />} />
<Route path="/history" element={<HistoryRoute />} />
<Route path="/trade/:id" element={<TradeDetailRoute />} />
<Route path="/inbox" element={<InboxRoute />} />
<Route path="/cashout" element={<CashoutRoute />} />
<Route path="/deposit" element={<DepositRoute />} />
Expand Down
3 changes: 1 addition & 2 deletions micopay/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
{claimMatch ? (
<ClaimQR requestId={claimMatch[1]} />
) : (
<App initialTradeId={tradeDetailMatch?.[1] ?? null} />
<App />
)}
</React.StrictMode>,
)
56 changes: 35 additions & 21 deletions micopay/frontend/src/pages/TradeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@ import {
cancelTradeRequest,
TradeDetailResponse,
} from '../services/api';
import { readJSON } from '../services/secureStorage';

type TradeDetailData = TradeDetailResponse['trade'] & {
platform_fee_mxn?: number;
release_tx_hash?: string | null;
completed_at?: string | null;
};

function getToken(): string | null {
interface TradeDetailProps {
buyerToken: string | null;
sellerToken: string | null;
onBack: () => void;
}

async function getStoredToken(): Promise<string | null> {
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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<TradeDetailData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<'not_found' | 'forbidden' | 'network' | null>(null);
const [token, setToken] = useState<string | null>(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) {
Expand All @@ -458,7 +468,7 @@ export default function TradeDetail() {
} finally {
setLoading(false);
}
}, [id, navigate]);
}, [id, navigate, token]);

// Fetch on mount
useEffect(() => {
Expand All @@ -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);
Expand Down Expand Up @@ -544,7 +554,7 @@ export default function TradeDetail() {
case 'revealing':
return <RevealingView trade={trade} />;
case 'revealed':
return <RevealedView trade={trade} onComplete={handleComplete} />;
return <RevealedView trade={trade} onComplete={handleComplete} token={token} />;
case 'completed':
return <CompletedView trade={trade} />;
case 'cancelled':
Expand All @@ -563,7 +573,7 @@ export default function TradeDetail() {
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/')}
onClick={onBack}
className="p-2 hover:bg-surface-container-low rounded-full transition-colors text-primary"
>
<span className="material-symbols-outlined">arrow_back</span>
Expand Down Expand Up @@ -591,3 +601,7 @@ export default function TradeDetail() {
</div>
);
}

export default function TradeDetail(props: TradeDetailProps) {
return <TradeDetailContent {...props} />;
}