TradeMind / frontend /src /AuthContext.js
KSS-1227's picture
fix: fetch profile after createProfile to resolve race condition causing null whatsapp_number
9a5d714
Raw History Blame Contribute Delete
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;
}