import React, { useCallback, useMemo, useState, useEffect } from "react"; import { Modal, Button, Text, ActionIcon } from "@mantine/core"; import { useMediaQuery } from "@mantine/hooks"; import { useAuth } from "@app/auth/UseSession"; import { isUserAnonymous } from "@app/auth/supabase"; import { useTranslation } from "react-i18next"; import LocalIcon from "@app/components/shared/LocalIcon"; import Overview from "@app/components/shared/config/configSections/Overview"; import { createSaasConfigNavSections } from "@app/components/shared/config/saasConfigNavSections"; import { NavKey } from "@app/components/shared/config/types"; import { withBasePath } from "@app/constants/app"; import { COOKIE_CONSENT_SCROLL_SHARD } from "@app/hooks/useCookieConsent"; import "@app/components/shared/AppConfigModal.css"; import { Z_INDEX_OVER_FULLSCREEN_SURFACE, Z_INDEX_OVER_SETTINGS_MODAL, } from "@app/styles/zIndex"; interface AppConfigModalProps { opened: boolean; onClose: () => void; } const AppConfigModal: React.FC = ({ opened, onClose }) => { const isMobile = useMediaQuery("(max-width: 1024px)"); const { signOut, user, creditBalance, refreshCredits } = useAuth(); const { t } = useTranslation(); const [confirmOpen, setConfirmOpen] = useState(false); const [active, setActive] = useState("overview"); const [notice, setNotice] = useState(null); // Check if user can access billing features (non-anonymous users only) const isAnonymous = user ? isUserAnonymous(user) : false; useEffect(() => { const handler = (ev: Event) => { const detail = (ev as CustomEvent).detail as { key?: NavKey } | undefined; if (detail?.key) { setActive(detail.key); } }; window.addEventListener("appConfig:navigate", handler as EventListener); return () => window.removeEventListener( "appConfig:navigate", handler as EventListener, ); }, []); // Listen for notice updates (e.g., "Not enough credits..." next to Plan title) useEffect(() => { const handler = (ev: Event) => { const detail = (ev as CustomEvent).detail as | { key?: NavKey; notice?: string } | undefined; if (detail?.notice && (detail?.key ? detail.key === "plan" : true)) { setNotice(detail.notice); } }; window.addEventListener("appConfig:notice", handler as EventListener); return () => window.removeEventListener("appConfig:notice", handler as EventListener); }, []); // Full-screen overlays that live inside our React tree (e.g. the PAYG // UpgradeModal, portal'd to document.body) announce themselves here so we // can hide — not unmount — while they're up. Unmounting would kill the // overlay itself since it's our descendant; hiding keeps all section state // (active tab, scroll, wallet data) intact for when the overlay closes. const [overlayActive, setOverlayActive] = useState(false); useEffect(() => { const handler = (ev: Event) => { const detail = (ev as CustomEvent).detail as | { open?: boolean } | undefined; setOverlayActive(Boolean(detail?.open)); }; window.addEventListener("appConfig:overlay", handler as EventListener); return () => window.removeEventListener("appConfig:overlay", handler as EventListener); }, []); // When the modal opens to Plan, proactively refresh credits and log values useEffect(() => { if (!opened) return; if (active !== "plan") return; console.log( "[AppConfigModal] Opening Plan section. Current creditBalance:", creditBalance, ); (async () => { try { await refreshCredits(); } catch (e) { console.warn( "[AppConfigModal] Failed to refresh credits on Plan open:", e, ); } })(); }, [opened, active]); useEffect(() => { if (!opened) return; if (active !== "plan") return; console.log( "[AppConfigModal] Credit balance updated while viewing Plan:", creditBalance, ); }, [opened, active, creditBalance]); const colors = useMemo( () => ({ navBg: "var(--modal-nav-bg)", sectionTitle: "var(--modal-nav-section-title)", navItem: "var(--modal-nav-item)", navItemActive: "var(--modal-nav-item-active)", navItemActiveBg: "var(--modal-nav-item-active-bg)", contentBg: "var(--modal-content-bg)", headerBorder: "var(--modal-header-border)", }), [], ); const isDev = process.env.NODE_ENV === "development"; const openLogoutConfirm = useCallback(() => setConfirmOpen(true), []); // Left navigation structure and icons. The Plan tab now internally branches // free vs subscribed × leader vs member via useWallet(), so the modal no // longer plumbs paygEnabled / isLeader through to the nav builder. const configNavSections = useMemo( () => createSaasConfigNavSections(Overview, openLogoutConfirm, { isDev, isAnonymous, t, }), [openLogoutConfirm, isDev, isAnonymous, t], ); const activeLabel = useMemo(() => { for (const section of configNavSections) { const found = section.items.find((i) => i.key === active); if (found) return found.label; } return ""; }, [configNavSections, active]); const activeComponent = useMemo(() => { for (const section of configNavSections) { const found = section.items.find((i) => i.key === active); if (found) return found.component; } return null; }, [configNavSections, active]); return ( <> {/* Confirm logout modal */} setConfirmOpen(false)} title="Sign out" centered zIndex={Z_INDEX_OVER_SETTINGS_MODAL} >
Are you sure you want to sign out?
); }; export default AppConfigModal;