learning_ai_invt_trdg/mobile/providers/MobileAuthProvider.tsx

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;
}