RTL Meshkee storefront with home/about/contact/products/blog, API auth/contact/blog wiring, and SEO sitemap-config. Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
4.6 KiB
TypeScript
177 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import { getMe, getTenant, refreshAuth } from "@/lib/meshkee";
|
|
import { BUSINESS_URL, CUSTOMER_URL, TOKEN_KEYS } from "@/lib/config";
|
|
import { displayName, isWebsiteAdmin } from "@/lib/format";
|
|
import type { MeshkeeUser } from "@/lib/types";
|
|
|
|
type AuthContextValue = {
|
|
user: MeshkeeUser | null;
|
|
loading: boolean;
|
|
isAdmin: boolean;
|
|
label: string;
|
|
customerUrl: string;
|
|
businessUrl: string;
|
|
profileUrl: string;
|
|
loginUrl: string;
|
|
logout: () => void;
|
|
refresh: () => Promise<void>;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
function readToken(key: string) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeTokens(access?: string | null, refresh?: string | null) {
|
|
try {
|
|
if (access) localStorage.setItem(TOKEN_KEYS.access, access);
|
|
else localStorage.removeItem(TOKEN_KEYS.access);
|
|
if (refresh) localStorage.setItem(TOKEN_KEYS.refresh, refresh);
|
|
else localStorage.removeItem(TOKEN_KEYS.refresh);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function captureTokensFromUrl() {
|
|
if (typeof window === "undefined") return;
|
|
const url = new URL(window.location.href);
|
|
const hash = new URLSearchParams(url.hash.replace(/^#/, ""));
|
|
const access =
|
|
url.searchParams.get("accessToken") ||
|
|
url.searchParams.get("token") ||
|
|
hash.get("accessToken") ||
|
|
hash.get("token");
|
|
const refresh =
|
|
url.searchParams.get("refreshToken") || hash.get("refreshToken");
|
|
|
|
if (!access && !refresh) return;
|
|
|
|
writeTokens(access, refresh);
|
|
url.searchParams.delete("accessToken");
|
|
url.searchParams.delete("refreshToken");
|
|
url.searchParams.delete("token");
|
|
url.hash = "";
|
|
window.history.replaceState({}, "", `${url.pathname}${url.search}`);
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<MeshkeeUser | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [businessId, setBusinessId] = useState<string | number | null>(null);
|
|
|
|
const loginUrl = useMemo(() => {
|
|
const returnTo =
|
|
typeof window !== "undefined"
|
|
? window.location.origin
|
|
: "https://www.plift.ir";
|
|
return `${CUSTOMER_URL}?redirect=${encodeURIComponent(returnTo)}`;
|
|
}, []);
|
|
|
|
const refresh = useCallback(async () => {
|
|
captureTokensFromUrl();
|
|
const access = readToken(TOKEN_KEYS.access);
|
|
const refreshToken = readToken(TOKEN_KEYS.refresh);
|
|
|
|
if (!access && !refreshToken) {
|
|
setUser(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let token = access;
|
|
if (!token && refreshToken) {
|
|
const refreshed = await refreshAuth(refreshToken);
|
|
writeTokens(refreshed.accessToken, refreshed.refreshToken);
|
|
token = refreshed.accessToken;
|
|
if (refreshed.user) {
|
|
setUser(refreshed.user);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
}
|
|
if (!token) {
|
|
setUser(null);
|
|
return;
|
|
}
|
|
const me = await getMe(token);
|
|
setUser(me);
|
|
} catch {
|
|
if (refreshToken) {
|
|
try {
|
|
const refreshed = await refreshAuth(refreshToken);
|
|
writeTokens(refreshed.accessToken, refreshed.refreshToken);
|
|
const me = refreshed.user || (await getMe(refreshed.accessToken));
|
|
setUser(me);
|
|
return;
|
|
} catch {
|
|
writeTokens(null, null);
|
|
setUser(null);
|
|
}
|
|
} else {
|
|
writeTokens(null, null);
|
|
setUser(null);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const tenant = await getTenant();
|
|
if (!cancelled) setBusinessId(tenant.id);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
if (!cancelled) await refresh();
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [refresh]);
|
|
|
|
const logout = useCallback(() => {
|
|
writeTokens(null, null);
|
|
setUser(null);
|
|
}, []);
|
|
|
|
const value: AuthContextValue = {
|
|
user,
|
|
loading,
|
|
isAdmin: user ? isWebsiteAdmin(user, businessId) : false,
|
|
label: user ? `${displayName(user)} عزیز` : "",
|
|
customerUrl: CUSTOMER_URL,
|
|
businessUrl: BUSINESS_URL,
|
|
profileUrl: `${CUSTOMER_URL}/profile`,
|
|
loginUrl,
|
|
logout,
|
|
refresh,
|
|
};
|
|
|
|
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;
|
|
}
|