mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Add SaaS frontend code (#5879)
# Description of Changes Adds the code for the SaaS frontend as proprietary code to the OSS repo. This version of the code is adapted from 22/1/2026, which was the last SaaS version based on the 'V2' design. This will move us closer to being able to have the OSS products understand whether the user has a SaaS account, and provide the correct UI in those cases.
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
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
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
session: null,
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
creditBalance: null,
|
||||
subscription: null,
|
||||
creditSummary: null,
|
||||
isPro: null,
|
||||
trialStatus: null,
|
||||
profilePictureUrl: null,
|
||||
profilePictureMetadata: null,
|
||||
signOut: async () => {},
|
||||
refreshSession: async () => {},
|
||||
hasSufficientCredits: () => ({ hasSufficientCredits: false, currentBalance: 0, requiredCredits: 0 }),
|
||||
updateCredits: () => {},
|
||||
refreshCredits: async () => {},
|
||||
refreshProStatus: async () => {},
|
||||
refreshTrialStatus: async () => {},
|
||||
refreshProfilePicture: 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 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
|
||||
}
|
||||
|
||||
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: any) {
|
||||
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])
|
||||
|
||||
const refreshCredits = useCallback(async () => {
|
||||
await fetchCredits()
|
||||
}, [fetchCredits])
|
||||
|
||||
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)
|
||||
}
|
||||
} catch (error: any) {
|
||||
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])
|
||||
|
||||
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 (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: any) {
|
||||
console.debug('[Auth Debug] Failed to fetch trial status:', error)
|
||||
setTrialStatus(null)
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const refreshTrialStatus = useCallback(async () => {
|
||||
await fetchTrialStatus()
|
||||
}, [fetchTrialStatus])
|
||||
|
||||
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')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.debug('[Auth Debug] Failed to fetch profile picture:', error)
|
||||
setProfilePictureUrl(null)
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const refreshProfilePicture = useCallback(async () => {
|
||||
await fetchProfilePicture()
|
||||
}, [fetchProfilePicture])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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: any) {
|
||||
console.debug('[Auth Debug] Failed to fetch profile picture metadata:', error)
|
||||
setProfilePictureMetadata(null)
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const refreshProfilePictureMetadata = useCallback(async () => {
|
||||
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
|
||||
}
|
||||
setCreditSummary(updatedSummary)
|
||||
}
|
||||
}, [creditSummary])
|
||||
|
||||
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])
|
||||
|
||||
const refreshSession = async () => {
|
||||
try {
|
||||
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)
|
||||
} else {
|
||||
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)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
console.error('[Auth Debug] Sign out error:', error)
|
||||
setError(error)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up global credit update callback
|
||||
useEffect(() => {
|
||||
setGlobalCreditUpdateCallback(updateCredits)
|
||||
}, [updateCredits])
|
||||
|
||||
useEffect(() => {
|
||||
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()
|
||||
|
||||
if (!mounted) return
|
||||
|
||||
if (error) {
|
||||
console.error('[Auth Debug] Initial session error:', error)
|
||||
setError(error)
|
||||
} else {
|
||||
console.debug('[Auth Debug] Initial session loaded:', {
|
||||
hasSession: !!data.session,
|
||||
userId: data.session?.user?.id,
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
console.error('[Auth Debug] Unexpected error during auth initialization:', err)
|
||||
if (mounted) {
|
||||
setError(err as AuthError)
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeAuth()
|
||||
|
||||
// Subscribe to auth state changes
|
||||
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()
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
])
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.debug('[Auth Debug] User data refreshed after upgrade')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[Auth Debug] Failed to synchronize user upgrade:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const value: AuthContextType = {
|
||||
session,
|
||||
user: session?.user ?? null,
|
||||
loading,
|
||||
error,
|
||||
creditBalance,
|
||||
subscription,
|
||||
creditSummary,
|
||||
isPro,
|
||||
trialStatus,
|
||||
profilePictureUrl,
|
||||
profilePictureMetadata,
|
||||
signOut,
|
||||
refreshSession,
|
||||
hasSufficientCredits,
|
||||
updateCredits,
|
||||
refreshCredits,
|
||||
refreshProStatus,
|
||||
refreshTrialStatus,
|
||||
refreshProfilePicture,
|
||||
refreshProfilePictureMetadata
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// Debug hook to expose auth state for debugging
|
||||
export function useAuthDebug() {
|
||||
const auth = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
return auth
|
||||
}
|
||||
Reference in New Issue
Block a user