'use client'; import { useState, useEffect, useCallback } from 'react'; import PortalLayout from '@/app/layout-portal'; import AuthGuard from '@/components/AuthGuard'; import { paymentService, PaymentProvider, type TokenInfo, type MAOSDiscountTier } from '@/lib/payment'; import { connectWallet, switchToChain270, getInjectedProvider, addWalletListener, disconnectWallet, type WalletProvider, } from '@/lib/wallet'; import { SkeletonCard } from '@/components/Skeleton'; import { useNotify } from '@/lib/notifications'; import Overview from './components/Overview'; import PricingView from './components/PricingView'; import TokensView from './components/TokensView'; import TiersView from './components/TiersView'; import ErrorBoundary from '@/components/ErrorBoundary'; import type { ViewMode, TxStatus } from './helpers'; import { WalletConnect, WrongChainWarning, WalletInfo, NavTabs, TxStatusToast } from './components'; export default function AdminPaymentPage() { const [address, setAddress] = useState(null); const [provider, setProvider] = useState(null); const [connecting, setConnecting] = useState(false); const [wrongChain, setWrongChain] = useState(false); const [walletType, setWalletType] = useState(null); const [view, setView] = useState('overview'); const [txStatus, setTxStatus] = useState(null); // Contract state const [owner, setOwner] = useState(null); const [deployed, setDeployed] = useState(false); const [basePrice, setBasePrice] = useState(BigInt(0)); const [freeTier, setFreeTier] = useState(BigInt(0)); const [tokens, setTokens] = useState([]); const [tiers, setTiers] = useState([]); const [totalUploads, setTotalUploads] = useState(BigInt(0)); const [revenues, setRevenues] = useState>({}); const [loading, setLoading] = useState(true); const [contractBalance, setContractBalance] = useState>({}); const isOwner = !!(address && owner && address.toLowerCase() === owner.toLowerCase()); const { notify } = useNotify(); // ── Load all contract state ── const refresh = useCallback(async () => { setLoading(true); setTxStatus(null); try { paymentService.isDeployed().then(setDeployed).catch((err) => { console.error('[AdminPayment] Deploy check failed:', err); setDeployed(false); }); const [own, price, free, t, tiersData, uploads] = await Promise.all([ paymentService.getOwner(), paymentService.getBasePricePerMB(), paymentService.getFreeTierBytes(), paymentService.getSupportedTokens(), paymentService.getMAOSDiscountTiers(), paymentService.getTotalUploads(), ]); setOwner(own); setBasePrice(price); setFreeTier(free); setTiers(tiersData); const seen = new Set(); const uniqueTokens = t.filter(tk => { if (seen.has(tk.symbol)) return false; seen.add(tk.symbol); return true; }); setTokens(uniqueTokens); const symbols = uniqueTokens.map(tk => tk.symbol); const revEntries = await Promise.all(symbols.map(s => paymentService.getTotalRevenue(s))); const revMap: Record = {}; symbols.forEach((s, i) => { revMap[s] = revEntries[i]; }); setRevenues(revMap); const balMap: Record = {}; for (const tk of t) { try { const client = (paymentService as any)['getPublicClient'](); if (tk.symbol === 'ETH') { const bal = await client.getBalance({ address: (paymentService as any)['contractAddress'] }); balMap['ETH'] = bal.toString(); } else if (tk.address && tk.address !== '0x0000000000000000000000000000000000000000') { const ERC20_BAL_ABI = [{ type: 'function' as const, name: 'balanceOf', inputs: [{ type: 'address' as const }], outputs: [{ type: 'uint256' as const }], stateMutability: 'view' as const }]; const bal = await client.readContract({ address: tk.address as `0x${string}`, abi: ERC20_BAL_ABI, functionName: 'balanceOf', args: [(paymentService as any)['contractAddress']], }); balMap[tk.symbol] = (bal as bigint).toString(); } } catch (err) { console.error('[AdminPayment] Failed to fetch balance for token:', err); } } setContractBalance(balMap); setTotalUploads(uploads); setTokens(t); } catch (e) { console.warn('Failed to load contract state:', e); } finally { setLoading(false); } }, []); useEffect(() => { refresh(); }, [refresh]); // ── Listen for external wallet disconnect ── useEffect(() => { if (!provider) return; const cb = (accounts: string[]) => { if (accounts.length === 0) { setAddress(null); setProvider(null); setConnecting(false); setWrongChain(false); setWalletType(null); notify({ type: 'info', title: 'Wallet disconnected' }); } }; addWalletListener('accountsChanged', cb); }, [provider, notify]); // ── Connect wallet ── async function handleConnect() { setConnecting(true); setTxStatus(null); try { const result = await connectWallet(); const prov = getInjectedProvider(); setAddress(result.address); setProvider(prov); if (result.chainId !== 270) { setWrongChain(true); const switched = await switchToChain270(prov || undefined); if (switched) setWrongChain(false); } if (window.ethereum?.isRabby) setWalletType('Rabby'); else if (window.ethereum?.isMetaMask) setWalletType('MetaMask'); else setWalletType('Wallet'); } catch (e: unknown) { setTxStatus({ ok: false, msg: e instanceof Error ? e.message : 'Connect failed' }); } finally { setConnecting(false); } } function handleDisconnect() { disconnectWallet(); setAddress(null); setProvider(null); setConnecting(false); setWrongChain(false); setWalletType(null); notify({ type: 'success', title: 'Wallet disconnected' }); } async function doTx(label: string, fn: () => Promise) { if (!provider) return; setTxStatus(null); try { const hash = await fn(); setTxStatus({ ok: true, msg: `${label} success: ${hash.slice(0, 10)}...` }); await refresh(); } catch (e: unknown) { setTxStatus({ ok: false, msg: `${label} failed: ${e instanceof Error ? e.message : String(e)}` }); } } // ── Form state ── const [priceInput, setPriceInput] = useState(''); const [freeInput, setFreeInput] = useState(''); const [tierBalance, setTierBalance] = useState(''); const [tierBps, setTierBps] = useState(''); const [tierRemoveIdx, setTierRemoveIdx] = useState(-1); const [tokenSymbol, setTokenSymbol] = useState(''); const [tokenAddr, setTokenAddr] = useState(''); const [tokenDec, setTokenDec] = useState('18'); const [tokenWei, setTokenWei] = useState(''); const [tokenEnabled, setTokenEnabled] = useState(true); return (

Payment Admin

Manage IPFS Portal payment contract

{address && ( )}
{!address && } {wrongChain && } {address && ( {loading ? (
{Array.from({ length: 6 }).map((_, i) => )}
) : ( <> {view === 'overview' && ( setView(v as ViewMode)} /> )} {view === 'pricing' && ( )} {view === 'tokens' && ( )} {view === 'tiers' && ( )} )}
)}
); }