Skip to content

Commit 7b02d5c

Browse files
YuvalYuval
authored andcommitted
fix(SocialLoginButtons): improve Google Sign-In script loading and error handling
1 parent 497c024 commit 7b02d5c

1 file changed

Lines changed: 111 additions & 49 deletions

File tree

03-Client/src/app/components/shared/SocialLoginButtons.tsx

Lines changed: 111 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ import { useAuth } from "../../context/AuthContext";
66
const GOOGLE_CLIENT_ID =
77
"82985006549-i2dn8m92d760lsdcsvj4sao19a4e46et.apps.googleusercontent.com";
88

9-
// Minimal type for Google Identity Services (loaded from script tag)
109
interface GoogleAccountsId {
1110
initialize: (config: {
1211
client_id: string;
1312
callback: (response: { credential: string }) => void;
13+
use_fedcm_for_prompt?: boolean;
14+
auto_select?: boolean;
15+
cancel_on_tap_outside?: boolean;
1416
}) => void;
1517
prompt: () => void;
18+
renderButton: (
19+
parent: HTMLElement,
20+
options: {
21+
type?: "standard" | "icon";
22+
theme?: "outline" | "filled_blue" | "filled_black";
23+
size?: "small" | "medium" | "large";
24+
shape?: "rectangular" | "pill" | "circle" | "square";
25+
text?: "signin_with" | "signup_with" | "continue_with" | "signin";
26+
logo_alignment?: "left" | "center";
27+
width?: number | string;
28+
},
29+
) => void;
1630
}
1731

1832
declare global {
@@ -27,15 +41,35 @@ declare global {
2741

2842
const GSI_SRC = "https://accounts.google.com/gsi/client";
2943

44+
// Single shared promise so multiple mounts / remounts do not append duplicate
45+
// <script> tags. Duplicates were the root cause of the "Failed to load Google
46+
// Sign-In script" error: each mount appended a new tag, and the onload/onerror
47+
// of the *second* tag never fired reliably in some browsers, leaving us in a
48+
// perpetual "loading" state that surfaced as the Hebrew error message.
49+
let gsiLoadPromise: Promise<void> | null = null;
50+
3051
function loadGoogleScript(): Promise<void> {
31-
return new Promise((resolve, reject) => {
32-
if (document.querySelector(`script[src="${GSI_SRC}"]`)) {
33-
// Already loaded or loading — wait a tick
34-
if (window.google?.accounts?.id) {
35-
resolve();
36-
return;
37-
}
52+
if (gsiLoadPromise) return gsiLoadPromise;
53+
54+
gsiLoadPromise = new Promise<void>((resolve, reject) => {
55+
if (window.google?.accounts?.id) {
56+
resolve();
57+
return;
58+
}
59+
60+
const existing = document.querySelector<HTMLScriptElement>(
61+
`script[src="${GSI_SRC}"]`,
62+
);
63+
if (existing) {
64+
existing.addEventListener("load", () => resolve(), { once: true });
65+
existing.addEventListener(
66+
"error",
67+
() => reject(new Error("Failed to load Google Sign-In script")),
68+
{ once: true },
69+
);
70+
return;
3871
}
72+
3973
const script = document.createElement("script");
4074
script.src = GSI_SRC;
4175
script.async = true;
@@ -44,6 +78,14 @@ function loadGoogleScript(): Promise<void> {
4478
script.onerror = () => reject(new Error("Failed to load Google Sign-In script"));
4579
document.head.appendChild(script);
4680
});
81+
82+
// If it rejects, allow a later mount to try again rather than being
83+
// permanently stuck on the cached rejection.
84+
gsiLoadPromise.catch(() => {
85+
gsiLoadPromise = null;
86+
});
87+
88+
return gsiLoadPromise;
4789
}
4890

4991
interface SocialLoginButtonsProps {
@@ -56,13 +98,19 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
5698
const navigate = useNavigate();
5799
const [loading, setLoading] = useState(false);
58100
const [error, setError] = useState<string | null>(null);
59-
const initializedRef = useRef(false);
101+
const [ready, setReady] = useState(false);
102+
const buttonHostRef = useRef<HTMLDivElement | null>(null);
60103

61104
useEffect(() => {
105+
let cancelled = false;
106+
62107
loadGoogleScript()
63108
.then(() => {
64-
if (initializedRef.current) return;
65-
if (!window.google?.accounts?.id) return;
109+
if (cancelled) return;
110+
if (!window.google?.accounts?.id) {
111+
setError("לא ניתן לטעון את שירות Google");
112+
return;
113+
}
66114

67115
window.google.accounts.id.initialize({
68116
client_id: GOOGLE_CLIENT_ID,
@@ -81,20 +129,31 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
81129
setLoading(false);
82130
}
83131
},
132+
use_fedcm_for_prompt: true,
133+
auto_select: false,
134+
cancel_on_tap_outside: true,
84135
});
85-
initializedRef.current = true;
136+
137+
if (buttonHostRef.current) {
138+
buttonHostRef.current.innerHTML = "";
139+
window.google.accounts.id.renderButton(buttonHostRef.current, {
140+
type: "icon",
141+
theme: "outline",
142+
size: "large",
143+
shape: "rectangular",
144+
});
145+
}
146+
147+
setReady(true);
86148
})
87-
.catch(() => setError("לא ניתן לטעון את שירות Google"));
88-
}, [loginWithGoogle, navigate, onSuccess]);
149+
.catch(() => {
150+
if (!cancelled) setError("לא ניתן לטעון את שירות Google");
151+
});
89152

90-
const handleGoogleClick = () => {
91-
if (!window.google?.accounts?.id) {
92-
setError("שירות Google עדיין לא מוכן, נסו שוב");
93-
return;
94-
}
95-
setError(null);
96-
window.google.accounts.id.prompt();
97-
};
153+
return () => {
154+
cancelled = true;
155+
};
156+
}, [loginWithGoogle, navigate, onSuccess]);
98157

99158
return (
100159
<div className="flex flex-col items-center gap-4">
@@ -103,37 +162,40 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
103162
</p>
104163
<div className="flex gap-3 justify-center">
105164
{/* Google — the only provider enabled right now.
106-
Facebook and Apple are intentionally commented out until we add server-side verification for them. */}
107-
<button
108-
type="button"
109-
onClick={handleGoogleClick}
110-
disabled={loading}
111-
aria-label="התחברות עם Google"
112-
className="bg-white border border-[#dadce0] hover:bg-[#f8f9fa] transition-colors w-[60px] h-[44px] rounded-[10px] flex items-center justify-center cursor-pointer shadow-[0px_1px_3px_rgba(0,0,0,0.08)] disabled:opacity-60"
113-
>
114-
{/* Official Google 'G' mark */}
115-
<svg width="22" height="22" viewBox="0 0 48 48" aria-hidden="true">
116-
<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" />
117-
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
118-
<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" />
119-
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-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" />
120-
</svg>
121-
</button>
165+
Facebook and Apple are intentionally commented out until we add server-side verification for them.
166+
167+
Implementation note: Google Identity Services' `prompt()` call is unreliable
168+
on modern browsers (FedCM, third-party cookie blockers, incognito). We use
169+
`renderButton()` instead, then visually overlay our branded button on top of
170+
the Google-rendered element so clicks fall through to Google's handler.
171+
This is the pattern Google recommends and it works across browsers. */}
172+
<div className="relative w-[60px] h-[44px]">
173+
{/* Branded visual — pointer-events:none so clicks hit the Google button below */}
174+
<div
175+
aria-hidden="true"
176+
className={`absolute inset-0 pointer-events-none bg-white border border-[#dadce0] rounded-[10px] flex items-center justify-center shadow-[0px_1px_3px_rgba(0,0,0,0.08)] ${
177+
!ready || loading ? "opacity-60" : ""
178+
}`}
179+
>
180+
<svg width="22" height="22" viewBox="0 0 48 48" aria-hidden="true">
181+
<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" />
182+
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
183+
<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" />
184+
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-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" />
185+
</svg>
186+
</div>
187+
{/* Google-rendered clickable surface — transparent, sits on top, handles auth */}
188+
<div
189+
ref={buttonHostRef}
190+
aria-label="התחברות עם Google"
191+
className="absolute inset-0 opacity-0"
192+
style={{ colorScheme: "light" }}
193+
/>
194+
</div>
122195

123196
{/*
124197
// Facebook — disabled until server-side Facebook token verification is added.
125-
<button className="bg-[#ececec] w-[60px] h-[44px] rounded-[10px] flex items-center justify-center cursor-pointer">
126-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
127-
<path d={svgPaths.pb8cc300} fill="black" />
128-
</svg>
129-
</button>
130-
131198
// Apple — disabled until server-side Apple token verification (JWKS + private key) is added.
132-
<button className="bg-[#ececec] w-[60px] h-[44px] rounded-[10px] flex items-center justify-center cursor-pointer">
133-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
134-
<path d={svgPaths.p1a1f100} fill="black" />
135-
</svg>
136-
</button>
137199
*/}
138200
</div>
139201
{loading && (

0 commit comments

Comments
 (0)