fix(auth): a Google connect names its tab's account too, and a refused tab says why
- /auth/google/connect is a navigation and carries no headers: the tab names the account it was built for in `expect`, checked by the same comparison as the request header (built_for_another_account); a mismatch lands on /?account=changed - the link marker names the OAuth state of its own flow: an older flow coming back no longer spends the marker of the connect still open (which then returned as a plain sign-in) - a 401 account_changed reloads the tab with a one-time flag, and the reloaded tab says it now shows the account signed in - a refused switch to a suspended or not-yet-approved account says so in the signed-in app's words, naming the address a request was recorded for - the prefs merge locks FOR NO KEY UPDATE, so a transaction that only added a row referring to the account does not hold the save up; a test pins the lock - giving up a lost page closes a channel page open over it - e2e README names the chromium-only specs
This commit is contained in:
@@ -370,6 +370,20 @@ describe("NavigationProvider · a page the account cannot open", () => {
|
||||
await settle();
|
||||
expect(result.current.page).toBe("feed");
|
||||
});
|
||||
|
||||
it("closes a channel page open over the lost page, on screen and in the entry", async () => {
|
||||
window.history.replaceState({ sfPage: "users" }, "", "/?page=users");
|
||||
const { result } = render();
|
||||
act(() => result.current.openChannel("UC1", "One"));
|
||||
expect(window.location.search).toContain("chan=UC1");
|
||||
act(() => {
|
||||
qc.setQueryData(qk.me(), member);
|
||||
});
|
||||
await settle();
|
||||
expect(result.current.channelView).toBeNull();
|
||||
expect(window.location.search).toBe("");
|
||||
expect(window.history.state?._chan).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("NavigationProvider · leaving a sub-view whose entry is the oldest of this document", () => {
|
||||
|
||||
@@ -67,16 +67,6 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
// and the address bar (stamped from it below) agree.
|
||||
const me = useMe();
|
||||
const page = reachablePage(chosenPage, me);
|
||||
// A page this account can no longer open (a role taken away, a module switched off mid-session)
|
||||
// is given up, not kept behind the feed: kept, the address bar would go on naming it, a click on
|
||||
// Feed would be a no-op (setPage's same-page guard sees the feed already on screen), and a later
|
||||
// grant would switch the page by itself. Adjusted during render; the history entry and the
|
||||
// remembered page follow in the effect below.
|
||||
const [givenUp, setGivenUp] = useState(0);
|
||||
if (page !== chosenPage) {
|
||||
setPageState(page);
|
||||
setGivenUp((n) => n + 1);
|
||||
}
|
||||
// Ephemeral: not persisted and not in the URL, so a reload returns to the normal feed (a search
|
||||
// spends quota, so we never auto-replay it) — UNLESS it was zero-quota scrape-sourced, which rides
|
||||
// in history.state and is free to re-fetch. The popstate handler derives it from history.state so
|
||||
@@ -92,6 +82,18 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
const [channelView, setChannelView] = useState<ChannelView | null>(() =>
|
||||
channelViewFrom(window.history.state, window.location.search),
|
||||
);
|
||||
// A page this account can no longer open (a role taken away, a module switched off mid-session)
|
||||
// is given up, not kept behind the feed: kept, the address bar would go on naming it, a click on
|
||||
// Feed would be a no-op (setPage's same-page guard sees the feed already on screen), and a later
|
||||
// grant would switch the page by itself. A channel page open over it closes with it, as on any
|
||||
// page switch. Adjusted during render; the history entry and the remembered page follow in the
|
||||
// effect below.
|
||||
const [givenUp, setGivenUp] = useState(0);
|
||||
if (page !== chosenPage) {
|
||||
setPageState(page);
|
||||
setChannelView(null);
|
||||
setGivenUp((n) => n + 1);
|
||||
}
|
||||
|
||||
// Latest-value refs so the STABLE action callbacks can read the latest state without depending on
|
||||
// it — a dep would recreate them on every navigation and re-render every actions consumer (e.g.
|
||||
@@ -211,12 +213,14 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
}, [signedIn, page, objectTitle, i18n.language, t]);
|
||||
|
||||
// The page given up above is the feed, and the current entry says so (replaced, not pushed: no
|
||||
// step was taken). Its own keys go with it, as on any page switch.
|
||||
// step was taken). Its own keys go with it, and the channel page closed with it, as on any page
|
||||
// switch — left in the entry, a reload would reopen what the screen no longer shows.
|
||||
useEffect(() => {
|
||||
if (!givenUp) return;
|
||||
setAccountRaw(PAGE_KEY, "feed");
|
||||
const { _chan: _closedChan, _chanName: _closedName, ...rest } = window.history.state ?? {};
|
||||
window.history.replaceState(
|
||||
{ ...window.history.state, sfPage: "feed" },
|
||||
{ ...rest, sfPage: "feed" },
|
||||
"",
|
||||
urlWithParams(pageRootParams("feed")),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user