import { useEffect, useState } from 'react' import { api, onUnauthorized, type Me } from '../api/client' import { setPrefsScope } from '../lib/prefs' import { setPackLang } from '../i18n' // useSession tracks who is writing, and notices the moment the server stops // recognising them. // // `signedOut` going true is not an error state to report — it's a state to // recover from: the app stops auto-saving, keeps the draft, and shows a warm // invitation to sign in again. Every API call routes its 401 here through the // client's single interceptor, so it fires once no matter which call noticed. export function useSession() { const [me, setMe] = useState(null) const [signedOut, setSignedOut] = useState(false) useEffect(() => { onUnauthorized(() => setSignedOut(true)) let cancelled = false api .me() .then((user) => { if (cancelled) return // Browser preferences (mute, petals, companion) belong to the writer, // not the machine. This is the moment their storage keys can stop being // shared — and the first account on this browser inherits whatever was // set back when Petal had no accounts at all. setPrefsScope(user.id) // …and so does the language Petal speaks back. Until this point the app // renders the default pack; a writer on another pair sees her own copy // from here on, without a reload. setPackLang(user.pair_lang) setMe(user) }) .catch(() => { // A 401 has already flipped signedOut through the interceptor; anything // else (the server briefly down) leaves `me` null, which only costs the // display name. }) return () => { cancelled = true } }, []) // Turn the pair around. The account is the source of truth for which // direction the editor is in — it decides whether the word list loads at all — // so the state moves only once the server has agreed, and it moves to what the // server *stored* rather than to what was asked for. const setDirection = async (direction: string) => { const updated = await api.setDirection(direction) setMe(updated) } // Move the pair, naming the direction with it. The two are validated together // server-side, so an account that is learning Chinese cannot change pair by // sending `pair_lang` alone — the combination it would ask for (French with // segmentation) does not exist and is refused. Saying both is how that move is // made, and routing it through here rather than through the picker's own // `api` call is what keeps `me.direction` — which decides whether the word // list stays loaded — in step with what was actually stored. const setPair = async (lang: string, direction: string) => { const updated = await api.setPair(lang, direction) setPackLang(updated.pair_lang) setMe(updated) } return { me, signedOut, setDirection, setPair } }