feat(ecommerce): login con Google para clientes

Implementa el flujo completo de OAuth social de Medusa para el
storefront: boton "Continuar con Google" en account-dialog, ruta proxy
que arranca el flujo (GET /api/customer/google/start) y pagina de
callback en /auth/google/callback (coincide con GOOGLE_CALLBACK_URL del
secret) que resuelve el code/state contra el backend, decodifica el JWT
para distinguir cliente nuevo vs existente, crea el customer si hace
falta y refresca el token a sesion final -- mismo patron de
store/customer-store.ts que login/register por email.

Flujo verificado en vivo contra el backend ya desplegado (GET
/auth/customer/google, /auth/customer/google/callback con state
invalido, /auth/token/refresh con bearer invalido) antes de escribir el
codigo, no solo contra la documentacion.
This commit is contained in:
devops committed 2026-08-29 17:01:32 +00:00
1 parent da10a4b688
commit 77389aea19
7 files changed
+163

No files matched your search

@@ -10,6 +10,17 @@ import { useCustomerStore } from "@/store/customer-store";
type Mode = "login" | "register";
function GoogleIcon() {
return (
<svg viewBox="0 0 48 48" className="size-4" aria-hidden="true">
<path fill="#FFC107" d="M43.6 20.5H42V20H24v8h11.3c-1.6 4.7-6.1 8-11.3 8-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.8 1.1 8 3l6-6C34 5.1 29.3 3 24 3 12.4 3 3 12.4 3 24s9.4 21 21 21 21-9.4 21-21c0-1.4-.1-2.7-.4-3.5z" />
<path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.6 15.9 18.9 13 24 13c3.1 0 5.8 1.1 8 3l6-6C34 5.1 29.3 3 24 3 16.1 3 9.3 7.6 6.3 14.7z" />
<path fill="#4CAF50" d="M24 45c5.2 0 9.9-2 13.4-5.2l-6.2-5.2c-2 1.5-4.6 2.4-7.2 2.4-5.2 0-9.6-3.3-11.3-8l-6.5 5C9.2 40.3 16 45 24 45z" />
<path fill="#1976D2" d="M43.6 20.5H42V20H24v8h11.3c-.8 2.3-2.2 4.3-4.1 5.6l6.2 5.2C41.4 35.9 45 30.5 45 24c0-1.4-.1-2.7-.4-3.5z" />
</svg>
);
}
export function AccountDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const { customer, hasHydrated, loading, error, login, register, logout } = useCustomerStore();
const [mode, setMode] = useState<Mode>("login");
@@ -57,6 +68,14 @@ export function AccountDialog({ open, onOpenChange }: { open: boolean; onOpenCha
{loading ? "Un momento…" : mode === "login" ? "Iniciar sesión" : "Crear cuenta"}
</Button>
</form>
<div className="mt-3 flex items-center gap-3 text-xs font-bold uppercase tracking-wide text-slate-400">
<span className="h-px flex-1 bg-slate-100" />o<span className="h-px flex-1 bg-slate-100" />
</div>
<Button asChild variant="outline" size="lg" className="mt-3 w-full">
<a href="/api/customer/google/start">
<GoogleIcon />Continuar con Google
</a>
</Button>
<button
type="button"
className="mt-3 text-sm font-bold text-ari-marino hover:underline"
@@ -0,0 +1,44 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useCustomerStore } from "@/store/customer-store";
export function GoogleCallbackClient() {
const router = useRouter();
const searchParams = useSearchParams();
const loginWithGoogle = useCustomerStore((state) => state.loginWithGoogle);
const [error, setError] = useState<string | null>(null);
const ran = useRef(false);
useEffect(() => {
// Guard contra el doble-mount de StrictMode -- el code de Google es
// de un solo uso, un segundo intento con el mismo code fallaria.
if (ran.current) return;
ran.current = true;
const code = searchParams.get("code");
const state = searchParams.get("state");
if (!code || !state) {
setError("Faltan parámetros de Google en la URL de retorno.");
return;
}
loginWithGoogle(code, state).then((ok) => {
router.replace(ok ? "/" : "/?googleAuthError=1");
});
}, [searchParams, loginWithGoogle, router]);
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-3 text-center">
{error ? (
<>
<p className="text-lg font-black text-rose-600">No se pudo iniciar sesión con Google</p>
<p className="text-sm text-slate-600">{error}</p>
</>
) : (
<p className="text-sm font-bold text-slate-600">Conectando con Google…</p>
)}
</div>
);
}