Download frontend/src/AuthContext.js from KSS-1227/TradeMind: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/KSS-1227/TradeMind/resolve/main/frontend/src/AuthContext.js
- Command line
-
hf download hf://spaces/KSS-1227/TradeMind/frontend/src/AuthContext.js
-
curl -L -o AuthContext.js https://huggingface.co/spaces/KSS-1227/TradeMind/resolve/main/frontend/src/AuthContext.js
11.9 kB
| // frontend/src/AuthContext.js | |
| import { createContext, useContext, useEffect, useState, useCallback } from "react"; | |
| import axios from "axios"; | |
| import { supabase } from "./supabaseClient"; | |
| import { normalizeWhatsAppNumber } from "./auth/validators"; | |
| const AuthContext = createContext(null); | |
| const HF_API = "https://kss-1227-trademind.hf.space"; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Supabase error β user-facing message map | |
| // We match on the raw message string because Supabase JS v2 does | |
| // not expose a stable numeric error code for auth errors. | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function mapSupabaseAuthError(error) { | |
| if (!error) return "An unexpected error occurred. Please try again."; | |
| const msg = (error.message || "").toLowerCase(); | |
| // ββ Sign-up errors βββββββββββββββββββββββββββββββββββββββ | |
| if (msg.includes("user already registered")) | |
| return "An account with this email already exists. Try signing in instead."; | |
| if (msg.includes("email rate limit") || msg.includes("over_email_send_rate_limit")) | |
| return "Too many signup attempts. Please wait a few minutes before trying again."; | |
| if (msg.includes("signup is disabled") || msg.includes("signups not allowed")) | |
| return "New signups are currently disabled. Please contact support."; | |
| if (msg.includes("email already in use") || msg.includes("email taken")) | |
| return "This email is already registered. Please sign in or use a different email."; | |
| if (msg.includes("password should be at least")) | |
| return "Your password is too short. Please use at least 8 characters."; | |
| if (msg.includes("unable to validate email address")) | |
| return "This email address doesn't appear to be valid."; | |
| // ββ Sign-in errors βββββββββββββββββββββββββββββββββββββββ | |
| if (msg.includes("invalid login credentials") || msg.includes("invalid credentials")) | |
| return "Incorrect email or password. Please try again."; | |
| if (msg.includes("email not confirmed")) | |
| return "Please confirm your email before signing in. Check your inbox."; | |
| if (msg.includes("too many requests") || msg.includes("rate limit")) | |
| return "Too many attempts. Please wait a moment and try again."; | |
| // ββ Generic fallback β show the raw message so we can debug β | |
| return error.message || "An unexpected error occurred. Please try again."; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Provider | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function AuthProvider({ children }) { | |
| const [session, setSession] = useState(null); | |
| const [profile, setProfile] = useState(null); | |
| const [loading, setLoading] = useState(true); | |
| // ββ fetchProfile βββββββββββββββββββββββββββββββββββββββββ | |
| // Uses .maybeSingle() instead of .single() so that a missing | |
| // row returns { data: null, error: null } instead of a | |
| // PGRST116 error β safe for new users with no profile yet. | |
| const fetchProfile = useCallback(async (userId) => { | |
| if (!userId) { | |
| setProfile(null); | |
| return; | |
| } | |
| const { data, error } = await supabase | |
| .from("profiles") | |
| .select("*") | |
| .eq("id", userId) | |
| .maybeSingle(); | |
| if (error) { | |
| console.warn("[AuthContext] fetchProfile failed:", error.code, error.message); | |
| setProfile(null); | |
| } else { | |
| setProfile(data ?? null); | |
| } | |
| }, []); | |
| // ββ Session bootstrap βββββββββββββββββββββββββββββββββββββ | |
| useEffect(() => { | |
| supabase.auth.getSession().then(({ data: { session } }) => { | |
| setSession(session); | |
| if (session?.user) fetchProfile(session.user.id); | |
| setLoading(false); | |
| }); | |
| const { data: listener } = supabase.auth.onAuthStateChange((event, session) => { | |
| setSession(session); | |
| if (session?.user) { | |
| fetchProfile(session.user.id); | |
| } else { | |
| setProfile(null); | |
| } | |
| }); | |
| return () => listener.subscription.unsubscribe(); | |
| }, [fetchProfile]); | |
| // ββ createProfile βββββββββββββββββββββββββββββββββββββββββ | |
| // Separated so it can be called after confirmed sign-in too. | |
| // NEVER throws β profile failure must not block auth flow. | |
| const createProfile = async ({ userId, fullName, username, email, whatsappNumber }) => { | |
| const normalized = whatsappNumber ? normalizeWhatsAppNumber(whatsappNumber) : null; | |
| const payload = { | |
| id: userId, | |
| full_name: fullName, | |
| username, | |
| email, | |
| whatsapp_number: normalized ?? whatsappNumber ?? null, | |
| }; | |
| console.log("[AuthContext] createProfile payload:", payload); | |
| const { data, error } = await supabase | |
| .from("profiles") | |
| .upsert(payload) | |
| .select() | |
| .maybeSingle(); | |
| if (error) { | |
| console.error("[AuthContext] Profile upsert FAILED:", { | |
| code: error.code, status: error.status, message: error.message, details: error.details, | |
| }); | |
| } else { | |
| console.log("[AuthContext] Profile upsert saved:", data); | |
| } | |
| }; | |
| // ββ sendWelcomeWhatsApp βββββββββββββββββββββββββββββββββββ | |
| // Fire-and-forget after signup. Never throws, never blocks auth flow. | |
| const sendWelcomeWhatsApp = async (whatsappNumber) => { | |
| const normalized = normalizeWhatsAppNumber(whatsappNumber); | |
| if (!normalized) return; | |
| try { | |
| await axios.post(`${HF_API}/whatsapp/welcome`, { phone: normalized }); | |
| } catch (e) { | |
| // Log only β signup must not fail because of this | |
| console.warn("[AuthContext] Welcome WhatsApp failed:", e.message); | |
| } | |
| }; | |
| // ββ signUp ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Production-safe flow: | |
| // | |
| // Case A β email confirmation DISABLED (session returned immediately): | |
| // β Create profile right away (session exists, RLS passes). | |
| // | |
| // Case B β email confirmation ENABLED (session is null): | |
| // β Skip profile creation entirely. | |
| // β Profile is created on first sign-in via onAuthStateChange | |
| // β fetchProfile (and a separate ensureProfile call if needed). | |
| // | |
| // Profile failure in Case A is LOGGED but does NOT fail signup. | |
| const signUp = async ({ fullName, username, email, password, whatsappNumber }) => { | |
| const { data, error } = await supabase.auth.signUp({ | |
| email, | |
| password, | |
| options: { | |
| data: { | |
| full_name: fullName, | |
| username, | |
| whatsapp_number: normalizeWhatsAppNumber(whatsappNumber) ?? whatsappNumber, | |
| }, | |
| }, | |
| }); | |
| if (error) { | |
| console.error("[AuthContext] signUp error:", { | |
| code: error.code, | |
| status: error.status, | |
| message: error.message, | |
| }); | |
| return { success: false, error }; | |
| } | |
| if (data.session) { | |
| await createProfile({ | |
| userId: data.user.id, | |
| fullName, | |
| username, | |
| email, | |
| whatsappNumber, | |
| }); | |
| // Refresh in-memory profile AFTER the row is written. | |
| // Without this, the onAuthStateChange fetchProfile() races ahead | |
| // of createProfile() and reads a null/incomplete row. | |
| await fetchProfile(data.user.id); | |
| // Welcome message β fire and forget, never blocks signup | |
| sendWelcomeWhatsApp(whatsappNumber); | |
| } | |
| return { success: true, data }; | |
| }; | |
| // ββ signIn ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const signIn = async (email, password, rememberMe = true) => { | |
| if (!rememberMe) { | |
| sessionStorage.setItem("tm_no_persist", "1"); | |
| } else { | |
| sessionStorage.removeItem("tm_no_persist"); | |
| } | |
| const { data, error } = await supabase.auth.signInWithPassword({ email, password }); | |
| if (error) { | |
| console.error("[AuthContext] signIn error:", { | |
| code: error.code, | |
| status: error.status, | |
| message: error.message, | |
| }); | |
| return { data: null, error }; | |
| } | |
| // Ensure profile exists β covers: | |
| // β’ Users who signed up with email confirmation (profile skipped at signup) | |
| // β’ Users whose profile insert failed at signup | |
| if (data.user) { | |
| const { data: existing } = await supabase | |
| .from("profiles") | |
| .select("id") | |
| .eq("id", data.user.id) | |
| .maybeSingle(); | |
| if (!existing) { | |
| await createProfile({ | |
| userId: data.user.id, | |
| fullName: data.user.user_metadata?.full_name ?? "", | |
| username: data.user.user_metadata?.username ?? "", | |
| email: data.user.email ?? email, | |
| whatsappNumber: data.user.user_metadata?.whatsapp_number ?? "", | |
| }); | |
| } | |
| } | |
| return { data, error: null }; | |
| }; | |
| // ββ signOut βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const signOut = async () => { | |
| sessionStorage.removeItem("tm_no_persist"); | |
| await supabase.auth.signOut(); | |
| }; | |
| // ββ forgotPassword ββββββββββββββββββββββββββββββββββββββββ | |
| const forgotPassword = async (email) => { | |
| return supabase.auth.resetPasswordForEmail(email, { | |
| redirectTo: window.location.origin, | |
| }); | |
| }; | |
| // ββ updatePassword ββββββββββββββββββββββββββββββββββββββββ | |
| const updatePassword = async (password) => { | |
| return supabase.auth.updateUser({ password }); | |
| }; | |
| // ββ updateProfile βββββββββββββββββββββββββββββββββββββββββ | |
| const updateProfile = async (fields) => { | |
| if (!session?.user) return { error: new Error("Not signed in") }; | |
| const { data, error } = await supabase | |
| .from("profiles") | |
| .update(fields) | |
| .eq("id", session.user.id) | |
| .select() | |
| .single(); | |
| if (!error) setProfile(data); | |
| return { data, error }; | |
| }; | |
| // ββ refreshProfile ββββββββββββββββββββββββββββββββββββββββ | |
| const refreshProfile = async () => { | |
| if (session?.user) await fetchProfile(session.user.id); | |
| }; | |
| const value = { | |
| session, | |
| user: session?.user ?? null, | |
| profile, | |
| loading, | |
| isAuthenticated: !!session, | |
| signIn, | |
| signUp, | |
| signOut, | |
| forgotPassword, | |
| updatePassword, | |
| updateProfile, | |
| refreshProfile, | |
| }; | |
| return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | |
| } | |
| export function useAuth() { | |
| const ctx = useContext(AuthContext); | |
| if (!ctx) throw new Error("useAuth() must be used inside <AuthProvider>"); | |
| return ctx; | |
| } | |