diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 75e9b17..2307da0 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -545,6 +545,11 @@ export function Workspace() { const [choosingExport, setChoosingExport] = useState(false); const [savingPhoto, setSavingPhoto] = useState(false); const [user, setUser] = useState(null); + // `api.me()` has answered — the account is known, guest or not. Opening a + // photo asks a tier question (see `loadFile`), so the session's photo is + // restored only once this is true; before that `user` is null and every + // account reads as a guest. + const [authReady, setAuthReady] = useState(false); // Three tiers, one account each (see config/tiers). Signed in is the basic // tier: recipes, the photo folder, a clean export of the photo it is editing. // PRO is the box the operator ticks in the admin users table — HSL, TOOLS, @@ -665,13 +670,32 @@ export function Workspace() { .catch((err) => alive && setError(String(err))); api .me() - .then((r) => alive && setUser(r.user)) - .catch(() => undefined); // signed out is a valid state — the demo needs no account - // ...and the photo half of the session, back out of IndexedDB. When that is - // empty but a RAW is still parked in OPFS, the RAW is what "the last photo" - // means: develop it again rather than open on nothing. - // A frame handed over by the catalogue (`/app?lib=`) wins over both: the - // visitor just clicked it, and it is newer than anything the session holds. + .then((r) => { + if (!alive) return; + // The account and the flag that says the account is known land in ONE + // batch: the handover effect below then sees a render that already + // carries `user`, so a PRO opening a RAW is not read as a guest. + setUser(r.user); + setAuthReady(true); + }) + .catch(() => alive && setAuthReady(true)); // signed out is a valid state — the demo needs no account + return () => { + alive = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // The photo half of the session, back out of IndexedDB. It waits for the + // account: which file may open depends on the tier, and the check reads it + // from the render — run it too early and a PRO's own RAW is turned away. + // When the stored session is empty but a RAW is still parked in OPFS, the RAW + // is what "the last photo" means: develop it again rather than open on + // nothing. A frame handed over by the catalogue (`/app?lib=`) wins over + // both: the visitor just clicked it, and it is newer than anything the + // session holds. + useEffect(() => { + if (!authReady) return; + let alive = true; const wanted = new URLSearchParams(window.location.search).get('lib'); if (wanted) openLibraryPhoto(wanted); else @@ -691,7 +715,7 @@ export function Workspace() { alive = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [authReady]); useEffect(() => { // The account's own two listings: the API answers a guest with a 401, and