Stage M8
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
'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<void>;
|
||||
register: (input: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone?: string;
|
||||
acceptsMarketing?: boolean;
|
||||
}) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const CustomerContext = createContext<CustomerSession | null>(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<CurrentUser | null>(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<CustomerSession['register']>(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<CustomerSession>(
|
||||
() => ({ customer, status, signIn, register, signOut }),
|
||||
[customer, status, signIn, register, signOut],
|
||||
);
|
||||
|
||||
return <CustomerContext.Provider value={value}>{children}</CustomerContext.Provider>;
|
||||
}
|
||||
|
||||
export function useCustomer(): CustomerSession {
|
||||
const context = useContext(CustomerContext);
|
||||
if (!context) {
|
||||
throw new Error('useCustomer must be used inside <CustomerProvider>');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user