'use client'; import { useState, useEffect, useCallback } from 'react'; import { usePaymentService, type PriceInfo, type TokenInfo, type MAOSDiscountTier, type UserUploadStats } from '@/lib/payment'; import { connectWallet, switchToChain270, getInjectedProvider, type WalletProvider, } from '@/lib/wallet'; import PaymentHeader from './PaymentHeader'; import PricingDisplay from './PricingDisplay'; import PaymentResult from './PaymentResult'; interface PaymentPanelProps { fileSize: number; // bytes fileName?: string; onPaid: (paymentInfo: { cid: string; txHash?: string; method: 'free' | 'eth' | 'token' }) => void; onUpload: () => Promise<{ cid: string; size: number }>; contractAddress?: string; } export default function PaymentPanel({ fileSize, fileName, onPaid, onUpload, contractAddress, }: PaymentPanelProps) { const paymentService = usePaymentService(); // Wallet state const [address, setAddress] = useState(null); const [provider, setProvider] = useState(null); const [connecting, setConnecting] = useState(false); const [walletType, setWalletType] = useState(null); const [wrongChain, setWrongChain] = useState(false); // Contract state const [deployed, setDeployed] = useState(false); const [price, setPrice] = useState(null); const [userStats, setUserStats] = useState(null); const [tokens, setTokens] = useState([]); const [discountTiers, setDiscountTiers] = useState([]); const [selectedToken, setSelectedToken] = useState('ETH'); const [freeTierBytes, setFreeTierBytes] = useState(BigInt(0)); // Upload state const [uploading, setUploading] = useState(false); const [paying, setPaying] = useState(false); const [status, setStatus] = useState<'idle' | 'paying' | 'uploading' | 'complete' | 'error'>('idle'); const [error, setError] = useState(null); const [txHash, setTxHash] = useState(null); const [uploadResult, setUploadResult] = useState<{ cid: string; size: number } | null>(null); // Init useEffect(() => { if (contractAddress) { paymentService.setContractAddress(contractAddress); } paymentService.isDeployed().then(setDeployed).catch((err) => { console.error('[PaymentPanel] Deploy check failed:', err); setDeployed(false); }); }, [contractAddress]); // Refresh price + stats when address or fileSize changes const refreshPricing = useCallback(async (addr: string, size: number) => { if (!deployed) return; try { const [p, stats, t, tiers, freeBytes] = await Promise.all([ paymentService.getPrice(size, addr as `0x${string}`), paymentService.getUserStats(addr as `0x${string}`), paymentService.getSupportedTokens(), paymentService.getMAOSDiscountTiers(), paymentService.getFreeTierBytes(), ]); setPrice(p); setUserStats(stats); setTokens(t.filter(tk => tk.enabled || tk.symbol === 'ETH')); setDiscountTiers(tiers); setFreeTierBytes(freeBytes); } catch (e) { console.warn('Payment contract read failed:', e); } }, [deployed]); // Connect wallet async function handleConnect() { setConnecting(true); setError(null); try { const result = await connectWallet(); const prov = getInjectedProvider(); setAddress(result.address); setProvider(prov); // Detect wallet type if (window.maosv6) setWalletType('MAOS Wallet'); else if (window.ethereum?.isRabby) setWalletType('Rabby'); else if (window.ethereum?.isMetaMask) setWalletType('MetaMask'); else setWalletType('Wallet'); // Check chain if (result.chainId !== 270) { setWrongChain(true); const switched = await switchToChain270(prov || undefined); if (switched) setWrongChain(false); } await refreshPricing(result.address, fileSize); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to connect wallet'); } finally { setConnecting(false); } } // Refresh when fileSize changes useEffect(() => { if (address && deployed) { refreshPricing(address, fileSize); } }, [fileSize, address, deployed, refreshPricing]); // Pay + Upload flow async function handlePayAndUpload() { if (!provider || !address) return; setPaying(true); setError(null); setStatus('paying'); try { // Step 1: Upload to IPFS setStatus('uploading'); const upload = await onUpload(); setUploadResult(upload); // Step 2: Pay (if not free) if (price && !price.isFree && price.finalWei > BigInt(0)) { if (selectedToken === 'ETH') { const hash = await paymentService.payWithETH(provider, upload.cid, fileSize, price.finalWei); setTxHash(hash); onPaid({ cid: upload.cid, txHash: hash, method: 'eth' }); } else { // Token payment — need to find token address const token = tokens.find(t => t.symbol === selectedToken); if (!token || token.address === '0x0000000000000000000000000000000000000000') { throw new Error(`${selectedToken} not configured on contract`); } // Calculate token amount const tokenAmount = (price.finalWei * BigInt(10) ** BigInt(token.decimals)) / token.weiPerToken; const result = await paymentService.approveAndPayWithToken( provider, token.address as `0x${string}`, upload.cid, fileSize, tokenAmount, selectedToken ); setTxHash(result.payHash); onPaid({ cid: upload.cid, txHash: result.payHash, method: 'token' }); } } else { // Free upload onPaid({ cid: upload.cid, method: 'free' }); } setStatus('complete'); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Payment/upload failed'); setStatus('error'); } finally { setPaying(false); } } /* ── Render ── */ return (
{deployed && address && !wrongChain && ( )}
); }