Add guest cart drawer, Continue handoff, and shared customer login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ad05241a77
commit
16340c3b48
+175
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
clearSharedAuthCookies,
|
||||
hydrateLocalTokensFromSharedCookies,
|
||||
SHARED_ACCESS_TOKEN_KEY,
|
||||
SHARED_REFRESH_TOKEN_KEY,
|
||||
writeSharedAuthCookies,
|
||||
} from "./shared-auth-cookie";
|
||||
|
||||
export type AuthBusinessMembership = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isOwner: boolean;
|
||||
teamRole: string | null;
|
||||
};
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
cellNumber?: string;
|
||||
email?: string;
|
||||
roles?: string[];
|
||||
isSuperAdmin?: boolean;
|
||||
businesses?: AuthBusinessMembership[];
|
||||
};
|
||||
|
||||
type SessionContextValue = {
|
||||
user: AuthUser | null;
|
||||
isAuthenticated: boolean;
|
||||
ready: boolean;
|
||||
businessId: string;
|
||||
isWebsiteAdmin: boolean;
|
||||
logout: () => void;
|
||||
getAccessToken: () => string | null;
|
||||
};
|
||||
|
||||
function normalizeUser(raw: Partial<AuthUser> & { id?: string | number }): AuthUser | null {
|
||||
if (raw.id == null) return null;
|
||||
return {
|
||||
id: String(raw.id),
|
||||
firstName: raw.firstName ?? undefined,
|
||||
lastName: raw.lastName ?? undefined,
|
||||
cellNumber: raw.cellNumber ?? undefined,
|
||||
email: raw.email ?? undefined,
|
||||
roles: Array.isArray(raw.roles) ? raw.roles : undefined,
|
||||
isSuperAdmin: Boolean(raw.isSuperAdmin),
|
||||
businesses: Array.isArray(raw.businesses)
|
||||
? raw.businesses.map((business) => ({
|
||||
id: String(business.id),
|
||||
name: business.name,
|
||||
slug: business.slug,
|
||||
isOwner: Boolean(business.isOwner),
|
||||
teamRole: business.teamRole ?? null,
|
||||
}))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function isWebsiteAdminForBusiness(user: AuthUser | null | undefined, businessId: string): boolean {
|
||||
if (!user || !businessId) return false;
|
||||
if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true;
|
||||
return Boolean(
|
||||
user.businesses?.some(
|
||||
(business) => String(business.id) === String(businessId) && (business.isOwner || Boolean(business.teamRole)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue | null>(null);
|
||||
|
||||
function readToken(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeToken(key: string, value: string | null) {
|
||||
try {
|
||||
if (value) localStorage.setItem(key, value);
|
||||
else localStorage.removeItem(key);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function SessionProvider({
|
||||
children,
|
||||
businessId,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
businessId: string;
|
||||
}) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const getAccessToken = useCallback(() => {
|
||||
hydrateLocalTokensFromSharedCookies(SHARED_ACCESS_TOKEN_KEY, SHARED_REFRESH_TOKEN_KEY);
|
||||
return readToken(SHARED_ACCESS_TOKEN_KEY);
|
||||
}, []);
|
||||
|
||||
const clearTokens = useCallback(() => {
|
||||
writeToken(SHARED_ACCESS_TOKEN_KEY, null);
|
||||
writeToken(SHARED_REFRESH_TOKEN_KEY, null);
|
||||
writeToken("accessToken", null);
|
||||
writeToken("meshkee_access_token", null);
|
||||
clearSharedAuthCookies();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
setReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const refresh = readToken(SHARED_REFRESH_TOKEN_KEY);
|
||||
writeSharedAuthCookies(token, refresh ?? undefined);
|
||||
|
||||
fetch("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(async (res) => {
|
||||
if (res.status === 401) {
|
||||
clearTokens();
|
||||
return;
|
||||
}
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as { user?: AuthUser } & AuthUser;
|
||||
const next = normalizeUser(data.user ?? data);
|
||||
if (next) setUser(next);
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => setReady(true));
|
||||
}, [clearTokens, getAccessToken]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearTokens();
|
||||
}, [clearTokens]);
|
||||
|
||||
const isWebsiteAdmin = useMemo(() => isWebsiteAdminForBusiness(user, businessId), [user, businessId]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
isAuthenticated: Boolean(user),
|
||||
ready,
|
||||
businessId,
|
||||
isWebsiteAdmin,
|
||||
logout,
|
||||
getAccessToken,
|
||||
}),
|
||||
[user, ready, businessId, isWebsiteAdmin, logout, getAccessToken],
|
||||
);
|
||||
|
||||
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
const ctx = useContext(SessionContext);
|
||||
if (!ctx) throw new Error("useSession must be used within SessionProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user