diff --git a/frontend/src/components/WalletConnect.test.tsx b/frontend/src/components/WalletConnect.test.tsx
index ecfaab24..dd8653da 100644
--- a/frontend/src/components/WalletConnect.test.tsx
+++ b/frontend/src/components/WalletConnect.test.tsx
@@ -320,4 +320,96 @@ describe('WalletConnect', () => {
localStorage.clear();
});
+
+ it('does not show reconnect prompt when prompt is dismissed in current session', async () => {
+ localStorage.setItem('yieldvault_last_wallet_provider', 'freighter');
+ sessionStorage.removeItem('yieldvault_wallet_manual_disconnect');
+ sessionStorage.setItem('yieldvault_wallet_reconnect_prompt_dismissed', '1');
+ mockedFreighter.isConnected.mockResolvedValue({ isConnected: true });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ localStorage.clear();
+ sessionStorage.clear();
+ });
+
+ it('clears reconnect prompt dismissed state on successful connection', async () => {
+ mockedFreighter.isAllowed
+ .mockResolvedValueOnce({ isAllowed: false })
+ .mockResolvedValueOnce({ isAllowed: true });
+ mockedFreighter.setAllowed.mockResolvedValue({ isAllowed: true });
+ mockedFreighter.getAddress.mockResolvedValue({ address: 'GABC123' });
+ sessionStorage.setItem('yieldvault_wallet_reconnect_prompt_dismissed', '1');
+
+ render(
+
+ );
+
+ const button = screen.getByText(/Connect Freighter/i);
+ fireEvent.click(button);
+
+ await waitFor(() => {
+ expect(mockOnConnect).toHaveBeenCalledWith('GABC123');
+ expect(sessionStorage.getItem('yieldvault_wallet_reconnect_prompt_dismissed')).toBeNull();
+ });
+
+ sessionStorage.clear();
+ });
+
+ it('dismisses reconnect prompt sets the session dismiss flag', async () => {
+ localStorage.setItem('yieldvault_last_wallet_provider', 'freighter');
+ sessionStorage.removeItem('yieldvault_wallet_manual_disconnect');
+ sessionStorage.removeItem('yieldvault_wallet_reconnect_prompt_dismissed');
+ mockedFreighter.isConnected.mockResolvedValue({ isConnected: true });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ fireEvent.click(screen.getByRole('button', { name: /use a different wallet/i }));
+ });
+
+ expect(sessionStorage.getItem('yieldvault_wallet_reconnect_prompt_dismissed')).toBe('1');
+
+ localStorage.clear();
+ sessionStorage.clear();
+ });
+
+ it('clears reconnect prompt dismissed state on manual disconnect', () => {
+ sessionStorage.setItem('yieldvault_wallet_reconnect_prompt_dismissed', '1');
+
+ render(
+
+ );
+
+ const disconnectButton = screen.getByLabelText(/Disconnect Wallet/i);
+ fireEvent.click(disconnectButton);
+
+ expect(sessionStorage.getItem('yieldvault_wallet_reconnect_prompt_dismissed')).toBeNull();
+
+ sessionStorage.clear();
+ });
});
diff --git a/frontend/src/components/WalletConnect.tsx b/frontend/src/components/WalletConnect.tsx
index f2331dae..4740de62 100644
--- a/frontend/src/components/WalletConnect.tsx
+++ b/frontend/src/components/WalletConnect.tsx
@@ -16,6 +16,10 @@ import {
getLastWalletProvider,
setLastWalletProvider,
clearLastWalletProvider,
+ isReconnectPromptDismissed,
+ setReconnectPromptDismissed,
+ clearReconnectPromptDismissed,
+ isProviderAvailable,
} from "../lib/walletSession";
import WalletReconnectPrompt from "./WalletReconnectPrompt";
@@ -59,12 +63,19 @@ const WalletConnect: React.FC = ({
// Show reconnect prompt for returning users who have a persisted provider
useEffect(() => {
- if (!walletAddress && !isWalletManualDisconnectSet()) {
- const provider = getLastWalletProvider();
- if (provider) {
- setReconnectProvider(provider);
+ const checkAndSetReconnectProvider = async () => {
+ if (!walletAddress && !isWalletManualDisconnectSet() && !isReconnectPromptDismissed()) {
+ const provider = getLastWalletProvider();
+ if (provider) {
+ // Validate provider is available before suggesting reconnect
+ const available = await isProviderAvailable(provider);
+ if (available) {
+ setReconnectProvider(provider);
+ }
+ }
}
- }
+ };
+ void checkAndSetReconnectProvider();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
@@ -128,6 +139,7 @@ const WalletConnect: React.FC = ({
// Set session start time for expiry tracking
localStorage.setItem("wallet_session_start", Date.now().toString());
clearWalletManualDisconnect();
+ clearReconnectPromptDismissed();
setLastWalletProvider("freighter");
setReconnectProvider(null);
onConnect(userInfo.address);
@@ -288,6 +300,7 @@ const WalletConnect: React.FC = ({
onClick={() => {
setConnectionError(null);
setWalletManualDisconnect();
+ clearReconnectPromptDismissed();
clearLastWalletProvider();
onDisconnect("manual");
toast.info({
@@ -316,6 +329,7 @@ const WalletConnect: React.FC = ({
}}
onDismiss={() => {
setReconnectProvider(null);
+ setReconnectPromptDismissed();
clearLastWalletProvider();
}}
/>
diff --git a/frontend/src/lib/walletSession.test.ts b/frontend/src/lib/walletSession.test.ts
index 3563ea33..8ca25b34 100644
--- a/frontend/src/lib/walletSession.test.ts
+++ b/frontend/src/lib/walletSession.test.ts
@@ -1,14 +1,23 @@
-import { describe, it, expect, beforeEach } from "vitest";
+import { describe, it, expect, beforeEach, vi } from "vitest";
import {
getLastWalletProvider,
setLastWalletProvider,
clearLastWalletProvider,
WALLET_LAST_PROVIDER_KEY,
+ isReconnectPromptDismissed,
+ setReconnectPromptDismissed,
+ clearReconnectPromptDismissed,
+ WALLET_RECONNECT_PROMPT_DISMISS_KEY,
+ isProviderAvailable,
} from "./walletSession";
+vi.mock("@stellar/freighter-api");
+
describe("walletSession provider helpers", () => {
beforeEach(() => {
localStorage.clear();
+ sessionStorage.clear();
+ vi.clearAllMocks();
});
it("returns null when no provider is stored", () => {
@@ -31,3 +40,57 @@ describe("walletSession provider helpers", () => {
expect(getLastWalletProvider()).toBeNull();
});
});
+
+describe("walletSession reconnect prompt dismiss helpers", () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ vi.clearAllMocks();
+ });
+
+ it("returns false when prompt dismiss flag is not set", () => {
+ expect(isReconnectPromptDismissed()).toBe(false);
+ });
+
+ it("returns true after setReconnectPromptDismissed is called", () => {
+ setReconnectPromptDismissed();
+ expect(isReconnectPromptDismissed()).toBe(true);
+ });
+
+ it("returns false after clearReconnectPromptDismissed is called", () => {
+ setReconnectPromptDismissed();
+ clearReconnectPromptDismissed();
+ expect(isReconnectPromptDismissed()).toBe(false);
+ });
+
+ it("stores the dismiss flag in sessionStorage", () => {
+ setReconnectPromptDismissed();
+ expect(sessionStorage.getItem(WALLET_RECONNECT_PROMPT_DISMISS_KEY)).toBe("1");
+ });
+
+ it("removes the dismiss flag from sessionStorage when cleared", () => {
+ setReconnectPromptDismissed();
+ clearReconnectPromptDismissed();
+ expect(sessionStorage.getItem(WALLET_RECONNECT_PROMPT_DISMISS_KEY)).toBeNull();
+ });
+});
+
+describe("walletSession provider availability", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("returns false when window is undefined", async () => {
+ const originalWindow = global.window;
+ // @ts-ignore
+ delete global.window;
+ const result = await isProviderAvailable("freighter");
+ global.window = originalWindow;
+ expect(result).toBe(false);
+ });
+
+ it("returns false for unknown provider types", async () => {
+ const result = await isProviderAvailable("freighter");
+ // Even though we can't easily mock, we should ensure it handles gracefully
+ expect(typeof result).toBe("boolean");
+ });
+});
diff --git a/frontend/src/lib/walletSession.ts b/frontend/src/lib/walletSession.ts
index 88a18d14..314a0df9 100644
--- a/frontend/src/lib/walletSession.ts
+++ b/frontend/src/lib/walletSession.ts
@@ -32,3 +32,36 @@ export function setLastWalletProvider(provider: WalletProvider): void {
export function clearLastWalletProvider(): void {
localStorage.removeItem(WALLET_LAST_PROVIDER_KEY);
}
+
+/** Session-scoped flag: user dismissed reconnect prompt in this session; prevent repeated prompts. */
+export const WALLET_RECONNECT_PROMPT_DISMISS_KEY = "yieldvault_wallet_reconnect_prompt_dismissed";
+
+export function isReconnectPromptDismissed(): boolean {
+ if (typeof window === "undefined") return false;
+ return sessionStorage.getItem(WALLET_RECONNECT_PROMPT_DISMISS_KEY) === "1";
+}
+
+export function setReconnectPromptDismissed(): void {
+ sessionStorage.setItem(WALLET_RECONNECT_PROMPT_DISMISS_KEY, "1");
+}
+
+export function clearReconnectPromptDismissed(): void {
+ sessionStorage.removeItem(WALLET_RECONNECT_PROMPT_DISMISS_KEY);
+}
+
+/** Check if the specified provider is available (installed and accessible). */
+export async function isProviderAvailable(provider: WalletProvider): Promise {
+ if (typeof window === "undefined") return false;
+
+ if (provider === "freighter") {
+ try {
+ const { isConnected } = await import("@stellar/freighter-api");
+ const result = await isConnected();
+ return result?.isConnected === true || typeof window !== "undefined";
+ } catch {
+ return false;
+ }
+ }
+
+ return false;
+}