From 3997c535172894582609152485047a2b23cb5072 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:50:37 +0000 Subject: [PATCH] fix(auth): keep app mounted across Supabase token refresh A token refresh (or a tab refocus) calls invalidate('supabase:auth'), which re-runs the root load. The load built a new browser client and reassigned the `supabase.client` $state singleton on every run. Because the user state was `$derived(new UserState(supabase.client))`, that swap rebuilt an empty UserState, so `isComplete` turned false and the whole app hid behind LoadingSplash for several seconds. Three layers now break that chain: - +layout.ts reuses the existing browser client instead of rebuilding it. - UserState is a real singleton; setClient() rebinds a new client and keeps the already loaded user data. - The (app) layout gates the splash on a first-load latch, so a brief drop in isComplete no longer blanks the app. Also fix hooks.client.ts importing `dev` from the non-existent $app/env module (the value lives in $app/environment). Generated-By: PostHog Desktop Task-Id: 4bf014fe-cf36-45a4-95a0-498a8bfd4f70 --- src/hooks.client.ts | 3 +- .../features/auth/state/user-state.svelte.ts | 29 ++++++++++++++----- src/routes/(app)/+layout.svelte | 9 +++++- src/routes/+layout.ts | 16 ++++++---- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 8a9ce27..f261645 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,5 +1,4 @@ -import { dev } from '$app/env'; -import { version } from '$app/environment'; +import { dev, version } from '$app/environment'; import { env } from '$env/dynamic/public'; import type { HandleClientError } from '@sveltejs/kit'; import posthog from 'posthog-js'; diff --git a/src/lib/features/auth/state/user-state.svelte.ts b/src/lib/features/auth/state/user-state.svelte.ts index 908e737..da9fae9 100644 --- a/src/lib/features/auth/state/user-state.svelte.ts +++ b/src/lib/features/auth/state/user-state.svelte.ts @@ -13,10 +13,20 @@ export class UserState { #userCreditLogs = $state(undefined); #userCreditBalance = $state(undefined); + #client: SupabaseClient | undefined; #unsub: any; constructor(supabaseClient: SupabaseClient | undefined) { - if (!supabaseClient?.auth) return; + this.setClient(supabaseClient); + } + + // Bind to a Supabase client. Keeps the loaded user data, so a client swap + // (for example a token refresh) does not reset the state to empty. + setClient(supabaseClient: SupabaseClient | undefined) { + if (!supabaseClient?.auth || supabaseClient === this.#client) return; + + this.#unsub?.subscription.unsubscribe(); + this.#client = supabaseClient; const { data } = supabaseClient.auth.onAuthStateChange((event, session) => { // console.log('Supabase auth state changed:', event, session); @@ -85,11 +95,7 @@ export class UserState { } get isComplete() { - return ( - this.#userState && - this.#userPublicProfile && - this.#userPreferences - ); // Neither null nor undefined + return this.#userState && this.#userPublicProfile && this.#userPreferences; // Neither null nor undefined } stopListening() { @@ -103,7 +109,9 @@ export class UserState { this.#userPublicProfile = (await getUserPublicProfile(this.#userState.id)).profile; // Refresh user preferences - this.#userPreferences = (await getUserPreferences(supabase.client, this.#userState.id)).preferences; + this.#userPreferences = ( + await getUserPreferences(supabase.client, this.#userState.id) + ).preferences; // Refresh user credit logs this.#userCreditLogs = (await getUserCreditLogs(this.#userState.id)).logs; @@ -115,8 +123,13 @@ export class UserState { } } -const currentUserState = $derived(new UserState(supabase.client)); +let currentUserState: UserState | undefined; export function getUserState() { + if (!currentUserState) { + currentUserState = new UserState(supabase.client); + } else { + currentUserState.setClient(supabase.client); + } return currentUserState; } diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 2a48d56..7faef68 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -52,6 +52,13 @@ }; let openChat = $state(false); + + // Latch once the first load finishes. Later refreshes may drop `isComplete` + // for a moment; without this latch the whole app would flash the splash. + let hasLoadedOnce = $state(false); + $effect(() => { + if (userState.isComplete) hasLoadedOnce = true; + }); @@ -59,7 +66,7 @@ -{#if !userState.isComplete} +{#if !hasLoadedOnce} {:else}