forked from maik/IPFS-portal
refactor: codebase opschoning — type safety, error handling, central config, page splits
- categorie A: alle catch(e: any) → catch(e: unknown) met instanceof check - categorie B: stille .catch() logging toegevoegd (SWRegister, admin, upload, auth) - categorie C: hardcoded 192.168.1.176 IPs vervangen door env var defaults - categorie D: page files >250L gesplitst (history, settings, explorer, admin/payment, ipns, users, upload, dashboard) in helpers/components - categorie E: eslint-disable vervangen in SearchInput.tsx - src/lib/config.ts centrale config module (localhost defaults) - CSP in next.config.ts dynamisch via env var - deploy.sh: geen hardcoded IP meer (REQUIRED arg) - lib/api/ lib/auth/ lib/wallet/ lib/search-index/ lib/storage/ gesplitst in modules - ongebruikte bestanden verwijderd: api.ts, auth.tsx, wallet.ts, search-index.ts, PaymentPanel.tsx, SearchBar.tsx, proxy.ts, serve-static.js
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
'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<string | null>(null);
|
||||
const [provider, setProvider] = useState<WalletProvider | null>(null);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [walletType, setWalletType] = useState<string | null>(null);
|
||||
const [wrongChain, setWrongChain] = useState(false);
|
||||
|
||||
// Contract state
|
||||
const [deployed, setDeployed] = useState(false);
|
||||
const [price, setPrice] = useState<PriceInfo | null>(null);
|
||||
const [userStats, setUserStats] = useState<UserUploadStats | null>(null);
|
||||
const [tokens, setTokens] = useState<TokenInfo[]>([]);
|
||||
const [discountTiers, setDiscountTiers] = useState<MAOSDiscountTier[]>([]);
|
||||
const [selectedToken, setSelectedToken] = useState('ETH');
|
||||
const [freeTierBytes, setFreeTierBytes] = useState<bigint>(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<string | null>(null);
|
||||
const [txHash, setTxHash] = useState<string | null>(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 (
|
||||
<div className="glass rounded-xl p-5 space-y-5">
|
||||
<PaymentHeader
|
||||
deployed={deployed}
|
||||
address={address}
|
||||
walletType={walletType}
|
||||
connecting={connecting}
|
||||
wrongChain={wrongChain}
|
||||
onConnect={handleConnect}
|
||||
/>
|
||||
|
||||
{deployed && address && !wrongChain && (
|
||||
<PricingDisplay
|
||||
price={price}
|
||||
userStats={userStats}
|
||||
tokens={tokens}
|
||||
discountTiers={discountTiers}
|
||||
selectedToken={selectedToken}
|
||||
freeTierBytes={freeTierBytes}
|
||||
fileSize={fileSize}
|
||||
status={status}
|
||||
paying={paying}
|
||||
uploading={uploading}
|
||||
address={address}
|
||||
onSelectToken={setSelectedToken}
|
||||
onPayAndUpload={handlePayAndUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PaymentResult
|
||||
uploadResult={uploadResult}
|
||||
txHash={txHash}
|
||||
status={status}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user