mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
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.
This commit is contained in:
@@ -27,6 +27,7 @@ import { synchronizeUserUpgrade } from "@app/services/userService";
|
|||||||
import {
|
import {
|
||||||
syncOAuthAvatar,
|
syncOAuthAvatar,
|
||||||
getProfilePictureMetadata,
|
getProfilePictureMetadata,
|
||||||
|
getProviderAvatarUrl,
|
||||||
type ProfilePictureMetadata,
|
type ProfilePictureMetadata,
|
||||||
} from "@app/services/avatarSyncService";
|
} from "@app/services/avatarSyncService";
|
||||||
|
|
||||||
@@ -321,6 +322,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
await fetchTrialStatus();
|
await fetchTrialStatus();
|
||||||
}, [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<string | null> => {
|
||||||
|
try {
|
||||||
|
const metadata = await getProfilePictureMetadata(user.id);
|
||||||
|
if (metadata?.source === "upload") return null;
|
||||||
|
return getProviderAvatarUrl(user);
|
||||||
|
} catch {
|
||||||
|
return getProviderAvatarUrl(user);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchProfilePicture = useCallback(
|
const fetchProfilePicture = useCallback(
|
||||||
async (sessionToUse?: Session | null) => {
|
async (sessionToUse?: Session | null) => {
|
||||||
const currentSession = sessionToUse ?? session;
|
const currentSession = sessionToUse ?? session;
|
||||||
@@ -351,7 +367,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
"[Auth Debug] Profile picture not available:",
|
"[Auth Debug] Profile picture not available:",
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
setProfilePictureUrl(null);
|
setProfilePictureUrl(
|
||||||
|
await providerAvatarFallback(currentSession.user),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setProfilePictureUrl(data.signedUrl);
|
setProfilePictureUrl(data.signedUrl);
|
||||||
console.debug(
|
console.debug(
|
||||||
@@ -360,10 +378,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.debug("[Auth Debug] Failed to fetch profile picture:", error);
|
console.debug("[Auth Debug] Failed to fetch profile picture:", error);
|
||||||
setProfilePictureUrl(null);
|
setProfilePictureUrl(await providerAvatarFallback(currentSession.user));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[session],
|
[session, providerAvatarFallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshProfilePicture = useCallback(async () => {
|
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
|
// Fetch credits, pro status, trial status, profile picture metadata, and profile picture using the session from the response
|
||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
// Sync OAuth avatar in background
|
// Sync OAuth avatar in background; fetch the picture once the
|
||||||
syncOAuthAvatar(data.session.user).catch((err) => {
|
// sync settles instead of guessing with a fixed delay.
|
||||||
|
syncOAuthAvatar(data.session.user)
|
||||||
|
.catch((err) => {
|
||||||
console.debug(
|
console.debug(
|
||||||
"[Auth Debug] Failed to sync OAuth avatar on init:",
|
"[Auth Debug] Failed to sync OAuth avatar on init:",
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
});
|
return false;
|
||||||
|
})
|
||||||
|
.then(() => fetchProfilePicture(data.session));
|
||||||
|
|
||||||
await fetchCredits(data.session);
|
await fetchCredits(data.session);
|
||||||
await fetchProStatus(data.session);
|
await fetchProStatus(data.session);
|
||||||
await fetchTrialStatus(data.session);
|
await fetchTrialStatus(data.session);
|
||||||
await fetchProfilePictureMetadata(data.session);
|
await fetchProfilePictureMetadata(data.session);
|
||||||
|
|
||||||
// Small delay to allow avatar sync to complete if quick
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchProfilePicture(data.session);
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -602,9 +619,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// null/loading states.
|
// null/loading states.
|
||||||
|
|
||||||
// Sync OAuth avatar in background (don't block other fetches)
|
// Sync OAuth avatar in background (don't block other fetches)
|
||||||
syncOAuthAvatar(newSession.user).catch((err) => {
|
const avatarSync = syncOAuthAvatar(newSession.user).catch(
|
||||||
console.debug("[Auth Debug] Failed to sync OAuth avatar:", err);
|
(err) => {
|
||||||
});
|
console.debug(
|
||||||
|
"[Auth Debug] Failed to sync OAuth avatar:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch user data in parallel
|
// Fetch user data in parallel
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -613,15 +636,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
fetchTrialStatus(newSession),
|
fetchTrialStatus(newSession),
|
||||||
fetchProfilePictureMetadata(newSession),
|
fetchProfilePictureMetadata(newSession),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
// Fetch profile picture AFTER sync has had time to complete
|
// Fetch the picture once the avatar sync settles rather than
|
||||||
// Use a small delay to allow avatar sync to finish if it's quick
|
// after a fixed 500ms the first-ever sync usually loses.
|
||||||
setTimeout(() => {
|
avatarSync.then(() => {
|
||||||
fetchProfilePicture(newSession).finally(() => {
|
fetchProfilePicture(newSession).finally(() => {
|
||||||
console.debug(
|
console.debug(
|
||||||
"[Auth Debug] User data fully loaded after sign in",
|
"[Auth Debug] User data fully loaded after sign in",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, 500);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (event === "TOKEN_REFRESHED") {
|
} else if (event === "TOKEN_REFRESHED") {
|
||||||
|
|||||||
@@ -2,8 +2,16 @@ import { Avatar } from "@mantine/core";
|
|||||||
import { useAuth } from "@app/auth/UseSession";
|
import { useAuth } from "@app/auth/UseSession";
|
||||||
|
|
||||||
export function useConfigButtonIcon(): React.ReactNode {
|
export function useConfigButtonIcon(): React.ReactNode {
|
||||||
const { profilePictureUrl } = useAuth();
|
const { profilePictureUrl, user } = useAuth();
|
||||||
return profilePictureUrl ? (
|
if (profilePictureUrl) {
|
||||||
<Avatar src={profilePictureUrl} radius="xl" size={24} />
|
return <Avatar src={profilePictureUrl} radius="xl" size={24} />;
|
||||||
|
}
|
||||||
|
// 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 ? (
|
||||||
|
<Avatar radius="xl" size={24} color="blue">
|
||||||
|
{initial}
|
||||||
|
</Avatar>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user