feat: add phantom integration
All checks were successful
build / lint (push) Successful in 11s
build / build (push) Successful in 38s
build / docker image (push) Successful in 52s

This commit is contained in:
2026-01-31 10:31:24 +00:00
parent dd8ecce947
commit e84d1cabd8
12 changed files with 234 additions and 46 deletions

View File

@@ -24,11 +24,13 @@ type Eip1193Provider = {
providers?: Eip1193Provider[];
isTrustWallet?: boolean;
isTrust?: boolean;
isPhantom?: boolean;
};
type EthereumWindow = Window & {
ethereum?: Eip1193Provider & { providers?: Eip1193Provider[] };
trustwallet?: Eip1193Provider;
phantom?: { ethereum?: Eip1193Provider };
};
const formatChain = (chainId: number | null) => {
@@ -45,6 +47,32 @@ type MessageEntry = {
createdAt: number;
};
function TrustIcon() {
return (
<svg className="wallet-icon" viewBox="0 0 64 64" aria-hidden="true">
<path
fill="#2b5dff"
d="M32 6c-5.7 4.8-12.1 7-18.4 7.4l.7 21c.3 9.5 6.3 18 17.7 23.6 11.4-5.6 17.4-14 17.7-23.6l.7-21C44.1 13 37.7 10.8 32 6Z"
/>
<path
fill="#fff"
d="M32 11.6c5 3.6 10.3 5.4 15.6 5.9l-.6 17.8c-.3 8.1-5.1 14.9-15 19.7-9.9-4.8-14.8-11.6-15-19.7l-.6-17.8c5.3-.5 10.6-2.3 15.6-5.9Z"
/>
</svg>
);
}
function PhantomIcon() {
return (
<svg className="wallet-icon" viewBox="0 0 64 64" aria-hidden="true">
<path
fill="#6c3bff"
d="M54.5 25.7C52 13.9 42.2 8 33.8 8 24.1 8 12 14.5 8 29.7 5.8 37.7 10.9 41.4 14.4 41.4c4 0 7.9-2.4 10.1-5.6.5 4.1 3.9 7.5 8.1 7.5 4.7 0 8.6-4 8.6-9 0-1.5-.3-2.9-.8-4.2 0 0 6.5.5 8.3 7.1 1.1 4.2 2 7 4 7 1.7 0 3.7-2.4 1.8-18.5ZM15.5 33c-1.4 0-2.5-1.5-2.5-3.3s1.1-3.3 2.5-3.3c1.4 0 2.5 1.5 2.5 3.3S16.9 33 15.5 33Zm11 0c-1.4 0-2.5-1.5-2.5-3.3s1.1-3.3 2.5-3.3c1.4 0 2.5 1.5 2.5 3.3S27.9 33 26.5 33Z"
/>
</svg>
);
}
export default function HomePage() {
const { locale, setLocale, t, localeOptions } = useI18n("en");
const [account, setAccount] = useState<string>("");
@@ -55,6 +83,7 @@ export default function HomePage() {
key: "status.waiting",
tone: "info",
});
const [walletChoice, setWalletChoice] = useState<"trust" | "phantom">("trust");
const [connecting, setConnecting] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
const [connected, setConnected] = useState<boolean>(false);
@@ -72,6 +101,7 @@ export default function HomePage() {
: status.tone === "error"
? "status-line status-error"
: "status-line";
const walletLabel = walletChoice === "trust" ? t("wallet.trust") : t("wallet.phantom");
const chainPillClass = onPolygon ? "pill pill-live" : "pill pill-quiet";
const chainPillText = onPolygon ? "Polygon" : chainLabel || t("live.chainRequired");
const disableSend = !connected || sending;
@@ -106,7 +136,7 @@ export default function HomePage() {
await disconnect(false);
try {
await connectInjected();
await connectInjected(walletChoice);
} catch (err) {
console.error("Connection failed", err);
setStatusMessage(normalizeError(err, t("errors.connectionFailed"), t), "error");
@@ -116,10 +146,14 @@ export default function HomePage() {
}
}
async function connectInjected() {
const injected = pickInjectedProvider();
async function connectInjected(choice: "trust" | "phantom") {
const injected = pickInjectedProvider(choice);
if (!injected) {
setStatusMessage(t("errors.noWallet"), "error", "errors.noWallet");
setStatusMessage(
t("errors.noWallet", { wallet: t(choice === "trust" ? "wallet.trust" : "wallet.phantom") }),
"error",
"errors.noWallet"
);
return;
}
@@ -174,6 +208,23 @@ export default function HomePage() {
setTxHash("—");
}
async function refreshSigner() {
if (!providerRef.current) return;
try {
const browserProvider = new ethers.BrowserProvider(
providerRef.current 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));
} catch (err) {
console.warn("Failed to refresh signer after chain change", err);
}
}
function attachProviderEvents(provider: Eip1193Provider) {
if (!provider?.on) return;
provider.on("accountsChanged", handleAccountsChanged);
@@ -210,6 +261,7 @@ export default function HomePage() {
if (typeof newChainId !== "string" && typeof newChainId !== "number") return;
const parsed = parseChainId(newChainId);
setChainId(parsed);
void refreshSigner();
}
async function ensurePolygonChain() {
@@ -220,6 +272,7 @@ export default function HomePage() {
params: [{ chainId: "0x89" }],
});
setChainId(137);
await refreshSigner();
return true;
} catch (err) {
console.warn("Chain switch rejected", err);
@@ -393,6 +446,33 @@ export default function HomePage() {
{theme === "dark" ? t("theme.light") : t("theme.dark")}
</button>
</div>
<div className="wallet-selector">
<div className="wallet-selector-head">
<p className="label">{t("wallet.pick")}</p>
<p className="muted">{t("wallet.helper")}</p>
</div>
<div className="wallet-toggle">
<button
type="button"
className={`wallet-chip ${walletChoice === "trust" ? "active" : ""}`}
onClick={() => setWalletChoice("trust")}
aria-pressed={walletChoice === "trust"}
>
<TrustIcon />
{t("wallet.trust")}
</button>
<button
type="button"
className={`wallet-chip ${walletChoice === "phantom" ? "active" : ""}`}
onClick={() => setWalletChoice("phantom")}
aria-pressed={walletChoice === "phantom"}
>
<PhantomIcon />
{t("wallet.phantom")}
</button>
</div>
</div>
</div>
</header>
@@ -413,7 +493,9 @@ export default function HomePage() {
onClick={connectWallet}
disabled={disableConnect}
>
{connecting ? t("compose.sending") : t("connection.connect")}
{connecting
? t("compose.sending")
: t("connection.connectWallet", { wallet: walletLabel })}
</button>
<button
id="disconnectBtn"
@@ -566,7 +648,7 @@ export default function HomePage() {
<footer className="footer">
<div className="footer-left">
<span className="version-pill">1</span>
<span className="version-pill">2</span>
</div>
<a
className="footer-link"
@@ -589,16 +671,33 @@ export default function HomePage() {
);
}
function pickInjectedProvider(): Eip1193Provider | null {
function pickInjectedProvider(choice: "trust" | "phantom"): 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);
const { ethereum, trustwallet, phantom } = window as EthereumWindow;
const providers = ethereum?.providers || [];
const isTrustProvider = (p: Eip1193Provider | null | undefined) =>
Boolean(p?.isTrustWallet || p?.isTrust);
const isPhantomProvider = (p: Eip1193Provider | null | undefined) =>
Boolean(p?.isPhantom || (phantom?.ethereum && p === phantom.ethereum));
if (choice === "trust") {
const trust = providers.find(isTrustProvider);
if (trust) return trust;
return ethereum.providers[0];
if (trustwallet) return trustwallet;
if (providers.length) return providers[0];
return ethereum || null;
}
if (trustwallet) return trustwallet;
return ethereum || null;
if (choice === "phantom") {
const phantomFromArray = providers.find(isPhantomProvider);
if (phantomFromArray) return phantomFromArray;
if (phantom?.ethereum) return phantom.ethereum;
if (ethereum?.isPhantom) return ethereum;
return ethereum || null;
}
return null;
}
function parseChainId(chainId: string | number): number {