"use client"; import { useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import Image from "next/image"; import { ethers } from "ethers"; import { LocaleSelector } from "@/components/LocaleSelector"; import { useI18n, type Locale, type MessageKey } from "@/hooks/useI18n"; import { useTheme } from "@/hooks/useTheme"; type StatusTone = "info" | "success" | "error"; type StatusState = { text: string; tone: StatusTone; key?: string; vars?: Record; }; type Eip1193Provider = { request: (args: { method: string; params?: unknown[] }) => Promise; on?: (event: string, fn: (...args: unknown[]) => void | Promise) => void; removeListener?: (event: string, fn: (...args: unknown[]) => void | Promise) => void; providers?: Eip1193Provider[]; isTrustWallet?: boolean; isTrust?: boolean; }; type EthereumWindow = Window & { ethereum?: Eip1193Provider & { providers?: Eip1193Provider[] }; trustwallet?: Eip1193Provider; }; const formatChain = (chainId: number | null) => { if (chainId === 137) return "Polygon"; if (typeof chainId === "number" && Number.isFinite(chainId)) return `Chain ${chainId}`; return "—"; }; type MessageEntry = { hash: string; to: string; from: string; message: string; createdAt: number; }; export default function HomePage() { const { locale, setLocale, t, localeOptions } = useI18n("en"); const [account, setAccount] = useState(""); const [chainId, setChainId] = useState(null); const [txHash, setTxHash] = useState("—"); const [status, setStatus] = useState({ text: t("status.waiting"), key: "status.waiting", tone: "info", }); const [connecting, setConnecting] = useState(false); const [sending, setSending] = useState(false); const [connected, setConnected] = useState(false); const [history, setHistory] = useState([]); const providerRef = useRef(null); const signerRef = useRef(null); const formRef = useRef(null); const chainLabel = formatChain(chainId); const onPolygon = chainId === 137; const statusClass = status.tone === "success" ? "status-line status-ok" : status.tone === "error" ? "status-line status-error" : "status-line"; const chainPillClass = onPolygon ? "pill pill-live" : "pill pill-quiet"; const chainPillText = onPolygon ? "Polygon" : chainLabel || t("live.chainRequired"); const disableSend = !connected || sending; const disableConnect = connecting; const disableDisconnect = !connected || connecting; const polygonscanBase = "https://polygonscan.com/tx/"; const hasTxHash = Boolean(txHash && txHash !== "—"); useEffect(() => { if (status.key) { setStatus((prev) => ({ ...prev, text: t(prev.key as MessageKey, prev.vars), })); } }, [locale, status.key, status.vars, t]); function setStatusMessage( text: string, tone: StatusTone = "info", key?: string, vars?: Record ) { setStatus({ text, tone, key, vars }); } async function connectWallet() { if (connecting) return; setConnecting(true); setTxHash("—"); setStatusMessage(t("status.preparingConnect"), "info", "status.preparingConnect"); await disconnect(false); try { await connectInjected(); } catch (err) { console.error("Connection failed", err); setStatusMessage(normalizeError(err, t("errors.connectionFailed"), t), "error"); resetState(); } finally { setConnecting(false); } } async function connectInjected() { const injected = pickInjectedProvider(); if (!injected) { setStatusMessage(t("errors.noWallet"), "error", "errors.noWallet"); return; } providerRef.current = injected; attachProviderEvents(injected); setStatusMessage(t("status.requesting"), "info", "status.requesting"); // Try to force a fresh permission prompt when possible try { await injected.request({ method: "wallet_requestPermissions", params: [{ eth_accounts: {} }], }); } catch (err) { // Some wallets do not support this; fall back silently console.warn("wallet_requestPermissions not supported or rejected", err); } const accounts = (await injected.request({ method: "eth_requestAccounts", })) as string[]; if (!accounts || accounts.length === 0) { throw new Error(t("errors.noAccounts")); } const browserProvider = new ethers.BrowserProvider( injected as unknown as ethers.Eip1193Provider ); const signer = await browserProvider.getSigner(); signerRef.current = signer; const address = await signer.getAddress(); const network = await browserProvider.getNetwork(); setAccount(address); setChainId(Number(network.chainId)); setConnected(true); setStatusMessage(t("status.connectedInjected"), "success", "status.connectedInjected"); } async function disconnect(showMessage = true) { removeProviderEvents(providerRef.current); providerRef.current = null; signerRef.current = null; resetState(); if (showMessage) setStatusMessage(t("status.disconnected"), "info", "status.disconnected"); } function resetState() { setConnected(false); setAccount(""); setChainId(null); setTxHash("—"); } function attachProviderEvents(provider: Eip1193Provider) { if (!provider?.on) return; provider.on("accountsChanged", handleAccountsChanged); provider.on("chainChanged", handleChainChanged); provider.on("disconnect", handleDisconnectEvent); } function removeProviderEvents(provider: Eip1193Provider | null) { if (!provider?.removeListener) return; provider.removeListener("accountsChanged", handleAccountsChanged); provider.removeListener("chainChanged", handleChainChanged); provider.removeListener("disconnect", handleDisconnectEvent); } function handleDisconnectEvent() { disconnect(true); } async function handleAccountsChanged(...args: unknown[]) { const accounts = Array.isArray(args[0]) ? args[0] : []; if (!accounts || accounts.length === 0) { await disconnect(true); return; } const first = accounts.find((acct): acct is string => typeof acct === "string"); if (!first) { await disconnect(true); return; } setAccount(first); } async function handleChainChanged(newChainId: unknown) { if (typeof newChainId !== "string" && typeof newChainId !== "number") return; const parsed = parseChainId(newChainId); setChainId(parsed); } async function ensurePolygonChain() { if (chainId === 137 || !providerRef.current) return true; try { await providerRef.current.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x89" }], }); setChainId(137); return true; } catch (err) { console.warn("Chain switch rejected", err); return false; } } async function handleCopyHash() { if (!hasTxHash) return; try { if (!("clipboard" in navigator)) { throw new Error("Clipboard API unavailable"); } await navigator.clipboard.writeText(txHash); setStatusMessage(t("live.copySuccess"), "success", "live.copySuccess"); } catch (err) { console.warn("Copy failed", err); setStatusMessage(t("live.copyFailed"), "error", "live.copyFailed"); } } async function handleSendMessage(event: FormEvent) { event.preventDefault(); const formEl = event.currentTarget; setTxHash("—"); if (!signerRef.current) { setStatusMessage(t("status.connectFirst"), "error", "status.connectFirst"); return; } const formData = new FormData(event.currentTarget); const message = (formData.get("message") || "").toString().trim(); let recipient = (formData.get("to") || "").toString().trim(); if (!message) { setStatusMessage(t("status.messageEmpty"), "error", "status.messageEmpty"); return; } if (!recipient) recipient = account; if (!ethers.isAddress(recipient)) { setStatusMessage(t("status.recipientInvalid"), "error", "status.recipientInvalid"); return; } const recipientIsEoa = recipient.toLowerCase() === account.toLowerCase(); if (recipientIsEoa) { setStatusMessage(t("status.eoaWarning"), "info", "status.eoaWarning"); } const canUsePolygon = await ensurePolygonChain(); if (!canUsePolygon) { setStatusMessage(t("status.switchPolygon"), "error", "status.switchPolygon"); return; } try { setSending(true); setStatusMessage(t("status.prepareTx"), "info", "status.prepareTx"); const data = ethers.hexlify(ethers.toUtf8Bytes(message)); const txRequest = { to: recipient, value: 0n, data }; let gasLimit: bigint; try { gasLimit = await signerRef.current.estimateGas(txRequest); } catch (estimateErr) { console.warn("Gas estimation failed, using fallback", estimateErr); setStatusMessage(t("status.gasFallback"), "info", "status.gasFallback"); gasLimit = 250000n; } const feeData = await signerRef.current.provider?.getFeeData?.(); const feeOverrides = feeData && feeData.maxFeePerGas && feeData.maxPriorityFeePerGas ? { maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, } : { // conservative Polygon fallbacks in gwei maxFeePerGas: ethers.parseUnits("50", "gwei"), maxPriorityFeePerGas: ethers.parseUnits("40", "gwei"), }; let tx; try { tx = await signerRef.current.sendTransaction({ ...txRequest, gasLimit, ...feeOverrides, }); } catch (sendErr) { const code = (sendErr as { code?: number | string })?.code; if (code === 4001 || code === "ACTION_REJECTED") { setStatusMessage(t("status.rejected"), "error", "status.rejected"); throw sendErr; } // Fallback to legacy gasPrice if EIP-1559 style send fails or simulates a revert setStatusMessage(t("status.feeFallback"), "info", "status.feeFallback"); const gasPrice = (await signerRef.current.provider?.getFeeData?.())?.gasPrice || ethers.parseUnits("50", "gwei"); tx = await signerRef.current.sendTransaction({ ...txRequest, gasLimit, gasPrice, }); } setTxHash(tx.hash); setStatusMessage(t("status.submitted", { hash: tx.hash }), "success", "status.submitted", { hash: tx.hash, }); await tx.wait(); setStatusMessage(t("status.onchain"), "success", "status.onchain"); formEl.reset(); setHistory((prev) => [ { hash: tx.hash, to: recipient, from: account, message, createdAt: Date.now(), }, ...prev, ]); } catch (err) { console.error("Send failed", err); setStatusMessage(normalizeError(err, t("errors.sendFailed"), t), "error"); } finally { setSending(false); } } const { theme, toggleTheme } = useTheme(); return (
PolyNote logo
{t("hero.eyebrow")}

{t("hero.title")}

{t("hero.lede")}

setLocale(code as Locale)} options={localeOptions} label={t("locale.label")} searchPlaceholder={t("locale.search")} />

{t("connection.title")}

{t("connection.subtitle")}

{t("connection.pill")}
{t("connection.account")}
{account || "—"}
{t("connection.chain")}
{chainLabel}
{t("connection.status")}
{connected ? t("connection.connected") : t("connection.notConnected")}

{t("compose.title")}

{t("compose.subtitle")}

{chainPillText}

{t("compose.messageHint")}

{t("live.title")}

{t("live.subtitle")}

{status.text}
{onPolygon ? t("live.chainReady") : t("live.chainRequired")}
{t("live.lastTx")}
{txHash || "—"}
{t("live.trackHint")}

{t("history.title")}

{t("history.subtitle")}

{history.length === 0 ? (
{t("history.empty")}
) : (
    {history.map((item) => (
  • {t("history.sent")} {new Date(item.createdAt).toLocaleTimeString()}
    {t("history.from")}: {item.from} {t("history.to")}: {item.to}
    {item.message}
    {item.hash.slice(0, 10)}…{item.hash.slice(-6)}
  • ))}
)}
); } function pickInjectedProvider(): Eip1193Provider | null { if (typeof window === "undefined") return null; const { ethereum, trustwallet } = window as EthereumWindow; if (ethereum?.providers?.length) { const trust = ethereum.providers.find((p) => p.isTrustWallet || p.isTrust); if (trust) return trust; return ethereum.providers[0]; } if (trustwallet) return trustwallet; return ethereum || null; } function parseChainId(chainId: string | number): number { if (typeof chainId === "string" && chainId.startsWith("0x")) { return parseInt(chainId, 16); } return Number(chainId); } function normalizeError( err: unknown, fallback: string, t: (key: string, vars?: Record) => string ): string { if (!err) return fallback; if (typeof err === "string") return err; if (typeof err === "object") { const maybeError = err as { code?: number | string; message?: string; reason?: string }; if (maybeError.code === 4001 || maybeError.message?.toLowerCase().includes("user rejected")) return t("status.rejected"); if (maybeError.message) return maybeError.message; if (maybeError.reason) return maybeError.reason; } return fallback; }