Files
IPFS-portal/src/app/admin/payment/page.tsx
T
maikrolf 361f11033d refactor: verwijder MAOSv6 wallet referenties
- Verwijder window.maosv6 provider uit wallet discovery
- Verwijder isMAOS veld uit DetectedWallet type
- Verwijder MAOS badge uit login page
- Update titel naar 'IPFS Portal' (geen MAOS meer)
- Update foutmeldingen naar wallet-agnostisch
- Update tests naar standaard wallet fallback
- 191 tests passing
2026-09-19 14:28:22 +02:00

286 lines
11 KiB
TypeScript

'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<string | null>(null);
const [provider, setProvider] = useState<WalletProvider | null>(null);
const [connecting, setConnecting] = useState(false);
const [wrongChain, setWrongChain] = useState(false);
const [walletType, setWalletType] = useState<string | null>(null);
const [view, setView] = useState<ViewMode>('overview');
const [txStatus, setTxStatus] = useState<TxStatus>(null);
// Contract state
const [owner, setOwner] = useState<string | null>(null);
const [deployed, setDeployed] = useState(false);
const [basePrice, setBasePrice] = useState<bigint>(BigInt(0));
const [freeTier, setFreeTier] = useState<bigint>(BigInt(0));
const [tokens, setTokens] = useState<TokenInfo[]>([]);
const [tiers, setTiers] = useState<MAOSDiscountTier[]>([]);
const [totalUploads, setTotalUploads] = useState<bigint>(BigInt(0));
const [revenues, setRevenues] = useState<Record<string, bigint>>({});
const [loading, setLoading] = useState(true);
const [contractBalance, setContractBalance] = useState<Record<string, string>>({});
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<string>();
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<string, bigint> = {};
symbols.forEach((s, i) => { revMap[s] = revEntries[i]; });
setRevenues(revMap);
const balMap: Record<string, string> = {};
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<string>) {
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<number>(-1);
const [tokenSymbol, setTokenSymbol] = useState('');
const [tokenAddr, setTokenAddr] = useState('');
const [tokenDec, setTokenDec] = useState('18');
const [tokenWei, setTokenWei] = useState('');
const [tokenEnabled, setTokenEnabled] = useState(true);
return (
<AuthGuard requireAdmin redirectTo="/dashboard">
<PortalLayout>
<ErrorBoundary label="Betaalbeheer">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-white">Payment Admin</h1>
<p className="text-sm text-surface-400 mt-1">Manage IPFS Portal payment contract</p>
</div>
{address && (
<WalletInfo
address={address}
isOwner={isOwner}
loading={loading}
onRefresh={refresh}
onDisconnect={handleDisconnect}
/>
)}
</div>
{!address && <WalletConnect connecting={connecting} onConnect={handleConnect} />}
{wrongChain && <WrongChainWarning />}
{address && (
<PaymentProvider>
<NavTabs view={view} onChange={setView} />
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 animate-fade-in">
{Array.from({ length: 6 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
) : (
<>
{view === 'overview' && (
<Overview
totalUploads={totalUploads}
tokens={tokens}
revenues={revenues}
contractBalance={contractBalance}
contractAddress={(paymentService as any)['contractAddress']}
owner={owner}
isOwner={isOwner}
address={address}
deployed={deployed}
onNavigate={(v: string) => setView(v as ViewMode)}
/>
)}
{view === 'pricing' && (
<PricingView
basePrice={basePrice}
freeTier={freeTier}
isOwner={isOwner}
tokens={tokens}
contractBalance={contractBalance}
priceInput={priceInput}
setPriceInput={setPriceInput}
freeInput={freeInput}
setFreeInput={setFreeInput}
doTx={doTx}
provider={provider}
address={address}
/>
)}
{view === 'tokens' && (
<TokensView
tokens={tokens}
isOwner={isOwner}
contractBalance={contractBalance}
tokenSymbol={tokenSymbol}
setTokenSymbol={setTokenSymbol}
tokenAddr={tokenAddr}
setTokenAddr={setTokenAddr}
tokenDec={tokenDec}
setTokenDec={setTokenDec}
tokenWei={tokenWei}
setTokenWei={setTokenWei}
tokenEnabled={tokenEnabled}
setTokenEnabled={setTokenEnabled}
doTx={doTx}
provider={provider}
address={address}
/>
)}
{view === 'tiers' && (
<TiersView
tiers={tiers}
isOwner={isOwner}
tierBalance={tierBalance}
setTierBalance={setTierBalance}
tierBps={tierBps}
setTierBps={setTierBps}
doTx={doTx}
provider={provider}
/>
)}
</>
)}
<TxStatusToast status={txStatus} />
</PaymentProvider>
)}
</ErrorBoundary>
</PortalLayout>
</AuthGuard>
);
}