diff --git a/src/components/InstallFreighterModal.tsx b/src/components/InstallFreighterModal.tsx
index d6d8c972..36b99d05 100644
--- a/src/components/InstallFreighterModal.tsx
+++ b/src/components/InstallFreighterModal.tsx
@@ -11,6 +11,11 @@ interface InstallFreighterModalProps {
* than imported so this modal stays free of wallet-provider dependencies.
*/
socialLogin?: ReactNode;
+ /**
+ * When true, the modal explains that Freighter is locked (password required)
+ * rather than asking the user to install it.
+ */
+ isLocked?: boolean;
}
function detectBrowser(): { name: string; supported: boolean } {
@@ -28,6 +33,7 @@ export default function InstallFreighterModal({
onClose,
onRetry,
socialLogin,
+ isLocked = false,
}: InstallFreighterModalProps) {
const browser = useMemo(
() => (isOpen ? detectBrowser() : { name: "", supported: false }),
@@ -53,13 +59,19 @@ export default function InstallFreighterModal({
- {socialLogin ? "Connect a wallet" : "Freighter Wallet Required"}
+ {socialLogin
+ ? "Connect a wallet"
+ : isLocked
+ ? "Freighter Wallet Locked"
+ : "Freighter Wallet Required"}
{socialLogin
? "Install the Freighter extension, or sign in with a social account and we will create a Stellar wallet for you."
- : "This app requires the Freighter browser extension to interact with the Stellar network."}
+ : isLocked
+ ? "Open the Freighter extension and unlock it with your password, then try again."
+ : "This app requires the Freighter browser extension to interact with the Stellar network."}
{!browser.supported && browser.name && (
@@ -70,21 +82,32 @@ export default function InstallFreighterModal({
)}
-
- Install Freighter
-
+ {isLocked ? (
+
+ Freighter is locked. Open the extension in your browser toolbar, enter your
+ password, and then click the button below.
+
+ ) : (
+
+ Install Freighter
+
+ )}
- {checking ? "Checking..." : "I installed it — check again"}
+ {checking
+ ? "Checking..."
+ : isLocked
+ ? "I unlocked it — check again"
+ : "I installed it — check again"}
{socialLogin}
diff --git a/src/components/WalletContext.tsx b/src/components/WalletContext.tsx
index 5bde1dde..19a577ce 100644
--- a/src/components/WalletContext.tsx
+++ b/src/components/WalletContext.tsx
@@ -25,6 +25,7 @@ import {
type SocialLoginProvider,
type SocialWalletSession,
} from "@/lib/socialWallet";
+import { isFreighterLockedError } from "@/utils/freighterErrors";
import InstallFreighterModal from "./InstallFreighterModal";
import SocialLoginButtons from "./SocialLoginButtons";
@@ -77,6 +78,7 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
const [isWalletConnected, setIsWalletConnected] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
+ const [isFreighterLocked, setIsFreighterLocked] = useState(false);
const [walletNetworkWarning, setWalletNetworkWarning] = useState
(null);
const [walletKind, setWalletKind] = useState(null);
const [socialProfile, setSocialProfile] = useState(null);
@@ -296,6 +298,12 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
return;
}
const key = await getAddress();
+ if (key.error && isFreighterLockedError(key.error)) {
+ setIsFreighterLocked(true);
+ setShowInstallPrompt(true);
+ setIsLoading(false);
+ return;
+ }
const network = await getNetwork();
if ((network.networkPassphrase || "") !== appNetworkPassphrase) {
const warning = `Switch Freighter to ${appNetworkLabel} to continue. Current wallet network does not match the app network.`;
@@ -316,12 +324,17 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
setWalletKind("freighter");
localStorage.setItem("stellar_wallet_public_key", key.address);
showSuccess("Wallet connected successfully.");
- } catch {
+ } catch (error) {
setPublicKey(null);
setIsWalletConnected(false);
setWalletKind(null);
setWalletNetworkWarning(null);
- showError("Failed to connect wallet. Please try again.");
+ if (isFreighterLockedError(error)) {
+ setIsFreighterLocked(true);
+ setShowInstallPrompt(true);
+ } else {
+ showError("Failed to connect wallet. Please try again.");
+ }
} finally {
setIsLoading(false);
}
@@ -362,11 +375,9 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
};
const handleRetryInstall = async () => {
- const installed = await isFreighterInstalled();
- if (installed) {
- setShowInstallPrompt(false);
- connectWallet();
- }
+ setShowInstallPrompt(false);
+ setIsFreighterLocked(false);
+ await connectWallet();
};
const disconnectWallet = () => {
@@ -432,8 +443,12 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => {
{children}
setShowInstallPrompt(false)}
+ onClose={() => {
+ setShowInstallPrompt(false);
+ setIsFreighterLocked(false);
+ }}
onRetry={handleRetryInstall}
+ isLocked={isFreighterLocked}
socialLogin={
isSocialLoginConfigured() ? (
{
expect(screen.getByTestId("isLoading")).toHaveTextContent("false");
});
+ it("connectWallet - freighter locked", async () => {
+ mockIsConnected.mockResolvedValue({ isConnected: true });
+ mockIsAllowed.mockResolvedValue({ isAllowed: true });
+ mockGetAddress.mockResolvedValue({ error: { message: "Freighter is locked" } });
+
+ renderWithProviders(
+
+
+ ,
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Connect"));
+ });
+
+ expect(screen.getByText("Freighter Wallet Locked")).toBeInTheDocument();
+ expect(screen.queryByText("Freighter Wallet Required")).not.toBeInTheDocument();
+ expect(screen.getByText(/unlock it with your password/)).toBeInTheDocument();
+ expect(screen.getByTestId("isLoading")).toHaveTextContent("false");
+ });
+
it("disconnectWallet", async () => {
// Start with a connected wallet
mockIsConnected.mockResolvedValue({ isConnected: true });
diff --git a/src/utils/freighterErrors.ts b/src/utils/freighterErrors.ts
index 139be811..ec79e656 100644
--- a/src/utils/freighterErrors.ts
+++ b/src/utils/freighterErrors.ts
@@ -1,9 +1,3 @@
-/**
- * Detects whether an error from Freighter's signTransaction represents
- * the user rejecting/cancelling the signature request, rather than a
- * genuine technical failure.
- */
-
export class UserCancelledError extends Error {
constructor() {
super("Transaction cancelled");
@@ -36,3 +30,24 @@ export function wrapFreighterError(error: unknown): never {
}
throw error;
}
+
+export class FreighterLockedError extends Error {
+ constructor() {
+ super("Freighter is locked");
+ this.name = "FreighterLockedError";
+ }
+}
+
+const LOCKED_PATTERNS = [
+ "freighter is locked",
+ "wallet is locked",
+ "unlock your wallet",
+ "extension is locked",
+];
+
+export function isFreighterLockedError(error: unknown): boolean {
+ if (!error) return false;
+ const msg = typeof error === "string" ? error : ((error as Error).message ?? "");
+ const lower = msg.toLowerCase();
+ return LOCKED_PATTERNS.some((p) => lower.includes(p));
+}