Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions apps/web/src/domains/nudges/nudge-store.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
/**
* Zustand store for GitHub + Discord nudge prefs.
*
* Replaces the hand-rolled `useSyncExternalStore` + per-key
* subscribe/snapshot caches that previously lived in `github-prefs.ts`
* and `discord-prefs.ts`. Both pref modules now read/write through this
* store; their public hooks (`useGitHubNudgeState`, `useDiscordNudgeState`)
* remain as thin selector wrappers so consumers don't change.
* Owns whether each nudge has been actioned (starred, joined) or
* dismissed (banner, sidebar) and when. `github-prefs.ts` and
* `discord-prefs.ts` expose thin selector hooks (`useGitHubNudgeState`,
* `useDiscordNudgeState`) backed by this store.
*
* **Storage model:**
*
* - The persist middleware serialises the whole nudge slice into a
* single localStorage key, `vellum:nudge-prefs`.
* - On first load (no `vellum:nudge-prefs` key present), the initial
* state is seeded from the legacy per-key entries the old modules
* wrote (`app.githubNudge.starred`, `app.discordNudge.joined`, etc.),
* so users carrying over from the platform deployment keep their
* dismissals.
* state is seeded from per-key boolean/number entries under
* `app.githubNudge.*` / `app.discordNudge.*` so a returning user
* keeps their dismissals.
* - Cross-tab updates: the persist middleware doesn't sync across tabs
* on its own. We listen for `storage` events on `vellum:nudge-prefs`
* and call `persist.rehydrate()` to pull in the other tab's write.
Expand Down Expand Up @@ -78,7 +76,7 @@ export interface NudgeActions {
export type NudgeStore = NudgeState & NudgeActions;

// ---------------------------------------------------------------------------
// Initial state — hydrate from legacy per-key localStorage entries
// Initial state — seeded from per-key localStorage entries on first load
// ---------------------------------------------------------------------------

function computeInitialFromLegacy(): NudgeState {
Expand Down
17 changes: 9 additions & 8 deletions apps/web/src/domains/onboarding/onboarding-store.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* Zustand store for onboarding boolean preferences.
*
* Replaces the hand-rolled `useSyncExternalStore` + per-key listener
* boilerplate that previously lived in `prefs.ts`. Public hooks
* (`useShareAnalytics`, `useShareDiagnostics`, `useTosAccepted`,
* `useAiDataConsent`, `useOnboardingCompleted`) remain in `prefs.ts`
* as thin wrappers around `.use.field()` selectors + setter actions.
* Owns the five onboarding/privacy flags consumed by the privacy page,
* the onboarding pages, the chat-gate, and Sentry. `prefs.ts` exposes
* thin hooks (`useShareAnalytics`, `useShareDiagnostics`,
* `useTosAccepted`, `useAiDataConsent`, `useOnboardingCompleted`) that
* wrap `.use.field()` selectors and setter actions on this store.
*
* **Storage model — strict per-key, with absence semantics preserved:**
*
* Each field maps 1:1 to its existing localStorage key:
* Each field maps 1:1 to its own localStorage key:
*
* | Field | localStorage key | Read by |
* |-------------------|-------------------------------|------------------------|
Expand All @@ -29,8 +29,9 @@
* privacy-safe default and ANY explicit `"true"` as opt-in.
*
* Instead, each setter writes only its own key via `setLocalSetting`,
* preserving the original per-key write semantics. Initial state is
* read once on module load via `computeInitialFromLS()`.
* so a field that was never explicitly set stays absent in localStorage
* — keeping the privacy-safe default intact. Initial state is read once
* on module load via `computeInitialFromLS()`.
*
* **Cross-tab + cross-surface sync:**
*
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/stores/auth-store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/**
* Zustand auth store.
*
* Replaces the React Context-based AuthProvider with a store that can be
* read from anywhere — middleware, loaders, API interceptors — via
* `useAuthStore.getState()`.
*
* Session lifecycle: probes the allauth session on `initSession()`,
* re-validates on window focus / visibility change, and synchronizes
* logout across tabs via BroadcastChannel.
* logout across tabs via BroadcastChannel. Middleware, loaders, and
* API interceptors read state synchronously via
* `useAuthStore.getState()`.
*
* References:
* - https://zustand.docs.pmnd.rs/guides/reading-and-writing-state-outside-components
Expand Down
17 changes: 7 additions & 10 deletions apps/web/src/stores/organization-store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/**
* Zustand organization store.
*
* Replaces the React Context-based OrganizationProvider with a store
* readable from anywhere — middleware, loaders, API interceptors — via
* `useOrganizationStore.getState()`.
*
* Fetches the organization list via the generated SDK client (not React
* Query) so the store is self-contained and usable outside the React tree.
* Persists the active organization to sessionStorage so page refreshes
* Owns the organization list and the active-organization selection.
* Fetches the list via the generated SDK client (not React Query) so
* middleware, loaders, and API interceptors can read state synchronously
* via `useOrganizationStore.getState()` outside the React tree. The
* active organization is persisted to sessionStorage so page refreshes
* and new tabs preserve the selection.
*
* Lifecycle (auth subscription + focus/visibility refetch) is registered
Expand Down Expand Up @@ -176,9 +174,8 @@ export function clearOrganization(): void {
* 1. Auth subscription — fetches orgs when the user logs in or switches
* accounts (including cross-tab via BroadcastChannel).
* 2. Window focus / visibility — refetches the org list when the tab
* regains focus, but only if data is stale (older than STALE_TIME_MS).
* Matches the old React Query `refetchOnWindowFocus` + `staleTime`
* behavior from `createQueryClient({ staleTime: 10_000 })`.
* regains focus, but only if data is older than STALE_TIME_MS so
* rapid focus events on fresh data don't trigger redundant fetches.
*/
export function setupOrganizationStore(): () => void {
const STALE_TIME_MS = 10_000;
Expand Down