Add frontend autoformatting and set CI to require formatted code for all languages (#6052)

# Description of Changes
Changes the strategy for autoformatting to reject PRs if they are not
formatted correctly instead of allowing them to merge and then spawning
a new PR to fix the formatting. The old strategy just caused more work
for us because we'd have to manually approve the followup PR and get it
merged, which required 2 reviewers so in practice it rarely got done and
just meant everyone's PRs ended up containing reformatting for unrelated
files, which makes code review unnecessarily difficult. If the PR's code
is not formatted correctly after this PR, a comment will be added
automatically to tell the author how to run the formatter script to fix
their code so it can go in.

This also enables autoformatting for the frontend code, using Prettier.
I've enabled it for pretty much everything in the frontend folder, other
than 3rd party files and files it doesn't make sense for. I also
excluded Markdown because it sounds likely to be more annoying to have
to autoformat the Markdown in the frontend folder but nowhere else. Open
to changing this though if people disagree.

> [!note]
> 
> Advice to reviewers: The first commit contains all of the actual logic
I've introduced (CI changes, Prettier config, etc.)
> The second commit is just the reformatting of the entire frontend
folder.
> The first commit needs proper review, the second one just give it a
spot-check that it's doing what you'd expect.
This commit is contained in:
James Brunton
2026-04-10 17:41:19 +01:00
committed by GitHub
parent 33b2b5827a
commit a3e45bc182
1359 changed files with 57784 additions and 57460 deletions
+425 -408
View File
@@ -1,44 +1,44 @@
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react'
import { supabase } from '@app/auth/supabase'
import type { Session, User as SupabaseUser, AuthError } from '@supabase/supabase-js'
import { CreditSummary, SubscriptionInfo, CreditCheckResult, ApiCredits } from '@app/types/credits'
import apiClient, { setGlobalCreditUpdateCallback } from '@app/services/apiClient'
import { synchronizeUserUpgrade } from '@app/services/userService'
import { syncOAuthAvatar, getProfilePictureMetadata, type ProfilePictureMetadata } from '@app/services/avatarSyncService'
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from "react";
import { supabase } from "@app/auth/supabase";
import type { Session, User as SupabaseUser, AuthError } from "@supabase/supabase-js";
import { CreditSummary, SubscriptionInfo, CreditCheckResult, ApiCredits } from "@app/types/credits";
import apiClient, { setGlobalCreditUpdateCallback } from "@app/services/apiClient";
import { synchronizeUserUpgrade } from "@app/services/userService";
import { syncOAuthAvatar, getProfilePictureMetadata, type ProfilePictureMetadata } from "@app/services/avatarSyncService";
// Extend Supabase User to include optional username for compatibility
export type User = SupabaseUser & { username?: string };
export interface TrialStatus {
isTrialing: boolean
trialEnd: string
daysRemaining: number
hasPaymentMethod: boolean
hasScheduledSub: boolean
status: string
isTrialing: boolean;
trialEnd: string;
daysRemaining: number;
hasPaymentMethod: boolean;
hasScheduledSub: boolean;
status: string;
}
interface AuthContextType {
session: Session | null
user: User | null
loading: boolean
error: AuthError | null
creditBalance: number | null
subscription: SubscriptionInfo | null
creditSummary: CreditSummary | null
isPro: boolean | null
trialStatus: TrialStatus | null
profilePictureUrl: string | null
profilePictureMetadata: ProfilePictureMetadata | null
signOut: () => Promise<void>
refreshSession: () => Promise<void>
hasSufficientCredits: (requiredCredits: number) => CreditCheckResult
updateCredits: (newBalance: number) => void
refreshCredits: () => Promise<void>
refreshProStatus: () => Promise<void>
refreshTrialStatus: () => Promise<void>
refreshProfilePicture: () => Promise<void>
refreshProfilePictureMetadata: () => Promise<void>
session: Session | null;
user: User | null;
loading: boolean;
error: AuthError | null;
creditBalance: number | null;
subscription: SubscriptionInfo | null;
creditSummary: CreditSummary | null;
isPro: boolean | null;
trialStatus: TrialStatus | null;
profilePictureUrl: string | null;
profilePictureMetadata: ProfilePictureMetadata | null;
signOut: () => Promise<void>;
refreshSession: () => Promise<void>;
hasSufficientCredits: (requiredCredits: number) => CreditCheckResult;
updateCredits: (newBalance: number) => void;
refreshCredits: () => Promise<void>;
refreshProStatus: () => Promise<void>;
refreshTrialStatus: () => Promise<void>;
refreshProfilePicture: () => Promise<void>;
refreshProfilePictureMetadata: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({
@@ -61,467 +61,488 @@ const AuthContext = createContext<AuthContextType>({
refreshProStatus: async () => {},
refreshTrialStatus: async () => {},
refreshProfilePicture: async () => {},
refreshProfilePictureMetadata: async () => {}
})
refreshProfilePictureMetadata: async () => {},
});
export function AuthProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<AuthError | null>(null)
const [creditBalance, setCreditBalance] = useState<number | null>(null)
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null)
const [creditSummary, setCreditSummary] = useState<CreditSummary | null>(null)
const [isPro, setIsPro] = useState<boolean | null>(null)
const [trialStatus, setTrialStatus] = useState<TrialStatus | null>(null)
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(null)
const [profilePictureMetadata, setProfilePictureMetadata] = useState<ProfilePictureMetadata | null>(null)
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<AuthError | null>(null);
const [creditBalance, setCreditBalance] = useState<number | null>(null);
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null);
const [creditSummary, setCreditSummary] = useState<CreditSummary | null>(null);
const [isPro, setIsPro] = useState<boolean | null>(null);
const [trialStatus, setTrialStatus] = useState<TrialStatus | null>(null);
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(null);
const [profilePictureMetadata, setProfilePictureMetadata] = useState<ProfilePictureMetadata | null>(null);
const fetchCredits = useCallback(async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session
const fetchCredits = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
if (!currentSession?.user) {
console.debug('[Auth Debug] No user session, skipping credit fetch')
setCreditBalance(null)
setCreditSummary(null)
setSubscription(null)
return
}
try {
console.debug('[Auth Debug] Fetching credits for user:', currentSession.user.id)
const response = await apiClient.get<ApiCredits>('/api/v1/credits')
const apiCredits = response.data
// Map server payload to app CreditSummary
const credits: CreditSummary = {
currentCredits: apiCredits.totalAvailableCredits,
maxCredits: apiCredits.weeklyCreditsAllocated + apiCredits.totalBoughtCredits,
creditsUsed: (apiCredits.weeklyCreditsAllocated - apiCredits.weeklyCreditsRemaining) + (apiCredits.totalBoughtCredits - apiCredits.boughtCreditsRemaining),
creditsRemaining: apiCredits.totalAvailableCredits,
resetDate: apiCredits.weeklyResetDate,
weeklyAllowance: apiCredits.weeklyCreditsAllocated
if (!currentSession?.user) {
console.debug("[Auth Debug] No user session, skipping credit fetch");
setCreditBalance(null);
setCreditSummary(null);
setSubscription(null);
return;
}
setCreditSummary(credits)
setCreditBalance(credits.creditsRemaining)
try {
console.debug("[Auth Debug] Fetching credits for user:", currentSession.user.id);
const response = await apiClient.get<ApiCredits>("/api/v1/credits");
const apiCredits = response.data;
const subscriptionInfo: SubscriptionInfo = {
status: 'active',
tier: (credits.weeklyAllowance || 0) > 100 ? 'premium' : 'free',
creditsPerWeek: credits.weeklyAllowance,
maxCredits: credits.maxCredits
// Map server payload to app CreditSummary
const credits: CreditSummary = {
currentCredits: apiCredits.totalAvailableCredits,
maxCredits: apiCredits.weeklyCreditsAllocated + apiCredits.totalBoughtCredits,
creditsUsed:
apiCredits.weeklyCreditsAllocated -
apiCredits.weeklyCreditsRemaining +
(apiCredits.totalBoughtCredits - apiCredits.boughtCreditsRemaining),
creditsRemaining: apiCredits.totalAvailableCredits,
resetDate: apiCredits.weeklyResetDate,
weeklyAllowance: apiCredits.weeklyCreditsAllocated,
};
setCreditSummary(credits);
setCreditBalance(credits.creditsRemaining);
const subscriptionInfo: SubscriptionInfo = {
status: "active",
tier: (credits.weeklyAllowance || 0) > 100 ? "premium" : "free",
creditsPerWeek: credits.weeklyAllowance,
maxCredits: credits.maxCredits,
};
setSubscription(subscriptionInfo);
console.debug("[Auth Debug] Credits fetched successfully:", credits);
} catch (error: unknown) {
console.debug("[Auth Debug] Failed to fetch credits:", error);
// Don't set error state for credit fetching failures to avoid disrupting auth flow
// Credits might not be available in all deployments
setCreditBalance(null);
setCreditSummary(null);
setSubscription(null);
}
setSubscription(subscriptionInfo)
console.debug('[Auth Debug] Credits fetched successfully:', credits)
} catch (error: unknown) {
console.debug('[Auth Debug] Failed to fetch credits:', error)
// Don't set error state for credit fetching failures to avoid disrupting auth flow
// Credits might not be available in all deployments
setCreditBalance(null)
setCreditSummary(null)
setSubscription(null)
}
}, [session])
},
[session],
);
const refreshCredits = useCallback(async () => {
await fetchCredits()
}, [fetchCredits])
await fetchCredits();
}, [fetchCredits]);
const fetchProStatus = useCallback(async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session
const fetchProStatus = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
if (!currentSession?.user) {
console.debug('[Auth Debug] No user session, skipping pro status fetch')
setIsPro(null)
return
}
try {
console.debug('[Auth Debug] Fetching pro status for user:', currentSession.user.id)
const { data: proStatus, error } = await supabase.rpc('is_pro')
if (error) {
console.error('[Auth Debug] Error checking Pro status:', error)
setIsPro(false) // Default to false if there's an error
} else {
const isProUser = Boolean(proStatus)
setIsPro(isProUser)
console.debug('[Auth Debug] Pro status fetched:', isProUser)
if (!currentSession?.user) {
console.debug("[Auth Debug] No user session, skipping pro status fetch");
setIsPro(null);
return;
}
} catch (error: unknown) {
console.debug('[Auth Debug] Failed to fetch pro status:', error)
setIsPro(false) // Default to false if there's an error
}
}, [session])
try {
console.debug("[Auth Debug] Fetching pro status for user:", currentSession.user.id);
const { data: proStatus, error } = await supabase.rpc("is_pro");
if (error) {
console.error("[Auth Debug] Error checking Pro status:", error);
setIsPro(false); // Default to false if there's an error
} else {
const isProUser = Boolean(proStatus);
setIsPro(isProUser);
console.debug("[Auth Debug] Pro status fetched:", isProUser);
}
} catch (error: unknown) {
console.debug("[Auth Debug] Failed to fetch pro status:", error);
setIsPro(false); // Default to false if there's an error
}
},
[session],
);
const refreshProStatus = useCallback(async () => {
await fetchProStatus()
}, [fetchProStatus])
await fetchProStatus();
}, [fetchProStatus]);
const fetchTrialStatus = useCallback(async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session
const fetchTrialStatus = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
if (!currentSession?.user) {
console.debug('[Auth Debug] No user session, skipping trial status fetch')
setTrialStatus(null)
return
}
try {
console.debug('[Auth Debug] Fetching trial status for user:', currentSession.user.id)
const { data, error } = await supabase
.from('billing_subscriptions')
.select('status, trial_end, has_payment_method, scheduled_subscription_id')
.in('status', ['trialing', 'incomplete_expired', 'canceled'])
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle()
if (error) {
console.error('[Auth Debug] Error fetching trial status:', error)
setTrialStatus(null)
return
if (!currentSession?.user) {
console.debug("[Auth Debug] No user session, skipping trial status fetch");
setTrialStatus(null);
return;
}
if (data?.trial_end) {
const trialEnd = new Date(data.trial_end)
const now = new Date()
const daysRemaining = Math.ceil((trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
try {
console.debug("[Auth Debug] Fetching trial status for user:", currentSession.user.id);
const { data, error } = await supabase
.from("billing_subscriptions")
.select("status, trial_end, has_payment_method, scheduled_subscription_id")
.in("status", ["trialing", "incomplete_expired", "canceled"])
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle();
setTrialStatus({
isTrialing: data.status === 'trialing' && daysRemaining > 0,
trialEnd: data.trial_end,
daysRemaining: Math.max(0, daysRemaining),
hasPaymentMethod: data.has_payment_method || false,
hasScheduledSub: !!data.scheduled_subscription_id,
status: data.status
})
console.debug('[Auth Debug] Trial status fetched:', {
status: data.status,
daysRemaining: Math.max(0, daysRemaining),
hasPaymentMethod: data.has_payment_method,
isTrialing: data.status === 'trialing' && daysRemaining > 0
})
} else {
setTrialStatus(null)
if (error) {
console.error("[Auth Debug] Error fetching trial status:", error);
setTrialStatus(null);
return;
}
if (data?.trial_end) {
const trialEnd = new Date(data.trial_end);
const now = new Date();
const daysRemaining = Math.ceil((trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
setTrialStatus({
isTrialing: data.status === "trialing" && daysRemaining > 0,
trialEnd: data.trial_end,
daysRemaining: Math.max(0, daysRemaining),
hasPaymentMethod: data.has_payment_method || false,
hasScheduledSub: !!data.scheduled_subscription_id,
status: data.status,
});
console.debug("[Auth Debug] Trial status fetched:", {
status: data.status,
daysRemaining: Math.max(0, daysRemaining),
hasPaymentMethod: data.has_payment_method,
isTrialing: data.status === "trialing" && daysRemaining > 0,
});
} else {
setTrialStatus(null);
}
} catch (error: unknown) {
console.debug("[Auth Debug] Failed to fetch trial status:", error);
setTrialStatus(null);
}
} catch (error: unknown) {
console.debug('[Auth Debug] Failed to fetch trial status:', error)
setTrialStatus(null)
}
}, [session])
},
[session],
);
const refreshTrialStatus = useCallback(async () => {
await fetchTrialStatus()
}, [fetchTrialStatus])
await fetchTrialStatus();
}, [fetchTrialStatus]);
const fetchProfilePicture = useCallback(async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session
const fetchProfilePicture = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
if (!currentSession?.user) {
console.debug('[Auth Debug] No user session, skipping profile picture fetch')
setProfilePictureUrl(null)
return
}
try {
const PROFILE_BUCKET = 'profile-pictures'
const profilePath = `${currentSession.user.id}/avatar`
console.debug('[Auth Debug] Fetching profile picture for user:', currentSession.user.id)
const { data, error } = await supabase
.storage
.from(PROFILE_BUCKET)
.createSignedUrl(profilePath, 60 * 60)
if (error) {
// Profile picture not found is expected for users without uploads
console.debug('[Auth Debug] Profile picture not available:', error.message)
setProfilePictureUrl(null)
} else {
setProfilePictureUrl(data.signedUrl)
console.debug('[Auth Debug] Profile picture URL fetched successfully')
if (!currentSession?.user) {
console.debug("[Auth Debug] No user session, skipping profile picture fetch");
setProfilePictureUrl(null);
return;
}
} catch (error: unknown) {
console.debug('[Auth Debug] Failed to fetch profile picture:', error)
setProfilePictureUrl(null)
}
}, [session])
try {
const PROFILE_BUCKET = "profile-pictures";
const profilePath = `${currentSession.user.id}/avatar`;
console.debug("[Auth Debug] Fetching profile picture for user:", currentSession.user.id);
const { data, error } = await supabase.storage.from(PROFILE_BUCKET).createSignedUrl(profilePath, 60 * 60);
if (error) {
// Profile picture not found is expected for users without uploads
console.debug("[Auth Debug] Profile picture not available:", error.message);
setProfilePictureUrl(null);
} else {
setProfilePictureUrl(data.signedUrl);
console.debug("[Auth Debug] Profile picture URL fetched successfully");
}
} catch (error: unknown) {
console.debug("[Auth Debug] Failed to fetch profile picture:", error);
setProfilePictureUrl(null);
}
},
[session],
);
const refreshProfilePicture = useCallback(async () => {
await fetchProfilePicture()
}, [fetchProfilePicture])
await fetchProfilePicture();
}, [fetchProfilePicture]);
const fetchProfilePictureMetadata = useCallback(async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session
const fetchProfilePictureMetadata = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
if (!currentSession?.user) {
console.debug('[Auth Debug] No user session, skipping profile picture metadata fetch')
setProfilePictureMetadata(null)
return
}
if (!currentSession?.user) {
console.debug("[Auth Debug] No user session, skipping profile picture metadata fetch");
setProfilePictureMetadata(null);
return;
}
try {
console.debug('[Auth Debug] Fetching profile picture metadata for user:', currentSession.user.id)
const metadata = await getProfilePictureMetadata(currentSession.user.id)
setProfilePictureMetadata(metadata)
console.debug('[Auth Debug] Profile picture metadata fetched:', metadata)
} catch (error: unknown) {
console.debug('[Auth Debug] Failed to fetch profile picture metadata:', error)
setProfilePictureMetadata(null)
}
}, [session])
try {
console.debug("[Auth Debug] Fetching profile picture metadata for user:", currentSession.user.id);
const metadata = await getProfilePictureMetadata(currentSession.user.id);
setProfilePictureMetadata(metadata);
console.debug("[Auth Debug] Profile picture metadata fetched:", metadata);
} catch (error: unknown) {
console.debug("[Auth Debug] Failed to fetch profile picture metadata:", error);
setProfilePictureMetadata(null);
}
},
[session],
);
const refreshProfilePictureMetadata = useCallback(async () => {
await fetchProfilePictureMetadata()
}, [fetchProfilePictureMetadata])
await fetchProfilePictureMetadata();
}, [fetchProfilePictureMetadata]);
const updateCredits = useCallback((newBalance: number) => {
console.debug('[Auth Debug] Updating credit balance:', { from: creditBalance, to: newBalance })
setCreditBalance(newBalance)
// Also update the creditSummary if it exists
if (creditSummary) {
const updatedSummary: CreditSummary = {
...creditSummary,
creditsRemaining: newBalance,
currentCredits: newBalance
const updateCredits = useCallback(
(newBalance: number) => {
console.debug("[Auth Debug] Updating credit balance:", { from: creditBalance, to: newBalance });
setCreditBalance(newBalance);
// Also update the creditSummary if it exists
if (creditSummary) {
const updatedSummary: CreditSummary = {
...creditSummary,
creditsRemaining: newBalance,
currentCredits: newBalance,
};
setCreditSummary(updatedSummary);
}
setCreditSummary(updatedSummary)
}
}, [creditSummary])
},
[creditSummary],
);
const hasSufficientCredits = useCallback((requiredCredits: number): CreditCheckResult => {
const currentBalance = creditBalance ?? 0
const hasSufficient = currentBalance >= requiredCredits
console.debug('[Auth Debug] Credit check:', { requiredCredits, currentBalance, hasSufficient })
const hasSufficientCredits = useCallback(
(requiredCredits: number): CreditCheckResult => {
const currentBalance = creditBalance ?? 0;
const hasSufficient = currentBalance >= requiredCredits;
console.debug("[Auth Debug] Credit check:", { requiredCredits, currentBalance, hasSufficient });
return {
hasSufficientCredits: hasSufficient,
currentBalance,
requiredCredits,
shortfall: hasSufficient ? undefined : requiredCredits - currentBalance
}
}, [creditBalance])
return {
hasSufficientCredits: hasSufficient,
currentBalance,
requiredCredits,
shortfall: hasSufficient ? undefined : requiredCredits - currentBalance,
};
},
[creditBalance],
);
const refreshSession = async () => {
try {
setLoading(true)
setError(null)
const { data, error } = await supabase.auth.refreshSession()
setLoading(true);
setError(null);
const { data, error } = await supabase.auth.refreshSession();
if (error) {
console.error('[Auth Debug] Session refresh error:', error)
setError(error)
setSession(null)
console.error("[Auth Debug] Session refresh error:", error);
setError(error);
setSession(null);
} else {
console.debug('[Auth Debug] Session refreshed successfully')
setSession(data.session)
console.debug("[Auth Debug] Session refreshed successfully");
setSession(data.session);
}
} catch (err) {
console.error('[Auth Debug] Unexpected error during session refresh:', err)
setError(err as AuthError)
console.error("[Auth Debug] Unexpected error during session refresh:", err);
setError(err as AuthError);
} finally {
setLoading(false)
setLoading(false);
}
}
};
const signOut = async () => {
try {
setError(null)
const { error } = await supabase.auth.signOut()
setError(null);
const { error } = await supabase.auth.signOut();
if (error) {
console.error('[Auth Debug] Sign out error:', error)
setError(error)
console.error("[Auth Debug] Sign out error:", error);
setError(error);
} else {
console.debug('[Auth Debug] Signed out successfully')
setSession(null)
console.debug("[Auth Debug] Signed out successfully");
setSession(null);
}
} catch (err) {
console.error('[Auth Debug] Unexpected error during sign out:', err)
setError(err as AuthError)
console.error("[Auth Debug] Unexpected error during sign out:", err);
setError(err as AuthError);
}
}
};
// Set up global credit update callback
useEffect(() => {
setGlobalCreditUpdateCallback(updateCredits)
}, [updateCredits])
setGlobalCreditUpdateCallback(updateCredits);
}, [updateCredits]);
useEffect(() => {
let mounted = true
let mounted = true;
// Load current session on first mount
const initializeAuth = async () => {
try {
console.debug('[Auth Debug] Initializing auth...')
const { data, error } = await supabase.auth.getSession()
console.debug("[Auth Debug] Initializing auth...");
const { data, error } = await supabase.auth.getSession();
if (!mounted) return
if (!mounted) return;
if (error) {
console.error('[Auth Debug] Initial session error:', error)
setError(error)
console.error("[Auth Debug] Initial session error:", error);
setError(error);
} else {
console.debug('[Auth Debug] Initial session loaded:', {
console.debug("[Auth Debug] Initial session loaded:", {
hasSession: !!data.session,
userId: data.session?.user?.id,
email: data.session?.user?.email
})
setSession(data.session)
email: data.session?.user?.email,
});
setSession(data.session);
// 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)
})
console.debug("[Auth Debug] Failed to sync OAuth avatar on init:", err);
});
await fetchCredits(data.session)
await fetchProStatus(data.session)
await fetchTrialStatus(data.session)
await fetchProfilePictureMetadata(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)
fetchProfilePicture(data.session);
}, 500);
}
}
} catch (err) {
console.error('[Auth Debug] Unexpected error during auth initialization:', err)
console.error("[Auth Debug] Unexpected error during auth initialization:", err);
if (mounted) {
setError(err as AuthError)
setError(err as AuthError);
}
} finally {
if (mounted) {
setLoading(false)
setLoading(false);
}
}
}
};
initializeAuth()
initializeAuth();
// Subscribe to auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, newSession) => {
if (!mounted) return
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, newSession) => {
if (!mounted) return;
console.debug('[Auth Debug] Auth state change:', {
event,
hasSession: !!newSession,
userId: newSession?.user?.id,
email: newSession?.user?.email,
timestamp: new Date().toISOString()
})
console.debug("[Auth Debug] Auth state change:", {
event,
hasSession: !!newSession,
userId: newSession?.user?.id,
email: newSession?.user?.email,
timestamp: new Date().toISOString(),
});
// Don't run supabase calls inside this callback; schedule them
setTimeout(() => {
if (mounted) {
setSession(newSession)
setError(null)
// Don't run supabase calls inside this callback; schedule them
setTimeout(() => {
if (mounted) {
setSession(newSession);
setError(null);
// Additional handling for specific events
if (event === 'SIGNED_OUT') {
console.debug('[Auth Debug] User signed out, clearing session')
// Clear credit data, pro status, trial status, profile picture, and metadata on sign out
setCreditBalance(null)
setCreditSummary(null)
setSubscription(null)
setIsPro(null)
setTrialStatus(null)
setProfilePictureUrl(null)
setProfilePictureMetadata(null)
} else if (event === 'SIGNED_IN') {
console.debug('[Auth Debug] User signed in successfully')
if (newSession?.user) {
setLoading(true)
// Additional handling for specific events
if (event === "SIGNED_OUT") {
console.debug("[Auth Debug] User signed out, clearing session");
// Clear credit data, pro status, trial status, profile picture, and metadata on sign out
setCreditBalance(null);
setCreditSummary(null);
setSubscription(null);
setIsPro(null);
setTrialStatus(null);
setProfilePictureUrl(null);
setProfilePictureMetadata(null);
} else if (event === "SIGNED_IN") {
console.debug("[Auth Debug] User signed in successfully");
if (newSession?.user) {
setLoading(true);
// 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)
// 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);
});
// Fetch user data in parallel
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
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(() => {
fetchProfilePicture(newSession).finally(() => {
setLoading(false);
console.debug("[Auth Debug] User data fully loaded after sign in");
});
}, 500);
});
}
} else if (event === "TOKEN_REFRESHED") {
console.debug("[Auth Debug] Token refreshed");
// Optionally refresh credits, pro status, trial status, profile picture metadata, and profile picture on token refresh
if (newSession?.user) {
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchTrialStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession),
]).then(() => {
console.debug("[Auth Debug] User data refreshed after token refresh");
});
}
} else if (event === "USER_UPDATED") {
console.debug("[Auth Debug] User updated");
// Check if this is a pending OAuth upgrade completion
const pendingUpgrade = sessionStorage.getItem("pendingUpgrade");
const upgradeProvider = sessionStorage.getItem("upgradeProvider");
if (pendingUpgrade && newSession?.user && newSession.user.is_anonymous === false) {
console.debug("[Auth Debug] Processing pending OAuth upgrade:", upgradeProvider);
// Clear the flags first to prevent loops
sessionStorage.removeItem("pendingUpgrade");
sessionStorage.removeItem("upgradeProvider");
// Synchronize with backend
synchronizeUserUpgrade(upgradeProvider || undefined)
.then(() => {
console.debug("[Auth Debug] User upgrade synchronized successfully");
// Refresh credits, pro status, trial status, profile picture metadata, and profile picture after upgrade
if (newSession?.user) {
return Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchTrialStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession),
]);
}
})
// Fetch user data in parallel
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
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(() => {
fetchProfilePicture(newSession).finally(() => {
setLoading(false)
console.debug('[Auth Debug] User data fully loaded after sign in')
})
}, 500)
.then(() => {
console.debug("[Auth Debug] User data refreshed after upgrade");
})
}
} else if (event === 'TOKEN_REFRESHED') {
console.debug('[Auth Debug] Token refreshed')
// Optionally refresh credits, pro status, trial status, profile picture metadata, and profile picture on token refresh
if (newSession?.user) {
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchTrialStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession)
]).then(() => {
console.debug('[Auth Debug] User data refreshed after token refresh')
})
}
} else if (event === 'USER_UPDATED') {
console.debug('[Auth Debug] User updated')
// Check if this is a pending OAuth upgrade completion
const pendingUpgrade = sessionStorage.getItem('pendingUpgrade')
const upgradeProvider = sessionStorage.getItem('upgradeProvider')
if (pendingUpgrade && newSession?.user && newSession.user.is_anonymous === false) {
console.debug('[Auth Debug] Processing pending OAuth upgrade:', upgradeProvider)
// Clear the flags first to prevent loops
sessionStorage.removeItem('pendingUpgrade')
sessionStorage.removeItem('upgradeProvider')
// Synchronize with backend
synchronizeUserUpgrade(upgradeProvider || undefined)
.then(() => {
console.debug('[Auth Debug] User upgrade synchronized successfully')
// Refresh credits, pro status, trial status, profile picture metadata, and profile picture after upgrade
if (newSession?.user) {
return Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchTrialStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession)
])
}
})
.then(() => {
console.debug('[Auth Debug] User data refreshed after upgrade')
})
.catch((err) => {
console.error('[Auth Debug] Failed to synchronize user upgrade:', err)
})
}
.catch((err) => {
console.error("[Auth Debug] Failed to synchronize user upgrade:", err);
});
}
}
}, 0)
}
)
}
}, 0);
});
return () => {
mounted = false
subscription.unsubscribe()
}
}, [])
mounted = false;
subscription.unsubscribe();
};
}, []);
const value: AuthContextType = {
session,
@@ -543,41 +564,37 @@ export function AuthProvider({ children }: { children: ReactNode }) {
refreshProStatus,
refreshTrialStatus,
refreshProfilePicture,
refreshProfilePictureMetadata
}
refreshProfilePictureMetadata,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext)
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
throw new Error("useAuth must be used within an AuthProvider");
}
return context
return context;
}
// Debug hook to expose auth state for debugging
export function useAuthDebug() {
const auth = useAuth()
const auth = useAuth();
useEffect(() => {
console.debug('[Auth Debug] Current auth state:', {
console.debug("[Auth Debug] Current auth state:", {
hasSession: !!auth.session,
hasUser: !!auth.user,
loading: auth.loading,
hasError: !!auth.error,
userId: auth.user?.id,
email: auth.user?.email,
provider: auth.user?.app_metadata?.provider
})
}, [auth.session, auth.user, auth.loading, auth.error])
provider: auth.user?.app_metadata?.provider,
});
}, [auth.session, auth.user, auth.loading, auth.error]);
return auth
return auth;
}
+76 -77
View File
@@ -1,56 +1,52 @@
import { createClient } from '@supabase/supabase-js'
import { createClient } from "@supabase/supabase-js";
// Debug helper to log Supabase configuration
const debugConfig = () => {
const url = import.meta.env.VITE_SUPABASE_URL
const key = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY
const url = import.meta.env.VITE_SUPABASE_URL;
const key = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
console.log('[Supabase Debug] Configuration:', {
url: url ? '✓ URL configured' : '✗ URL missing',
key: key ? '✓ Key configured' : '✗ Key missing',
urlValue: url || 'undefined',
keyValue: key ? `${key.substring(0, 20)}...` : 'undefined'
})
console.log("[Supabase Debug] Configuration:", {
url: url ? "✓ URL configured" : "✗ URL missing",
key: key ? "✓ Key configured" : "✗ Key missing",
urlValue: url || "undefined",
keyValue: key ? `${key.substring(0, 20)}...` : "undefined",
});
return { url, key }
}
return { url, key };
};
const config = debugConfig()
const config = debugConfig();
if (!config.url) {
throw new Error('Missing VITE_SUPABASE_URL environment variable')
throw new Error("Missing VITE_SUPABASE_URL environment variable");
}
if (!config.key) {
throw new Error('Missing VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY environment variable')
throw new Error("Missing VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY environment variable");
}
export const supabase = createClient(
config.url,
config.key,
{
auth: {
persistSession: true, // keep session in localStorage
autoRefreshToken: true,
detectSessionInUrl: true, // helpful on first load after redirect
// debug: import.meta.env.DEV, // Enable debug logs in development
},
}
)
export const supabase = createClient(config.url, config.key, {
auth: {
persistSession: true, // keep session in localStorage
autoRefreshToken: true,
detectSessionInUrl: true, // helpful on first load after redirect
// debug: import.meta.env.DEV, // Enable debug logs in development
},
});
// Debug helper for auth events
export const debugAuthEvents = () => {
supabase.auth.onAuthStateChange((event, session) => {
console.log('[Supabase Debug] Auth state change:', {
console.log("[Supabase Debug] Auth state change:", {
event,
hasSession: !!session,
userId: session?.user?.id,
email: session?.user?.email,
provider: session?.user?.app_metadata?.provider,
timestamp: new Date().toISOString()
})
})
}
timestamp: new Date().toISOString(),
});
});
};
// Debug auth events can be manually enabled by calling debugAuthEvents()
// Commented out to prevent excessive logging on every page load
@@ -61,105 +57,108 @@ export const debugAuthEvents = () => {
// Anonymous authentication functions
export const signInAnonymously = async () => {
try {
const { data, error } = await supabase.auth.signInAnonymously()
const { data, error } = await supabase.auth.signInAnonymously();
if (error) {
console.error('[Supabase] Anonymous sign-in error:', error)
throw error
console.error("[Supabase] Anonymous sign-in error:", error);
throw error;
}
console.log('[Supabase] Anonymous sign-in successful:', {
console.log("[Supabase] Anonymous sign-in successful:", {
userId: data.user?.id,
isAnonymous: data.user?.is_anonymous
})
isAnonymous: data.user?.is_anonymous,
});
return { data, error }
return { data, error };
} catch (error) {
console.error('[Supabase] Anonymous sign-in failed:', error)
throw error
console.error("[Supabase] Anonymous sign-in failed:", error);
throw error;
}
}
};
// Account linking functions
export const linkEmailIdentity = async (email: string, password?: string) => {
try {
const updateData: { email: string; password?: string } = { email }
const updateData: { email: string; password?: string } = { email };
if (password) {
updateData.password = password
updateData.password = password;
}
const { data, error } = await supabase.auth.updateUser(updateData)
const { data, error } = await supabase.auth.updateUser(updateData);
if (error) {
console.error('[Supabase] Email linking error:', error)
throw error
console.error("[Supabase] Email linking error:", error);
throw error;
}
// Refresh session to get updated token with new user metadata
const { error: refreshError } = await supabase.auth.refreshSession()
const { error: refreshError } = await supabase.auth.refreshSession();
if (refreshError) {
console.warn('[Supabase] Session refresh after email linking failed:', refreshError)
console.warn("[Supabase] Session refresh after email linking failed:", refreshError);
// Don't throw - linking was successful, refresh is just for consistency
} else {
console.log('[Supabase] Session refreshed after email linking')
console.log("[Supabase] Session refreshed after email linking");
}
console.log('[Supabase] Email linked successfully:', {
console.log("[Supabase] Email linked successfully:", {
email,
userId: data.user?.id
})
userId: data.user?.id,
});
return { data, error }
return { data, error };
} catch (error) {
console.error('[Supabase] Email linking failed:', error)
throw error
console.error("[Supabase] Email linking failed:", error);
throw error;
}
}
};
export const linkOAuthIdentity = async (provider: 'google' | 'github' | 'apple' | 'azure', redirectTo?: string) => {
export const linkOAuthIdentity = async (provider: "google" | "github" | "apple" | "azure", redirectTo?: string) => {
try {
const { data, error } = await supabase.auth.linkIdentity({
provider: provider,
options: redirectTo ? { redirectTo } : undefined
})
options: redirectTo ? { redirectTo } : undefined,
});
if (error) {
console.error('[Supabase] OAuth linking error:', error)
throw error
console.error("[Supabase] OAuth linking error:", error);
throw error;
}
console.log('[Supabase] OAuth identity linked successfully:', {
console.log("[Supabase] OAuth identity linked successfully:", {
provider,
redirectTo,
url: data.url
})
url: data.url,
});
return { data, error }
return { data, error };
} catch (error) {
console.error('[Supabase] OAuth linking failed:', error)
throw error
console.error("[Supabase] OAuth linking failed:", error);
throw error;
}
}
};
// Helper function to check if user is anonymous
export const isUserAnonymous = (user: { is_anonymous?: boolean }) => {
return user?.is_anonymous === true
}
return user?.is_anonymous === true;
};
// Get current user session
export const getCurrentUser = async () => {
try {
const { data: { user }, error } = await supabase.auth.getUser()
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
console.error('[Supabase] Get user error:', error)
throw error
console.error("[Supabase] Get user error:", error);
throw error;
}
return user
return user;
} catch (error) {
console.error('[Supabase] Get user failed:', error)
throw error
console.error("[Supabase] Get user failed:", error);
throw error;
}
}
};