Files
Pay2Gateway/components/checkout/CryptoCheckout.tsx

253 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect } from 'react';
import {
Copy,
CheckCircle2,
RefreshCw,
AlertCircle,
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, 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() {
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;
}
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, selectedToken, selectedNetwork]);
// Auto-polling for payment verification
useEffect(() => {
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);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const verifyPayment = async () => {
setIsVerifying(true);
try {
const response = await fetch('/api/crypto-sweep', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txId: txId,
network: selectedNetwork.id,
token: selectedToken.symbol
})
});
const data = await response.json();
if (data.success) {
setStatus('success');
onSuccess(data.hashes?.merchant || '0x_mock_hash');
} else if (data.status === 'waiting') {
setStatus('waiting');
}
} catch (err) {
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 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="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>
<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-[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-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>
) : (
<>
<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 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-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 active:scale-95"
>
{copied ? <CheckCircle2 size={18} /> : <span>Kopyala</span>}
</button>
</div>
</div>
<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 || 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"
>
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
{isVerifying ? 'Doğrulanıyor...' : 'Otomatik Tarama Aktif'}
</button>
</>
)}
</div>
);
}