Skip to content

Commit ece5971

Browse files
committed
Move post-unlock navigation from SessionLockListener to UnlockAccount
The SessionLockListener was navigating to /account on SESSION_UNLOCKED from the /unlock-account route. Because runtime.sendMessage broadcasts to every extension context — including the popup that initiated the unlock via confirmPassword — this navigation raced the UnlockAccount post-submit navigation. In the grant-access flow that meant the popup sometimes landed on /account instead of the /grant-access destination preserved via state.from, which broke the freighterApiIntegration.test.ts "should get public key when logged out" e2e test. Move navigation responsibility to UnlockAccount itself: it watches hasPrivateKey in redux and navigates to from || /account on the false → true transition. This unifies both surfaces — the one that submitted the password and the passive surface that received the cross-surface SESSION_UNLOCKED broadcast — through a single code path and removes the race. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2013aad commit ece5971

3 files changed

Lines changed: 33 additions & 22 deletions

File tree

extension/src/popup/components/SessionLockListener/index.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,20 @@ export const SessionLockListener = () => {
7272
return undefined;
7373
}
7474

75+
// SESSION_UNLOCKED. Refresh this surface's auth state so passive
76+
// surfaces (e.g. sidebar parked on /unlock-account) reflect the
77+
// unlocked wallet. We deliberately do NOT navigate here:
78+
// `runtime.sendMessage` is broadcast by the background to every
79+
// extension context including the popup that *initiated* the
80+
// unlock via `confirmPassword`, so navigating from this listener
81+
// would race the unlock view's own post-submit navigation (and
82+
// in the grant-access flow would land the user on /account
83+
// instead of /grant-access). The unlock screens watch
84+
// `hasPrivateKey` and navigate themselves once auth state
85+
// flips, which covers both the active and passive surfaces.
7586
void (async () => {
7687
const account = await loadAccount();
7788
dispatch(saveAccount(account));
78-
if (
79-
location.pathname === ROUTES.unlockAccount ||
80-
location.pathname === ROUTES.verifyAccount
81-
) {
82-
navigate(ROUTES.account, { replace: true });
83-
}
8489
})();
8590
return undefined;
8691
};

extension/src/popup/components/__tests__/SessionLockListener.test.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,12 @@ describe("SessionLockListener", () => {
168168
expect(auth.allAccounts).toEqual([{ publicKey: "GBUPDATED" }]);
169169
});
170170

171-
it("navigates to account from unlockAccount on SESSION_UNLOCKED", async () => {
171+
it("does not navigate from any route on SESSION_UNLOCKED (unlock views navigate themselves via redux state)", async () => {
172172
renderListener(ROUTES.unlockAccount);
173173

174174
await emitMessage({ type: SERVICE_TYPES.SESSION_UNLOCKED });
175175

176-
expect(lastLocation?.pathname).toBe(ROUTES.account);
177-
});
178-
179-
it("navigates to account from verifyAccount on SESSION_UNLOCKED", async () => {
180-
renderListener(ROUTES.verifyAccount);
181-
182-
await emitMessage({ type: SERVICE_TYPES.SESSION_UNLOCKED });
183-
184-
expect(lastLocation?.pathname).toBe(ROUTES.account);
176+
expect(lastLocation?.pathname).toBe(ROUTES.unlockAccount);
185177
});
186178

187179
it("does not navigate from a non-lock route on SESSION_UNLOCKED", async () => {

extension/src/popup/views/UnlockAccount/index.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Button } from "@stellar/design-system";
22
import get from "lodash/get";
3-
import React, { useEffect, useState } from "react";
3+
import React, { useEffect, useRef, useState } from "react";
44
import { useTranslation } from "react-i18next";
5-
import { useDispatch } from "react-redux";
5+
import { useDispatch, useSelector } from "react-redux";
66
import { useLocation, useNavigate } from "react-router-dom";
77

88
import { newTabHref } from "helpers/urls";
@@ -12,6 +12,7 @@ import { openTab } from "popup/helpers/navigate";
1212
import { View } from "popup/basics/layout/View";
1313
import {
1414
confirmPassword,
15+
hasPrivateKeySelector,
1516
loadLastUsedAccount,
1617
} from "popup/ducks/accountServices";
1718
import { EnterPassword } from "popup/components/EnterPassword";
@@ -30,14 +31,27 @@ export const UnlockAccount = () => {
3031
const [accountAddress, setAccountAddress] = useState("");
3132

3233
const dispatch = useDispatch<AppDispatch>();
34+
const hasPrivateKey = useSelector(hasPrivateKeySelector);
35+
// Track the initial auth state so we don't auto-navigate if the
36+
// component happens to mount while the wallet is already unlocked
37+
// (e.g. a stale `/unlock-account` route). We only redirect on the
38+
// false → true transition that signals an unlock just happened
39+
// (either via this surface's password submit or via a cross-surface
40+
// SESSION_UNLOCKED broadcast saving fresh auth state).
41+
const wasLockedOnMount = useRef(!hasPrivateKey);
3342

3443
const handleSubmit = async (password: string) => {
35-
const res = await dispatch(confirmPassword(password));
36-
if (confirmPassword.fulfilled.match(res) && res.payload.publicKey) {
37-
// skip this location in history, we won't need to come back here after unlocking account
44+
await dispatch(confirmPassword(password));
45+
// Navigation is handled by the `hasPrivateKey` effect below so
46+
// that both password-submit and cross-surface SESSION_UNLOCKED
47+
// broadcasts converge on the same destination.
48+
};
49+
50+
useEffect(() => {
51+
if (wasLockedOnMount.current && hasPrivateKey) {
3852
navigate(`${destination}${queryParams}`, { replace: true });
3953
}
40-
};
54+
}, [hasPrivateKey, destination, queryParams, navigate]);
4155

4256
useEffect(() => {
4357
const fetchLastUsedAccount = async () => {

0 commit comments

Comments
 (0)