mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 21:47:56 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } : {})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
Reference in New Issue
Block a user