From 5456deb727e91f82b03ec4327dcc7b80099d7f06 Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Fri, 27 Feb 2026 15:38:47 +0000 Subject: [PATCH] feat: Refactor authentication to leverage AppContext, removing redundant API calls and simplifying loading state. --- web/src/components/auth/RequireAuth.tsx | 22 +-- web/src/context/AppContext.tsx | 232 +++++++++--------------- 2 files changed, 95 insertions(+), 159 deletions(-) diff --git a/web/src/components/auth/RequireAuth.tsx b/web/src/components/auth/RequireAuth.tsx index 86e4636..e5e487e 100644 --- a/web/src/components/auth/RequireAuth.tsx +++ b/web/src/components/auth/RequireAuth.tsx @@ -1,20 +1,14 @@ -import { useEffect, useState } from 'react' +import { useApp } from '../../context/AppContext' import { Navigate } from 'react-router-dom' -import { getUser } from '../../lib/auth' +/** + * Protege rotas que requerem autenticação. + * Usa o AppContext (que já tem o estado auth sincronizado com Supabase) + * para evitar uma chamada de rede extra que causava blank screen. + */ export function RequireAuth({ children }: { children: React.ReactNode }) { - const [loading, setLoading] = useState(true) - const [ok, setOk] = useState(false) + const { user } = useApp() - useEffect(() => { - ;(async () => { - const user = await getUser() - setOk(!!user) - setLoading(false) - })() - }, []) - - if (loading) return null - if (!ok) return + if (!user) return return <>{children} } diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx index cc611db..a5e342d 100644 --- a/web/src/context/AppContext.tsx +++ b/web/src/context/AppContext.tsx @@ -1,9 +1,6 @@ /** * @file AppContext.tsx * @description Gestor de Estado Global (Context API) da aplicação Web. - * Este ficheiro gere uma arquitetura híbrida: coordena a Autenticação e Perfis (Profiles) - * diretamente com o Supabase, enquanto mantém as entidades transacionais - * (Shops, Appointments, Cart) num regime de persistência local 'mockada' via LocalStorage. */ import React, { createContext, useContext, useEffect, useState } from 'react'; import { nanoid } from 'nanoid'; @@ -51,7 +48,7 @@ type AppContextValue = State & { const initialState: State = { user: undefined, users: mockUsers, - shops: [], // Removes mockShops integration + shops: [], appointments: [], orders: [], cart: [], @@ -61,16 +58,7 @@ const initialState: State = { const AppContext = createContext(undefined); export const AppProvider = ({ children }: { children: React.ReactNode }) => { - // Se já há sessão guardada, não mostra loading (app aparece de imediato) - const hasStoredUser = Boolean((() => { - try { - const raw = localStorage.getItem('smart-agenda'); - if (!raw) return false; - const parsed = JSON.parse(raw); - return parsed?.user != null; - } catch { return false; } - })()); - const [loading, setLoading] = useState(!hasStoredUser); + const [loading, setLoading] = useState(true); const [state, setState] = useState(() => { const stored = storage.get | null>('smart-agenda', null); const safeStored = stored && typeof stored === 'object' ? stored : {}; @@ -86,7 +74,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { ...safeStored, user: safeUser, users: safeUsers, - shops: [], // Start empty, will be populated by refreshShops + shops: [], appointments: safeAppointments, orders: safeOrders, cart: safeCart, @@ -95,30 +83,41 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }); const refreshShops = async () => { - const { data: shopsData, error: shopsError } = await supabase.from('shops').select('*'); - if (shopsError) return; + try { + const { data: shopsData, error: shopsError } = await supabase.from('shops').select('*'); + if (shopsError || !shopsData) return; - const { data: servicesData, error: servicesError } = await supabase.from('services').select('*'); - if (servicesError) return; + const { data: servicesData } = await supabase.from('services').select('*'); - const fetchedShops = shopsData.map((shop) => ({ - ...shop, - services: servicesData.filter((s) => s.shop_id === shop.id), - products: [], - barbers: [], - })); + const fetchedShops: BarberShop[] = shopsData.map((shop) => ({ + id: shop.id, + name: shop.name, + address: shop.address ?? '', + rating: shop.rating ?? 0, + imageUrl: shop.image_url ?? shop.imageUrl ?? undefined, + services: (servicesData ?? []) + .filter((s) => s.shop_id === shop.id) + .map((s) => ({ + id: s.id, + name: s.name, + price: s.price ?? 0, + duration: s.duration ?? 30, + barberIds: s.barber_ids ?? [], + })), + products: [], + barbers: [], + })); - setState((s) => ({ ...s, shops: fetchedShops })); + setState((s) => ({ ...s, shops: fetchedShops })); + } catch (err) { + console.error('refreshShops error:', err); + } }; useEffect(() => { storage.set('smart-agenda', state); }, [state]); - /** - * Hook de Inicialização Master — sequencial para evitar race condition. - * Ordem garantida: autenticar utilizador → carregar shops da BD → libertar UI. - */ useEffect(() => { let mounted = true; @@ -126,10 +125,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { setState((s) => ({ ...s, user: undefined })); }; - /** - * Sincroniza o Perfil do Utilizador a partir do Supabase Database ('profiles'). - * Cria automaticamente a shop se a Role for 'barbearia'. - */ const applyProfile = async (userId: string, email?: string | null) => { const { data, error } = await supabase .from('profiles') @@ -147,6 +142,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const displayName = data.name?.trim() || (email ? email.split('@')[0] : 'Utilizador'); const shopId = role === 'barbearia' ? userId : undefined; + let needsInsert = false; + let shopNameToInsert = ''; + setState((s) => { const nextUser: User = { id: userId, @@ -162,92 +160,66 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { : [...s.users, nextUser]; let shops = s.shops; - if (data.shop_name) { - const shopIdFromOwner = userId; - const exists = shops.some((shop) => shop.id === shopIdFromOwner); - + if (data.shop_name && shopId) { + const exists = shops.some((shop) => shop.id === shopId); if (!exists) { - const shopName = data.shop_name.trim(); - const shop: BarberShop = { - id: shopIdFromOwner, - name: shopName, + shopNameToInsert = data.shop_name.trim(); + needsInsert = true; + const newShop: BarberShop = { + id: shopId, + name: shopNameToInsert, address: 'Endereço a definir', rating: 0, barbers: [], services: [], products: [], }; - shops = [...shops, shop]; - - // 🔹 INSERT NA BD SUPABASE - void supabase.from('shops').insert([ - { - id: shopIdFromOwner, - name: shopName, - address: 'Endereço a definir', - rating: 0, - } - ]).then(({ error: insertError }) => { - if (insertError && insertError.code !== '23505') { - console.error('Erro ao sincronizar shop nova na BD:', insertError); - } - }); + shops = [...shops, newShop]; } } - return { - ...s, - user: nextUser, - users, - shops, - }; + return { ...s, user: nextUser, users, shops }; }); - }; - /** - * Boot sequencial: autenticar primeiro, depois carregar shops. - * Isso garante que quando o Dashboard renderiza, a shop já está no estado. - */ - const boot = async () => { - try { - const { data } = await supabase.auth.getSession(); - if (!mounted) return; - - const session = data.session; - if (session?.user) { - // 1. Aplicar perfil (adiciona shop ao estado local se necessário) - await applyProfile(session.user.id, session.user.email); - // 2. Recarregar todas as shops da BD (garante sync completo) - if (mounted) await refreshShops(); - } else { - clearUser(); - // Mesmo sem user, carrega shops para o Explore page - if (mounted) await refreshShops(); + // Inserir na BD se necessário (fora do setState) + if (needsInsert && shopId) { + const { error: insertErr } = await supabase.from('shops').insert([{ + id: shopId, + name: shopNameToInsert, + address: 'Endereço a definir', + rating: 0, + }]); + if (insertErr && insertErr.code !== '23505') { + console.error('Erro ao inserir shop:', insertErr); } - } catch (err) { - console.error('Erro durante o boot:', err); - } finally { - if (mounted) setLoading(false); } }; - void boot(); - - /** - * Listener de eventos Auth (login/logout em tempo real). - */ - const { data: sub } = supabase.auth.onAuthStateChange(async (_event, session) => { + // Usa onAuthStateChange que dispara INITIAL_SESSION automaticamente + // Evita duplicação com getSession() separado + const { data: sub } = supabase.auth.onAuthStateChange(async (event, session) => { if (!mounted) return; + if (session?.user) { await applyProfile(session.user.id, session.user.email); if (mounted) await refreshShops(); } else { clearUser(); + if (mounted) await refreshShops(); } + + // Liberta o loading após o primeiro evento + if (mounted) setLoading(false); }); + // Fallback: se o onAuthStateChange não disparar em 5s, liberta o loading + const fallback = setTimeout(() => { + if (mounted) setLoading(false); + }, 5000); + return () => { mounted = false; + clearTimeout(fallback); sub.subscription.unsubscribe(); }; }, []); @@ -292,21 +264,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { products: [], }; const user: User = { ...payload, id: userId, role: 'barbearia', shopId }; - setState((s) => ({ - ...s, - user, - users: [...s.users, user], - shops: [...s.shops, shop], - })); + setState((s) => ({ ...s, user, users: [...s.users, user], shops: [...s.shops, shop] })); return true; } const user: User = { ...payload, id: nanoid(), role: 'cliente' }; - setState((s) => ({ - ...s, - user, - users: [...s.users, user], - })); + setState((s) => ({ ...s, user, users: [...s.users, user] })); return true; }; @@ -337,13 +300,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { ); if (exists) return null; - const appointment: Appointment = { - ...input, - id: nanoid(), - status: 'pendente', - total: svc.price, - }; - + const appointment: Appointment = { ...input, id: nanoid(), status: 'pendente', total: svc.price }; setState((s) => ({ ...s, appointments: [...s.appointments, appointment] })); return appointment; }; @@ -369,15 +326,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return sum + price * item.qty; }, 0); - return { - id: nanoid(), - shopId, - customerId, - items, - total, - status: 'pendente', - createdAt: new Date().toISOString(), - }; + return { id: nanoid(), shopId, customerId, items, total, status: 'pendente', createdAt: new Date().toISOString() }; }); setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] })); @@ -385,17 +334,11 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => { - setState((s) => ({ - ...s, - appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)), - })); + setState((s) => ({ ...s, appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)) })); }; const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => { - setState((s) => ({ - ...s, - orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)), - })); + setState((s) => ({ ...s, orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)) })); }; const addService: AppContextValue['addService'] = (shopId, service) => { @@ -477,16 +420,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const updateShopDetails: AppContextValue['updateShopDetails'] = async (shopId, payload) => { - const { error } = await supabase - .from('shops') - .update(payload) - .eq('id', shopId); - - if (error) { - console.error('Failed to update shop details:', error); - return; - } - + const { error } = await supabase.from('shops').update(payload).eq('id', shopId); + if (error) { console.error('updateShopDetails error:', error); return; } setState((s) => ({ ...s, shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, ...payload } : shop)), @@ -520,13 +455,20 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { refreshShops, }; - // Loading Shield — mostra spinner enquanto autentica pela primeira vez - if (loading) return ( -
-
- -
- ); + if (loading) { + return ( +
+
+
+

A carregar...

+
+ +
+ ); + } return {children}; };