mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-24 23:37:54 +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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user