import React, { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useLocation } from "react-router-dom"; import { isAuthRoute } from "@core/constants/routes"; import { useCheckout } from "@app/contexts/CheckoutContext"; import { InfoBanner } from "@app/components/shared/InfoBanner"; import { SERVER_LICENSE_REQUEST_EVENT, type ServerLicenseRequestPayload, UPGRADE_BANNER_TEST_EVENT, type UpgradeBannerTestPayload, type UpgradeBannerTestScenario, UPGRADE_BANNER_ALERT_EVENT, } from "@core/constants/events"; import { useServerExperience } from "@app/hooks/useServerExperience"; import { isOnboardingCompleted } from "@core/components/onboarding/orchestrator/onboardingStorage"; const FRIENDLY_LAST_SEEN_KEY = "upgradeBannerFriendlyLastShownAt"; const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; const UpgradeBanner: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); // Check if we're on an auth route (evaluated after hooks, used in render) const onAuthRoute = isAuthRoute(location.pathname); const { openCheckout } = useCheckout(); const { loginEnabled, totalUsers, userCountResolved, userCountLoading, effectiveIsAdmin: configIsAdmin, hasPaidLicense, licenseLoading, freeTierLimit, overFreeTierLimit, weeklyActiveUsers, scenarioKey, } = useServerExperience(); const onboardingComplete = isOnboardingCompleted(); const [friendlyVisible, setFriendlyVisible] = useState(() => { if (typeof window === "undefined") return false; const lastShownRaw = window.localStorage.getItem(FRIENDLY_LAST_SEEN_KEY); if (!lastShownRaw) return false; const lastShown = parseInt(lastShownRaw, 10); if (!Number.isFinite(lastShown)) return false; return Date.now() - lastShown >= WEEK_IN_MS; }); const isDev = import.meta.env.DEV; const [testScenario, setTestScenario] = useState(null); useEffect(() => { if (!isDev || typeof window === "undefined") { return; } const handleTestEvent = (event: Event) => { const { detail } = event as CustomEvent; setTestScenario(detail?.scenario ?? null); if (detail?.scenario === "friendly") { setFriendlyVisible(true); } else if (!detail?.scenario) { setFriendlyVisible(false); } }; window.addEventListener(UPGRADE_BANNER_TEST_EVENT, handleTestEvent as EventListener); return () => { window.removeEventListener(UPGRADE_BANNER_TEST_EVENT, handleTestEvent as EventListener); }; }, [isDev]); const isAdmin = configIsAdmin; const scenario = isDev ? testScenario : null; const scenarioIsFriendly = scenario === "friendly"; const scenarioIsUrgentUser = scenario === "urgent-user"; const userCountKnown = typeof totalUsers === "number"; const isUnderLimit = userCountKnown ? totalUsers < freeTierLimit : null; const isOverLimit = userCountKnown ? totalUsers > freeTierLimit : overFreeTierLimit; const baseTotalUsersLoaded = userCountResolved && !userCountLoading; const scenarioProvidesInfo = scenarioKey && scenarioKey !== "unknown" && scenarioKey !== "licensed"; const derivedIsAdmin = scenarioProvidesInfo ? scenarioKey.includes("admin") : isAdmin; const derivedHasPaidLicense = scenarioKey === "licensed" ? true : scenarioKey === "unknown" ? hasPaidLicense : false; const derivedIsUnderLimit = scenarioProvidesInfo ? scenarioKey.includes("under-limit") : isUnderLimit === true; const derivedIsOverLimit = scenarioProvidesInfo ? scenarioKey.includes("over-limit") : isOverLimit === true; const effectiveIsAdmin = scenario ? (scenarioIsUrgentUser ? false : true) : derivedIsAdmin; const effectiveTotalUsers = scenario != null ? (scenarioIsFriendly ? 3 : 8) : totalUsers; const effectiveTotalUsersLoaded = scenario != null ? true : baseTotalUsersLoaded; const effectiveHasPaidLicense = scenario != null ? false : derivedHasPaidLicense; const effectiveIsUnderLimit = scenario != null ? scenarioIsFriendly : derivedIsUnderLimit; const effectiveIsOverLimit = scenario != null ? !scenarioIsFriendly : derivedIsOverLimit; const isDerivedAdmin = scenario ? !scenarioIsUrgentUser : scenarioKey === "login-user-over-limit-no-license" ? false : effectiveIsAdmin; const shouldShowFriendlyBase = Boolean( isDerivedAdmin && !effectiveHasPaidLicense && effectiveIsUnderLimit && effectiveTotalUsersLoaded, ); const shouldShowUrgentBase = Boolean( !effectiveHasPaidLicense && effectiveTotalUsersLoaded && (effectiveIsOverLimit || scenarioKey === "login-user-over-limit-no-license"), ); const shouldEvaluateFriendly = scenario ? scenarioIsFriendly : Boolean(shouldShowFriendlyBase && !licenseLoading && effectiveTotalUsersLoaded && onboardingComplete); // Urgent banner should always show when over-limit const shouldEvaluateUrgent = scenario ? Boolean(scenario && !scenarioIsFriendly) : Boolean(shouldShowUrgentBase && !licenseLoading); useEffect(() => { if (scenario === "friendly") { return; } if (!shouldEvaluateFriendly) { setFriendlyVisible(false); return; } if (friendlyVisible || typeof window === "undefined" || userCountLoading) { return; } const lastShownRaw = window.localStorage.getItem(FRIENDLY_LAST_SEEN_KEY); const lastShown = lastShownRaw ? parseInt(lastShownRaw, 10) : 0; const now = Date.now(); const due = !Number.isFinite(lastShown) || now - lastShown >= WEEK_IN_MS; setFriendlyVisible(due); }, [scenario, shouldEvaluateFriendly, friendlyVisible, userCountLoading]); useEffect(() => { if (typeof window === "undefined") { return; } const detail = shouldEvaluateUrgent ? { active: true, audience: effectiveIsAdmin ? "admin" : "user", totalUsers: effectiveTotalUsers ?? null, freeTierLimit, } : { active: false }; window.dispatchEvent(new CustomEvent(UPGRADE_BANNER_ALERT_EVENT, { detail })); }, [shouldEvaluateUrgent, effectiveIsAdmin, effectiveTotalUsers, scenario, freeTierLimit]); useEffect(() => { return () => { if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent(UPGRADE_BANNER_ALERT_EVENT, { detail: { active: false } })); } }; }, []); const recordFriendlyLastShown = useCallback(() => { if (typeof window === "undefined") { return; } window.localStorage.setItem(FRIENDLY_LAST_SEEN_KEY, Date.now().toString()); }, []); const handleUpgrade = () => { recordFriendlyLastShown(); const hideBanner = () => setFriendlyVisible(false); const navigateFallback = () => { navigate("/settings/adminPlan"); hideBanner(); }; try { openCheckout("server", { minimumSeats: 1, onSuccess: () => { hideBanner(); }, onError: () => { navigateFallback(); }, }); } catch (error) { console.error("[UpgradeBanner] Failed to open checkout, redirecting instead", error); navigateFallback(); return; } // Keep legacy behavior so banner disappears once the user initiates checkout hideBanner(); }; const handleFriendlyDismiss = () => { recordFriendlyLastShown(); setFriendlyVisible(false); }; const handleSeeInfo = () => { if (typeof window === "undefined" || !effectiveIsAdmin) { return; } const detail: ServerLicenseRequestPayload = { licenseNotice: { totalUsers: effectiveTotalUsers ?? null, freeTierLimit, isOverLimit: effectiveIsOverLimit ?? false, }, selfReportedAdmin: true, deferUntilTourComplete: false, }; window.dispatchEvent(new CustomEvent(SERVER_LICENSE_REQUEST_EVENT, { detail })); }; const renderUrgentBanner = () => { if (!shouldEvaluateUrgent) { return null; } const buttonText = effectiveIsAdmin ? t("upgradeBanner.seeInfo", "See info") : undefined; const attentionMessage = effectiveIsAdmin ? t("upgradeBanner.attentionBodyAdmin", "Review the license requirements to keep this server compliant.") : t("upgradeBanner.attentionBody", "Your admin needs to sign in to see more info. Please contact them immediately."); return ( ); }; const suppressForNoLogin = !loginEnabled || (!loginEnabled && (weeklyActiveUsers ?? Number.POSITIVE_INFINITY) > 5); // Don't show on auth routes or if neither banner type should show // Also suppress entirely for no-login servers (treat them as regular users only) // and, per request, never surface upgrade messaging there when WAU > 5. if (onAuthRoute || suppressForNoLogin || (!friendlyVisible && !shouldEvaluateUrgent)) { return null; } return ( <> {friendlyVisible && ( )} {renderUrgentBanner()} ); }; export default UpgradeBanner;