import React, { useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "../../auth/AuthContext"; import { registerUser, updateProfileUsername } from "../../api/client"; import AuthToast from "../Auth/AuthToast"; import { promptInstall, isRunningAsPWA, canInstall } from '../../pwaInstall'; import { trackEvent } from "../../utils/analytics"; const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" }; const LAST_SIGNUP_KEY = "sociowire:lastSignup"; function initialLetter(name = "") { const s = (name || "").trim(); return s ? s[0].toUpperCase() : "G"; } function truthyParam(v) { if (!v) return false; const x = String(v).toLowerCase().trim(); return x === "1" || x === "true" || x === "yes" || x === "ok"; } function readLastSignup() { try { const raw = localStorage.getItem(LAST_SIGNUP_KEY); if (!raw) return null; const j = JSON.parse(raw); if (!j || typeof j !== "object") return null; return { username: (j.username || "").toString().trim(), email: (j.email || "").toString().trim(), }; } catch { return null; } } function parseMergedParams() { const search = window.location.search || ""; const hash = window.location.hash || ""; const qs = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search); let hashQs = ""; if (hash.includes("?")) hashQs = hash.slice(hash.indexOf("?") + 1); const hs = new URLSearchParams(hashQs); const get = (k) => { const a = qs.get(k); if (a != null && a !== "") return a; const b = hs.get(k); return b != null ? b : null; }; return { qs, hs, get, hash }; } export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland }) { const { loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled, avatarUrl, profileLoaded, needsUsername, needsEmailVerification, lastError, lastInfo, clearError, clearInfo, } = useAuth(); const [showAuth, setShowAuth] = useState(false); const [mode, setMode] = useState("login"); const [showUsernameModal, setShowUsernameModal] = useState(false); const [desiredUsername, setDesiredUsername] = useState(""); const [usernameError, setUsernameError] = useState(""); const [usernameInfo, setUsernameInfo] = useState(""); const [usernameSaving, setUsernameSaving] = useState(false); const usernamePromptTimer = useRef(null); const invalidSinceRef = useRef(null); const [stableUsername, setStableUsername] = useState(""); const displayUsername = (() => { const raw = (stableUsername || username || "user").toString(); if (!stableUsername && raw.includes("@")) return "user"; if (raw.length <= 9) return raw; return raw.slice(0, 9) + ".."; })(); const [loginUser, setLoginUser] = useState(""); const [loginPass, setLoginPass] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [regUser, setRegUser] = useState(""); const [regEmail, setRegEmail] = useState(""); const [regPass, setRegPass] = useState(""); const [signupError, setSignupError] = useState(""); const [signupInfo, setSignupInfo] = useState(""); const [signupLoading, setSignupLoading] = useState(false); const [showInstallBtn, setShowInstallBtn] = useState(false); const [authNotice, setAuthNotice] = useState(""); const resetPasswordUrl = useMemo(() => { const base = (import.meta?.env?.VITE_KC_URL || "https://auth.sociowire.com").replace(/\/+$/, ""); const realm = import.meta?.env?.VITE_KC_REALM || "sociowire"; const clientId = import.meta?.env?.VITE_KC_CLIENT_ID || "sociowire-frontend"; const redirectUri = window.location.origin + window.location.pathname; const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, }); return `${base}/realms/${realm}/login-actions/reset-credentials?${params.toString()}`; }, []); const openModal = (nextMode) => { setMode(nextMode); setShowAuth(true); setSignupError(""); trackEvent("auth_modal_open", { mode: nextMode || "login" }); }; const closeModal = () => { setShowAuth(false); setLoginPass(""); setSignupError(""); setAuthNotice(""); }; const handleUsernameSave = async (e) => { e.preventDefault(); const next = (desiredUsername || "").trim(); const re = /^[A-Za-z0-9_.]{3,24}$/; if (!re.test(next) || next.includes("@")) { setUsernameError("Use 3-24 letters, numbers, _ or ."); return; } setUsernameSaving(true); setUsernameError(""); setUsernameInfo(""); const res = await updateProfileUsername(next); setUsernameSaving(false); if (!res.ok) { if (res.status === 409) { setUsernameError("Username already taken."); } else { setUsernameError(res.text || "Could not update username."); } return; } setUsernameInfo("Username saved."); setShowUsernameModal(false); await restore(); }; useEffect(() => { if (authenticated) setShowAuth(false); }, [authenticated]); useEffect(() => { if (!authenticated) { setStableUsername(""); return; } const next = (username || "").trim(); if (!next) return; const looksLikeEmail = next.includes("@"); if (!looksLikeEmail) { setStableUsername(next); return; } if (stableUsername) return; }, [authenticated, username, stableUsername]); useEffect(() => { const requiresUsername = (name) => { if (!name) return false; if (name.includes("@")) return true; const re = /^[A-Za-z0-9_.]{3,24}$/; return !re.test(name); }; if (usernamePromptTimer.current) { clearTimeout(usernamePromptTimer.current); usernamePromptTimer.current = null; } if (loading) return; if (!authenticated) { setShowUsernameModal(false); return; } const shouldPrompt = !!needsUsername || requiresUsername(username); if (shouldPrompt) { if (!showUsernameModal) { const base = (username || "").split("@")[0] || ""; const cleaned = base.replace(/[^A-Za-z0-9_.]/g, "").slice(0, 24); setDesiredUsername(cleaned); setUsernameError(""); setUsernameInfo(""); usernamePromptTimer.current = setTimeout(() => { setShowUsernameModal(true); }, 600); } } else if (showUsernameModal) { setShowUsernameModal(false); } }, [authenticated, username, showUsernameModal, loading, needsUsername]); useEffect(() => { return () => { if (usernamePromptTimer.current) { clearTimeout(usernamePromptTimer.current); usernamePromptTimer.current = null; } }; }, []); useEffect(() => { const handler = (e) => { const detail = e?.detail || {}; const msg = (detail.message || "").toString().trim() || "Please sign in or create a free account to continue."; setAuthNotice(msg); setMode(detail.mode === "signup" ? "signup" : "login"); setShowAuth(true); }; window.addEventListener("sociowire:auth-required", handler); return () => window.removeEventListener("sociowire:auth-required", handler); }, []); // Check if PWA install should be shown (simple: hide if already PWA) useEffect(() => { const checkInstall = () => { // Hide if already running as PWA if (isRunningAsPWA()) { setShowInstallBtn(false); return; } // Show if can install setShowInstallBtn(canInstall()); }; checkInstall(); const handlePrompt = () => { setTimeout(checkInstall, 100); }; window.addEventListener('beforeinstallprompt', handlePrompt); window.addEventListener('appinstalled', () => setShowInstallBtn(false)); return () => { window.removeEventListener('beforeinstallprompt', handlePrompt); window.removeEventListener('appinstalled', () => setShowInstallBtn(false)); }; }, []); useEffect(() => { try { const { get, qs, hs, hash } = parseMergedParams(); const verified = get("verified") || get("email_verified") || get("emailVerified") || get("kc_verified"); const signup = get("signup"); const notice = get("notice"); const email = get("email") || get("user_email") || get("mail"); const uname = get("username") || get("user") || get("preferred_username") || get("login"); const last = readLastSignup(); if (truthyParam(verified)) { const who = (email || uname || last?.email || last?.username || "").trim(); setSignupInfo("✅ Email confirmed" + (who ? " (" + who + ")" : "") + ". You can log in."); setMode("login"); setShowAuth(true); const prefill = (uname || email || last?.username || last?.email || "").trim(); if (prefill) setLoginUser(prefill); } else if (truthyParam(signup)) { const who = (email || uname || last?.email || last?.username || "").trim(); setSignupInfo("✅ Account created" + (who ? " (" + who + ")" : "") + ". Check your emails (spam too), then log in."); setMode("login"); setShowAuth(true); const prefill = (uname || email || last?.username || last?.email || "").trim(); if (prefill) setLoginUser(prefill); } else if (notice) { const msg = decodeURIComponent(notice); if (msg && msg.trim()) { setSignupInfo(msg.trim()); setMode("login"); setShowAuth(true); } } const touched = qs.has("verified") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") || hs.has("verified") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice"); if (touched) { const cleanHash = hash.includes("?") ? hash.split("?")[0] : hash; const clean = window.location.pathname + cleanHash; window.history.replaceState({}, "", clean); } } catch (e) { // ignore } }, []); const handleLoginSubmit = async (e) => { e.preventDefault(); trackEvent("login_submit"); const res = await login(loginUser, loginPass); if (res?.ok) trackEvent("login_success"); }; const handleSignupSubmit = async (e) => { e.preventDefault(); setSignupError(""); setSignupInfo(""); if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) { setSignupError("First name, last name, username, email and password are required."); return; } try { setSignupLoading(true); trackEvent("signup_submit"); const u = regUser.trim(); const em = regEmail.trim(); await registerUser({ username: u, email: em, password: regPass, firstName: firstName.trim(), lastName: lastName.trim(), }); try { localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em })); } catch {} setSignupInfo("✅ Account created (" + em + "). Check your emails (spam too), then log in."); trackEvent("signup_success"); setMode("login"); setLoginUser(u || em); setLoginPass(""); } catch (err) { console.error(err); setSignupError(err.message || "Registration error."); } finally { setSignupLoading(false); } }; const toast = useMemo(() => { if (lastError) { const t = needsEmailVerification ? "warn" : "error"; const title = needsEmailVerification ? "Email verification required" : "Login failed"; return { type: t, title, message: lastError }; } if (lastInfo) { return { type: "success", title: "", message: lastInfo }; } return null; }, [lastError, lastInfo, needsEmailVerification]); return ( <> {/* Toast area (under topbar) */} {toast?.message ? (