mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 00:47:55 +00:00
fix: resolve ESLint/Prettier issues in P2P components
- Fix prettier formatting across all P2P files - Fix setState-in-useEffect by using useCallback pattern - Add missing React import for keyboard event type - Wrap fetch functions in useCallback for exhaustive-deps
This commit is contained in:
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pezkuwi-telegram-miniapp",
|
"name": "pezkuwi-telegram-miniapp",
|
||||||
"version": "1.0.193",
|
"version": "1.0.221",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pezkuwi-telegram-miniapp",
|
"name": "pezkuwi-telegram-miniapp",
|
||||||
"version": "1.0.193",
|
"version": "1.0.221",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pezkuwi/api": "^16.5.36",
|
"@pezkuwi/api": "^16.5.36",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tronweb": "^6.2.0",
|
"tronweb": "^6.2.0",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.4.1",
|
"vite": "^5.4.1",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pezkuwi-telegram-miniapp",
|
"name": "pezkuwi-telegram-miniapp",
|
||||||
"version": "1.0.224",
|
"version": "1.0.225",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
|
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
|
||||||
"author": "Pezkuwichain Team",
|
"author": "Pezkuwichain Team",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tronweb": "^6.2.0",
|
"tronweb": "^6.2.0",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.4.1",
|
"vite": "^5.4.1",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Wallet, Lock, RefreshCw } from 'lucide-react';
|
import { Wallet, Lock, RefreshCw } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@@ -19,7 +19,7 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const fetchBalances = async () => {
|
const fetchBalances = useCallback(async () => {
|
||||||
if (!sessionToken) return;
|
if (!sessionToken) return;
|
||||||
try {
|
try {
|
||||||
const data = await getInternalBalance(sessionToken);
|
const data = await getInternalBalance(sessionToken);
|
||||||
@@ -30,11 +30,11 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [sessionToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBalances();
|
fetchBalances();
|
||||||
}, [sessionToken]);
|
}, [fetchBalances]);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
hapticImpact('light');
|
hapticImpact('light');
|
||||||
@@ -65,7 +65,9 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
|
|||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
className="p-1.5 rounded-full hover:bg-muted/50 transition-colors"
|
className="p-1.5 rounded-full hover:bg-muted/50 transition-colors"
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn('w-4 h-4 text-muted-foreground', refreshing && 'animate-spin')} />
|
<RefreshCw
|
||||||
|
className={cn('w-4 h-4 text-muted-foreground', refreshing && 'animate-spin')}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { cn } from '@/lib/utils';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useTranslation } from '@/i18n';
|
import { useTranslation } from '@/i18n';
|
||||||
import { useTelegram } from '@/hooks/useTelegram';
|
import { useTelegram } from '@/hooks/useTelegram';
|
||||||
import { createP2POffer, getPaymentMethods, getInternalBalance, type PaymentMethod } from '@/lib/p2p-api';
|
import {
|
||||||
|
createP2POffer,
|
||||||
|
getPaymentMethods,
|
||||||
|
getInternalBalance,
|
||||||
|
type PaymentMethod,
|
||||||
|
} from '@/lib/p2p-api';
|
||||||
|
|
||||||
interface CreateOfferModalProps {
|
interface CreateOfferModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -39,9 +44,7 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !sessionToken) return;
|
if (!isOpen || !sessionToken) return;
|
||||||
getPaymentMethods(sessionToken, fiatCurrency)
|
getPaymentMethods(sessionToken, fiatCurrency).then(setPaymentMethods).catch(console.error);
|
||||||
.then(setPaymentMethods)
|
|
||||||
.catch(console.error);
|
|
||||||
}, [isOpen, sessionToken, fiatCurrency]);
|
}, [isOpen, sessionToken, fiatCurrency]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -148,19 +151,25 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
||||||
>
|
>
|
||||||
{TOKENS.map((tk) => (
|
{TOKENS.map((tk) => (
|
||||||
<option key={tk} value={tk}>{tk}</option>
|
<option key={tk} value={tk}>
|
||||||
|
{tk}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.fiatCurrency')}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.fiatCurrency')}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={fiatCurrency}
|
value={fiatCurrency}
|
||||||
onChange={(e) => setFiatCurrency(e.target.value)}
|
onChange={(e) => setFiatCurrency(e.target.value)}
|
||||||
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
||||||
>
|
>
|
||||||
{FIAT_CURRENCIES.map((c) => (
|
{FIAT_CURRENCIES.map((c) => (
|
||||||
<option key={c} value={c}>{c}</option>
|
<option key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +197,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
|
|
||||||
{/* Fiat Amount */}
|
{/* Fiat Amount */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.fiatTotal')} ({fiatCurrency})</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.fiatTotal')} ({fiatCurrency})
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={fiatAmount}
|
value={fiatAmount}
|
||||||
@@ -199,14 +210,18 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
/>
|
/>
|
||||||
{pricePerUnit > 0 && (
|
{pricePerUnit > 0 && (
|
||||||
<p className="text-xs text-cyan-400 mt-1">
|
<p className="text-xs text-cyan-400 mt-1">
|
||||||
{t('p2p.pricePerUnit')}: {pricePerUnit.toLocaleString(undefined, { maximumFractionDigits: 2 })} {fiatCurrency}/{token}
|
{t('p2p.pricePerUnit')}:{' '}
|
||||||
|
{pricePerUnit.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
|
||||||
|
{fiatCurrency}/{token}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment Method */}
|
{/* Payment Method */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.paymentMethod')}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.paymentMethod')}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={paymentMethodId}
|
value={paymentMethodId}
|
||||||
onChange={(e) => setPaymentMethodId(e.target.value)}
|
onChange={(e) => setPaymentMethodId(e.target.value)}
|
||||||
@@ -214,14 +229,18 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
>
|
>
|
||||||
<option value="">{t('p2p.selectPaymentMethod')}</option>
|
<option value="">{t('p2p.selectPaymentMethod')}</option>
|
||||||
{paymentMethods.map((pm) => (
|
{paymentMethods.map((pm) => (
|
||||||
<option key={pm.id} value={pm.id}>{pm.method_name}</option>
|
<option key={pm.id} value={pm.id}>
|
||||||
|
{pm.method_name}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment Details */}
|
{/* Payment Details */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.paymentDetails')}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.paymentDetails')}
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={paymentDetails}
|
value={paymentDetails}
|
||||||
onChange={(e) => setPaymentDetails(e.target.value)}
|
onChange={(e) => setPaymentDetails(e.target.value)}
|
||||||
@@ -234,7 +253,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
{/* Order Limits */}
|
{/* Order Limits */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.minOrder')}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.minOrder')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={minOrder}
|
value={minOrder}
|
||||||
@@ -245,7 +266,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.maxOrder')}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.maxOrder')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxOrder}
|
value={maxOrder}
|
||||||
@@ -266,7 +289,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
|
|||||||
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
|
||||||
>
|
>
|
||||||
{TIME_LIMITS.map((tl) => (
|
{TIME_LIMITS.map((tl) => (
|
||||||
<option key={tl} value={tl}>{tl} {t('p2p.minutes')}</option>
|
<option key={tl} value={tl}>
|
||||||
|
{tl} {t('p2p.minutes')}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ export function DisputeModal({ isOpen, onClose, tradeId, onDisputeOpened }: Disp
|
|||||||
|
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.disputeCategory')}</label>
|
<label className="text-sm text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.disputeCategory')}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(e) => setCategory(e.target.value)}
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
@@ -89,14 +91,18 @@ export function DisputeModal({ isOpen, onClose, tradeId, onDisputeOpened }: Disp
|
|||||||
>
|
>
|
||||||
<option value="">{t('p2p.selectCategory')}</option>
|
<option value="">{t('p2p.selectCategory')}</option>
|
||||||
{DISPUTE_CATEGORIES.map((cat) => (
|
{DISPUTE_CATEGORIES.map((cat) => (
|
||||||
<option key={cat} value={cat}>{t(`p2p.dispute.${cat}`)}</option>
|
<option key={cat} value={cat}>
|
||||||
|
{t(`p2p.dispute.${cat}`)}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reason */}
|
{/* Reason */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.disputeReason')}</label>
|
<label className="text-sm text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.disputeReason')}
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={reason}
|
value={reason}
|
||||||
onChange={(e) => setReason(e.target.value)}
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Star, Clock, ChevronDown, Loader2 } from 'lucide-react';
|
import { Star, Clock, ChevronDown, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@@ -29,31 +29,34 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
|
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
|
||||||
const fetchOffers = async (p = 1) => {
|
const fetchOffers = useCallback(
|
||||||
if (!sessionToken) return;
|
async (p = 1) => {
|
||||||
setLoading(true);
|
if (!sessionToken) return;
|
||||||
try {
|
setLoading(true);
|
||||||
const result = await getP2POffers({
|
try {
|
||||||
sessionToken,
|
const result = await getP2POffers({
|
||||||
adType,
|
sessionToken,
|
||||||
token: selectedToken || undefined,
|
adType,
|
||||||
fiatCurrency: selectedCurrency || undefined,
|
token: selectedToken || undefined,
|
||||||
page: p,
|
fiatCurrency: selectedCurrency || undefined,
|
||||||
limit,
|
page: p,
|
||||||
});
|
limit,
|
||||||
setOffers(result.offers);
|
});
|
||||||
setTotal(result.total);
|
setOffers(result.offers);
|
||||||
setPage(p);
|
setTotal(result.total);
|
||||||
} catch (err) {
|
setPage(p);
|
||||||
console.error('Failed to fetch offers:', err);
|
} catch (err) {
|
||||||
} finally {
|
console.error('Failed to fetch offers:', err);
|
||||||
setLoading(false);
|
} finally {
|
||||||
}
|
setLoading(false);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[sessionToken, adType, selectedToken, selectedCurrency]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOffers(1);
|
fetchOffers(1);
|
||||||
}, [sessionToken, adType, selectedCurrency, selectedToken]);
|
}, [fetchOffers]);
|
||||||
|
|
||||||
const handleAccept = (offer: P2POffer) => {
|
const handleAccept = (offer: P2POffer) => {
|
||||||
hapticImpact('medium');
|
hapticImpact('medium');
|
||||||
@@ -82,7 +85,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
>
|
>
|
||||||
<option value="">{t('p2p.allTokens')}</option>
|
<option value="">{t('p2p.allTokens')}</option>
|
||||||
{TOKENS.map((tk) => (
|
{TOKENS.map((tk) => (
|
||||||
<option key={tk} value={tk}>{tk}</option>
|
<option key={tk} value={tk}>
|
||||||
|
{tk}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
@@ -92,7 +97,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
>
|
>
|
||||||
<option value="">{t('p2p.allCurrencies')}</option>
|
<option value="">{t('p2p.allCurrencies')}</option>
|
||||||
{FIAT_CURRENCIES.map((c) => (
|
{FIAT_CURRENCIES.map((c) => (
|
||||||
<option key={c} value={c}>{c}</option>
|
<option key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,10 +117,7 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{offers.map((offer) => (
|
{offers.map((offer) => (
|
||||||
<div
|
<div key={offer.id} className="bg-card rounded-xl border border-border p-3 space-y-2">
|
||||||
key={offer.id}
|
|
||||||
className="bg-card rounded-xl border border-border p-3 space-y-2"
|
|
||||||
>
|
|
||||||
{/* Top: Token + Price */}
|
{/* Top: Token + Price */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -121,7 +125,8 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
<span className="text-xs text-muted-foreground">/ {offer.fiat_currency}</span>
|
<span className="text-xs text-muted-foreground">/ {offer.fiat_currency}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-cyan-400">
|
<span className="text-sm font-bold text-cyan-400">
|
||||||
{offer.price_per_unit?.toLocaleString(undefined, { maximumFractionDigits: 2 })} {offer.fiat_currency}
|
{offer.price_per_unit?.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
|
||||||
|
{offer.fiat_currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,7 +184,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
|
|||||||
>
|
>
|
||||||
{t('p2p.prev')}
|
{t('p2p.prev')}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-muted-foreground">{page} / {totalPages}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{page} / {totalPages}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => fetchOffers(page + 1)}
|
onClick={() => fetchOffers(page + 1)}
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { X, Send } from 'lucide-react';
|
import { X, Send } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@@ -20,11 +20,12 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const userId = user?.id;
|
const userId = user?.id;
|
||||||
|
|
||||||
const fetchMessages = async () => {
|
const fetchMessages = useCallback(async () => {
|
||||||
if (!sessionToken) return;
|
if (!sessionToken) return;
|
||||||
try {
|
try {
|
||||||
const msgs = await getTradeMessages(sessionToken, tradeId);
|
const msgs = await getTradeMessages(sessionToken, tradeId);
|
||||||
@@ -34,13 +35,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [sessionToken, tradeId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMessages();
|
fetchMessages();
|
||||||
const interval = setInterval(fetchMessages, 5000);
|
const interval = setInterval(fetchMessages, 5000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [sessionToken, tradeId]);
|
}, [fetchMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -71,10 +72,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={cn(
|
<div
|
||||||
'flex items-center justify-between p-4 border-b border-border safe-area-top',
|
className={cn(
|
||||||
isRTL && 'direction-rtl'
|
'flex items-center justify-between p-4 border-b border-border safe-area-top',
|
||||||
)} dir={isRTL ? 'rtl' : 'ltr'}>
|
isRTL && 'direction-rtl'
|
||||||
|
)}
|
||||||
|
dir={isRTL ? 'rtl' : 'ltr'}
|
||||||
|
>
|
||||||
<h2 className="text-lg font-semibold text-foreground">{t('p2p.chat')}</h2>
|
<h2 className="text-lg font-semibold text-foreground">{t('p2p.chat')}</h2>
|
||||||
<button onClick={onClose} className="p-2 rounded-full hover:bg-muted transition-colors">
|
<button onClick={onClose} className="p-2 rounded-full hover:bg-muted transition-colors">
|
||||||
<X className="w-5 h-5 text-muted-foreground" />
|
<X className="w-5 h-5 text-muted-foreground" />
|
||||||
@@ -100,8 +104,7 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex',
|
'flex',
|
||||||
isSystem ? 'justify-center' :
|
isSystem ? 'justify-center' : isOwn ? 'justify-end' : 'justify-start'
|
||||||
isOwn ? 'justify-end' : 'justify-start'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSystem ? (
|
{isSystem ? (
|
||||||
@@ -118,11 +121,16 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-sm break-words">{msg.message}</p>
|
<p className="text-sm break-words">{msg.message}</p>
|
||||||
<p className={cn(
|
<p
|
||||||
'text-[10px] mt-1',
|
className={cn(
|
||||||
isOwn ? 'text-white/60' : 'text-muted-foreground'
|
'text-[10px] mt-1',
|
||||||
)}>
|
isOwn ? 'text-white/60' : 'text-muted-foreground'
|
||||||
{new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
)}
|
||||||
|
>
|
||||||
|
{new Date(msg.created_at).toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -134,10 +142,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div className={cn(
|
<div
|
||||||
'flex items-center gap-2 p-4 border-t border-border safe-area-bottom',
|
className={cn(
|
||||||
isRTL && 'direction-rtl'
|
'flex items-center gap-2 p-4 border-t border-border safe-area-bottom',
|
||||||
)} dir={isRTL ? 'rtl' : 'ltr'}>
|
isRTL && 'direction-rtl'
|
||||||
|
)}
|
||||||
|
dir={isRTL ? 'rtl' : 'ltr'}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newMessage}
|
value={newMessage}
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
|
|||||||
if (!isOpen || !offer) return null;
|
if (!isOpen || !offer) return null;
|
||||||
|
|
||||||
const numAmount = parseFloat(amount) || 0;
|
const numAmount = parseFloat(amount) || 0;
|
||||||
const fiatTotal = numAmount > 0 && offer.price_per_unit
|
const fiatTotal = numAmount > 0 && offer.price_per_unit ? numAmount * offer.price_per_unit : 0;
|
||||||
? numAmount * offer.price_per_unit
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
numAmount > 0 &&
|
numAmount > 0 &&
|
||||||
@@ -108,7 +106,9 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
|
|||||||
|
|
||||||
{/* Amount Input */}
|
{/* Amount Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.amount')} ({offer.token})</label>
|
<label className="text-sm text-muted-foreground mb-1 block">
|
||||||
|
{t('p2p.amount')} ({offer.token})
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={amount}
|
value={amount}
|
||||||
@@ -133,7 +133,8 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-muted-foreground">{t('p2p.fiatTotal')}</span>
|
<span className="text-sm text-muted-foreground">{t('p2p.fiatTotal')}</span>
|
||||||
<span className="text-lg font-bold text-cyan-400">
|
<span className="text-lg font-bold text-cyan-400">
|
||||||
{fiatTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })} {offer.fiat_currency}
|
{fiatTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
|
||||||
|
{offer.fiat_currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { ArrowLeft, Clock, CheckCircle2, XCircle, AlertTriangle, MessageCircle } from 'lucide-react';
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
MessageCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useTranslation } from '@/i18n';
|
import { useTranslation } from '@/i18n';
|
||||||
@@ -67,13 +74,23 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
|
|||||||
const [timeRemaining, setTimeRemaining] = useState('');
|
const [timeRemaining, setTimeRemaining] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!trade) return;
|
if (!trade) return;
|
||||||
const deadline = trade.status === 'pending' ? trade.payment_deadline :
|
const deadline =
|
||||||
trade.status === 'payment_sent' ? trade.confirmation_deadline : null;
|
trade.status === 'pending'
|
||||||
if (!deadline) { setTimeRemaining(''); return; }
|
? trade.payment_deadline
|
||||||
|
: trade.status === 'payment_sent'
|
||||||
|
? trade.confirmation_deadline
|
||||||
|
: null;
|
||||||
|
if (!deadline) {
|
||||||
|
setTimeRemaining('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const diff = new Date(deadline).getTime() - Date.now();
|
const diff = new Date(deadline).getTime() - Date.now();
|
||||||
if (diff <= 0) { setTimeRemaining(t('p2p.expired')); return; }
|
if (diff <= 0) {
|
||||||
|
setTimeRemaining(t('p2p.expired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const mins = Math.floor(diff / 60000);
|
const mins = Math.floor(diff / 60000);
|
||||||
const secs = Math.floor((diff % 60000) / 1000);
|
const secs = Math.floor((diff % 60000) / 1000);
|
||||||
setTimeRemaining(`${mins}:${secs.toString().padStart(2, '0')}`);
|
setTimeRemaining(`${mins}:${secs.toString().padStart(2, '0')}`);
|
||||||
@@ -146,7 +163,9 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-sm text-muted-foreground">{t('p2p.tradeNotFound')}</p>
|
<p className="text-sm text-muted-foreground">{t('p2p.tradeNotFound')}</p>
|
||||||
<button onClick={onBack} className="text-cyan-400 text-sm mt-2">{t('common.back')}</button>
|
<button onClick={onBack} className="text-cyan-400 text-sm mt-2">
|
||||||
|
{t('common.back')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,7 +184,12 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Banner */}
|
{/* Status Banner */}
|
||||||
<div className={cn('flex items-center gap-2 p-3 rounded-xl', `${statusConfig.color} bg-current/10`)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 p-3 rounded-xl',
|
||||||
|
`${statusConfig.color} bg-current/10`
|
||||||
|
)}
|
||||||
|
>
|
||||||
<StatusIcon className={cn('w-5 h-5', statusConfig.color)} />
|
<StatusIcon className={cn('w-5 h-5', statusConfig.color)} />
|
||||||
<span className={cn('text-sm font-medium', statusConfig.color)}>
|
<span className={cn('text-sm font-medium', statusConfig.color)}>
|
||||||
{t(`p2p.status.${trade.status}`)}
|
{t(`p2p.status.${trade.status}`)}
|
||||||
@@ -208,11 +232,7 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
|
|||||||
{/* Timeline */}
|
{/* Timeline */}
|
||||||
<div className="bg-card rounded-xl border border-border p-4 space-y-3">
|
<div className="bg-card rounded-xl border border-border p-4 space-y-3">
|
||||||
<h3 className="text-sm font-medium text-foreground">{t('p2p.timeline')}</h3>
|
<h3 className="text-sm font-medium text-foreground">{t('p2p.timeline')}</h3>
|
||||||
<TimelineStep
|
<TimelineStep done label={t('p2p.tradeCreated')} time={trade.created_at} />
|
||||||
done
|
|
||||||
label={t('p2p.tradeCreated')}
|
|
||||||
time={trade.created_at}
|
|
||||||
/>
|
|
||||||
<TimelineStep
|
<TimelineStep
|
||||||
done={!!trade.buyer_marked_paid_at}
|
done={!!trade.buyer_marked_paid_at}
|
||||||
active={trade.status === 'pending'}
|
active={trade.status === 'pending'}
|
||||||
@@ -297,23 +317,29 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat Modal */}
|
{/* Chat Modal */}
|
||||||
{showChat && (
|
{showChat && <TradeChat tradeId={trade.id} onClose={() => setShowChat(false)} />}
|
||||||
<TradeChat tradeId={trade.id} onClose={() => setShowChat(false)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dispute Modal */}
|
{/* Dispute Modal */}
|
||||||
<DisputeModal
|
<DisputeModal
|
||||||
isOpen={showDispute}
|
isOpen={showDispute}
|
||||||
onClose={() => setShowDispute(false)}
|
onClose={() => setShowDispute(false)}
|
||||||
tradeId={trade.id}
|
tradeId={trade.id}
|
||||||
onDisputeOpened={() => { setShowDispute(false); fetchTrade(); }}
|
onDisputeOpened={() => {
|
||||||
|
setShowDispute(false);
|
||||||
|
fetchTrade();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline step sub-component
|
// Timeline step sub-component
|
||||||
function TimelineStep({ done, active, label, time }: {
|
function TimelineStep({
|
||||||
|
done,
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
time,
|
||||||
|
}: {
|
||||||
done?: boolean;
|
done?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -324,18 +350,16 @@ function TimelineStep({ done, active, label, time }: {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-3 h-3 rounded-full border-2 flex-shrink-0',
|
'w-3 h-3 rounded-full border-2 flex-shrink-0',
|
||||||
done ? 'bg-green-400 border-green-400' :
|
done
|
||||||
active ? 'border-cyan-400 animate-pulse' :
|
? 'bg-green-400 border-green-400'
|
||||||
'border-muted-foreground/30'
|
: active
|
||||||
|
? 'border-cyan-400 animate-pulse'
|
||||||
|
: 'border-muted-foreground/30'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className={cn('text-sm', done ? 'text-foreground' : 'text-muted-foreground')}>{label}</p>
|
<p className={cn('text-sm', done ? 'text-foreground' : 'text-muted-foreground')}>{label}</p>
|
||||||
{time && (
|
{time && <p className="text-xs text-muted-foreground">{new Date(time).toLocaleString()}</p>}
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{new Date(time).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+68
-40
@@ -126,9 +126,7 @@ async function callEdgeFunction<T>(
|
|||||||
|
|
||||||
// ─── Balance ─────────────────────────────────────────────────
|
// ─── Balance ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getInternalBalance(
|
export async function getInternalBalance(sessionToken: string): Promise<InternalBalance[]> {
|
||||||
sessionToken: string
|
|
||||||
): Promise<InternalBalance[]> {
|
|
||||||
const result = await callEdgeFunction<{ success: boolean; balances: InternalBalance[] }>(
|
const result = await callEdgeFunction<{ success: boolean; balances: InternalBalance[] }>(
|
||||||
'get-internal-balance',
|
'get-internal-balance',
|
||||||
{ sessionToken }
|
{ sessionToken }
|
||||||
@@ -172,14 +170,18 @@ export async function getP2POffers(params: GetOffersParams): Promise<GetOffersRe
|
|||||||
'get-p2p-offers',
|
'get-p2p-offers',
|
||||||
params as unknown as Record<string, unknown>
|
params as unknown as Record<string, unknown>
|
||||||
);
|
);
|
||||||
return { offers: result.offers || [], total: result.total || 0, page: result.page || 1, limit: result.limit || 20 };
|
return {
|
||||||
|
offers: result.offers || [],
|
||||||
|
total: result.total || 0,
|
||||||
|
page: result.page || 1,
|
||||||
|
limit: result.limit || 20,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMyOffers(sessionToken: string): Promise<P2POffer[]> {
|
export async function getMyOffers(sessionToken: string): Promise<P2POffer[]> {
|
||||||
const result = await callEdgeFunction<{ success: boolean; offers: P2POffer[] }>(
|
const result = await callEdgeFunction<{ success: boolean; offers: P2POffer[] }>('get-my-offers', {
|
||||||
'get-my-offers',
|
sessionToken,
|
||||||
{ sessionToken }
|
});
|
||||||
);
|
|
||||||
return result.offers || [];
|
return result.offers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +194,9 @@ interface AcceptOfferParams {
|
|||||||
buyerWallet: string;
|
buyerWallet: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptP2POffer(params: AcceptOfferParams): Promise<{ tradeId: string; trade: P2PTrade }> {
|
export async function acceptP2POffer(
|
||||||
|
params: AcceptOfferParams
|
||||||
|
): Promise<{ tradeId: string; trade: P2PTrade }> {
|
||||||
return callEdgeFunction<{ success: boolean; tradeId: string; trade: P2PTrade }>(
|
return callEdgeFunction<{ success: boolean; tradeId: string; trade: P2PTrade }>(
|
||||||
'accept-p2p-offer',
|
'accept-p2p-offer',
|
||||||
params as unknown as Record<string, unknown>
|
params as unknown as Record<string, unknown>
|
||||||
@@ -215,7 +219,9 @@ interface CreateOfferParams {
|
|||||||
adType?: 'buy' | 'sell';
|
adType?: 'buy' | 'sell';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createP2POffer(params: CreateOfferParams): Promise<{ offerId: string; offer: P2POffer }> {
|
export async function createP2POffer(
|
||||||
|
params: CreateOfferParams
|
||||||
|
): Promise<{ offerId: string; offer: P2POffer }> {
|
||||||
const result = await callEdgeFunction<{ success: boolean; offer_id: string; offer: P2POffer }>(
|
const result = await callEdgeFunction<{ success: boolean; offer_id: string; offer: P2POffer }>(
|
||||||
'create-offer-telegram',
|
'create-offer-telegram',
|
||||||
params as unknown as Record<string, unknown>
|
params as unknown as Record<string, unknown>
|
||||||
@@ -239,26 +245,37 @@ export async function getP2PTrades(
|
|||||||
// ─── Trade Actions ───────────────────────────────────────────
|
// ─── Trade Actions ───────────────────────────────────────────
|
||||||
|
|
||||||
export async function markTradePaid(sessionToken: string, tradeId: string): Promise<P2PTrade> {
|
export async function markTradePaid(sessionToken: string, tradeId: string): Promise<P2PTrade> {
|
||||||
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>(
|
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
|
||||||
'trade-action',
|
sessionToken,
|
||||||
{ sessionToken, tradeId, action: 'mark_paid' }
|
tradeId,
|
||||||
);
|
action: 'mark_paid',
|
||||||
|
});
|
||||||
return result.trade;
|
return result.trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmTradePayment(sessionToken: string, tradeId: string): Promise<P2PTrade> {
|
export async function confirmTradePayment(
|
||||||
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>(
|
sessionToken: string,
|
||||||
'trade-action',
|
tradeId: string
|
||||||
{ sessionToken, tradeId, action: 'confirm' }
|
): Promise<P2PTrade> {
|
||||||
);
|
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
|
||||||
|
sessionToken,
|
||||||
|
tradeId,
|
||||||
|
action: 'confirm',
|
||||||
|
});
|
||||||
return result.trade;
|
return result.trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelTrade(sessionToken: string, tradeId: string, reason?: string): Promise<P2PTrade> {
|
export async function cancelTrade(
|
||||||
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>(
|
sessionToken: string,
|
||||||
'trade-action',
|
tradeId: string,
|
||||||
{ sessionToken, tradeId, action: 'cancel', payload: { reason } }
|
reason?: string
|
||||||
);
|
): Promise<P2PTrade> {
|
||||||
|
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
|
||||||
|
sessionToken,
|
||||||
|
tradeId,
|
||||||
|
action: 'cancel',
|
||||||
|
payload: { reason },
|
||||||
|
});
|
||||||
return result.trade;
|
return result.trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +285,12 @@ export async function rateTrade(
|
|||||||
rating: number,
|
rating: number,
|
||||||
review?: string
|
review?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await callEdgeFunction<{ success: boolean }>(
|
await callEdgeFunction<{ success: boolean }>('trade-action', {
|
||||||
'trade-action',
|
sessionToken,
|
||||||
{ sessionToken, tradeId, action: 'rate', payload: { rating, review } }
|
tradeId,
|
||||||
);
|
action: 'rate',
|
||||||
|
payload: { rating, review },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Messages ────────────────────────────────────────────────
|
// ─── Messages ────────────────────────────────────────────────
|
||||||
@@ -281,10 +300,12 @@ export async function sendTradeMessage(
|
|||||||
tradeId: string,
|
tradeId: string,
|
||||||
message: string
|
message: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const result = await callEdgeFunction<{ success: boolean; messageId: string }>(
|
const result = await callEdgeFunction<{ success: boolean; messageId: string }>('p2p-messages', {
|
||||||
'p2p-messages',
|
sessionToken,
|
||||||
{ sessionToken, action: 'send', tradeId, message }
|
action: 'send',
|
||||||
);
|
tradeId,
|
||||||
|
message,
|
||||||
|
});
|
||||||
return result.messageId;
|
return result.messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,10 +328,13 @@ export async function openDispute(
|
|||||||
reason: string,
|
reason: string,
|
||||||
category: string
|
category: string
|
||||||
): Promise<P2PDispute> {
|
): Promise<P2PDispute> {
|
||||||
const result = await callEdgeFunction<{ success: boolean; dispute: P2PDispute }>(
|
const result = await callEdgeFunction<{ success: boolean; dispute: P2PDispute }>('p2p-dispute', {
|
||||||
'p2p-dispute',
|
sessionToken,
|
||||||
{ sessionToken, action: 'open', tradeId, reason, category }
|
action: 'open',
|
||||||
);
|
tradeId,
|
||||||
|
reason,
|
||||||
|
category,
|
||||||
|
});
|
||||||
return result.dispute;
|
return result.dispute;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,8 +345,12 @@ export async function addDisputeEvidence(
|
|||||||
evidenceType: string,
|
evidenceType: string,
|
||||||
description?: string
|
description?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await callEdgeFunction<{ success: boolean }>(
|
await callEdgeFunction<{ success: boolean }>('p2p-dispute', {
|
||||||
'p2p-dispute',
|
sessionToken,
|
||||||
{ sessionToken, action: 'add_evidence', tradeId, evidenceUrl, evidenceType, description }
|
action: 'add_evidence',
|
||||||
);
|
tradeId,
|
||||||
|
evidenceUrl,
|
||||||
|
evidenceType,
|
||||||
|
description,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-37
@@ -40,23 +40,33 @@ export function P2PSection() {
|
|||||||
|
|
||||||
const fetchMyTrades = useCallback(async () => {
|
const fetchMyTrades = useCallback(async () => {
|
||||||
if (!sessionToken) return;
|
if (!sessionToken) return;
|
||||||
|
setMyDataLoading(true);
|
||||||
try {
|
try {
|
||||||
const trades = await getP2PTrades(sessionToken, 'all');
|
const trades = await getP2PTrades(sessionToken, 'all');
|
||||||
setMyTrades(trades);
|
setMyTrades(trades);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch my trades:', err);
|
console.error('Failed to fetch my trades:', err);
|
||||||
|
} finally {
|
||||||
|
setMyDataLoading(false);
|
||||||
}
|
}
|
||||||
}, [sessionToken]);
|
}, [sessionToken]);
|
||||||
|
|
||||||
|
const fetchMyOffersFull = useCallback(async () => {
|
||||||
|
setMyDataLoading(true);
|
||||||
|
try {
|
||||||
|
await fetchMyOffers();
|
||||||
|
} finally {
|
||||||
|
setMyDataLoading(false);
|
||||||
|
}
|
||||||
|
}, [fetchMyOffers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'myAds') {
|
if (activeTab === 'myAds') {
|
||||||
setMyDataLoading(true);
|
fetchMyOffersFull();
|
||||||
fetchMyOffers().finally(() => setMyDataLoading(false));
|
|
||||||
} else if (activeTab === 'myTrades') {
|
} else if (activeTab === 'myTrades') {
|
||||||
setMyDataLoading(true);
|
fetchMyTrades();
|
||||||
fetchMyTrades().finally(() => setMyDataLoading(false));
|
|
||||||
}
|
}
|
||||||
}, [activeTab, fetchMyOffers, fetchMyTrades]);
|
}, [activeTab, fetchMyOffersFull, fetchMyTrades]);
|
||||||
|
|
||||||
const handleTabChange = (tab: Tab) => {
|
const handleTabChange = (tab: Tab) => {
|
||||||
hapticImpact('light');
|
hapticImpact('light');
|
||||||
@@ -81,10 +91,7 @@ export function P2PSection() {
|
|||||||
if (activeTradeId) {
|
if (activeTradeId) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-y-auto p-4 safe-area-top">
|
<div className="h-full overflow-y-auto p-4 safe-area-top">
|
||||||
<TradeView
|
<TradeView tradeId={activeTradeId} onBack={() => setActiveTradeId(null)} />
|
||||||
tradeId={activeTradeId}
|
|
||||||
onBack={() => setActiveTradeId(null)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,13 +105,21 @@ export function P2PSection() {
|
|||||||
|
|
||||||
const statusColor = (s: string) => {
|
const statusColor = (s: string) => {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'open': return 'text-green-400';
|
case 'open':
|
||||||
case 'pending': return 'text-amber-400';
|
return 'text-green-400';
|
||||||
case 'payment_sent': return 'text-blue-400';
|
case 'pending':
|
||||||
case 'completed': return 'text-green-400';
|
return 'text-amber-400';
|
||||||
case 'cancelled': case 'refunded': return 'text-red-400';
|
case 'payment_sent':
|
||||||
case 'disputed': return 'text-orange-400';
|
return 'text-blue-400';
|
||||||
default: return 'text-muted-foreground';
|
case 'completed':
|
||||||
|
return 'text-green-400';
|
||||||
|
case 'cancelled':
|
||||||
|
case 'refunded':
|
||||||
|
return 'text-red-400';
|
||||||
|
case 'disputed':
|
||||||
|
return 'text-orange-400';
|
||||||
|
default:
|
||||||
|
return 'text-muted-foreground';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +133,10 @@ export function P2PSection() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold text-foreground">{t('p2p.title')}</h1>
|
<h1 className="text-xl font-bold text-foreground">{t('p2p.title')}</h1>
|
||||||
<button
|
<button
|
||||||
onClick={() => { hapticImpact('medium'); setShowCreateOffer(true); }}
|
onClick={() => {
|
||||||
|
hapticImpact('medium');
|
||||||
|
setShowCreateOffer(true);
|
||||||
|
}}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-sm font-medium rounded-lg transition-colors"
|
className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-sm font-medium rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@@ -137,9 +155,7 @@ export function P2PSection() {
|
|||||||
onClick={() => handleTabChange(tab.id)}
|
onClick={() => handleTabChange(tab.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 py-2 text-xs font-medium rounded-lg transition-colors',
|
'flex-1 py-2 text-xs font-medium rounded-lg transition-colors',
|
||||||
activeTab === tab.id
|
activeTab === tab.id ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground'
|
||||||
? 'bg-card text-foreground shadow-sm'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -159,8 +175,8 @@ export function P2PSection() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* My Ads */}
|
{/* My Ads */}
|
||||||
{activeTab === 'myAds' && (
|
{activeTab === 'myAds' &&
|
||||||
myDataLoading ? (
|
(myDataLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
@@ -177,13 +193,20 @@ export function P2PSection() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{myOffers.map((offer) => (
|
{myOffers.map((offer) => (
|
||||||
<div key={offer.id} className="bg-card rounded-xl border border-border p-3 space-y-1">
|
<div
|
||||||
|
key={offer.id}
|
||||||
|
className="bg-card rounded-xl border border-border p-3 space-y-1"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={cn(
|
<span
|
||||||
'text-xs font-medium px-2 py-0.5 rounded-full',
|
className={cn(
|
||||||
offer.ad_type === 'sell' ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'
|
'text-xs font-medium px-2 py-0.5 rounded-full',
|
||||||
)}>
|
offer.ad_type === 'sell'
|
||||||
|
? 'bg-red-500/10 text-red-400'
|
||||||
|
: 'bg-green-500/10 text-green-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{offer.ad_type === 'sell' ? t('p2p.sell') : t('p2p.buy')}
|
{offer.ad_type === 'sell' ? t('p2p.sell') : t('p2p.buy')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-bold text-foreground">{offer.token}</span>
|
<span className="text-sm font-bold text-foreground">{offer.token}</span>
|
||||||
@@ -193,18 +216,21 @@ export function P2PSection() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>{offer.remaining_amount}/{offer.amount_crypto} {offer.token}</span>
|
<span>
|
||||||
<span>{offer.price_per_unit?.toLocaleString()} {offer.fiat_currency}</span>
|
{offer.remaining_amount}/{offer.amount_crypto} {offer.token}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{offer.price_per_unit?.toLocaleString()} {offer.fiat_currency}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* My Trades */}
|
{/* My Trades */}
|
||||||
{activeTab === 'myTrades' && (
|
{activeTab === 'myTrades' &&
|
||||||
myDataLoading ? (
|
(myDataLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +244,10 @@ export function P2PSection() {
|
|||||||
{myTrades.map((trade) => (
|
{myTrades.map((trade) => (
|
||||||
<button
|
<button
|
||||||
key={trade.id}
|
key={trade.id}
|
||||||
onClick={() => { hapticImpact('light'); setActiveTradeId(trade.id); }}
|
onClick={() => {
|
||||||
|
hapticImpact('light');
|
||||||
|
setActiveTradeId(trade.id);
|
||||||
|
}}
|
||||||
className="w-full text-left bg-card rounded-xl border border-border p-3 space-y-1 hover:border-cyan-500/50 transition-colors"
|
className="w-full text-left bg-card rounded-xl border border-border p-3 space-y-1 hover:border-cyan-500/50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -230,14 +259,15 @@ export function P2PSection() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>{trade.fiat_amount?.toLocaleString()} {trade.fiat_currency || ''}</span>
|
<span>
|
||||||
|
{trade.fiat_amount?.toLocaleString()} {trade.fiat_currency || ''}
|
||||||
|
</span>
|
||||||
<span>{new Date(trade.created_at).toLocaleDateString()}</span>
|
<span>{new Date(trade.created_at).toLocaleDateString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trade Accept Modal */}
|
{/* Trade Accept Modal */}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.224",
|
"version": "1.0.225",
|
||||||
"buildTime": "2026-02-25T16:46:10.349Z",
|
"buildTime": "2026-02-26T14:51:09.120Z",
|
||||||
"buildNumber": 1772037970349
|
"buildNumber": 1772117469121
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user