import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback } from 'react' import type { Session } from '@supabase/supabase-js' import { Stack } from '@junwon/aesthetics' import { getServerAddress } from '@junwon/config' import { getDatabase } from '../hooks/useDatabase' export interface Response { success: boolean error?: Error data?: { session?: Session } } interface Profile { email: string | null } type MembershipState = 'loading' | 'active' | 'inactive' interface VisaContextType { profile: Profile membershipState: MembershipState emailToVerify: string | null isSignedIn: boolean loading: boolean signIn: (email: string, redirectUrl: string) => Promise signOut: () => Promise verifyOtp: ( token: string, type: 'email' | 'email_change', redirectTo: string ) => Promise updateAccount: (updates: Partial, redirectUrl: string) => Promise deleteAccount: () => Promise getStripePortalLink: (returnUrl: string) => Promise getSession: () => Promise } const VisaContext = createContext(null) export const useVisa = () => { const context = useContext(VisaContext) if (!context) { throw new Error('useVisa must be used within a VisaProvider') } return context } const getAuthHeaders = async (database: any) => { const { data: { session }, } = await database.current.auth.getSession() if (!session) throw new Error('No session found') return { Authorization: `Bearer ${session.access_token}`, 'Refresh-Token': session.refresh_token, 'Content-Type': 'application/json', } } export function VisaProvider({ children }: { children: React.ReactNode }) { const database = useRef(getDatabase()) const serverAddress = useMemo(() => getServerAddress(), []) const [profile, setProfile] = useState({ email: null }) const [membershipState, setMembershipState] = useState('loading') const [loading, setLoading] = useState(true) const [isSignedIn, setIsSignedIn] = useState(false) const [emailToVerify, setEmailToVerify] = useState(null) useEffect(() => { let isMounted = true database.current.auth.getSession().then(({ data: { session } }) => { if (isMounted) { setProfile({ email: session?.user?.email ?? null, }) setIsSignedIn(!!session?.user) setLoading(false) } }) const { data: authListener } = database.current.auth.onAuthStateChange((event, session) => { if (!isMounted) return setProfile({ email: session?.user?.email ?? null }) setIsSignedIn(!!session?.user) }) return () => { isMounted = false authListener.subscription.unsubscribe() } }, []) useEffect(() => { let isMounted = true const checkMembership = async () => { setMembershipState('loading') try { const hasActiveMembership = await getMembershipState() if (isMounted) { setMembershipState(hasActiveMembership ? 'active' : 'inactive') } } catch (error) { console.log('Error checking membership:', error) if (isMounted) { setMembershipState('loading') } } } if (isSignedIn && profile.email) { checkMembership() } return () => { isMounted = false } }, [profile]) const signIn = useCallback( async (email: string, redirectUrl: string): Promise => { try { const response = await fetch(`${serverAddress}/visa/signin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, redirect_url: redirectUrl }), }) if (!response.ok) throw new Error('Sign in failed') setEmailToVerify(email) return { success: true } } catch (error) { console.log('Sign in error:', error) return { success: false } } }, [serverAddress] ) const verifyOtp = useCallback( async ( token: string, type: 'email' | 'email_change', redirectTo: string ): Promise => { if (!emailToVerify) { return { success: false, error: new Error('No email provided'), } } try { setLoading(true) const response = await fetch(`${serverAddress}/visa/verifyOtp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: emailToVerify, token, type, redirect_to: redirectTo, }), }) setLoading(false) if (!response.ok) throw new Error('Verification failed') const data = await response.json() await database.current.auth.setSession({ access_token: data.session.access_token, refresh_token: data.session.refresh_token, }) setEmailToVerify(null) return { success: true, data: { session: data.session }, } } catch (error) { console.log('Verification error:', error) return { success: false, error: new Error('Verification failed'), } } }, [emailToVerify, serverAddress] ) const signOut = useCallback(async (): Promise => { try { await database.current.auth.signOut() setEmailToVerify(null) setIsSignedIn(false) return { success: true } } catch (error) { console.log('Sign out error:', error) return { success: false } } }, []) const updateAccount = useCallback( async (updates: Partial, redirectUrl: string): Promise => { try { const headers = await getAuthHeaders(database) if (updates.email) { const response = await fetch(`${serverAddress}/visa`, { method: 'PUT', headers, body: JSON.stringify({ email: updates.email, redirectTo: redirectUrl, }), }) if (!response.ok) throw new Error('Failed to update account') setEmailToVerify(updates.email) return { success: true } } return { success: false, error: new Error('No updates provided') } } catch (error) { console.log('Update account error:', error) return { success: false, error: new Error('Failed to update account'), } } }, [serverAddress] ) const deleteAccount = useCallback(async (): Promise => { try { const headers = await getAuthHeaders(database) const response = await fetch(`${serverAddress}/visa`, { method: 'DELETE', headers, }) if (!response.ok) throw new Error('Failed to delete account') await database.current.auth.signOut() setEmailToVerify(null) setIsSignedIn(false) return { success: true } } catch (error) { console.log('Delete account error:', error) return { success: false, error: new Error('Failed to delete account'), } } }, [serverAddress]) const getMembershipState = useCallback(async (): Promise => { try { const headers = await getAuthHeaders(database) const response = await fetch(`${serverAddress}/visa/membership/state`, { method: 'POST', headers, }) if (!response.ok) { throw new Error('Failed to get membership state') } const data = await response.json() return data.hasActiveMembership } catch (error) { console.log('Error getting membership state:', error) throw error } }, [serverAddress]) const getStripePortalLink = useCallback( async (returnUrl: string): Promise => { try { const headers = await getAuthHeaders(database) const response = await fetch(`${serverAddress}/visa/membership/portal`, { method: 'POST', headers, body: JSON.stringify({ return_url: returnUrl }), }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.detail || 'Failed to get Stripe portal link') } const data = await response.json() return data.portalLink } catch (error) { console.log('Error getting Stripe portal link:', error) throw error } }, [serverAddress] ) const getSession = useCallback(async () => { const { data: { session }, } = await database.current.auth.getSession() return session }, []) const value = useMemo( () => ({ profile, emailToVerify, isSignedIn, loading, signIn, signOut, verifyOtp, updateAccount, deleteAccount, getStripePortalLink, getSession, membershipState, }), [ profile, emailToVerify, isSignedIn, loading, signIn, signOut, verifyOtp, updateAccount, deleteAccount, getStripePortalLink, getSession, membershipState, ] ) return ( {children} ) }