diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index a7d0ee4..c70f00a 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -2,15 +2,11 @@
import { useEffect } from "react";
import { useRouter } from "next/navigation";
-import { useAuth } from "@/hooks/use-auth";
+import { AuthProvider, useAuth } from "@/hooks/use-auth";
import { Sidebar } from "@/components/layout/sidebar";
import { Header } from "@/components/layout/header";
-export default function DashboardLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
+function DashboardShell({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const router = useRouter();
@@ -31,9 +27,7 @@ export default function DashboardLayout({
);
}
- if (!user) {
- return null;
- }
+ if (!user) return null;
return (
@@ -45,3 +39,15 @@ export default function DashboardLayout({
);
}
+
+export default function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/settings/tag-manager.tsx b/src/components/settings/tag-manager.tsx
index 9318775..db67d14 100644
--- a/src/components/settings/tag-manager.tsx
+++ b/src/components/settings/tag-manager.tsx
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Plus, X, Loader2 } from 'lucide-react';
import { createClient } from '@/lib/supabase/client';
+import { useAuth } from '@/hooks/use-auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -31,6 +32,7 @@ const PRESET_COLORS = [
export function TagManager() {
const supabase = createClient();
+ const { user, loading: authLoading } = useAuth();
const [loading, setLoading] = useState(true);
const [tags, setTags] = useState([]);
@@ -43,19 +45,23 @@ export function TagManager() {
const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[3].value);
useEffect(() => {
- fetchTags();
- }, []);
+ if (authLoading) return;
+ if (!user) {
+ setLoading(false);
+ return;
+ }
+ fetchTags(user.id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [authLoading, user?.id]);
- async function fetchTags() {
+ async function fetchTags(userId: string) {
try {
setLoading(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
- if (!user) return;
const { data, error } = await supabase
.from('tags')
.select('*')
- .eq('user_id', user.id)
+ .eq('user_id', userId)
.order('created_at', { ascending: true });
if (error) throw error;
@@ -76,7 +82,6 @@ export function TagManager() {
try {
setSaving(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
if (!user) {
toast.error('Not authenticated');
return;
@@ -96,7 +101,7 @@ export function TagManager() {
setDialogOpen(false);
setNewTagName('');
setSelectedColor(PRESET_COLORS[3].value);
- await fetchTags();
+ if (user) await fetchTags(user.id);
} catch (err) {
console.error('Create error:', err);
toast.error('Failed to create tag');
diff --git a/src/components/settings/template-manager.tsx b/src/components/settings/template-manager.tsx
index 89945a8..70d9a77 100644
--- a/src/components/settings/template-manager.tsx
+++ b/src/components/settings/template-manager.tsx
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Plus, Trash2, Loader2 } from 'lucide-react';
import { createClient } from '@/lib/supabase/client';
+import { useAuth } from '@/hooks/use-auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -63,6 +64,7 @@ const emptyForm: TemplateFormData = {
export function TemplateManager() {
const supabase = createClient();
+ const { user, loading: authLoading } = useAuth();
const [loading, setLoading] = useState(true);
const [templates, setTemplates] = useState([]);
@@ -71,19 +73,23 @@ export function TemplateManager() {
const [form, setForm] = useState(emptyForm);
useEffect(() => {
- fetchTemplates();
- }, []);
+ if (authLoading) return;
+ if (!user) {
+ setLoading(false);
+ return;
+ }
+ fetchTemplates(user.id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [authLoading, user?.id]);
- async function fetchTemplates() {
+ async function fetchTemplates(userId: string) {
try {
setLoading(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
- if (!user) return;
const { data, error } = await supabase
.from('message_templates')
.select('*')
- .eq('user_id', user.id)
+ .eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
@@ -108,7 +114,6 @@ export function TemplateManager() {
try {
setSaving(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
if (!user) {
toast.error('Not authenticated');
return;
@@ -134,7 +139,7 @@ export function TemplateManager() {
toast.success('Template created successfully');
setDialogOpen(false);
setForm(emptyForm);
- await fetchTemplates();
+ if (user) await fetchTemplates(user.id);
} catch (err) {
console.error('Save error:', err);
toast.error('Failed to create template');
diff --git a/src/components/settings/whatsapp-config.tsx b/src/components/settings/whatsapp-config.tsx
index 9b7a8ad..fc63253 100644
--- a/src/components/settings/whatsapp-config.tsx
+++ b/src/components/settings/whatsapp-config.tsx
@@ -13,6 +13,7 @@ import {
Zap,
} from 'lucide-react';
import { createClient } from '@/lib/supabase/client';
+import { useAuth } from '@/hooks/use-auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -30,6 +31,7 @@ const MASKED_TOKEN = '••••••••••••••••';
export function WhatsAppConfig() {
const supabase = createClient();
+ const { user, loading: authLoading } = useAuth();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -50,19 +52,23 @@ export function WhatsAppConfig() {
: '';
useEffect(() => {
- fetchConfig();
- }, []);
+ if (authLoading) return;
+ if (!user) {
+ setLoading(false);
+ return;
+ }
+ fetchConfig(user.id);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [authLoading, user?.id]);
- async function fetchConfig() {
+ async function fetchConfig(userId: string) {
try {
setLoading(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
- if (!user) return;
const { data, error } = await supabase
.from('whatsapp_config')
.select('*')
- .eq('user_id', user.id)
+ .eq('user_id', userId)
.single();
if (error && error.code !== 'PGRST116') {
@@ -97,7 +103,6 @@ export function WhatsAppConfig() {
try {
setSaving(true);
- const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
if (!user) {
toast.error('Not authenticated');
return;
@@ -134,7 +139,7 @@ export function WhatsAppConfig() {
}
toast.success('Configuration saved successfully');
- await fetchConfig();
+ if (user) await fetchConfig(user.id);
} catch (err) {
console.error('Save error:', err);
toast.error('Failed to save configuration');
diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.tsx
similarity index 52%
rename from src/hooks/use-auth.ts
rename to src/hooks/use-auth.tsx
index 8d353ae..5085b04 100644
--- a/src/hooks/use-auth.ts
+++ b/src/hooks/use-auth.tsx
@@ -1,6 +1,13 @@
"use client";
-import { useEffect, useState, useCallback } from "react";
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ useCallback,
+ type ReactNode,
+} from "react";
import { createClient } from "@/lib/supabase/client";
import type { User } from "@supabase/supabase-js";
@@ -11,7 +18,21 @@ interface Profile {
avatar_url: string | null;
}
-export function useAuth() {
+interface AuthContextValue {
+ user: User | null;
+ profile: Profile | null;
+ loading: boolean;
+ signOut: () => Promise;
+}
+
+const AuthContext = createContext(null);
+
+/**
+ * AuthProvider — wrap this around the dashboard layout.
+ * Makes ONE getSession() call for the whole tree instead of one per
+ * component, avoiding internal lock contention in the Supabase client.
+ */
+export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
@@ -20,18 +41,15 @@ export function useAuth() {
const supabase = createClient();
let mounted = true;
- // Safety net: 3s is plenty for localStorage reads. If it hangs here,
- // something else is wrong (likely storage permissions in an iframe).
const safetyTimer = setTimeout(() => {
if (mounted) {
- console.warn("[useAuth] getSession() timed out after 3s, clearing loading state");
+ console.warn("[AuthProvider] getSession() timed out after 3s");
setLoading(false);
}
}, 3000);
const fetchProfile = async (userId: string) => {
try {
- // profiles.user_id (NOT profiles.id) references auth.users.id
const { data, error } = await supabase
.from("profiles")
.select("id, full_name, email, avatar_url")
@@ -39,7 +57,7 @@ export function useAuth() {
.maybeSingle();
if (error) {
- console.error("[useAuth] fetchProfile error:", {
+ console.error("[AuthProvider] fetchProfile error:", {
message: error.message,
details: error.details,
hint: error.hint,
@@ -48,38 +66,32 @@ export function useAuth() {
return;
}
- if (data && mounted) {
- setProfile(data);
- }
+ if (data && mounted) setProfile(data);
} catch (err) {
- console.error("[useAuth] fetchProfile threw:", err);
+ console.error("[AuthProvider] fetchProfile threw:", err);
}
};
const init = async () => {
try {
- // getSession() reads from localStorage — instant, no network call.
- // The middleware already validates the JWT server-side with getUser()
- // on every request, and RLS enforces authorization at the DB level,
- // so the client can trust the local session for UI purposes.
const {
data: { session },
error,
} = await supabase.auth.getSession();
- if (error) {
- console.error("[useAuth] getSession error:", error.message);
- }
+ if (error) console.error("[AuthProvider] getSession error:", error.message);
if (!mounted) return;
const currentUser = session?.user ?? null;
setUser(currentUser);
if (currentUser) {
- await fetchProfile(currentUser.id);
+ // Don't block loading on profile fetch — let the UI render
+ // with the user info we already have, profile enriches async.
+ fetchProfile(currentUser.id);
}
} catch (err) {
- console.error("[useAuth] init threw:", err);
+ console.error("[AuthProvider] init threw:", err);
} finally {
if (mounted) setLoading(false);
clearTimeout(safetyTimer);
@@ -90,13 +102,13 @@ export function useAuth() {
const {
data: { subscription },
- } = supabase.auth.onAuthStateChange(async (_event, session) => {
+ } = supabase.auth.onAuthStateChange((_event, session) => {
if (!mounted) return;
const currentUser = session?.user ?? null;
setUser(currentUser);
if (currentUser) {
- await fetchProfile(currentUser.id);
+ fetchProfile(currentUser.id);
} else {
setProfile(null);
}
@@ -109,8 +121,6 @@ export function useAuth() {
clearTimeout(safetyTimer);
subscription.unsubscribe();
};
- // Intentionally run once on mount — createClient() is a singleton
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const signOut = useCallback(async () => {
@@ -121,5 +131,30 @@ export function useAuth() {
window.location.href = "/login";
}, []);
- return { user, profile, loading, signOut };
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * useAuth — read the shared auth state from context.
+ * Must be used inside an .
+ */
+export function useAuth(): AuthContextValue {
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ // Fallback for components rendered outside the provider (shouldn't
+ // happen in normal flow, but don't crash the page).
+ return {
+ user: null,
+ profile: null,
+ loading: false,
+ signOut: async () => {
+ window.location.href = "/login";
+ },
+ };
+ }
+ return ctx;
}