Files
falah-mobile/src/lib/AuthContext.tsx
T

215 lines
5.7 KiB
TypeScript

"use client";
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
interface User {
id: string;
email: string;
name: string;
isPremium: boolean;
isPro: boolean;
flhBalance: number;
dailyMsgCount: number;
preferredName?: string;
coachPersona?: string;
madhab?: string;
coachingGoals?: string;
createdAt?: string;
trialEndsAt?: string;
}
interface StreakData {
currentStreak: number;
longestStreak: number;
lastClaimDate: string | null;
canClaim: boolean;
streakFreeze: number;
todayReward: number;
}
interface XpData {
level: number;
xp: number;
xpToNext: number;
totalXp: number;
}
interface AuthContextType {
user: User | null;
token: string | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, name: string, password: string) => Promise<void>;
oauthLogin: (provider: string) => Promise<void>;
logout: () => void;
refreshUser: () => Promise<void>;
streak: StreakData | null;
xp: XpData | null;
refreshStreak: () => Promise<void>;
refreshXp: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [streak, setStreak] = useState<StreakData | null>(null);
const [xp, setXp] = useState<XpData | null>(null);
useEffect(() => {
const stored = localStorage.getItem("flh_token");
if (stored) {
setToken(stored);
fetchUser(stored);
} else {
setLoading(false);
}
// Listen for token changes (e.g., from OAuth popup)
const handleStorage = (e: StorageEvent) => {
if (e.key === "flh_token" && e.newValue) {
setToken(e.newValue);
fetchUser(e.newValue);
}
};
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, []);
const fetchUser = async (t: string) => {
try {
const res = await fetch("/mobile/api/auth/me", {
headers: { Authorization: `Bearer ${t}` },
});
if (res.ok) {
const data = await res.json();
setUser(data.user);
} else {
localStorage.removeItem("flh_token");
setToken(null);
}
} catch {
// ignore
} finally {
setLoading(false);
}
};
const login = useCallback(async (email: string, password: string) => {
const res = await fetch("/mobile/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Login failed");
}
const data = await res.json();
localStorage.setItem("flh_token", data.token);
setToken(data.token);
setUser(data.user);
}, []);
const register = useCallback(async (email: string, name: string, password: string) => {
const res = await fetch("/mobile/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, name, password }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Registration failed");
}
const data = await res.json();
localStorage.setItem("flh_token", data.token);
setToken(data.token);
setUser(data.user);
}, []);
const oauthLogin = useCallback(async (provider: string) => {
// Open a popup window for OAuth
const width = 600;
const height = 700;
const left = window.screenX + (window.innerWidth - width) / 2;
const top = window.screenY + (window.innerHeight - height) / 2;
const popup = window.open(
`/mobile/api/auth/${provider}`,
`oauth-${provider}`,
`width=${width},height=${height},left=${left},top=${top},popup=1`
);
if (!popup) {
// Popup blocked — fall back to direct navigation
window.location.href = `/mobile/api/auth/${provider}`;
}
// The popup will close itself after successful auth via the callback page
}, []);
const logout = useCallback(() => {
localStorage.removeItem("flh_token");
setToken(null);
setUser(null);
setStreak(null);
setXp(null);
}, []);
const refreshUser = useCallback(async () => {
if (!token) return;
await fetchUser(token);
}, [token]);
const refreshStreak = useCallback(async () => {
if (!token) return;
try {
const res = await fetch("/mobile/api/gamification/streak", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setStreak(data);
}
} catch {
// ignore
}
}, [token]);
const refreshXp = useCallback(async () => {
if (!token) return;
try {
const res = await fetch("/mobile/api/gamification/xp", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setXp(data);
}
} catch {
// ignore
}
}, [token]);
// Fetch streak and XP when token becomes available
useEffect(() => {
if (token && user) {
refreshStreak();
refreshXp();
}
}, [token, user, refreshStreak, refreshXp]);
return (
<AuthContext.Provider value={{ user, token, loading, login, register, oauthLogin, logout, refreshUser, streak, xp, refreshStreak, refreshXp }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}