Files
devops 77389aea19 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.
2026-08-29 17:01:32 +00:00

100 lines
5.5 KiB
TypeScript

"use client";
import Link from "next/link";
import { LogOut, MessageCircle, PackageCheck } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
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");
const [form, setForm] = useState({ email: "", password: "", firstName: "", lastName: "" });
async function submit(event: FormEvent) {
event.preventDefault();
const ok = mode === "login"
? await login(form.email, form.password)
: await register(form.email, form.password, form.firstName, form.lastName);
if (ok) setForm({ email: "", password: "", firstName: "", lastName: "" });
}
const isLoggedIn = hasHydrated && customer !== null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg p-7">
<div className="pr-10">
{isLoggedIn ? (
<>
<span className="text-xs font-black uppercase tracking-[.16em] text-ari-coral">Tu cuenta</span>
<h2 className="mt-3 text-3xl font-black">Hola, {customer.firstName ?? customer.email}</h2>
<p className="mt-3 text-sm leading-6 text-slate-600">{customer.email}</p>
<div className="mt-6 grid gap-3">
<Button asChild variant="outline"><Link href="/catalog" onClick={() => onOpenChange(false)}><PackageCheck />Ver mis opciones de compra</Link></Button>
<Button variant="ghost" onClick={logout}><LogOut className="size-4" />Cerrar sesión</Button>
</div>
</>
) : (
<>
<span className="text-xs font-black uppercase tracking-[.16em] text-ari-coral">Atención personalizada</span>
<h2 className="mt-3 text-3xl font-black">{mode === "login" ? "Inicia sesión" : "Crea tu cuenta"}</h2>
<form onSubmit={submit} className="mt-5 space-y-3">
{mode === "register" && (
<div className="grid gap-3 sm:grid-cols-2">
<Input placeholder="Nombre" required value={form.firstName} onChange={(event) => setForm({ ...form, firstName: event.target.value })} />
<Input placeholder="Apellido" required value={form.lastName} onChange={(event) => setForm({ ...form, lastName: event.target.value })} />
</div>
)}
<Input type="email" placeholder="Correo" required value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />
<Input type="password" placeholder="Contraseña" required minLength={8} value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} />
{error && <p className="text-sm font-bold text-rose-600">{error}</p>}
<Button type="submit" variant="primary" size="lg" className="w-full" disabled={loading}>
{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"
onClick={() => setMode(mode === "login" ? "register" : "login")}
>
{mode === "login" ? "¿No tienes cuenta? Regístrate" : "¿Ya tienes cuenta? Inicia sesión"}
</button>
<div className="mt-6 border-t border-slate-100 pt-5">
<Button asChild variant="outline" className="w-full">
<a href={process.env.NEXT_PUBLIC_WHATSAPP_URL ?? "https://wa.link/rvcqqc"} target="_blank" rel="noopener noreferrer">
<MessageCircle />Hablar con ARI
</a>
</Button>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
);
}