201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import type { Session, User } from '@supabase/supabase-js';
|
|
import { mobileSupabase } from '@/lib/supabase';
|
|
import { clearMobileSessionStorage } from '@/lib/secureSessionStorage';
|
|
import { tableNameUsers } from '@/lib/tables';
|
|
import { mobileTelemetry, trackMobileError } from '@/lib/telemetry';
|
|
|
|
export interface MobileUserProfile {
|
|
user_id: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
email?: string;
|
|
role?: string;
|
|
trade_enable?: boolean;
|
|
}
|
|
|
|
interface MobileAuthContextValue {
|
|
session: Session | null;
|
|
user: User | null;
|
|
profile: MobileUserProfile | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
signIn: (email: string, password: string) => Promise<{ error?: string }>;
|
|
signOut: () => Promise<void>;
|
|
invalidateSession: (reason: string) => Promise<void>;
|
|
refreshProfile: () => Promise<void>;
|
|
accessToken: string | null;
|
|
}
|
|
|
|
const MobileAuthContext = createContext<MobileAuthContextValue | null>(null);
|
|
|
|
export function MobileAuthProvider({ children }: { children: ReactNode }) {
|
|
const [session, setSession] = useState<Session | null>(null);
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [profile, setProfile] = useState<MobileUserProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
|
|
async function bootstrap() {
|
|
try {
|
|
const { data } = await mobileSupabase.auth.getSession();
|
|
if (!active) {
|
|
return;
|
|
}
|
|
setSession(data.session ?? null);
|
|
setUser(data.session?.user ?? null);
|
|
if (data.session?.user) {
|
|
mobileTelemetry.trackEvent('info', 'auth', 'session_restored', {
|
|
userId: data.session.user.id,
|
|
});
|
|
await fetchProfile(data.session.user.id);
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
} catch (bootstrapError) {
|
|
trackMobileError('auth', 'session_restore_failed', bootstrapError);
|
|
setError(bootstrapError instanceof Error ? bootstrapError.message : 'Failed to restore session');
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
void bootstrap();
|
|
|
|
const { data: authListener } = mobileSupabase.auth.onAuthStateChange((event, nextSession) => {
|
|
setSession(nextSession);
|
|
setUser(nextSession?.user ?? null);
|
|
if (nextSession?.user) {
|
|
mobileTelemetry.trackEvent('info', 'auth', 'session_changed', {
|
|
userId: nextSession.user.id,
|
|
});
|
|
void fetchProfile(nextSession.user.id);
|
|
} else {
|
|
if (event === 'SIGNED_OUT') {
|
|
mobileTelemetry.trackEvent('info', 'auth', 'session_signed_out');
|
|
}
|
|
setProfile(null);
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
active = false;
|
|
authListener.subscription.unsubscribe();
|
|
};
|
|
}, []);
|
|
|
|
async function fetchProfile(userId: string) {
|
|
try {
|
|
const { data, error: profileError } = await mobileSupabase
|
|
.from(tableNameUsers)
|
|
.select('user_id,first_name,last_name,email,role,trade_enable')
|
|
.eq('user_id', userId)
|
|
.single();
|
|
|
|
if (profileError) {
|
|
setError(profileError.message);
|
|
trackMobileError('auth', 'profile_load_failed', profileError, { userId });
|
|
} else {
|
|
setProfile((data || null) as MobileUserProfile | null);
|
|
setError(null);
|
|
}
|
|
} catch (fetchError) {
|
|
setError(fetchError instanceof Error ? fetchError.message : 'Failed to load profile');
|
|
trackMobileError('auth', 'profile_load_failed', fetchError, { userId });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function signIn(email: string, password: string) {
|
|
setLoading(true);
|
|
setError(null);
|
|
const { error: authError } = await mobileSupabase.auth.signInWithPassword({ email, password });
|
|
if (authError) {
|
|
setError(authError.message);
|
|
trackMobileError('auth', 'sign_in_failed', authError, { email });
|
|
setLoading(false);
|
|
return { error: authError.message };
|
|
}
|
|
mobileTelemetry.trackEvent('info', 'auth', 'sign_in_succeeded', {
|
|
message: email,
|
|
});
|
|
return {};
|
|
}
|
|
|
|
async function signOut() {
|
|
setLoading(true);
|
|
try {
|
|
await mobileSupabase.auth.signOut();
|
|
mobileTelemetry.trackEvent('info', 'auth', 'sign_out_succeeded', {
|
|
userId: user?.id,
|
|
});
|
|
} finally {
|
|
await clearMobileSessionStorage();
|
|
setProfile(null);
|
|
setSession(null);
|
|
setUser(null);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function invalidateSession(reason: string) {
|
|
setLoading(true);
|
|
mobileTelemetry.trackEvent('warn', 'auth', 'session_invalidated', {
|
|
message: reason,
|
|
userId: user?.id,
|
|
});
|
|
|
|
try {
|
|
await mobileSupabase.auth.signOut({ scope: 'local' });
|
|
} catch (invalidateError) {
|
|
trackMobileError('auth', 'session_invalidation_signout_failed', invalidateError);
|
|
} finally {
|
|
await clearMobileSessionStorage();
|
|
setProfile(null);
|
|
setSession(null);
|
|
setUser(null);
|
|
setError(reason);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function refreshProfile() {
|
|
if (!user) {
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
await fetchProfile(user.id);
|
|
}
|
|
|
|
const value = useMemo<MobileAuthContextValue>(
|
|
() => ({
|
|
session,
|
|
user,
|
|
profile,
|
|
loading,
|
|
error,
|
|
signIn,
|
|
signOut,
|
|
invalidateSession,
|
|
refreshProfile,
|
|
accessToken: session?.access_token ?? null,
|
|
}),
|
|
[session, user, profile, loading, error]
|
|
);
|
|
|
|
return <MobileAuthContext.Provider value={value}>{children}</MobileAuthContext.Provider>;
|
|
}
|
|
|
|
export function useMobileAuth() {
|
|
const context = useContext(MobileAuthContext);
|
|
if (!context) {
|
|
throw new Error('useMobileAuth must be used within a MobileAuthProvider');
|
|
}
|
|
return context;
|
|
}
|