Files
artcraft/frontend/apps/artcraft-webapp/src/components/auth/GoogleLoginButton.tsx
T
2026-09-01 10:25:21 +07:00

115 lines
3.6 KiB
TypeScript

import { GoogleLogin, CredentialResponse } from "@react-oauth/google";
import { UsersApi } from "@storyteller/api";
import {
getLandingUrl,
getReferralCode,
getReferralUsername,
getReferrer,
} from "@storyteller/common";
import { LoaderCircleIcon } from "lucide-react";
import { useState } from "react";
interface GoogleLoginButtonProps {
onSuccess: (isNewUser: boolean) => void;
onError: (error: string) => void;
mode?: "login" | "signup";
className?: string;
}
export function GoogleLoginButton({
onSuccess,
onError,
mode = "login",
className = "",
}: GoogleLoginButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const handleGoogleSuccess = async (
credentialResponse: CredentialResponse,
) => {
if (!credentialResponse.credential) {
onError("No credential received from Google");
return;
}
setIsLoading(true);
try {
const api = new UsersApi();
const result = await api.GoogleSSO({
credential: credentialResponse.credential,
maybeReferralUrl: getReferrer(),
maybeLandingUrl: getLandingUrl(),
maybeReferralUsername: getReferralUsername(),
maybeReferralCode: getReferralCode(),
});
if (result.success) {
window.dispatchEvent(new Event("auth-change"));
onSuccess(result.data?.usernameNotYetCustomized ?? false);
} else {
onError(result.errorMessage || "Authentication failed");
}
} catch (err) {
onError("Network error");
} finally {
setIsLoading(false);
}
};
return (
<div
className={`group relative w-full h-10 transition-transform duration-150 active:scale-95 ${className}`}
>
{/* Custom visual button - shown behind */}
<div className="absolute inset-0 w-full h-full flex items-center justify-center gap-3 rounded-[3px] bg-white text-black font-medium group-hover:bg-white/80 transition-colors">
{isLoading ? (
<LoaderCircleIcon className="animate-spin text-lg text-black/60" />
) : (
<>
<GoogleIcon />
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em]">
Continue with Google
</span>
</>
)}
</div>
{/* Google's actual button - invisible overlay that receives clicks */}
<div
className="absolute inset-0 w-full h-full overflow-hidden cursor-pointer flex items-center justify-center"
style={{ opacity: 0.01, zIndex: 10, transform: "scaleY(1.2)" }}
>
<GoogleLogin
onSuccess={handleGoogleSuccess}
onError={() => onError("Google login failed")}
size="large"
width={400}
text="continue_with"
/>
</div>
</div>
);
}
function GoogleIcon() {
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"
fill="#4285F4"
/>
<path
d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009 18z"
fill="#34A853"
/>
<path
d="M3.964 10.71A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"
fill="#FBBC05"
/>
<path
d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 00.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"
fill="#EA4335"
/>
</svg>
);
}