From bf18af4708f90657e47a433e6cad875a23a4861c Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+frooodle@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:25:19 +0100 Subject: [PATCH] fix: show user avatar on the home page settings button The bottom-left settings button and the settings page both read profilePictureUrl, but only the settings page had a fallback (initials avatar) - the button silently fell back to a gear. The URL itself was usually null because fetchProfilePicture raced the background OAuth avatar sync with a fixed 500ms delay and never retried, and a missing bucket object simply resolved to null. - useConfigButtonIcon: fall back to the same initials avatar as the settings page instead of the gear when no picture URL is available. - UseSession: fetch the profile picture when syncOAuthAvatar settles (init and SIGNED_IN) instead of after an arbitrary 500ms. - fetchProfilePicture: when the bucket copy is missing, fall back to the OAuth provider's own photo URL so the picture shows immediately on first login - unless the user explicitly uploaded/removed a picture (metadata source 'upload'), preserving the remove flow. --- frontend/editor/src/saas/auth/UseSession.tsx | 67 +++++++++++++------ .../src/saas/hooks/useConfigButtonIcon.tsx | 14 +++- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/frontend/editor/src/saas/auth/UseSession.tsx b/frontend/editor/src/saas/auth/UseSession.tsx index c493df006..0fd02bb5f 100644 --- a/frontend/editor/src/saas/auth/UseSession.tsx +++ b/frontend/editor/src/saas/auth/UseSession.tsx @@ -27,6 +27,7 @@ import { synchronizeUserUpgrade } from "@app/services/userService"; import { syncOAuthAvatar, getProfilePictureMetadata, + getProviderAvatarUrl, type ProfilePictureMetadata, } from "@app/services/avatarSyncService"; @@ -321,6 +322,21 @@ export function AuthProvider({ children }: { children: ReactNode }) { await fetchTrialStatus(); }, [fetchTrialStatus]); + // Provider photo as interim fallback when the bucket copy is missing — + // skipped when the user explicitly chose upload/removal (source "upload"). + const providerAvatarFallback = useCallback( + async (user: SupabaseUser): Promise => { + try { + const metadata = await getProfilePictureMetadata(user.id); + if (metadata?.source === "upload") return null; + return getProviderAvatarUrl(user); + } catch { + return getProviderAvatarUrl(user); + } + }, + [], + ); + const fetchProfilePicture = useCallback( async (sessionToUse?: Session | null) => { const currentSession = sessionToUse ?? session; @@ -351,7 +367,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { "[Auth Debug] Profile picture not available:", error.message, ); - setProfilePictureUrl(null); + setProfilePictureUrl( + await providerAvatarFallback(currentSession.user), + ); } else { setProfilePictureUrl(data.signedUrl); console.debug( @@ -360,10 +378,10 @@ export function AuthProvider({ children }: { children: ReactNode }) { } } catch (error: unknown) { console.debug("[Auth Debug] Failed to fetch profile picture:", error); - setProfilePictureUrl(null); + setProfilePictureUrl(await providerAvatarFallback(currentSession.user)); } }, - [session], + [session, providerAvatarFallback], ); const refreshProfilePicture = useCallback(async () => { @@ -522,23 +540,22 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Fetch credits, pro status, trial status, profile picture metadata, and profile picture using the session from the response if (data.session?.user) { - // Sync OAuth avatar in background - syncOAuthAvatar(data.session.user).catch((err) => { - console.debug( - "[Auth Debug] Failed to sync OAuth avatar on init:", - err, - ); - }); + // Sync OAuth avatar in background; fetch the picture once the + // sync settles instead of guessing with a fixed delay. + syncOAuthAvatar(data.session.user) + .catch((err) => { + console.debug( + "[Auth Debug] Failed to sync OAuth avatar on init:", + err, + ); + return false; + }) + .then(() => fetchProfilePicture(data.session)); await fetchCredits(data.session); await fetchProStatus(data.session); await fetchTrialStatus(data.session); await fetchProfilePictureMetadata(data.session); - - // Small delay to allow avatar sync to complete if quick - setTimeout(() => { - fetchProfilePicture(data.session); - }, 500); } } } catch (err) { @@ -602,9 +619,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { // null/loading states. // Sync OAuth avatar in background (don't block other fetches) - syncOAuthAvatar(newSession.user).catch((err) => { - console.debug("[Auth Debug] Failed to sync OAuth avatar:", err); - }); + const avatarSync = syncOAuthAvatar(newSession.user).catch( + (err) => { + console.debug( + "[Auth Debug] Failed to sync OAuth avatar:", + err, + ); + return false; + }, + ); // Fetch user data in parallel Promise.all([ @@ -613,15 +636,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { fetchTrialStatus(newSession), fetchProfilePictureMetadata(newSession), ]).then(() => { - // Fetch profile picture AFTER sync has had time to complete - // Use a small delay to allow avatar sync to finish if it's quick - setTimeout(() => { + // Fetch the picture once the avatar sync settles rather than + // after a fixed 500ms the first-ever sync usually loses. + avatarSync.then(() => { fetchProfilePicture(newSession).finally(() => { console.debug( "[Auth Debug] User data fully loaded after sign in", ); }); - }, 500); + }); }); } } else if (event === "TOKEN_REFRESHED") { diff --git a/frontend/editor/src/saas/hooks/useConfigButtonIcon.tsx b/frontend/editor/src/saas/hooks/useConfigButtonIcon.tsx index 904eeb088..3d8761a27 100644 --- a/frontend/editor/src/saas/hooks/useConfigButtonIcon.tsx +++ b/frontend/editor/src/saas/hooks/useConfigButtonIcon.tsx @@ -2,8 +2,16 @@ import { Avatar } from "@mantine/core"; import { useAuth } from "@app/auth/UseSession"; export function useConfigButtonIcon(): React.ReactNode { - const { profilePictureUrl } = useAuth(); - return profilePictureUrl ? ( - + const { profilePictureUrl, user } = useAuth(); + if (profilePictureUrl) { + return ; + } + // Mirror the settings page fallback (initials avatar) so both spots show + // the same identity badge while the picture URL is unavailable. + const initial = user?.email?.trim()?.charAt(0)?.toUpperCase(); + return initial ? ( + + {initial} + ) : null; }