src/components/auth/google-auth-button.tsx
Rama main · réplica de lectura del repositorio original.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ButtonSpinner } from "@/components/ui/button-spinner";
import { useConfig } from "@/contexts/ConfigContext";
import { beginGoogleSignIn } from "@/lib/google-oauth";
import { hardNavigate } from "@/lib/navigation";
import { GoogleIcon } from "@/components/icons/google-icon";
interface GoogleAuthButtonProps {
onError: (message: string) => void;
// Where to land after a successful sign-in, when the visitor was headed
// somewhere specific (see `useAuthGuard`/`AuthForm`). Handed to
// `beginGoogleSignIn`, which stores it inside the attempt it mints: this flow
// leaves the tab, so a prop alone would not survive the trip to Google.
redirectTo?: string | null;
// The join link's slug or invite-link token on `/join`, stored in the attempt
// the same way.
via?: string;
}
// "Continue with Google" - one of the alternatives to the email field on the
// single entry point into the app (see `AuthForm`).
//
// **Nothing of Google's is loaded to render this.** The button is an ordinary
// `Button`, and pressing it builds an authorization URL and navigates there (see
// `lib/google-oauth.ts`). A signed-out visitor who never presses it never
// contacts Google at all, which is the entire point: the press is the consent,
// and until it happens Google has not been given the visitor's IP address, their
// user agent, or an opportunity to set anything in their browser.
//
// Not Google's own rendered button stacked over a custom visual at `opacity: 0`:
// that needs `https://accounts.google.com/gsi/client` injected at mount, and
// rests on assumptions about Google's internals. This is one real control
// carrying its own accessible name - one tab stop, operable by Enter and Space
// like any other button.
//
// The "Or" divider above this lives in `AuthForm`: with more than one alternative
// method it has to be drawn once above whichever of them an instance has, and
// neither can own it.
//
// Google Identity Services never distinguished sign in from sign up, and neither
// does this - `POST /auth/google` transparently starts onboarding on first use
// instead of signing straight in.
export function GoogleAuthButton({
onError,
redirectTo,
via,
}: GoogleAuthButtonProps) {
// Runtime configuration, not a build-time constant: a published image has to be
// able to learn its client ID from the container it runs in - see
// `lib/runtime-config.ts`. Unset, the whole button renders nothing.
const { googleClientId: clientId } = useConfig();
const [isLeaving, setIsLeaving] = useState(false);
// Renders nothing if Google sign-in hasn't been configured (no client ID),
// rather than rendering a button that can never succeed.
if (!clientId) {
return null;
}
const handleClick = async () => {
setIsLeaving(true);
try {
// A real page load, not a client-side route change - the destination is
// another origin. `hardNavigate` also marks this document as on its way
// out, which stands down any effect that would otherwise start a
// navigation nobody will see.
hardNavigate(await beginGoogleSignIn({ clientId, redirectTo, via }));
} catch {
// The only way this fails before leaving is `crypto.subtle` being
// unavailable, which means a non-secure context. Nothing has been sent
// anywhere, so the visitor can simply choose another method.
setIsLeaving(false);
onError("Couldn't start Google sign-in. Please try another method.");
}
};
return (
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleClick}
disabled={isLeaving}
>
{isLeaving ? (
<div className="flex items-center space-x-2">
<ButtonSpinner />
<span>Redirecting...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<GoogleIcon className="h-4 w-4" />
<span>Continue with Google</span>
</div>
)}
</Button>
);
}