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 aa3a49f0f6
commit a80a2cfb07
25 changed files with 594 additions and 237 deletions
+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'));