'use client'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import type { CurrentUser } from '@sport/types'; import { browserApi, customerTokenStore } from '@/lib/api'; interface CustomerSession { customer: CurrentUser | null; /** Distinguishes "not signed in" from "we have not checked yet". */ status: 'loading' | 'authenticated' | 'anonymous'; signIn: (email: string, password: string) => Promise; register: (input: { email: string; password: string; firstName: string; lastName: string; phone?: string; acceptsMarketing?: boolean; }) => Promise; signOut: () => Promise; } const CustomerContext = createContext(null); /** * The signed-in shopper, for the whole storefront. * * Mounted at the root rather than inside `/account`, because the header, the * wishlist button on a product card and checkout prefill all need to know who * is signed in — and none of those live under `/account`. * * On mount it attempts one silent refresh. The access token lives only in * memory, so a page reload always starts with nothing; the httpOnly cookie is * what makes the session survive, and this is where it gets cashed in. */ export function CustomerProvider({ children }: { children: ReactNode }) { const [customer, setCustomer] = useState(null); const [status, setStatus] = useState<'loading' | 'authenticated' | 'anonymous'>('loading'); useEffect(() => { let cancelled = false; async function restore() { try { const refreshed = await browserApi.auth.refresh(); customerTokenStore.set(refreshed.accessToken); const me = await browserApi.auth.me(); if (!cancelled) { setCustomer(me); setStatus('authenticated'); } } catch { // No cookie, or it expired. Being signed out is the normal case for a // storefront, not an error worth surfacing. if (!cancelled) { customerTokenStore.set(null); setStatus('anonymous'); } } } void restore(); return () => { cancelled = true; }; }, []); useEffect(() => { // Fires when a background refresh finally fails — the session ended while // the shopper was doing something else. customerTokenStore.onLost(() => { setCustomer(null); setStatus('anonymous'); }); return () => customerTokenStore.onLost(null); }, []); const signIn = useCallback(async (email: string, password: string) => { const result = await browserApi.auth.login({ email, password }); customerTokenStore.set(result.accessToken); setCustomer(result.user); setStatus('authenticated'); }, []); const register = useCallback(async (input) => { const result = await browserApi.auth.register(input); customerTokenStore.set(result.accessToken); setCustomer(result.user); setStatus('authenticated'); }, []); const signOut = useCallback(async () => { try { await browserApi.auth.logout(); } finally { // Local state is cleared even if the call failed: a shopper who pressed // "sign out" must end up signed out on this device regardless. customerTokenStore.set(null); setCustomer(null); setStatus('anonymous'); } }, []); const value = useMemo( () => ({ customer, status, signIn, register, signOut }), [customer, status, signIn, register, signOut], ); return {children}; } export function useCustomer(): CustomerSession { const context = useContext(CustomerContext); if (!context) { throw new Error('useCustomer must be used inside '); } return context; }