import React, { useEffect, useRef, useState } from "react"; import { Box, Button, Chip, Typography } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { clearAuthClientState, getAuthPersistencePreference } from "../auth"; import TwoFactorChallenge from "./TwoFactorChallenge"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { PageSection } from "../layout/PageLayout"; declare global { interface Window { google?: { accounts?: { id?: GoogleIdentityApi; }; }; } } type GoogleCredentialResponse = { credential?: string; }; type GoogleIdentityApi = { initialize: (options: { client_id: string; callback: (response: GoogleCredentialResponse) => void }) => void; renderButton: (host: HTMLElement, options: Record) => void; }; let initializedIdentityApi: GoogleIdentityApi | null = null; let initializedClientId = ""; let activeCredentialHandler: ((response: GoogleCredentialResponse) => void) | null = null; function initializeGoogleIdentity(identityApi: GoogleIdentityApi, clientId: string) { if (initializedIdentityApi === identityApi && initializedClientId === clientId) return; identityApi.initialize({ client_id: clientId, callback: (response) => activeCredentialHandler?.(response), }); initializedIdentityApi = identityApi; initializedClientId = clientId; } 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, presentation = "account" }: { onSignedIn?: () => void; presentation?: "account" | "sign-in" }) { const { toast } = useToast(); const { t, language } = useI18n(); const [me, setMe] = useState(null); const [working, setWorking] = useState(false); const [allowRegistration, setAllowRegistration] = useState(false); const [pendingToken, setPendingToken] = useState(null); const hostRef = useRef(null); const clientId = (process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "").trim(); const signInOnly = presentation === "sign-in"; const signedIn = Boolean(me?.provider); const actionLabel = signInOnly || !signedIn ? t("continueWithGoogle") : me?.provider === "local" && !me?.googleLink?.linked ? t("linkWithGoogle") : t("signInWithGoogle"); async function refreshMe() { try { const res = await api.get("/auth/me"); setMe(res.data); } catch { setMe(null); } } useEffect(() => { if (signInOnly) return; void refreshMe(); api.get<{ allowRegistration: boolean }>("/auth/config").then((res) => { setAllowRegistration(Boolean(res.data?.allowRegistration)); }).catch(() => setAllowRegistration(false)); }, [signInOnly]); useEffect(() => { if (signInOnly) return; const onAuthChanged = () => { void refreshMe(); }; window.addEventListener("auth-changed", onAuthChanged); return () => window.removeEventListener("auth-changed", onAuthChanged); }, [signInOnly]); useEffect(() => { const host = hostRef.current; if (!clientId || !host) return; const shouldRenderButton = signInOnly || !signedIn || (me?.provider === "local" && !me?.googleLink?.linked); host.replaceChildren(); if (!shouldRenderButton) return; let active = true; void loadGoogleScript() .then(() => { const identityApi = window.google?.accounts?.id; if (!active || !identityApi || !hostRef.current) return; hostRef.current.replaceChildren(); const credentialHandler = async (resp: GoogleCredentialResponse) => { const credential = resp?.credential as string | undefined; if (!credential) return; setWorking(true); try { if (!signInOnly && me?.provider === "local") { const res = await api.post<{ linked: boolean; email?: string | null }>("/auth/google/link", { token: credential, rememberMe: getAuthPersistencePreference() === "local" }); toast(res.data?.email ? t("googleLinkedSuccessWithEmail", { email: res.data.email }) : t("googleLinkedSuccess"), "success"); await refreshMe(); } else { const res = await api.post<{ requiresTwoFactor?: boolean; pendingToken?: string }>("/auth/google/exchange", { token: credential, rememberMe: getAuthPersistencePreference() === "local" }); if (res.data?.requiresTwoFactor && res.data.pendingToken) { setPendingToken(res.data.pendingToken); } else { window.dispatchEvent(new Event("auth-changed")); toast(t("googleSignedIn"), "success"); onSignedIn?.(); } } } catch (e: unknown) { toast(getApiErrorMessage(e, t("googleAuthFailed")), "error"); } finally { setWorking(false); } }; activeCredentialHandler = credentialHandler; initializeGoogleIdentity(identityApi, clientId); identityApi.renderButton(hostRef.current, { theme: "outline", size: "large", type: "standard", shape: "pill", text: !signInOnly && me?.provider === "local" ? "continue_with" : "signin_with", }); }) .catch(() => toast(t("googleScriptLoadFailed"), "error")); return () => { active = false; activeCredentialHandler = null; host.replaceChildren(); }; }, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, signInOnly, signedIn, toast, t]); const signedInName = me?.userName || me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || ""; if (signInOnly) { if (pendingToken) { return setPendingToken(null)} onSuccess={() => { setPendingToken(null); window.dispatchEvent(new Event("auth-changed")); toast(t("googleSignedIn"), "success"); onSignedIn?.(); }} />; } return clientId ?
: ; } return ( {!clientId && ( {t("googleSetupHint")} )} {clientId && pendingToken && ( setPendingToken(null)} onSuccess={() => { setPendingToken(null); window.dispatchEvent(new Event("auth-changed")); toast(t("googleSignedIn"), "success"); onSignedIn?.(); }} /> )} {clientId && !pendingToken && ( {me?.googleLink?.linkedAt ? : null} {!signedIn ? ( {allowRegistration ? t("googleSignInHintSelfServe") : t("googleSignInHint")} ) : me?.provider === "local" ? ( {me.googleLink?.linked ? t("googleLinkedTo", { email: me.googleLink.email || t("googleLinkedToYourAccount") }) : t("googleBindHint")} ) : ( {t("googleExchangeHint")} )} {actionLabel}
{signedIn ? ( ) : null} {me?.provider === "local" && me.googleLink?.linked ? ( ) : null} {signedIn && me?.email ? ( {t("signedInAs", { name: signedInName })} ) : null} )} ); }