Initial commit: Hoomat storefront with Meshkee API and static-image slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
getMe,
|
||||
getTenant,
|
||||
refreshTokens,
|
||||
} from "@/lib/meshkee/api";
|
||||
import {
|
||||
ACCESS_TOKEN_KEY,
|
||||
REFRESH_TOKEN_KEY,
|
||||
} from "@/lib/meshkee/config";
|
||||
import type { AuthUser } from "@/lib/meshkee/types";
|
||||
|
||||
type AuthContextValue = {
|
||||
user: AuthUser | null;
|
||||
isAuthenticated: boolean;
|
||||
ready: boolean;
|
||||
businessId: string | null;
|
||||
isWebsiteAdmin: boolean;
|
||||
getAccessToken: () => string | null;
|
||||
logout: () => void;
|
||||
refreshUser: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
function readToken(key: string) {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function writeTokens(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [businessId, setBusinessId] = useState<string | null>(null);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearTokens();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const getAccessToken = useCallback(() => readToken(ACCESS_TOKEN_KEY), []);
|
||||
|
||||
const refreshUser = useCallback(async () => {
|
||||
let accessToken = readToken(ACCESS_TOKEN_KEY);
|
||||
const refreshToken = readToken(REFRESH_TOKEN_KEY);
|
||||
if (!accessToken && !refreshToken) {
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!accessToken && refreshToken) {
|
||||
const tokens = await refreshTokens(refreshToken);
|
||||
writeTokens(tokens.accessToken, tokens.refreshToken);
|
||||
accessToken = tokens.accessToken;
|
||||
}
|
||||
if (!accessToken) {
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
const me = await getMe(accessToken);
|
||||
setUser(me);
|
||||
} catch {
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const tokens = await refreshTokens(refreshToken);
|
||||
writeTokens(tokens.accessToken, tokens.refreshToken);
|
||||
const me = await getMe(tokens.accessToken);
|
||||
setUser(me);
|
||||
return;
|
||||
} catch {
|
||||
clearTokens();
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
clearTokens();
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
try {
|
||||
const tenant = await getTenant();
|
||||
if (!cancelled) setBusinessId(String(tenant.id));
|
||||
} catch {
|
||||
if (!cancelled) setBusinessId(null);
|
||||
}
|
||||
await refreshUser();
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
clearTokens();
|
||||
setUser(null);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setReady(true);
|
||||
}
|
||||
})().catch(() => {
|
||||
// swallow unexpected bootstrap rejections (e.g. aborted HMR)
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [refreshUser]);
|
||||
|
||||
const isWebsiteAdmin = useMemo(() => {
|
||||
if (!user || !businessId) return false;
|
||||
if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true;
|
||||
return Boolean(
|
||||
user.businesses?.some(
|
||||
(biz) =>
|
||||
String(biz.id) === String(businessId) &&
|
||||
(biz.isOwner || Boolean(biz.teamRole)),
|
||||
),
|
||||
);
|
||||
}, [user, businessId]);
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({
|
||||
user,
|
||||
isAuthenticated: Boolean(user),
|
||||
ready,
|
||||
businessId,
|
||||
isWebsiteAdmin,
|
||||
getAccessToken,
|
||||
logout,
|
||||
refreshUser,
|
||||
}),
|
||||
[
|
||||
user,
|
||||
ready,
|
||||
businessId,
|
||||
isWebsiteAdmin,
|
||||
getAccessToken,
|
||||
logout,
|
||||
refreshUser,
|
||||
],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user