Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { ApiError, getAccessToken, isAbortError, migrateTokensToSharedCookies } from "@/lib/meshkee-api";
|
||||
import {
|
||||
getAdminDashboardOrigin,
|
||||
getCustomerDashboardOrigin,
|
||||
hostAppFor,
|
||||
type HostApp,
|
||||
} from "@/lib/host";
|
||||
import {
|
||||
clearManagedBusinessId,
|
||||
getManagedBusinessId,
|
||||
setManagedBusinessId,
|
||||
} from "@/lib/managed-business";
|
||||
import {
|
||||
fetchCurrentUser,
|
||||
login as loginRequest,
|
||||
logout as logoutRequest,
|
||||
register as registerRequest,
|
||||
type RegisterPayload,
|
||||
} from "@/services/auth";
|
||||
import type { AuthUser, DashboardType } from "@/types/auth";
|
||||
|
||||
interface AuthContextValue {
|
||||
user: AuthUser | null;
|
||||
isLoading: boolean;
|
||||
activeBusinessId: string | null;
|
||||
isBusinessOwner: boolean;
|
||||
isSuperAdmin: boolean;
|
||||
login: (cellNumber: string, password: string) => Promise<DashboardType>;
|
||||
register: (payload: RegisterPayload) => Promise<DashboardType>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function pickActiveBusinessId(user: AuthUser): string | null {
|
||||
const owned = user.businesses.find((item) => item.isOwner);
|
||||
const first = owned ?? user.businesses[0];
|
||||
return first ? String(first.id) : null;
|
||||
}
|
||||
|
||||
export function userCanAccessApp(user: AuthUser, app: HostApp): boolean {
|
||||
if (app === "admin") return user.dashboard === "super_admin";
|
||||
if (app === "customer") {
|
||||
// Shared entry: customer, business owners, and superadmins all land here first.
|
||||
return (
|
||||
user.dashboard === "customer" ||
|
||||
user.dashboard === "business" ||
|
||||
user.dashboard === "super_admin"
|
||||
);
|
||||
}
|
||||
// Business dashboard: owners/staff, or superadmin (via managed business id).
|
||||
if (user.dashboard === "super_admin") return true;
|
||||
return Boolean(pickActiveBusinessId(user));
|
||||
}
|
||||
|
||||
function currentHost(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return window.location.host;
|
||||
}
|
||||
|
||||
/** Prefer ?businessId= on the URL, then stored managed id. */
|
||||
function syncManagedBusinessFromUrl() {
|
||||
if (typeof window === "undefined") return getManagedBusinessId();
|
||||
const fromUrl = new URLSearchParams(window.location.search)
|
||||
.get("businessId")
|
||||
?.trim();
|
||||
if (fromUrl) {
|
||||
setManagedBusinessId(fromUrl);
|
||||
return fromUrl;
|
||||
}
|
||||
return getManagedBusinessId();
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [managedBusinessId, setManagedBusinessIdState] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
logoutRequest();
|
||||
clearManagedBusinessId();
|
||||
setManagedBusinessIdState(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
async function init() {
|
||||
// Handoff installs tokens via inline script and redirects — don't race it.
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.location.pathname === "/auth/handoff"
|
||||
) {
|
||||
if (!controller.signal.aborted) setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
migrateTokensToSharedCookies();
|
||||
setManagedBusinessIdState(syncManagedBusinessFromUrl());
|
||||
|
||||
if (!getAccessToken()) {
|
||||
if (!controller.signal.aborted) setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { user: currentUser } = await fetchCurrentUser(controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
const app = hostAppFor(currentHost());
|
||||
if (!userCanAccessApp(currentUser, app)) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
const managed = syncManagedBusinessFromUrl();
|
||||
setManagedBusinessIdState(managed);
|
||||
setUser(currentUser);
|
||||
} catch (err) {
|
||||
if (isAbortError(err) || controller.signal.aborted) return;
|
||||
// On business host, keep tokens so a transient /me failure doesn't
|
||||
// bounce manage-business back to admin.
|
||||
if (hostAppFor(currentHost()) === "business") {
|
||||
if (!controller.signal.aborted) setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
logout();
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void init();
|
||||
return () => controller.abort();
|
||||
}, [logout]);
|
||||
|
||||
const login = useCallback(async (cellNumber: string, password: string) => {
|
||||
const data = await loginRequest(cellNumber, password);
|
||||
const app = hostAppFor(currentHost());
|
||||
|
||||
if (!userCanAccessApp(data.user, app)) {
|
||||
logoutRequest();
|
||||
const message =
|
||||
app === "admin"
|
||||
? "This account does not have super admin access."
|
||||
: app === "business"
|
||||
? "No business is linked to this account."
|
||||
: "This account cannot use the customer dashboard.";
|
||||
throw new ApiError(message, 403);
|
||||
}
|
||||
|
||||
if (app === "business" && data.user.dashboard === "super_admin") {
|
||||
const managed = syncManagedBusinessFromUrl();
|
||||
setManagedBusinessIdState(managed);
|
||||
if (!managed && !pickActiveBusinessId(data.user)) {
|
||||
logoutRequest();
|
||||
throw new ApiError(
|
||||
"Open a business from the admin dashboard and use Manage business.",
|
||||
403,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setManagedBusinessIdState(syncManagedBusinessFromUrl());
|
||||
setUser(data.user);
|
||||
return data.user.dashboard;
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (payload: RegisterPayload) => {
|
||||
const data = await registerRequest(payload);
|
||||
|
||||
if (!data.accessToken || !data.refreshToken || !data.user) {
|
||||
throw new ApiError(
|
||||
data.message ||
|
||||
"Registration needs phone verification before you can continue.",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
const app = hostAppFor(currentHost());
|
||||
if (!userCanAccessApp(data.user, app)) {
|
||||
logoutRequest();
|
||||
throw new ApiError(
|
||||
"This account cannot use the customer dashboard.",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
setManagedBusinessIdState(syncManagedBusinessFromUrl());
|
||||
setUser(data.user);
|
||||
return data.user.dashboard;
|
||||
}, []);
|
||||
|
||||
const isSuperAdmin = user?.dashboard === "super_admin";
|
||||
|
||||
/**
|
||||
* Shared business.* host — which Meshkee business?
|
||||
* - Superadmin: managed id from “Manage business” handoff (required for other businesses)
|
||||
* - Everyone else: first owned / linked business on the user
|
||||
*/
|
||||
const activeBusinessId = useMemo(() => {
|
||||
if (!user) return null;
|
||||
if (user.dashboard === "super_admin") {
|
||||
return managedBusinessId || pickActiveBusinessId(user);
|
||||
}
|
||||
return pickActiveBusinessId(user);
|
||||
}, [user, managedBusinessId]);
|
||||
|
||||
const isBusinessOwner = useMemo(
|
||||
() => Boolean(user?.businesses.some((item) => item.isOwner)),
|
||||
[user],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
isLoading,
|
||||
activeBusinessId,
|
||||
isBusinessOwner,
|
||||
isSuperAdmin: Boolean(isSuperAdmin),
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
}),
|
||||
[
|
||||
user,
|
||||
isLoading,
|
||||
activeBusinessId,
|
||||
isBusinessOwner,
|
||||
isSuperAdmin,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* After login: stay on the host you signed into.
|
||||
* Superadmin can open the admin dashboard from customer home when needed.
|
||||
*/
|
||||
export function postLoginHref(dashboard: DashboardType): string {
|
||||
const app = hostAppFor(currentHost());
|
||||
|
||||
if (app === "admin") {
|
||||
return dashboard === "super_admin" ? "/" : `${getCustomerDashboardOrigin()}/`;
|
||||
}
|
||||
|
||||
if (app === "business") return "/profile";
|
||||
if (app === "customer") return "/";
|
||||
|
||||
if (dashboard === "super_admin") return `${getAdminDashboardOrigin()}/`;
|
||||
return `${getCustomerDashboardOrigin()}/`;
|
||||
}
|
||||
Reference in New Issue
Block a user