feat: replace supabase auth with citizen/visa identity system for P2P

Replace all supabase.auth.getUser() calls with P2PIdentityContext that
resolves identity from on-chain citizen NFT or off-chain visa system.

- Add identityToUUID() in shared/lib/identity.ts (UUID v5 from citizen/visa number)
- Add P2PIdentityContext with citizen NFT detection and visa fallback
- Add p2p_visa migration for off-chain visa issuance
- Refactor p2p-fiat.ts: all functions now accept userId parameter
- Fix all P2P components to use useP2PIdentity() instead of useAuth()
- Update verify-deposit edge function: walletToUUID -> identityToUUID
- Add P2PLayout with identity gate (wallet/citizen/visa checks)
- Wrap all P2P routes with P2PLayout in App.tsx
This commit is contained in:
2026-02-23 19:54:57 +03:00
parent 350b65dec3
commit bb772668ba
25 changed files with 594 additions and 237 deletions
+6 -5
View File
@@ -10,6 +10,7 @@ import { AuthProvider } from '@/contexts/AuthContext';
import { DashboardProvider } from '@/contexts/DashboardContext';
import { ReferralProvider } from '@/contexts/ReferralContext';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import { P2PLayout } from '@/components/p2p/P2PLayout';
import { Toaster } from '@/components/ui/toaster';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { initSentry } from '@/lib/sentry';
@@ -180,27 +181,27 @@ function App() {
} />
<Route path="/p2p" element={
<ProtectedRoute allowTelegramSession>
<P2PPlatform />
<P2PLayout><P2PPlatform /></P2PLayout>
</ProtectedRoute>
} />
<Route path="/p2p/trade/:tradeId" element={
<ProtectedRoute allowTelegramSession>
<P2PTrade />
<P2PLayout><P2PTrade /></P2PLayout>
</ProtectedRoute>
} />
<Route path="/p2p/orders" element={
<ProtectedRoute allowTelegramSession>
<P2POrders />
<P2PLayout><P2POrders /></P2PLayout>
</ProtectedRoute>
} />
<Route path="/p2p/dispute/:disputeId" element={
<ProtectedRoute allowTelegramSession>
<P2PDispute />
<P2PLayout><P2PDispute /></P2PLayout>
</ProtectedRoute>
} />
<Route path="/p2p/merchant" element={
<ProtectedRoute allowTelegramSession>
<P2PMerchantDashboard />
<P2PLayout><P2PMerchantDashboard /></P2PLayout>
</ProtectedRoute>
} />
<Route path="/dex" element={
+8 -8
View File
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Loader2, Shield, Zap } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { TradeModal } from './TradeModal';
import { MerchantTierBadge } from './MerchantTierBadge';
import { getUserReputation, type P2PFiatOffer, type P2PReputation } from '@shared/lib/p2p-fiat';
@@ -25,7 +25,7 @@ interface OfferWithReputation extends P2PFiatOffer {
export function AdList({ type, filters }: AdListProps) {
const { t } = useTranslation();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const [offers, setOffers] = useState<OfferWithReputation[]>([]);
const [loading, setLoading] = useState(true);
const [selectedOffer, setSelectedOffer] = useState<OfferWithReputation | null>(null);
@@ -46,7 +46,7 @@ export function AdList({ type, filters }: AdListProps) {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type, user, filters]);
}, [type, userId, filters]);
const fetchOffers = async () => {
setLoading(true);
@@ -62,9 +62,9 @@ export function AdList({ type, filters }: AdListProps) {
} else if (type === 'sell') {
// Sell tab = show BUY offers (user wants to sell to buyers)
query = query.eq('ad_type', 'buy').eq('status', 'open').gt('remaining_amount', 0);
} else if (type === 'my-ads' && user) {
} else if (type === 'my-ads' && userId) {
// My offers - show all of user's offers
query = query.eq('seller_id', user.id);
query = query.eq('seller_id', userId);
}
// Apply filters if provided
@@ -263,16 +263,16 @@ export function AdList({ type, filters }: AdListProps) {
{/* Action */}
<div className="flex flex-col items-end gap-1">
{offer.seller_id === user?.id && type !== 'my-ads' && (
{offer.seller_id === userId && type !== 'my-ads' && (
<Badge variant="outline" className="text-xs bg-blue-500/10 text-blue-400 border-blue-500/30">
{t('p2pAd.yourAd')}
</Badge>
)}
<Button
onClick={() => setSelectedOffer(offer)}
disabled={type === 'my-ads' || offer.seller_id === user?.id}
disabled={type === 'my-ads' || offer.seller_id === userId}
className="w-full md:w-auto"
title={offer.seller_id === user?.id ? t('p2pAd.cantTradeOwnAd') : ''}
title={offer.seller_id === userId ? t('p2pAd.cantTradeOwnAd') : ''}
>
{type === 'buy' ? t('p2pAd.buyToken', { token: offer.token }) : t('p2pAd.sellToken', { token: offer.token })}
</Button>
+7 -7
View File
@@ -24,7 +24,7 @@ import {
Building2, AlertTriangle
} from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat';
@@ -80,19 +80,19 @@ export function BlockTrade() {
const [requests, setRequests] = useState<BlockTradeRequest[]>([]);
const { t } = useTranslation();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const fiatSymbol = SUPPORTED_FIATS.find(f => f.code === fiat)?.symbol || '';
const minAmount = MINIMUM_BLOCK_AMOUNTS[token];
// Fetch user's block trade requests
React.useEffect(() => {
if (!user) return;
if (!userId) return;
const fetchRequests = async () => {
const { data, error } = await supabase
.from('p2p_block_trade_requests')
.select('*')
.eq('user_id', user.id)
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (!error && data) {
@@ -101,10 +101,10 @@ export function BlockTrade() {
};
fetchRequests();
}, [user]);
}, [userId]);
const handleSubmitRequest = async () => {
if (!user) {
if (!userId) {
toast.error(t('p2pBlock.loginRequired'));
return;
}
@@ -120,7 +120,7 @@ export function BlockTrade() {
const { data, error } = await supabase
.from('p2p_block_trade_requests')
.insert({
user_id: user.id,
user_id: userId,
type,
token,
fiat_currency: fiat,
+20 -39
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/contexts/AuthContext';
import { useWallet } from '@/contexts/WalletContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -9,8 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
import { supabase } from '@/lib/supabase';
import {
createFiatOffer,
getPaymentMethods,
validatePaymentDetails,
type PaymentMethod,
@@ -24,8 +24,8 @@ interface CreateAdProps {
export function CreateAd({ onAdCreated }: CreateAdProps) {
const { t } = useTranslation();
const { user } = useAuth();
const { account } = useWallet();
const { selectedAccount } = usePezkuwi();
const { userId } = useP2PIdentity();
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
@@ -77,11 +77,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
};
const handleCreateAd = async () => {
console.log('🔥 handleCreateAd called', { account, user: user?.id });
if (!account || !user) {
if (!selectedAccount || !userId) {
toast.error(t('p2p.connectWalletAndLogin'));
console.log('❌ No account or user', { account, user });
return;
}
@@ -129,37 +126,21 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
setLoading(true);
try {
// Insert offer into Supabase
// Note: payment_details_encrypted is stored as JSON string (encryption handled server-side in prod)
const { data, error } = await supabase
.from('p2p_fiat_offers')
.insert({
seller_id: user.id,
seller_wallet: account,
ad_type: adType,
token,
amount_crypto: cryptoAmt,
remaining_amount: cryptoAmt,
fiat_currency: fiatCurrency,
fiat_amount: fiatAmt,
payment_method_id: selectedPaymentMethod.id,
payment_details_encrypted: JSON.stringify(paymentDetails),
time_limit_minutes: timeLimit,
min_order_amount: minOrderAmount ? parseFloat(minOrderAmount) : null,
max_order_amount: maxOrderAmount ? parseFloat(maxOrderAmount) : null,
status: 'open'
})
.select()
.single();
await createFiatOffer({
userId,
sellerWallet: selectedAccount.address,
token,
amountCrypto: cryptoAmt,
fiatCurrency,
fiatAmount: fiatAmt,
paymentMethodId: selectedPaymentMethod.id,
paymentDetails,
timeLimitMinutes: timeLimit,
minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : undefined,
maxOrderAmount: maxOrderAmount ? parseFloat(maxOrderAmount) : undefined,
adType,
});
if (error) {
console.error('❌ Supabase error:', error);
toast.error(error.message || t('p2pCreate.failedToCreate'));
return;
}
console.log('✅ Offer created successfully:', data);
toast.success(t('p2pCreate.adCreated'));
onAdCreated();
} catch (error) {
if (import.meta.env.DEV) console.error('Create ad error:', error);
+3
View File
@@ -31,6 +31,7 @@ import {
} from 'lucide-react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import {
@@ -50,6 +51,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
const { t } = useTranslation();
const { api, selectedAccount } = usePezkuwi();
const { balances, signTransaction } = useWallet();
const { identityId } = useP2PIdentity();
const [step, setStep] = useState<DepositStep>('select');
const [token, setToken] = useState<CryptoToken>('HEZ');
@@ -192,6 +194,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
token,
expectedAmount: depositAmount,
walletAddress: selectedAccount?.address,
identityId,
...(blockNumber ? { blockNumber } : {})
})
});
+5 -4
View File
@@ -22,6 +22,7 @@ import {
import { AlertTriangle, Upload, X, FileText } from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { toast } from 'sonner';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
interface DisputeModalProps {
isOpen: boolean;
@@ -62,6 +63,7 @@ export function DisputeModal({
isBuyer,
}: DisputeModalProps) {
const { t } = useTranslation();
const { userId } = useP2PIdentity();
const fileInputRef = useRef<HTMLInputElement>(null);
const [reason, setReason] = useState('');
@@ -172,15 +174,14 @@ export function DisputeModal({
setIsSubmitting(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
if (!userId) throw new Error('Not authenticated');
// Create dispute
const { data: dispute, error: disputeError } = await supabase
.from('p2p_disputes')
.insert({
trade_id: tradeId,
opened_by: user.id,
opened_by: userId,
reason,
description,
status: 'open',
@@ -197,7 +198,7 @@ export function DisputeModal({
// Insert evidence records
const evidenceRecords = evidenceUrls.map((url, index) => ({
dispute_id: dispute.id,
uploaded_by: user.id,
uploaded_by: userId,
evidence_type: evidenceFiles[index].type === 'image' ? 'screenshot' : 'document',
file_url: url,
description: `Evidence ${index + 1}`,
+9 -7
View File
@@ -16,7 +16,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Zap, ArrowRight, Shield, Clock, Star, AlertCircle, CheckCircle2 } from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
@@ -69,7 +70,8 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
const [isProcessing, setIsProcessing] = useState(false);
const { t } = useTranslation();
const { user } = useAuth();
const { selectedAccount } = usePezkuwi();
const { userId } = useP2PIdentity();
const navigate = useNavigate();
// Calculate conversion
@@ -140,8 +142,8 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
// Handle express trade
const handleExpressTrade = async () => {
if (!user) {
toast.error(t('p2pExpress.loginRequired'));
if (!userId || !selectedAccount) {
toast.error(t('p2p.connectWalletAndLogin'));
return;
}
@@ -160,8 +162,8 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
// Accept the best offer
const { data: result, error } = await supabase.rpc('accept_p2p_offer', {
p_offer_id: bestOffer.id,
p_buyer_id: user.id,
p_buyer_wallet: '', // Will be set from user profile
p_buyer_id: userId,
p_buyer_wallet: selectedAccount.address,
p_amount: cryptoAmount
});
@@ -342,7 +344,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
className="w-full bg-yellow-500 hover:bg-yellow-600 text-black font-semibold"
size="lg"
onClick={handleExpressTrade}
disabled={!bestOffer || isLoading || isProcessing || !user}
disabled={!bestOffer || isLoading || isProcessing || !userId || !selectedAccount}
>
{isProcessing ? (
<>{t('p2pExpress.processing')}</>
@@ -13,6 +13,7 @@ import {
Unlock
} from 'lucide-react';
import { getInternalBalances, type InternalBalance } from '@shared/lib/p2p-fiat';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
interface InternalBalanceCardProps {
onDeposit?: () => void;
@@ -21,13 +22,15 @@ interface InternalBalanceCardProps {
export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCardProps) {
const { t } = useTranslation();
const { userId } = useP2PIdentity();
const [balances, setBalances] = useState<InternalBalance[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const fetchBalances = async () => {
if (!userId) return;
try {
const data = await getInternalBalances();
const data = await getInternalBalances(userId);
setBalances(data);
} catch (error) {
console.error('Failed to fetch balances:', error);
@@ -39,7 +42,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
useEffect(() => {
fetchBalances();
}, []);
}, [userId]);
const handleRefresh = async () => {
setIsRefreshing(true);
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/lib/supabase';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
@@ -107,6 +108,7 @@ const TIER_COLORS = {
export function MerchantApplication() {
const { t } = useTranslation();
const { userId } = useP2PIdentity();
const [loading, setLoading] = useState(true);
const [requirements, setRequirements] = useState<TierRequirements[]>(DEFAULT_REQUIREMENTS);
const [userStats, setUserStats] = useState<UserStats>({ completed_trades: 0, completion_rate: 0, volume_30d: 0 });
@@ -123,8 +125,7 @@ export function MerchantApplication() {
const fetchData = async () => {
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
if (!userId) return;
// Fetch tier requirements
const { data: reqData } = await supabase
@@ -140,21 +141,21 @@ export function MerchantApplication() {
const { data: repData } = await supabase
.from('p2p_reputation')
.select('completed_trades')
.eq('user_id', user.id)
.eq('user_id', userId)
.single();
// Fetch merchant stats
const { data: statsData } = await supabase
.from('p2p_merchant_stats')
.select('completion_rate_30d, total_volume_30d')
.eq('user_id', user.id)
.eq('user_id', userId)
.single();
// Fetch current tier
const { data: tierData } = await supabase
.from('p2p_merchant_tiers')
.select('tier, application_status, applied_for_tier')
.eq('user_id', user.id)
.eq('user_id', userId)
.single();
setUserStats({
@@ -207,11 +208,10 @@ export function MerchantApplication() {
setApplying(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
if (!userId) throw new Error('Not authenticated');
const { data, error } = await supabase.rpc('apply_for_tier_upgrade', {
p_user_id: user.id,
p_user_id: userId,
p_target_tier: selectedTier
});
+12 -12
View File
@@ -22,7 +22,7 @@ import {
Loader2,
CheckCheck,
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { supabase } from '@/lib/supabase';
interface Notification {
@@ -40,7 +40,7 @@ interface Notification {
export function NotificationBell() {
const { t } = useTranslation();
const navigate = useNavigate();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(true);
@@ -48,13 +48,13 @@ export function NotificationBell() {
// Fetch notifications
const fetchNotifications = useCallback(async () => {
if (!user) return;
if (!userId) return;
try {
const { data, error } = await supabase
.from('p2p_notifications')
.select('*')
.eq('user_id', user.id)
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(20);
@@ -67,7 +67,7 @@ export function NotificationBell() {
} finally {
setLoading(false);
}
}, [user]);
}, [userId]);
// Initial fetch
useEffect(() => {
@@ -76,17 +76,17 @@ export function NotificationBell() {
// Real-time subscription
useEffect(() => {
if (!user) return;
if (!userId) return;
const channel = supabase
.channel(`notifications-${user.id}`)
.channel(`notifications-${userId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'p2p_notifications',
filter: `user_id=eq.${user.id}`,
filter: `user_id=eq.${userId}`,
},
(payload) => {
const newNotif = payload.new as Notification;
@@ -99,7 +99,7 @@ export function NotificationBell() {
return () => {
supabase.removeChannel(channel);
};
}, [user]);
}, [userId]);
// Mark as read
const markAsRead = async (notificationId: string) => {
@@ -120,13 +120,13 @@ export function NotificationBell() {
// Mark all as read
const markAllAsRead = async () => {
if (!user) return;
if (!userId) return;
try {
await supabase
.from('p2p_notifications')
.update({ is_read: true })
.eq('user_id', user.id)
.eq('user_id', userId)
.eq('is_read', false);
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
@@ -184,7 +184,7 @@ export function NotificationBell() {
return t('p2p.daysAgo', { count: days });
};
if (!user) return null;
if (!userId) return null;
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
+8 -8
View File
@@ -16,7 +16,7 @@ import { WithdrawModal } from './WithdrawModal';
import { ExpressMode } from './ExpressMode';
import { BlockTrade } from './BlockTrade';
import { DEFAULT_FILTERS, type P2PFilters } from './types';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { supabase } from '@/lib/supabase';
interface UserStats {
@@ -34,7 +34,7 @@ export function P2PDashboard() {
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [balanceRefreshKey, setBalanceRefreshKey] = useState(0);
const navigate = useNavigate();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const handleBalanceUpdated = () => {
setBalanceRefreshKey(prev => prev + 1);
@@ -43,28 +43,28 @@ export function P2PDashboard() {
// Fetch user stats
useEffect(() => {
const fetchStats = async () => {
if (!user) return;
if (!userId) return;
try {
// Count active trades
const { count: activeCount } = await supabase
.from('p2p_fiat_trades')
.select('*', { count: 'exact', head: true })
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
.or(`seller_id.eq.${userId},buyer_id.eq.${userId}`)
.in('status', ['pending', 'payment_sent']);
// Count completed trades
const { count: completedCount } = await supabase
.from('p2p_fiat_trades')
.select('*', { count: 'exact', head: true })
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
.or(`seller_id.eq.${userId},buyer_id.eq.${userId}`)
.eq('status', 'completed');
// Calculate total volume
const { data: trades } = await supabase
.from('p2p_fiat_trades')
.select('fiat_amount')
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
.or(`seller_id.eq.${userId},buyer_id.eq.${userId}`)
.eq('status', 'completed');
const totalVolume = trades?.reduce((sum, t) => sum + (t.fiat_amount || 0), 0) || 0;
@@ -80,7 +80,7 @@ export function P2PDashboard() {
};
fetchStats();
}, [user]);
}, [userId]);
return (
<div className="container mx-auto px-4 py-8 max-w-7xl">
@@ -120,7 +120,7 @@ export function P2PDashboard() {
</div>
{/* Stats Cards and Balance Card */}
{user && (
{userId && (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
{/* Internal Balance Card - Takes more space */}
<div className="lg:col-span-1">
+121
View File
@@ -0,0 +1,121 @@
import { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { P2PIdentityProvider, useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Loader2, Shield, UserCheck, Wallet, Home } from 'lucide-react';
function IdentityGate({ children }: { children: ReactNode }) {
const { t } = useTranslation();
const navigate = useNavigate();
const { selectedAccount } = usePezkuwi();
const { hasIdentity, loading, applyForVisa } = useP2PIdentity();
// Loading state
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground">{t('p2pIdentity.resolving')}</p>
</div>
);
}
// No wallet connected
if (!selectedAccount) {
return (
<div className="container mx-auto px-4 py-16 max-w-lg">
<Card>
<CardHeader className="text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Wallet className="h-8 w-8 text-primary" />
</div>
<CardTitle>{t('p2pIdentity.connectWalletTitle')}</CardTitle>
<CardDescription>{t('p2pIdentity.connectWalletDesc')}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Button variant="outline" onClick={() => navigate('/')}>
<Home className="h-4 w-4 mr-2" />
{t('p2p.backToHome')}
</Button>
</CardContent>
</Card>
</div>
);
}
// Has identity - show content with identity badge
if (hasIdentity) {
return <>{children}</>;
}
// No identity - show visa application
return (
<div className="container mx-auto px-4 py-16 max-w-lg">
<Card>
<CardHeader className="text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-yellow-500/10 flex items-center justify-center">
<Shield className="h-8 w-8 text-yellow-500" />
</div>
<CardTitle>{t('p2pIdentity.identityRequired')}</CardTitle>
<CardDescription>{t('p2pIdentity.identityRequiredDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center gap-3 mb-2">
<UserCheck className="h-5 w-5 text-green-500" />
<span className="font-medium">{t('p2pIdentity.citizenOption')}</span>
<Badge className="bg-green-500/20 text-green-500 border-green-500/30">{t('p2pIdentity.fullAccess')}</Badge>
</div>
<p className="text-sm text-muted-foreground">{t('p2pIdentity.citizenDesc')}</p>
<Button
variant="outline"
className="mt-3 w-full"
onClick={() => navigate('/be-citizen')}
>
{t('p2pIdentity.applyCitizenship')}
</Button>
</div>
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center gap-3 mb-2">
<Shield className="h-5 w-5 text-blue-500" />
<span className="font-medium">{t('p2pIdentity.visaOption')}</span>
<Badge className="bg-blue-500/20 text-blue-500 border-blue-500/30">{t('p2pIdentity.limitedAccess')}</Badge>
</div>
<p className="text-sm text-muted-foreground">{t('p2pIdentity.visaDesc')}</p>
<Button
className="mt-3 w-full"
onClick={async () => {
const result = await applyForVisa();
if (!result) {
// toast handled inside applyForVisa or context
}
}}
>
{t('p2pIdentity.applyVisa')}
</Button>
</div>
</div>
<Button variant="ghost" className="w-full" onClick={() => navigate('/')}>
<Home className="h-4 w-4 mr-2" />
{t('p2p.backToHome')}
</Button>
</CardContent>
</Card>
</div>
);
}
export function P2PLayout({ children }: { children: ReactNode }) {
return (
<P2PIdentityProvider>
<IdentityGate>{children}</IdentityGate>
</P2PIdentityProvider>
);
}
+5 -5
View File
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Star, Loader2, ThumbsUp, ThumbsDown } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/lib/supabase';
@@ -34,14 +34,14 @@ export function RatingModal({
isBuyer,
}: RatingModalProps) {
const { t } = useTranslation();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const [rating, setRating] = useState(0);
const [hoveredRating, setHoveredRating] = useState(0);
const [review, setReview] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!user || rating === 0) {
if (!userId || rating === 0) {
toast.error(t('p2pRating.selectRatingError'));
return;
}
@@ -54,7 +54,7 @@ export function RatingModal({
.from('p2p_ratings')
.select('id')
.eq('trade_id', tradeId)
.eq('rater_id', user.id)
.eq('rater_id', userId)
.single();
if (existingRating) {
@@ -66,7 +66,7 @@ export function RatingModal({
// Insert rating
const { error: ratingError } = await supabase.from('p2p_ratings').insert({
trade_id: tradeId,
rater_id: user.id,
rater_id: userId,
rated_id: counterpartyId,
rating,
review: review.trim() || null,
+14 -14
View File
@@ -13,7 +13,7 @@ import {
Clock,
Bot,
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/lib/supabase';
@@ -43,7 +43,7 @@ export function TradeChat({
isTradeActive,
}: TradeChatProps) {
const { t } = useTranslation();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState('');
const [loading, setLoading] = useState(true);
@@ -72,9 +72,9 @@ export function TradeChat({
setMessages(data || []);
// Mark messages as read
if (user && data && data.length > 0) {
if (userId && data && data.length > 0) {
const unreadIds = data
.filter(m => m.sender_id !== user.id && !m.is_read)
.filter(m => m.sender_id !== userId && !m.is_read)
.map(m => m.id);
if (unreadIds.length > 0) {
@@ -89,7 +89,7 @@ export function TradeChat({
} finally {
setLoading(false);
}
}, [tradeId, user]);
}, [tradeId, userId]);
// Initial fetch
useEffect(() => {
@@ -117,7 +117,7 @@ export function TradeChat({
});
// Mark as read if from counterparty
if (user && newMsg.sender_id !== user.id) {
if (userId && newMsg.sender_id !== userId) {
supabase
.from('p2p_messages')
.update({ is_read: true })
@@ -130,7 +130,7 @@ export function TradeChat({
return () => {
supabase.removeChannel(channel);
};
}, [tradeId, user]);
}, [tradeId, userId]);
// Scroll to bottom on new messages
useEffect(() => {
@@ -139,7 +139,7 @@ export function TradeChat({
// Send message
const handleSendMessage = async () => {
if (!newMessage.trim() || !user || sending) return;
if (!newMessage.trim() || !userId || sending) return;
const messageText = newMessage.trim();
setNewMessage('');
@@ -148,7 +148,7 @@ export function TradeChat({
try {
const { error } = await supabase.from('p2p_messages').insert({
trade_id: tradeId,
sender_id: user.id,
sender_id: userId,
message: messageText,
message_type: 'text',
is_read: false,
@@ -186,7 +186,7 @@ export function TradeChat({
// Upload image
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !user) return;
if (!file || !userId) return;
// Validate file
if (!file.type.startsWith('image/')) {
@@ -218,7 +218,7 @@ export function TradeChat({
// Insert message with image
const { error: msgError } = await supabase.from('p2p_messages').insert({
trade_id: tradeId,
sender_id: user.id,
sender_id: userId,
message: t('p2pChat.sentImage'),
message_type: 'image',
attachment_url: urlData.publicUrl,
@@ -258,7 +258,7 @@ export function TradeChat({
// Render message
const renderMessage = (message: Message) => {
const isOwn = message.sender_id === user?.id;
const isOwn = message.sender_id === userId;
const isSystem = message.message_type === 'system';
if (isSystem) {
@@ -332,9 +332,9 @@ export function TradeChat({
<CardHeader className="py-3 px-4 border-b border-gray-800">
<CardTitle className="text-white text-base flex items-center gap-2">
<span>{t('p2pChat.title')}</span>
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length > 0 && (
{messages.filter(m => m.sender_id !== userId && !m.is_read).length > 0 && (
<span className="px-1.5 py-0.5 text-xs bg-green-500 text-white rounded-full">
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length}
{messages.filter(m => m.sender_id !== userId && !m.is_read).length}
</span>
)}
</CardTitle>
+7 -7
View File
@@ -14,8 +14,8 @@ 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 { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { acceptFiatOffer, type P2PFiatOffer } from '@shared/lib/p2p-fiat';
@@ -27,8 +27,8 @@ interface TradeModalProps {
export function TradeModal({ offer, onClose }: TradeModalProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const { user } = useAuth();
const { api, selectedAccount } = usePezkuwi();
const { selectedAccount } = usePezkuwi();
const { userId } = useP2PIdentity();
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
@@ -41,13 +41,13 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
const meetsMaxOrder = !offer.max_order_amount || cryptoAmount <= offer.max_order_amount;
const handleInitiateTrade = async () => {
if (!api || !selectedAccount || !user) {
if (!selectedAccount || !userId) {
toast.error(t('p2p.connectWalletAndLogin'));
return;
}
// Prevent self-trading
if (offer.seller_id === user.id) {
if (offer.seller_id === userId) {
toast.error(t('p2pTrade.cannotTradeOwn'));
return;
}
@@ -71,9 +71,9 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
try {
const tradeId = await acceptFiatOffer({
api,
account: selectedAccount,
offerId: offer.id,
buyerUserId: userId,
buyerWallet: selectedAccount.address,
amount: cryptoAmount
});
+7 -3
View File
@@ -30,6 +30,7 @@ import {
Info
} from 'lucide-react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import {
getInternalBalances,
@@ -51,6 +52,7 @@ type WithdrawStep = 'form' | 'confirm' | 'success';
export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps) {
const { t } = useTranslation();
const { selectedAccount } = usePezkuwi();
const { userId } = useP2PIdentity();
const [step, setStep] = useState<WithdrawStep>('form');
const [token, setToken] = useState<CryptoToken>('HEZ');
@@ -80,9 +82,10 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
const fetchData = async () => {
setLoading(true);
try {
if (!userId) return;
const [balanceData, historyData] = await Promise.all([
getInternalBalances(),
getDepositWithdrawHistory()
getInternalBalances(userId),
getDepositWithdrawHistory(userId)
]);
setBalances(balanceData);
// Filter for pending withdrawal requests
@@ -180,7 +183,8 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
try {
const withdrawAmount = parseFloat(amount);
const id = await requestWithdraw(token, withdrawAmount, walletAddress);
if (!userId) throw new Error('Identity required');
const id = await requestWithdraw(userId, token, withdrawAmount, walletAddress);
setRequestId(id);
setStep('success');
onSuccess?.();
+141
View File
@@ -0,0 +1,141 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useDashboard } from '@/contexts/DashboardContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { identityToUUID } from '@shared/lib/identity';
import { supabase } from '@/lib/supabase';
interface P2PIdentity {
/** UUID v5 derived from identityId, used as user_id in DB */
userId: string | null;
/** Full citizen number (#42-0-832967) or visa number (V-123456) */
identityId: string | null;
/** Wallet address (SS58) */
walletAddress: string | null;
/** Whether user is a citizen with on-chain NFT */
isCitizen: boolean;
/** Whether user has an off-chain visa */
isVisa: boolean;
/** Whether user has any P2P identity (citizen or visa) */
hasIdentity: boolean;
/** Loading state during identity resolution */
loading: boolean;
/** Apply for a visa (for non-citizens) */
applyForVisa: () => Promise<string | null>;
}
const P2PIdentityContext = createContext<P2PIdentity | undefined>(undefined);
export function P2PIdentityProvider({ children }: { children: ReactNode }) {
const { citizenNumber, nftDetails, loading: dashboardLoading } = useDashboard();
const { selectedAccount } = usePezkuwi();
const [userId, setUserId] = useState<string | null>(null);
const [identityId, setIdentityId] = useState<string | null>(null);
const [visaNumber, setVisaNumber] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const walletAddress = selectedAccount?.address || null;
const isCitizen = !!(nftDetails.citizenNFT && citizenNumber !== 'N/A');
const isVisa = !!visaNumber;
const hasIdentity = isCitizen || isVisa;
// Resolve identity when wallet/dashboard data changes
useEffect(() => {
if (dashboardLoading) return;
const resolve = async () => {
setLoading(true);
try {
if (isCitizen) {
// Citizen: use full citizen number as identity
const fullCitizenNumber = `#${nftDetails.citizenNFT!.collectionId}-${nftDetails.citizenNFT!.itemId}-${citizenNumber}`;
setIdentityId(fullCitizenNumber);
const uuid = await identityToUUID(fullCitizenNumber);
setUserId(uuid);
setVisaNumber(null);
} else if (walletAddress) {
// Non-citizen: check for existing visa
const { data: visa } = await supabase
.from('p2p_visa')
.select('visa_number, status')
.eq('wallet_address', walletAddress)
.eq('status', 'active')
.maybeSingle();
if (visa) {
setVisaNumber(visa.visa_number);
setIdentityId(visa.visa_number);
const uuid = await identityToUUID(visa.visa_number);
setUserId(uuid);
} else {
setVisaNumber(null);
setIdentityId(null);
setUserId(null);
}
} else {
setIdentityId(null);
setUserId(null);
setVisaNumber(null);
}
} catch (error) {
console.error('P2P identity resolution error:', error);
setIdentityId(null);
setUserId(null);
} finally {
setLoading(false);
}
};
resolve();
}, [isCitizen, citizenNumber, nftDetails.citizenNFT, walletAddress, dashboardLoading]);
const applyForVisa = async (): Promise<string | null> => {
if (!walletAddress) return null;
if (isCitizen) return null; // Citizens don't need visas
try {
const { data, error } = await supabase.rpc('issue_p2p_visa', {
p_wallet_address: walletAddress,
});
if (error) throw error;
if (data?.success) {
const vn = data.visa_number as string;
setVisaNumber(vn);
setIdentityId(vn);
const uuid = await identityToUUID(vn);
setUserId(uuid);
return vn;
}
return null;
} catch (error) {
console.error('Failed to apply for visa:', error);
return null;
}
};
return (
<P2PIdentityContext.Provider value={{
userId,
identityId,
walletAddress,
isCitizen,
isVisa,
hasIdentity,
loading,
applyForVisa,
}}>
{children}
</P2PIdentityContext.Provider>
);
}
export function useP2PIdentity() {
const context = useContext(P2PIdentityContext);
if (context === undefined) {
throw new Error('useP2PIdentity must be used within a P2PIdentityProvider');
}
return context;
}
+9 -12
View File
@@ -30,6 +30,7 @@ import {
import { supabase } from '@/lib/supabase';
import { toast } from 'sonner';
import { formatAddress } from '@pezkuwi/utils/formatting';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
interface DisputeDetails {
id: string;
@@ -105,11 +106,11 @@ export default function P2PDispute() {
const navigate = useNavigate();
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const { userId } = useP2PIdentity();
const [dispute, setDispute] = useState<DisputeDetails | null>(null);
const [evidence, setEvidence] = useState<Evidence[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
@@ -118,10 +119,6 @@ export default function P2PDispute() {
if (!disputeId) return;
try {
// Get current user
const { data: { user } } = await supabase.auth.getUser();
setCurrentUserId(user?.id || null);
// Fetch dispute with trade info
const { data: disputeData, error: disputeError } = await supabase
.from('p2p_disputes')
@@ -199,7 +196,7 @@ export default function P2PDispute() {
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0 || !dispute || !currentUserId) return;
if (!files || files.length === 0 || !dispute || !userId) return;
setIsUploading(true);
@@ -224,7 +221,7 @@ export default function P2PDispute() {
// Insert evidence record
await supabase.from('p2p_dispute_evidence').insert({
dispute_id: dispute.id,
uploaded_by: currentUserId,
uploaded_by: userId,
evidence_type: file.type.startsWith('image/') ? 'screenshot' : 'document',
file_url: urlData.publicUrl,
description: file.name,
@@ -244,11 +241,11 @@ export default function P2PDispute() {
};
const isParticipant = dispute?.trade &&
(dispute.trade.buyer_id === currentUserId || dispute.trade.seller_id === currentUserId);
(dispute.trade.buyer_id === userId || dispute.trade.seller_id === userId);
const isBuyer = dispute?.trade?.buyer_id === currentUserId;
const isSeller = dispute?.trade?.seller_id === currentUserId;
const isOpener = dispute?.opened_by === currentUserId;
const isBuyer = dispute?.trade?.buyer_id === userId;
const isSeller = dispute?.trade?.seller_id === userId;
const isOpener = dispute?.opened_by === userId;
if (isLoading) {
return (
@@ -444,7 +441,7 @@ export default function P2PDispute() {
{evidence.map((item) => {
const isImage = item.evidence_type === 'screenshot' ||
item.file_url.match(/\.(jpg|jpeg|png|gif|webp)$/i);
const isMyEvidence = item.uploaded_by === currentUserId;
const isMyEvidence = item.uploaded_by === userId;
return (
<div
+14 -12
View File
@@ -12,6 +12,7 @@ import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { MerchantTierBadge } from '@/components/p2p/MerchantTierBadge';
import { MerchantApplication } from '@/components/p2p/MerchantApplication';
import { CreateAd } from '@/components/p2p/CreateAd';
@@ -91,6 +92,7 @@ interface ChartDataPoint {
export default function P2PMerchantDashboard() {
const navigate = useNavigate();
const { t } = useTranslation();
const { userId } = useP2PIdentity();
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<MerchantStats | null>(null);
const [tierInfo, setTierInfo] = useState<MerchantTier | null>(null);
@@ -111,19 +113,18 @@ export default function P2PMerchantDashboard() {
// Fetch merchant data
const fetchData = useCallback(async () => {
if (!userId) {
setLoading(false);
return;
}
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
navigate('/login');
return;
}
// Fetch stats
const { data: statsData } = await supabase
.from('p2p_merchant_stats')
.select('*')
.eq('user_id', user.id)
.eq('user_id', userId)
.single();
if (statsData) {
@@ -134,7 +135,7 @@ export default function P2PMerchantDashboard() {
const { data: tierData } = await supabase
.from('p2p_merchant_tiers')
.select('tier, max_pending_orders, max_order_amount, featured_ads_allowed')
.eq('user_id', user.id)
.eq('user_id', userId)
.single();
if (tierData) {
@@ -145,7 +146,7 @@ export default function P2PMerchantDashboard() {
const { data: adsData } = await supabase
.from('p2p_fiat_offers')
.select('*')
.eq('seller_id', user.id)
.eq('seller_id', userId)
.in('status', ['open', 'paused'])
.order('created_at', { ascending: false });
@@ -160,7 +161,7 @@ export default function P2PMerchantDashboard() {
const { data: tradesData } = await supabase
.from('p2p_fiat_trades')
.select('created_at, fiat_amount, status')
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
.or(`seller_id.eq.${userId},buyer_id.eq.${userId}`)
.gte('created_at', thirtyDaysAgo.toISOString())
.order('created_at', { ascending: true });
@@ -197,7 +198,7 @@ export default function P2PMerchantDashboard() {
} finally {
setLoading(false);
}
}, [navigate]);
}, [userId]);
useEffect(() => {
fetchData();
@@ -290,13 +291,14 @@ export default function P2PMerchantDashboard() {
// Save auto-reply message
const saveAutoReply = async () => {
if (!userId) return;
setSavingAutoReply(true);
try {
// Save to all active ads
const { error } = await supabase
.from('p2p_fiat_offers')
.update({ auto_reply_message: autoReplyMessage })
.eq('seller_id', (await supabase.auth.getUser()).data.user?.id);
.eq('seller_id', userId);
if (error) throw error;
+8 -8
View File
@@ -18,7 +18,7 @@ import {
RefreshCw,
FileText,
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { supabase } from '@/lib/supabase';
import { type P2PFiatTrade, type P2PFiatOffer } from '@shared/lib/p2p-fiat';
@@ -34,7 +34,7 @@ interface TradeWithOffer extends P2PFiatTrade {
export default function P2POrders() {
const navigate = useNavigate();
const { t } = useTranslation();
const { user } = useAuth();
const { userId } = useP2PIdentity();
const [trades, setTrades] = useState<TradeWithOffer[]>([]);
const [loading, setLoading] = useState(true);
@@ -42,7 +42,7 @@ export default function P2POrders() {
// Fetch user's trades
const fetchTrades = async () => {
if (!user) {
if (!userId) {
setLoading(false);
return;
}
@@ -53,7 +53,7 @@ export default function P2POrders() {
const { data: tradesData, error } = await supabase
.from('p2p_fiat_trades')
.select('*')
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
.or(`seller_id.eq.${userId},buyer_id.eq.${userId}`)
.order('created_at', { ascending: false });
if (error) throw error;
@@ -86,7 +86,7 @@ export default function P2POrders() {
useEffect(() => {
fetchTrades();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
}, [userId]);
// Filter trades by status
const activeTrades = trades.filter(t =>
@@ -149,7 +149,7 @@ export default function P2POrders() {
// Render trade card
const renderTradeCard = (trade: TradeWithOffer) => {
const isBuyer = trade.buyer_id === user?.id;
const isBuyer = trade.buyer_id === userId;
const counterpartyWallet = isBuyer
? trade.offer?.seller_wallet || 'Unknown'
: trade.buyer_wallet;
@@ -245,7 +245,7 @@ export default function P2POrders() {
</div>
);
if (!user) {
if (!userId) {
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<Card className="bg-gray-900 border-gray-800">
@@ -253,7 +253,7 @@ export default function P2POrders() {
<AlertTriangle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">{t('p2p.loginRequired')}</h2>
<p className="text-gray-400 mb-6">{t('p2p.loginToView')}</p>
<Button onClick={() => navigate('/login')}>{t('p2p.logIn')}</Button>
<Button onClick={() => navigate('/p2p')}>{t('p2pTrade.backToP2P')}</Button>
</CardContent>
</Card>
</div>
+11 -30
View File
@@ -33,13 +33,13 @@ import {
RefreshCw,
Star,
} from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useP2PIdentity } from '@/contexts/P2PIdentityContext';
import { toast } from 'sonner';
import { supabase } from '@/lib/supabase';
import {
markPaymentSent,
confirmPaymentReceived,
cancelTrade,
getUserReputation,
type P2PFiatTrade,
type P2PFiatOffer,
@@ -74,8 +74,7 @@ export default function P2PTrade() {
const { tradeId } = useParams<{ tradeId: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const { user } = useAuth();
const { api, selectedAccount } = usePezkuwi();
const { userId } = useP2PIdentity();
const [trade, setTrade] = useState<TradeWithDetails | null>(null);
const [loading, setLoading] = useState(true);
@@ -91,8 +90,8 @@ export default function P2PTrade() {
const [cancelReason, setCancelReason] = useState('');
// Determine user role
const isSeller = trade?.seller_id === user?.id;
const isBuyer = trade?.buyer_id === user?.id;
const isSeller = trade?.seller_id === userId;
const isBuyer = trade?.buyer_id === userId;
const isParticipant = isSeller || isBuyer;
// Fetch trade details
@@ -265,7 +264,7 @@ export default function P2PTrade() {
// Handle mark as paid
const handleMarkAsPaid = async () => {
if (!trade || !user) return;
if (!trade || !userId) return;
setActionLoading(true);
try {
@@ -293,14 +292,14 @@ export default function P2PTrade() {
// Handle release crypto
const handleReleaseCrypto = async () => {
if (!trade || !api || !selectedAccount) {
toast.error(t('p2p.connectWallet'));
if (!trade || !userId) {
toast.error(t('p2p.connectWalletAndLogin'));
return;
}
setActionLoading(true);
try {
await confirmPaymentReceived(api, selectedAccount, trade.id);
await confirmPaymentReceived(trade.id, userId);
toast.success(t('p2pTrade.cryptoReleasedToast'));
fetchTrade();
} catch (error) {
@@ -312,29 +311,11 @@ export default function P2PTrade() {
// Handle cancel trade
const handleCancelTrade = async () => {
if (!trade) return;
if (!trade || !userId) return;
setActionLoading(true);
try {
const { error } = await supabase
.from('p2p_fiat_trades')
.update({
status: 'cancelled',
cancelled_by: user?.id,
cancel_reason: cancelReason,
})
.eq('id', trade.id);
if (error) throw error;
// Restore offer remaining amount
await supabase
.from('p2p_fiat_offers')
.update({
remaining_amount: (trade.offer?.remaining_amount || 0) + trade.crypto_amount,
status: 'open',
})
.eq('id', trade.offer_id);
await cancelTrade(trade.id, userId, cancelReason || undefined);
setShowCancelModal(false);
toast.success(t('p2pTrade.tradeCancelledToast'));