From cd4f5dba522df9d6134549b514bf758b24b3ede3 Mon Sep 17 00:00:00 2001 From: ZacLou Date: Sun, 6 Sep 2026 03:48:30 +0800 Subject: [PATCH] feat(wallets): handle Freighter not-installed with friendly error (#772) - Add FreighterNotInstalledError with install URL in message - Check window.freighter presence before connect, sign, and getAddress - Throw FreighterNotInstalledError instead of raw TypeError - Existing logic unchanged when extension is present - Add unit tests for missing extension, error message, and normal flow Closes #772 --- src/wallets/adapters/FreighterAdapter.ts | 7 ++-- test/freighterAdapter.test.ts | 51 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 test/freighterAdapter.test.ts diff --git a/src/wallets/adapters/FreighterAdapter.ts b/src/wallets/adapters/FreighterAdapter.ts index 5b8b001..67b0f66 100644 --- a/src/wallets/adapters/FreighterAdapter.ts +++ b/src/wallets/adapters/FreighterAdapter.ts @@ -3,6 +3,7 @@ */ import type { WalletAdapter } from "../../types.js"; +import { FreighterNotInstalledError } from "../../errors.js"; type Unsubscribe = () => void; @@ -24,7 +25,7 @@ export class FreighterAdapter implements WalletAdapter { async connect(): Promise { if (!window.freighter) { - throw new Error("Freighter wallet not installed"); + throw new FreighterNotInstalledError(); } const address = await window.freighter.getPublicKey(); @@ -38,7 +39,7 @@ export class FreighterAdapter implements WalletAdapter { async sign(xdr: string, network: string): Promise { if (!window.freighter) { - throw new Error("Freighter wallet not installed"); + throw new FreighterNotInstalledError(); } return await window.freighter.signTransaction(xdr, network); @@ -46,7 +47,7 @@ export class FreighterAdapter implements WalletAdapter { async getAddress(): Promise { if (!window.freighter) { - throw new Error("Freighter wallet not installed"); + throw new FreighterNotInstalledError(); } return await window.freighter.getPublicKey(); diff --git a/test/freighterAdapter.test.ts b/test/freighterAdapter.test.ts new file mode 100644 index 0000000..3db25fd --- /dev/null +++ b/test/freighterAdapter.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi } from "vitest"; +import { FreighterAdapter } from "../src/wallets/adapters/FreighterAdapter.js"; +import { FreighterNotInstalledError } from "../src/errors.js"; + +describe("FreighterAdapter not-installed handling", () => { + it("throws FreighterNotInstalledError on connect when window.freighter is absent", async () => { + const adapter = new FreighterAdapter(); + // Ensure window.freighter is undefined + (globalThis as any).window = { freighter: undefined }; + await expect(adapter.connect()).rejects.toThrow(FreighterNotInstalledError); + }); + + it("throws FreighterNotInstalledError on sign when window.freighter is absent", async () => { + const adapter = new FreighterAdapter(); + (globalThis as any).window = { freighter: undefined }; + await expect(adapter.sign("xdr", "testnet")).rejects.toThrow(FreighterNotInstalledError); + }); + + it("throws FreighterNotInstalledError on getAddress when window.freighter is absent", async () => { + const adapter = new FreighterAdapter(); + (globalThis as any).window = { freighter: undefined }; + await expect(adapter.getAddress()).rejects.toThrow(FreighterNotInstalledError); + }); + + it("error message includes the Freighter install URL", async () => { + const adapter = new FreighterAdapter(); + (globalThis as any).window = { freighter: undefined }; + try { + await adapter.connect(); + expect.fail("Should have thrown"); + } catch (err) { + expect(err).toBeInstanceOf(FreighterNotInstalledError); + expect((err as Error).message).toContain("https://www.freighter.app"); + } + }); + + it("connects normally when window.freighter is present", async () => { + const adapter = new FreighterAdapter(); + (globalThis as any).window = { + freighter: { + isConnected: vi.fn().mockResolvedValue(true), + getPublicKey: vi.fn().mockResolvedValue("GABC..."), + signTransaction: vi.fn().mockResolvedValue("signed-xdr"), + }, + }; + + const address = await adapter.connect(); + expect(address).toBe("GABC..."); + adapter.disconnect(); + }); +});