import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Button, Chip, Paper, Typography } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { clearAuthToken, decodeJwtPayload, getAuthPersistencePreference, getAuthToken, setAuthToken } from "../auth"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; declare global { interface Window { google?: any; } } type MeResponse = { provider?: "local" | "google" | "external"; email?: string; userName?: string; displayName?: string; firstName?: string; lastName?: string; googleLink?: { linked: boolean; email?: string | null; linkedAt?: string | null; } | null; }; function loadGoogleScript(): Promise { return new Promise((resolve, reject) => { if (window.google?.accounts?.id) return resolve(); const existing = document.getElementById("google-gsi"); if (existing) { existing.addEventListener("load", () => resolve(), { once: true }); return; } const s = document.createElement("script"); s.id = "google-gsi"; s.src = "https://accounts.google.com/gsi/client"; s.async = true; s.defer = true; s.onload = () => resolve(); s.onerror = () => reject(new Error("Failed to load Google script")); document.head.appendChild(s); }); } export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void }) { const { toast } = useToast(); const { t } = useI18n(); const [token, setToken] = useState(() => getAuthToken()); const [me, setMe] = useState(null); const [working, setWorking] = useState(false); const hostRef = useRef(null); const clientId = (process.env.REACT_APP_GOOGLE_CLIENT_ID || "").trim(); const payload = useMemo(() => (token ? decodeJwtPayload(token) : null), [token]); const isRawGoogleToken = payload?.iss === "accounts.google.com" || payload?.iss === "https://accounts.google.com"; const actionLabel = !token ? t("continueWithGoogle") : me?.provider === "local" && !me?.googleLink?.linked ? t("linkWithGoogle") : t("signInWithGoogle"); async function refreshMe() { if (!getAuthToken()) { setMe(null); return; } try { const res = await api.get("/auth/me"); setMe(res.data); } catch { setMe(null); } } useEffect(() => { void refreshMe(); }, [token]); useEffect(() => { if (!token || !isRawGoogleToken) return; let cancelled = false; const exchange = async () => { try { const res = await api.post<{ accessToken: string }>("/auth/google/exchange", { token }); if (cancelled) return; setAuthToken(res.data.accessToken, getAuthPersistencePreference()); setToken(res.data.accessToken); toast(t("googleSignedIn"), "success"); onSignedIn?.(); } catch { if (cancelled) return; clearAuthToken(); setToken(null); toast(t("googleNotLinkedYet"), "info"); } }; void exchange(); return () => { cancelled = true; }; }, [token, isRawGoogleToken, onSignedIn, toast, t]); useEffect(() => { const host = hostRef.current; if (!clientId || !host) return; const shouldRenderButton = !token || isRawGoogleToken || (me?.provider === "local" && !me?.googleLink?.linked); host.replaceChildren(); if (!shouldRenderButton) return; let active = true; void loadGoogleScript() .then(() => { if (!active || !window.google?.accounts?.id || !hostRef.current) return; hostRef.current.replaceChildren(); window.google.accounts.id.initialize({ client_id: clientId, callback: async (resp: any) => { const credential = resp?.credential as string | undefined; if (!credential) return; setWorking(true); try { if (me?.provider === "local") { const res = await api.post<{ linked: boolean; email?: string | null }>("/auth/google/link", { token: credential }); toast(res.data?.email ? t("googleLinkedSuccessWithEmail", { email: res.data.email }) : t("googleLinkedSuccess"), "success"); await refreshMe(); } else { const res = await api.post<{ accessToken: string }>("/auth/google/exchange", { token: credential }); setAuthToken(res.data.accessToken, getAuthPersistencePreference()); setToken(res.data.accessToken); toast(t("googleSignedIn"), "success"); onSignedIn?.(); } } catch (e: any) { toast(getApiErrorMessage(e, t("googleAuthFailed")), "error"); } finally { setWorking(false); } }, }); window.google.accounts.id.renderButton(hostRef.current, { theme: "outline", size: "large", type: "standard", shape: "pill", text: me?.provider === "local" ? "continue_with" : "signin_with", }); }) .catch(() => toast(t("googleScriptLoadFailed"), "error")); return () => { active = false; host.replaceChildren(); }; }, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, isRawGoogleToken, token, toast, t]); const signedInName = me?.userName || me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || ""; return ( {t("googleAccountTitle")} {!clientId && ( {t("googleSetupHint")} )} {clientId && ( {me?.googleLink?.linkedAt ? : null} {!token ? ( {t("googleSignInHint")} ) : me?.provider === "local" ? ( {me.googleLink?.linked ? t("googleLinkedTo", { email: me.googleLink.email || t("googleLinkedToYourAccount") }) : t("googleBindHint")} ) : ( {t("googleExchangeHint")} )} {actionLabel}
{token ? ( ) : null} {me?.provider === "local" && me.googleLink?.linked ? ( ) : null} {token && me?.email ? ( {t("signedInAs", { name: signedInName })} ) : null} )} ); }