201 lines
5.6 KiB
TypeScript
201 lines
5.6 KiB
TypeScript
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { mobileTelemetry, trackMobileError } from '@/lib/telemetry';
|
|
import {
|
|
clearPlatformSession,
|
|
loginPlatformSession,
|
|
restorePlatformSession,
|
|
type PlatformAuthUser,
|
|
type StoredPlatformSession,
|
|
} from '@/lib/platformAuth';
|
|
|
|
export interface MobileUserProfile {
|
|
user_id: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
email?: string;
|
|
role?: string;
|
|
trade_enable?: boolean;
|
|
}
|
|
|
|
interface MobileAuthContextValue {
|
|
session: StoredPlatformSession | null;
|
|
user: PlatformAuthUser | 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<StoredPlatformSession | null>(null);
|
|
const [user, setUser] = useState<PlatformAuthUser | 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 {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
const restored = await restorePlatformSession();
|
|
setSession(restored);
|
|
setUser(restored?.user ?? null);
|
|
if (restored?.user) {
|
|
mobileTelemetry.trackEvent('info', 'auth', 'session_restored', {
|
|
userId: restored.user.id,
|
|
});
|
|
setProfile(buildProfile(restored.user));
|
|
setError(null);
|
|
setLoading(false);
|
|
} 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();
|
|
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
function buildProfile(nextUser: PlatformAuthUser | null): MobileUserProfile | null {
|
|
if (!nextUser?.id) {
|
|
return null;
|
|
}
|
|
|
|
const displayName = String(nextUser.displayName || '').trim();
|
|
const parts = displayName ? displayName.split(/\s+/) : [];
|
|
|
|
return {
|
|
user_id: nextUser.id,
|
|
first_name: parts[0] || undefined,
|
|
last_name: parts.slice(1).join(' ') || undefined,
|
|
email: nextUser.email,
|
|
role: nextUser.role,
|
|
trade_enable: true,
|
|
};
|
|
}
|
|
|
|
async function signIn(email: string, password: string) {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const nextSession = await loginPlatformSession(email, password);
|
|
setSession(nextSession);
|
|
setUser(nextSession.user);
|
|
setProfile(buildProfile(nextSession.user));
|
|
setError(null);
|
|
mobileTelemetry.trackEvent('info', 'auth', 'sign_in_succeeded', {
|
|
message: email,
|
|
userId: nextSession.user.id,
|
|
});
|
|
return {};
|
|
} catch (authError) {
|
|
const message = authError instanceof Error ? authError.message : 'Sign in failed';
|
|
setError(message);
|
|
trackMobileError('auth', 'sign_in_failed', authError, { email });
|
|
return { error: message };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function signOut() {
|
|
setLoading(true);
|
|
try {
|
|
await clearPlatformSession();
|
|
mobileTelemetry.trackEvent('info', 'auth', 'sign_out_succeeded', {
|
|
userId: user?.id,
|
|
});
|
|
} finally {
|
|
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 clearPlatformSession();
|
|
} catch (invalidateError) {
|
|
trackMobileError('auth', 'session_invalidation_signout_failed', invalidateError);
|
|
} finally {
|
|
setProfile(null);
|
|
setSession(null);
|
|
setUser(null);
|
|
setError(reason);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function refreshProfile() {
|
|
if (!session) {
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const restored = await restorePlatformSession();
|
|
const nextUser = restored?.user ?? null;
|
|
setSession(restored);
|
|
setUser(nextUser);
|
|
setProfile(buildProfile(nextUser));
|
|
setError(null);
|
|
} catch (refreshError) {
|
|
setError(refreshError instanceof Error ? refreshError.message : 'Failed to refresh profile');
|
|
trackMobileError('auth', 'profile_refresh_failed', refreshError, { userId: user?.id || 'unknown' });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
const value = useMemo<MobileAuthContextValue>(
|
|
() => ({
|
|
session,
|
|
user,
|
|
profile,
|
|
loading,
|
|
error,
|
|
signIn,
|
|
signOut,
|
|
invalidateSession,
|
|
refreshProfile,
|
|
accessToken: session?.accessToken ?? 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;
|
|
}
|