Spaces:
Running
Running
File size: 5,469 Bytes
dd87944 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import React, { useCallback, useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import { getApiBase } from "../lib/api";
import { frontendUrl } from "../lib/paths";
const GIS_SRC = "https://accounts.google.com/gsi/client";
const ENV_CLIENT_ID = (process.env.REACT_APP_GOOGLE_CLIENT_ID || "").trim();
/** Client OAuth public — fallback si le build n'a pas REACT_APP_GOOGLE_CLIENT_ID */
export const PROD_GOOGLE_CLIENT_ID =
"791552572109-va37rj7pooi3opca3bqe61h15ka8gob9.apps.googleusercontent.com";
let gisLoadPromise = null;
function loadGisScript() {
if (window.google?.accounts?.id) return Promise.resolve();
if (gisLoadPromise) return gisLoadPromise;
gisLoadPromise = new Promise((resolve, reject) => {
const done = () => {
if (window.google?.accounts?.id) resolve();
else reject(new Error("Google Identity Services indisponible"));
};
const existing = document.querySelector(`script[src="${GIS_SRC}"]`);
if (existing) {
if (window.google?.accounts?.id) return resolve();
existing.addEventListener("load", done, { once: true });
existing.addEventListener("error", () => reject(new Error("Script Google bloqué")), { once: true });
return;
}
const script = document.createElement("script");
script.src = GIS_SRC;
script.async = true;
script.defer = true;
script.onload = done;
script.onerror = () => reject(new Error("Script Google bloqué"));
document.head.appendChild(script);
});
return gisLoadPromise;
}
export function getGoogleClientId(override) {
return (override || ENV_CLIENT_ID || PROD_GOOGLE_CLIENT_ID).trim();
}
export async function loadGoogleIdentity(clientId, onCredential) {
const cid = getGoogleClientId(clientId);
if (!cid) throw new Error("Google client_id manquant");
await loadGisScript();
window.google.accounts.id.initialize({
client_id: cid,
callback: (response) => {
if (response?.credential) onCredential(response.credential);
},
auto_select: false,
cancel_on_tap_outside: true,
itp_support: true,
ux_mode: "popup",
});
return cid;
}
async function triggerGooglePopup(clientId, onCredential) {
const cid = await loadGoogleIdentity(clientId, onCredential);
const temp = document.createElement("div");
temp.style.cssText = "position:fixed;left:-9999px;top:0;width:320px;height:48px;opacity:0.01;";
document.body.appendChild(temp);
try {
window.google.accounts.id.renderButton(temp, {
type: "standard",
theme: "outline",
size: "large",
text: "continue_with",
width: 320,
locale: "fr",
});
await new Promise((r) => setTimeout(r, 400));
const btn = temp.querySelector('[role="button"]') || temp.querySelector("div[tabindex]");
if (btn) {
btn.click();
return true;
}
return false;
} finally {
setTimeout(() => temp.remove(), 8000);
}
}
export function GoogleSignInButton({ clientId, onCredential, disabled, busy, onBusyChange }) {
const cid = getGoogleClientId(clientId);
const [gisReady, setGisReady] = useState(false);
useEffect(() => {
if (!cid) return;
loadGoogleIdentity(cid, onCredential)
.then(() => setGisReady(true))
.catch(() => setGisReady(false));
}, [cid, onCredential]);
const handleClick = useCallback(async () => {
if (disabled || busy || !cid) return;
onBusyChange?.(true);
try {
const opened = await triggerGooglePopup(cid, onCredential);
if (!opened) throw new Error("popup");
// Credential callback sets busy again; release after popup opens only if no callback within 90s
window.setTimeout(() => onBusyChange?.(false), 90000);
} catch (_) {
onBusyChange?.(false);
const redirectUrl = frontendUrl("/auth/google/callback");
window.location.assign(
`${getApiBase()}/auth/google/login?redirect=${encodeURIComponent(redirectUrl)}`
);
}
}, [cid, disabled, busy, onCredential, onBusyChange]);
if (!cid) return null;
return (
<button
type="button"
data-testid="google-login-btn"
onClick={handleClick}
disabled={disabled || busy}
className="google-btn w-full flex items-center justify-center gap-3 py-2.5 rounded-lg text-sm font-medium transition-colors mb-4 disabled:opacity-60"
>
{busy ? (
<>
<Loader2 size={16} className="animate-spin opacity-70" />
Connexion…
</>
) : (
<>
<svg width="16" height="16" viewBox="0 0 48 48" aria-hidden="true">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.56 2.95-2.24 5.45-4.78 7.12l7.73 6.01C43.44 37.74 46.98 31.64 46.98 24.55z" />
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6.01c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
</svg>
Continuer avec Google
{!gisReady && <span className="sr-only"> (chargement)</span>}
</>
)}
</button>
);
}
export default GoogleSignInButton;
|