web: the session's photo waits for the account before it opens — a PRO clicking a RAW in the LIBRARY was read as a guest, asked to sign in, and the RAW never opened
Both halves of the boot ran in one effect: `api.me()` and the photo handover, with the handover not waiting for the answer. Which file may open is a tier question — `loadFile` turns a RAW away unless the account is PRO — and the tier came from the render that effect closed over, which was the FIRST one, where `user` is still null. So a PRO's own RAW was read as a guest's, `promptPro` raised the sign-in modal, and because the handover had already cleared `?lib=` out of the URL with `history.replaceState`, there was nothing left to retry: signing in landed on an empty workspace, with the frame the visitor clicked sitting in the library behind it. The account and a new `authReady` flag now land in ONE batch, and the handover is an effect of its own that returns until the flag is set. Both the batch and the wait matter: one setState per render is what lets the handover effect see a render that already carries `user`, and the flag is what says so. Checked against a stubbed `api.me()` answering after 800ms (scratchpad bug1-probe.mjs, a PRO opening `/app?lib=probe-frame`): on the old bundle the `?lib=` is gone at the first sample, 765ms BEFORE the answer, and the session photo opens on the wrong tier; here it survives until 193ms AFTER the answer. The catalogue itself cannot be stood up in a check — its frames are real FileSystemFileHandles in IndexedDB — so the RAW actually opening is a manual step on a real catalogue. Skipped: a GUEST who clicks a RAW still loses it across the sign-in — the handover has run by then and the `?lib=` is gone. Add when someone reports it; the tier that can open a RAW is the operator's own account, which is the case this fixes.
This commit is contained in:
@@ -545,6 +545,11 @@ export function Workspace() {
|
||||
const [choosingExport, setChoosingExport] = useState(false);
|
||||
const [savingPhoto, setSavingPhoto] = useState(false);
|
||||
const [user, setUser] = useState<User | null>(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=<id>`) 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=<id>`) 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
|
||||
|
||||
Reference in New Issue
Block a user