'use client'; import { useState, useEffect, useCallback } from 'react'; import { paymentService, type PriceInfo, type TokenInfo, type MAOSDiscountTier, type UserUploadStats } from '@/lib/payment'; import { connectWallet, switchToChain270, getInjectedProvider, formatWeiToETH, formatBytes, type WalletProvider, } from '@/lib/wallet'; import { Wallet, Coins, Loader2, CheckCircle, XCircle, Zap, Shield, Tag, ArrowRight, ExternalLink, Copy, } from 'lucide-react'; /* ── Token icons ── */ const TOKEN_ICONS: Record = { ETH: '⟠', MAOS: 'Ⓜ', USDC: '⬡', }; 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) { // 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); const [copied, setCopied] = useState(false); // Init useEffect(() => { if (contractAddress) { paymentService.setContractAddress(contractAddress); } paymentService.isDeployed().then(setDeployed).catch(() => 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: any) { setError(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 ** 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: any) { setError(e.message || 'Payment/upload failed'); setStatus('error'); } finally { setPaying(false); } } async function copyCID() { if (!uploadResult) return; try { await navigator.clipboard.writeText(uploadResult.cid); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { /* ignore */ } } /* ── Render ── */ return (
{/* ── Header ── */}

Crypto Payment

{address && walletType && ( {walletType} )}
{!deployed ? ( /* ── Not deployed ── */
Payment contract not deployed — uploads are free
) : !address ? ( /* ── Connect wallet ── */ ) : wrongChain ? ( /* ── Wrong chain ── */
Please switch to zkSync Local (chain 270)
) : ( /* ── Connected — show pricing ── */
{/* Wallet address */}
Connected {address.slice(0, 6)}...{address.slice(-4)}
{/* Free tier info */} {userStats && (
Free tier
{formatBytes(userStats.remainingFree)} / {formatBytes(freeTierBytes)} remaining
)} {/* Price info */} {price && (
File size {formatBytes(fileSize)}
{price.isFree ? (
Free upload (within free tier)
) : ( <>
Price {formatWeiToETH(price.totalWei, 6)} ETH
{price.discountBps > 0 && (
MAOS discount
-{price.discountBps / 100}%
)}
Final {formatWeiToETH(price.finalWei, 6)} ETH
{/* Token selector */} {tokens.length > 1 && (
{tokens.filter(t => t.enabled || t.symbol === 'ETH').map(token => ( ))}
)} )}
)} {/* Action button */}
)} {/* ── Result ── */} {uploadResult && status === 'complete' && (
Upload + Payment successful
CID
{txHash && ( )}
)} {/* ── Error ── */} {error && (
{error}
)} {/* ── MAOS Discount tiers ── */} {address && discountTiers.length > 0 && (
MAOS discount tiers
{discountTiers.map((tier, i) => (
{formatWeiToETH(tier.minBalance, 0)} MAOS {tier.discountBps / 100}% off
))}
)}
); }