Refactor: Fully migrated to direct PostgreSQL, implemented Public API v1, fixed Vercel deployment conflicts, and updated documentation

This commit is contained in:
mstfyldz
2026-03-12 21:54:57 +03:00
parent 321f25a15c
commit 515d513c1f
29 changed files with 1002 additions and 675 deletions

View File

@@ -2,55 +2,110 @@
import React, { useState, useEffect } from 'react';
import {
Coins,
Copy,
CheckCircle2,
ExternalLink,
RefreshCw,
AlertCircle,
QrCode
ChevronDown
} from 'lucide-react';
import cryptoConfig from '@/lib/crypto-config.json';
interface CryptoCheckoutProps {
amount: number;
currency: string;
txId: string;
wallets?: {
EVM: string;
SOLANA: string;
};
onSuccess: (txHash: string) => void;
}
export default function CryptoCheckout({ amount, currency, txId, onSuccess }: CryptoCheckoutProps) {
const [selectedCoin, setSelectedCoin] = useState('SOL');
const [depositAddress, setDepositAddress] = useState<string>('');
export default function CryptoCheckout({ amount, currency, txId, wallets, onSuccess }: CryptoCheckoutProps) {
const [selectedNetwork, setSelectedNetwork] = useState(cryptoConfig.networks[0]);
const [selectedToken, setSelectedToken] = useState(selectedNetwork.tokens[0]);
const [depositAddress, setDepositAddress] = useState<string>('Yükleniyor...');
const [isVerifying, setIsVerifying] = useState(false);
const [copied, setCopied] = useState(false);
const [status, setStatus] = useState<'waiting' | 'verifying' | 'success' | 'error'>('waiting');
const [cryptoAmount, setCryptoAmount] = useState<string>('Hesaplanıyor...');
const [unitPrice, setUnitPrice] = useState<string>('');
const [unitPriceUsd, setUnitPriceUsd] = useState<string>('');
// Update address based on selected network
useEffect(() => {
if (wallets) {
const addr = selectedNetwork.id === 'SOLANA' ? wallets.SOLANA : wallets.EVM;
setDepositAddress(addr);
}
}, [selectedNetwork, wallets]);
// Fetch exchange rate
useEffect(() => {
async function fetchExchangeRate() {
if (currency === 'TL' || currency === 'TRY') {
try {
const symbol = selectedCoin === 'SOL' ? 'SOLTRY' : 'USDTTRY';
const res = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${symbol}`);
const data = await res.json();
const rate = parseFloat(data.price);
setCryptoAmount((amount / rate).toFixed(selectedCoin === 'SOL' ? 4 : 2));
} catch (error) {
// Fallback rate if API fails
setCryptoAmount((amount / 5500).toFixed(selectedCoin === 'SOL' ? 4 : 2));
setCryptoAmount('...');
try {
const symbol = selectedToken.symbol;
let rateInTry = 32.5;
let rateInUsd = 1.0;
// 1. Get USDTRY rate first
const tryRes = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=USDTTRY`);
const tryData = await tryRes.json();
const tryToUsdRate = parseFloat(tryData.price) || 32.5;
const isStable = ['USDT', 'USDC', 'BUSD', 'DAI'].includes(symbol);
if (!isStable) {
// Try to fetch [SYMBOL]USDT
try {
const pair = `${symbol}USDT`;
const res = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${pair}`);
const data = await res.json();
if (data.price) {
rateInUsd = parseFloat(data.price);
rateInTry = rateInUsd * tryToUsdRate;
} else {
// Try fallback mappings if direct USDT pair fails
rateInTry = tryToUsdRate; // Default to 1 USD value
}
} catch (e) {
rateInTry = tryToUsdRate;
}
} else {
rateInTry = tryToUsdRate;
rateInUsd = 1.0;
}
} else {
// If already USD or USDT, 1:1 ratio
setCryptoAmount(amount.toFixed(2));
setUnitPrice(rateInTry.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 4 }));
setUnitPriceUsd(rateInUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 }));
const finalAmount = (amount / rateInTry).toFixed(selectedToken.decimals < 9 ? 4 : 6);
setCryptoAmount(finalAmount);
} catch (error) {
setCryptoAmount((amount / 32.5).toFixed(2));
}
}
fetchExchangeRate();
}, [amount, currency]);
}, [amount, selectedToken, selectedNetwork]);
// Auto-polling for payment verification
useEffect(() => {
// Use a real valid Solana test wallet so Phantom doesn't say "Invalid Address"
setDepositAddress('5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe');
}, [selectedCoin]);
let interval: NodeJS.Timeout;
if (status === 'waiting' && depositAddress !== 'Yükleniyor...') {
interval = setInterval(() => {
verifyPayment();
}, 8000); // Check every 8 seconds
}
return () => clearInterval(interval);
}, [status, depositAddress, selectedNetwork, selectedToken]);
const changeNetwork = (networkId: string) => {
const network = cryptoConfig.networks.find(n => n.id === networkId) || cryptoConfig.networks[0];
setSelectedNetwork(network);
setSelectedToken(network.tokens[0]);
};
const handleCopy = () => {
navigator.clipboard.writeText(depositAddress);
@@ -60,118 +115,136 @@ export default function CryptoCheckout({ amount, currency, txId, onSuccess }: Cr
const verifyPayment = async () => {
setIsVerifying(true);
setStatus('verifying');
try {
const response = await fetch('/api/crypto-sweep', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txId: txId,
merchantAddress: '5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe' // A placeholder valid Solana Devnet Wallet
network: selectedNetwork.id,
token: selectedToken.symbol
})
});
const data = await response.json();
if (data.success) {
setStatus('success');
onSuccess(data.hashes.merchant);
} else {
setStatus('error');
onSuccess(data.hashes?.merchant || '0x_mock_hash');
} else if (data.status === 'waiting') {
setStatus('waiting');
}
} catch (err) {
setStatus('error');
console.error(err);
} finally {
setIsVerifying(false);
}
};
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${depositAddress}`;
return (
<div className="bg-white p-8 lg:p-12 rounded-[40px] border border-gray-100 shadow-sm space-y-8 animate-in fade-in zoom-in duration-500 w-full max-w-lg">
<div className="flex items-center justify-between">
<div className="bg-white p-8 rounded-[40px] border border-gray-100 shadow-sm space-y-6 w-full max-w-lg">
{/* Crypto Selection Header */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1"> Seçin</label>
<div className="relative group">
<select
value={selectedNetwork.id}
onChange={(e) => changeNetwork(e.target.value)}
className="w-full bg-gray-50 border-none rounded-2xl p-4 text-sm font-bold text-gray-900 appearance-none cursor-pointer focus:ring-2 focus:ring-blue-500/20"
>
{cryptoConfig.networks.map(net => (
<option key={net.id} value={net.id}>{net.icon} {net.name}</option>
))}
</select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1">Varlık</label>
<div className="relative">
<select
value={selectedToken.symbol}
onChange={(e) => setSelectedToken(selectedNetwork.tokens.find(t => t.symbol === e.target.value) || selectedNetwork.tokens[0])}
className="w-full bg-gray-50 border-none rounded-2xl p-4 text-sm font-bold text-gray-900 appearance-none cursor-pointer focus:ring-2 focus:ring-blue-500/20"
>
{selectedNetwork.tokens.map(token => (
<option key={token.symbol} value={token.symbol}>{token.symbol}</option>
))}
</select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
</div>
</div>
</div>
<div className="p-6 bg-gray-50 rounded-3xl flex items-center justify-between border border-gray-100">
<div className="flex items-center gap-3">
<div className="p-3 bg-orange-50 rounded-2xl text-orange-600">
<Coins size={24} />
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-2xl shadow-sm">
{selectedToken.symbol === 'SOL' ? '☀️' : selectedToken.symbol.startsWith('U') ? '💵' : '🪙'}
</div>
<div>
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Kripto Ödeme</h3>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest">On-Chain Güvenli Transfer</p>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-tight">Ödenecek Tutar</p>
<h4 className="text-2xl font-black text-gray-900 leading-none">{cryptoAmount} {selectedToken.symbol}</h4>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-black text-gray-900">{cryptoAmount} <span className="text-xs text-gray-400">{selectedCoin}</span></p>
<p className="text-[10px] text-gray-400 font-bold uppercase">: Solana (Devnet)</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">Kur</p>
<p className="text-[10px] font-black text-blue-600 mb-1">1 {selectedToken.symbol} = {unitPrice} TRY</p>
<p className="text-[10px] font-bold text-gray-400">($ {unitPriceUsd})</p>
</div>
</div>
{status === 'success' ? (
<div className="py-10 text-center space-y-6">
<div className="w-20 h-20 bg-emerald-50 rounded-[32px] flex items-center justify-center text-emerald-500 mx-auto animate-bounce">
<CheckCircle2 size={40} />
</div>
<div className="space-y-2">
<h2 className="text-2xl font-black text-gray-900 uppercase tracking-tight">Ödeme Onaylandı!</h2>
<p className="text-gray-400 font-bold uppercase tracking-widest text-[10px]">İşleminiz başarıyla blokzincirine kaydedildi.</p>
<div className="py-8 text-center space-y-4 bg-emerald-50 rounded-3xl border border-emerald-100 animate-in fade-in slide-in-from-bottom-4">
<div className="w-16 h-16 bg-emerald-500 rounded-full flex items-center justify-center text-white mx-auto">
<CheckCircle2 size={32} />
</div>
<h2 className="text-xl font-black text-emerald-900 uppercase">Ödeme Alındı</h2>
<p className="text-emerald-700 font-bold text-[10px] uppercase tracking-widest">Mağazaya yönlendiriliyorsunuz...</p>
</div>
) : (
<>
{/* QR Code Placeholder */}
<div className="bg-gray-50 aspect-square rounded-[32px] flex flex-col items-center justify-center border border-gray-100 relative group cursor-pointer">
<QrCode size={180} className="text-gray-200 group-hover:text-gray-400 transition-colors" />
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<span className="bg-white px-4 py-2 rounded-xl text-[10px] font-black uppercase shadow-lg border border-gray-100">Büyüt</span>
</div>
</div>
<div className="space-y-4">
<div className="bg-white aspect-square rounded-[32px] flex flex-col items-center justify-center border-2 border-dashed border-gray-100 relative group overflow-hidden p-8">
{depositAddress !== 'Yükleniyor...' ? (
<img src={qrUrl} alt="Wallet QR" className="w-full h-full object-contain" />
) : (
<RefreshCw className="w-12 h-12 text-gray-200 animate-spin" />
)}
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Cüzdan Adresi</label>
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-1">Cüzdan Adresi ({selectedNetwork.name})</label>
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-50 p-4 rounded-2xl border border-gray-100 font-mono text-[10px] text-gray-600 break-all leading-tight">
<div className="flex-1 bg-gray-50 p-4 rounded-2xl border border-gray-100 font-mono text-[10px] text-gray-500 break-all leading-tight">
{depositAddress}
</div>
<button
onClick={handleCopy}
className="p-4 bg-gray-900 text-white rounded-2xl hover:bg-black transition-all shadow-lg shadow-gray-200 active:scale-95"
className="p-4 bg-gray-900 text-white rounded-2xl hover:bg-black transition-all shadow-lg active:scale-95"
>
{copied ? <CheckCircle2 size={18} /> : <Copy size={18} />}
{copied ? <CheckCircle2 size={18} /> : <span>Kopyala</span>}
</button>
</div>
</div>
<div className="p-6 bg-blue-50/50 rounded-3xl border border-blue-100 space-y-2">
<div className="flex items-center gap-2 text-blue-600">
<AlertCircle size={14} />
<span className="text-[9px] font-black uppercase tracking-widest">Önemli Uyarı</span>
</div>
<p className="text-[10px] text-blue-800 leading-relaxed font-medium">
Lütfen sadece test amaçlı <b>Solana (Devnet)</b> ı üzerinden test SOL'ü gönderimi yapın. Gerçek ağ veya USDT bu ortamda kabul edilmez.
<div className="p-4 bg-orange-50 rounded-2xl border border-orange-100 flex gap-3">
<AlertCircle size={18} className="text-orange-600 shrink-0" />
<p className="text-[10px] text-orange-800 leading-relaxed font-bold">
Sadece <span className="underline text-orange-950">{selectedNetwork.name}</span> ı üzerinden <span className="underline text-orange-950">{selectedToken.symbol}</span> gönderin.
</p>
</div>
</div>
<button
onClick={verifyPayment}
disabled={isVerifying}
className="w-full py-5 bg-gray-900 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-black transition-all shadow-xl shadow-gray-200 flex items-center justify-center gap-3 disabled:opacity-50"
disabled={isVerifying || depositAddress === 'Yükleniyor...'}
className="w-full py-5 bg-gray-900 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl flex items-center justify-center gap-3 disabled:opacity-50"
>
{isVerifying ? (
<>
<RefreshCw size={18} className="animate-spin" />
Doğrulanıyor...
</>
) : (
'Ödemeyi Doğrula'
)}
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
{isVerifying ? 'Doğrulanıyor...' : 'Otomatik Tarama Aktif'}
</button>
<div className="flex justify-center gap-6">
<div className="flex items-center gap-2 text-gray-400 hover:text-gray-600 cursor-pointer transition-colors group">
<ExternalLink size={14} />
<span className="text-[9px] font-black uppercase tracking-widest group-hover:underline">Explorer'da Gör</span>
</div>
</div>
</>
)}
</div>