Skip to content
Draft
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
3 changes: 1 addition & 2 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
29 changes: 21 additions & 8 deletions src/lib/features/auth/state/user-state.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ export class UserState {
#userCreditLogs = $state<UserCreditLogs | undefined | null>(undefined);
#userCreditBalance = $state<UserCreditBalance | undefined | null>(undefined);

#client: SupabaseClient<Database> | undefined;
#unsub: any;

constructor(supabaseClient: SupabaseClient<Database> | 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<Database> | 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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -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;
}
9 changes: 8 additions & 1 deletion src/routes/(app)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,21 @@
};

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;
});
</script>

<ModeWatcher />
<Toaster />

<!-- Hide the app if the user was not welcomed yet (prevents flickering between state changes) -->
<!-- TODO userDocState.doc.checklist.welcome === false -->
{#if !userState.isComplete}
{#if !hasLoadedOnce}
<LoadingSplash />
{:else}
<!-- {#if openChat}
Expand Down
16 changes: 11 additions & 5 deletions src/routes/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ export const load: LayoutLoad = async ({ fetch, depends }) => {
return { supabase: null, claims: null };
}

const sb = createBrowserClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
global: {
fetch
}
});
// Reuse the existing client across load re-runs. A token refresh calls
// `invalidate('supabase:auth')`, which re-runs this load; building a new
// client each time would reset the `supabase.client` singleton and the
// user state that depends on it, blanking the app behind the splash.
const sb =
supabase.client ??
createBrowserClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, {
global: {
fetch
}
});

// The singleton is browser-only: it's created once per page on the client.
supabase.client = sb;
Expand Down