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.
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { create } from "zustand";
|
|
import { createJSONStorage, persist } from "zustand/middleware";
|
|
|
|
// Cuenta de cliente real conectada a Medusa (Store API, via
|
|
// /api/customer/*), mismo patron que cart-store.ts: token + datos basicos
|
|
// persistidos en localStorage, resincronizados contra el servidor al
|
|
// rehidratar (el token puede llevar dias, o haber sido revocado).
|
|
export interface Customer {
|
|
id: string;
|
|
email: string;
|
|
firstName: string | null;
|
|
lastName: string | null;
|
|
}
|
|
|
|
interface CustomerState {
|
|
token: string | null;
|
|
customer: Customer | null;
|
|
hasHydrated: boolean;
|
|
loading: boolean;
|
|
error: string | null;
|
|
setHasHydrated: (value: boolean) => void;
|
|
register: (email: string, password: string, firstName: string, lastName: string) => Promise<boolean>;
|
|
login: (email: string, password: string) => Promise<boolean>;
|
|
loginWithGoogle: (code: string, state: string) => Promise<boolean>;
|
|
logout: () => void;
|
|
syncCustomer: () => Promise<void>;
|
|
}
|
|
|
|
async function postJson(url: string, body: unknown) {
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
function toCustomer(data: Record<string, unknown>): Customer {
|
|
return {
|
|
id: data.id as string,
|
|
email: data.email as string,
|
|
firstName: (data.firstName as string | null) ?? null,
|
|
lastName: (data.lastName as string | null) ?? null,
|
|
};
|
|
}
|
|
|
|
export const useCustomerStore = create<CustomerState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
token: null,
|
|
customer: null,
|
|
hasHydrated: false,
|
|
loading: false,
|
|
error: null,
|
|
setHasHydrated: (value) => set({ hasHydrated: value }),
|
|
|
|
register: async (email, password, firstName, lastName) => {
|
|
set({ loading: true, error: null });
|
|
const result = await postJson("/api/customer/register", { email, password, firstName, lastName });
|
|
if (result.error) {
|
|
set({ loading: false, error: result.error });
|
|
return false;
|
|
}
|
|
set({ loading: false, token: result.token, customer: toCustomer(result) });
|
|
return true;
|
|
},
|
|
|
|
login: async (email, password) => {
|
|
set({ loading: true, error: null });
|
|
const result = await postJson("/api/customer/login", { email, password });
|
|
if (result.error) {
|
|
set({ loading: false, error: result.error });
|
|
return false;
|
|
}
|
|
set({ loading: false, token: result.token, customer: toCustomer(result) });
|
|
return true;
|
|
},
|
|
|
|
loginWithGoogle: async (code, state) => {
|
|
set({ loading: true, error: null });
|
|
const result = await postJson("/api/customer/google/callback", { code, state });
|
|
if (result.error) {
|
|
set({ loading: false, error: result.error });
|
|
return false;
|
|
}
|
|
set({ loading: false, token: result.token, customer: toCustomer(result) });
|
|
return true;
|
|
},
|
|
|
|
logout: () => set({ token: null, customer: null, error: null }),
|
|
|
|
// El token guardado en localStorage puede llevar dias o haber sido
|
|
// revocado -- se refresca contra el servidor en vez de confiar en el
|
|
// ultimo snapshot local.
|
|
syncCustomer: async () => {
|
|
const token = get().token;
|
|
if (!token) return;
|
|
try {
|
|
const response = await fetch(`/api/customer/me?token=${encodeURIComponent(token)}`);
|
|
if (!response.ok) {
|
|
set({ token: null, customer: null });
|
|
return;
|
|
}
|
|
const result = await response.json();
|
|
if (result.id) set({ customer: toCustomer(result) });
|
|
} catch {
|
|
// Sin red: se queda con el ultimo estado local conocido.
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: "ari-shopping-customer-v1",
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({ token: state.token, customer: state.customer }),
|
|
onRehydrateStorage: () => (state) => {
|
|
state?.setHasHydrated(true);
|
|
void state?.syncCustomer();
|
|
},
|
|
},
|
|
),
|
|
);
|