fix(auth): initialize Google Identity once
This commit is contained in:
@@ -10,10 +10,38 @@ import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: any;
|
||||
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<string, string>) => 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;
|
||||
@@ -100,37 +128,37 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }:
|
||||
let active = true;
|
||||
void loadGoogleScript()
|
||||
.then(() => {
|
||||
if (!active || !window.google?.accounts?.id || !hostRef.current) return;
|
||||
const identityApi = window.google?.accounts?.id;
|
||||
if (!active || !identityApi || !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 (!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();
|
||||
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 {
|
||||
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?.();
|
||||
}
|
||||
window.dispatchEvent(new Event("auth-changed"));
|
||||
toast(t("googleSignedIn"), "success");
|
||||
onSignedIn?.();
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(getApiErrorMessage(e, t("googleAuthFailed")), "error");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
window.google.accounts.id.renderButton(hostRef.current, {
|
||||
} 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",
|
||||
@@ -142,6 +170,7 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }:
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
activeCredentialHandler = null;
|
||||
host.replaceChildren();
|
||||
};
|
||||
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, signInOnly, signedIn, toast, t]);
|
||||
|
||||
@@ -155,8 +155,9 @@ describe('LoginPage', () => {
|
||||
it('completes a Google credential return without account-linking copy', async () => {
|
||||
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID = 'google-client';
|
||||
let callback: ((response: { credential: string }) => void) | undefined;
|
||||
const initialize = jest.fn((options: any) => { callback = options.callback; });
|
||||
(window as any).google = { accounts: { id: {
|
||||
initialize: jest.fn((options: any) => { callback = options.callback; }),
|
||||
initialize,
|
||||
renderButton: jest.fn((host: HTMLElement) => {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Continue with Google';
|
||||
@@ -172,6 +173,7 @@ describe('LoginPage', () => {
|
||||
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/auth/google/exchange', { token: 'synthetic-google-token', rememberMe: true }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
expect(initialize).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByText(/create your account automatically/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user