1
0
forked from maik/IPFS-portal
Files
IPFS-portal-deploy/src/components/payment/PaymentPanel.tsx
T
maikrolf f72b775379 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
2026-07-19 15:35:18 +02:00

207 lines
6.9 KiB
TypeScript

'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>
);
}