Files
pwap/web/src/components/p2p/TradeModal.tsx
T
pezkuwichain 8d30519efc Fix all shadow deprecation warnings across entire mobile app
- Replaced shadowColor/shadowOffset/shadowOpacity/shadowRadius with boxShadow
- Fixed 28 files (21 screens + 7 components)
- Preserved elevation for Android compatibility
- All React Native Web deprecation warnings resolved

Files fixed:
- All screen components
- All reusable components
- Navigation components
- Modal components
2026-01-14 15:05:10 +03:00

205 lines
6.6 KiB
TypeScript

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, AlertTriangle, Clock } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { toast } from 'sonner';
import { acceptFiatOffer, type P2PFiatOffer } from '@shared/lib/p2p-fiat';
interface TradeModalProps {
offer: P2PFiatOffer;
onClose: () => void;
}
export function TradeModal({ offer, onClose }: TradeModalProps) {
const navigate = useNavigate();
const { user } = useAuth();
const { api, selectedAccount } = usePezkuwi();
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const cryptoAmount = parseFloat(amount) || 0;
const fiatAmount = cryptoAmount * offer.price_per_unit;
const isValidAmount = cryptoAmount > 0 && cryptoAmount <= offer.remaining_amount;
// Check min/max order amounts
const meetsMinOrder = !offer.min_order_amount || cryptoAmount >= offer.min_order_amount;
const meetsMaxOrder = !offer.max_order_amount || cryptoAmount <= offer.max_order_amount;
const handleInitiateTrade = async () => {
if (!api || !selectedAccount || !user) {
toast.error('Please connect your wallet and log in');
return;
}
// Prevent self-trading
if (offer.seller_id === user.id) {
toast.error('You cannot trade with your own offer');
return;
}
if (!isValidAmount) {
toast.error('Invalid amount');
return;
}
if (!meetsMinOrder) {
toast.error(`Minimum order: ${offer.min_order_amount} ${offer.token}`);
return;
}
if (!meetsMaxOrder) {
toast.error(`Maximum order: ${offer.max_order_amount} ${offer.token}`);
return;
}
setLoading(true);
try {
const tradeId = await acceptFiatOffer({
api,
account: selectedAccount,
offerId: offer.id,
amount: cryptoAmount
});
toast.success('Trade initiated! Proceed to payment.');
onClose();
// Navigate to trade page
navigate(`/p2p/trade/${tradeId}`);
} catch (error) {
if (import.meta.env.DEV) console.error('Accept offer error:', error);
// Error toast already shown in acceptFiatOffer
} finally {
setLoading(false);
}
};
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
<DialogHeader>
<DialogTitle>Buy {offer.token}</DialogTitle>
<DialogDescription className="text-gray-400">
Trading with {offer.seller_wallet.slice(0, 6)}...{offer.seller_wallet.slice(-4)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Price Info */}
<div className="p-4 bg-gray-800 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="text-gray-400">Price</span>
<span className="text-xl font-bold text-green-400">
{offer.price_per_unit.toFixed(2)} {offer.fiat_currency}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Available</span>
<span className="text-white">{offer.remaining_amount} {offer.token}</span>
</div>
</div>
{/* Amount Input */}
<div>
<Label htmlFor="buyAmount">Amount to Buy ({offer.token})</Label>
<Input
id="buyAmount"
type="number"
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
/>
{offer.min_order_amount && (
<p className="text-xs text-gray-500 mt-1">
Min: {offer.min_order_amount} {offer.token}
</p>
)}
{offer.max_order_amount && (
<p className="text-xs text-gray-500 mt-1">
Max: {offer.max_order_amount} {offer.token}
</p>
)}
</div>
{/* Calculation */}
{cryptoAmount > 0 && (
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-sm text-gray-400 mb-1">You will pay</p>
<p className="text-2xl font-bold text-green-400">
{fiatAmount.toFixed(2)} {offer.fiat_currency}
</p>
</div>
)}
{/* Warnings */}
{!meetsMinOrder && cryptoAmount > 0 && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Minimum order: {offer.min_order_amount} {offer.token}
</AlertDescription>
</Alert>
)}
{!meetsMaxOrder && cryptoAmount > 0 && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Maximum order: {offer.max_order_amount} {offer.token}
</AlertDescription>
</Alert>
)}
{/* Payment Time Limit */}
<Alert>
<Clock className="h-4 w-4" />
<AlertDescription>
Payment deadline: {offer.time_limit_minutes} minutes after accepting
</AlertDescription>
</Alert>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={onClose}
disabled={loading}
className="bg-gray-800 border-gray-700 hover:bg-gray-700"
>
Cancel
</Button>
<Button
onClick={handleInitiateTrade}
disabled={!isValidAmount || !meetsMinOrder || !meetsMaxOrder || loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Initiating...
</>
) : (
'Accept & Continue'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}