Cabeza arriba: este tutorial cubre una versión anterior de WooGraphQL y ya no está al día. Una versión reescrita está en progreso — nos vincularemos a ella desde aquí una vez que sea publicada.
Cuadro de contenidos
- Configuración de WordPress para su aplicación de eCommerce sin cabeza
- Crear y configurar su aplicación eCommerce para el desarrollo
- Páginas de listado de edificios con WooGraphQL
- Creación de sesión de usuario y navegación
- Creación de la página del producto y Opciones del carro
Bienvenidos a la próxima instalación en nuestra serie tutorial, donde vamos a enriquecer nuestra tienda de eCommerce incorporando la navegación del usuario y una página de inicio de sesión. Este capítulo trata de mejorar la interfaz de usuario y las capacidades de interacción, creando así una experiencia de usuario más atractiva e inigualable.
Creación de sesión de usuario y navegación
Una de las funcionalidades básicas de cualquier sitio de eCommerce es la capacidad de los usuarios para navegar a través de diferentes secciones del sitio y tener la capacidad de iniciar sesión y gestionar su cuenta. La barra de navegación superior sirve como el centro central de navegación, permitiendo a los usuarios acceder a varias partes del sitio de forma fácil y eficiente. Añadiendo la navegación de los usuarios a la barra de navegación superior no sólo mejorará la usabilidad, sino que también ayudará a los usuarios a acceder sin esfuerzo a sus datos de cuenta y carrito.
Además, vamos a estar caminando a través del proceso de agregar una página de inicio de sesión a su tienda de comercio electrónico. Una página de inicio bien diseñada es crítica para cualquier tienda online ya que es la puerta de entrada para personalizar la experiencia del usuario, permitiendo a los usuarios seguir sus pedidos, guardar sus productos favoritos, y acelerar el proceso de checkout.
Para endulzar el trato más allá, estaremos explorando un truco que podría ahorrar tiempo y esfuerzo. Le mostraremos cómo utilizar los enlaces desplegables de sesión para pasar la sesión de usuario final de nuestra aplicación Next.js a la instalación de WordPress que alberga nuestra tienda WooCommerce. El beneficio de este método es que nos permite evitar la necesidad de crear ciertas páginas en nuestra aplicación, en particular aquellas que podrían consumir mucho tiempo para construir o que dependen de la funcionalidad no fácilmente disponible en nuestra aplicación frontal.
Prerrequisitos
Antes de empezar, debemos actualizar nuestra .env.local archivo con algunas variables que serán vitales para este capítulo.
SESSION_TOKEN_LS_KEY=[SESSION_TOKEN_LS_KEY]
REFRESH_TOKEN_LS_KEY=[REFRESH_TOKEN_LS_KEY]
AUTH_TOKEN_SS_KEY=[AUTH_TOKEN_SS_KEY]
AUTH_TOKEN_EXPIRY_SS_KEY=[AUTH_TOKEN_EXPIRY_SS_KEY]
CLIENT_CREDENTIALS_LS_KEY=[CLIENT_CREDENTIALS_LS_KEY]
CLIENT_SESSION_SS_KEY=[CLIENT_SESSION_SS_KEY]
CLIENT_SESSION_EXP_SS_KEY=[CLIENT_SESSION_EXP_SS_KEY]
NONCE_KEY=[NONCE_KEY]
NONCE_SALT=[NONCE_SALT]Los valores pueden ser aleatorios excepto para NONCE_KEY y NONCE_SALT el debe igualar sus equivalentes en WordPress. Usted puede típicamente encontrar estos valores comprobando usted instalación de WordPress wp-config.php archivo. Recuerde actualizar su next.config.js también
...
const nextConfig = {
...
},
env: {
...
SESSION_TOKEN_LS_KEY: process.env.SESSION_TOKEN_LS_KEY,
REFRESH_TOKEN_LS_KEY: process.env.REFRESH_TOKEN_LS_KEY,
AUTH_TOKEN_SS_KEY: process.env.AUTH_TOKEN_SS_KEY,
AUTH_TOKEN_EXP_SS_KEY: process.env.AUTH_TOKEN_EXP_SS_KEY,
CLIENT_CREDENTIALS_LS_KEY: process.env.CLIENT_CREDENTIALS_LS_KEY,
CLIENT_SESSION_SS_KEY: process.env.CLIENT_SESSION_SS_KEY,
CLIENT_SESSION_EXP_SS_KEY: process.env.CLIENT_SESSION_EXP_SS_KEY,
NONCE_KEY: process.env.NONCE_KEY,
NONCE_SALT: process.env.NONCE_SALT,
},
}
...
Al final de este tutorial, usted estará equipado con el conocimiento para mejorar la navegación de los usuarios, añadir una página de inicio de sesión fácil de usar, e implementar enlaces desplegables de sesión, llevando su tienda eCommerce al siguiente nivel de interactividad y funcionalidad del usuario. Entremos y empecemos.
Parte 1 Crear página de inicio de sesión y UserNav
1. Instala zod, react-hook-form, y @hookform/resolvers paquetes:
npm install zod react-hook-form @hookform/resolversUsaremos estos paquetes para administrar y validar el formulario de inicio de sesión.
2. Crear /app/login/page.tsx:
// app/login/page.tsx
import { Login } from '@/client/Login';
export default function LoginPage() {
return (
<div className="min-h-screen">
<Login/>
</div>
);
}Bastante simple. Adelante.
3. Crear /client/Login:
// cliente/Login/Login.tsx
import { useEffect } from 'react';
import * as z from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useSession } from '@/client/SessionProvider';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/ui/form';
import { Input } from '@/ui/input';
import { Button } from '@/ui/button';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
export const LoginSchema = z.object({
username: z.string().min(4, {
message: 'Username must be at least 4 characters long',
}),
password: z.string().min(1, {
message: 'Password must enter a password',
})
});
export function Login() {
const { login, isAuthenticated, fetching } = useSession();
const router = useRouter();
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
username: '',
password: '',
},
});
useEffect(() => {
if (isAuthenticated) {
router.push('/');
}
}, [isAuthenticated]);
const onSubmit = (data: z.infer<typeof LoginSchema>) => {
login(data.username, data.password);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-screen-lg mx-auto px-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter your username or e-mail associate with your account." {...field}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter your password." {...field}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Button
type="submit"
disabled={fetching}
className="flex gap-x-2 items-center"
>
Submit
{fetching && <LoadingSpinner noText/>}
</Button>
</form>
</Form>
);
}Si estás familiarizado con react-hook-form Esto no debería parecer anormal para ti. La parte que me gustaría llamar es
const { login, isAuthenticated, fetching } = useSession();useSession es un gancho para el SessionProvider que aún no hemos implementado, así que no esperes que esto funcione de inmediato. Llegaremos pronto.
4. Crear /client/UserNav:
import { useRouter } from 'next/navigation';
import { cn } from '@/utils/ui';
import { deleteClientSessionId } from '@woographql/utils/client';
import { useSession } from '@/client/SessionProvider';
import { NavLink, linkClassName } from '@/ui/NavLink';
import { Button } from '@/ui/button';
export function UserNav() {
const { push } = useRouter();
const {
cart,
customer,
cartUrl,
checkoutUrl,
accountUrl,
logout: killSession,
isAuthenticated,
fetching,
} = useSession();
const goToCartPage = () => {
deleteClientSessionId();
console.log('Go to cart page');
window.location.href = cartUrl;
};
const goToCheckoutPage = () => {
deleteClientSessionId();
window.location.href = checkoutUrl;
};
const goToAccountPage = () => {
deleteClientSessionId();
window.location.href = accountUrl;
};
const logout = () => {
killSession(`Goodbye, ${customer?.firstName}`);
};
return (
<>
<li className="group">
<Button
className={cn(
'flex flex-row gap-x-2 items-center p-0 hover:no-underline text-base font-normal',
linkClassName,
)}
disabled={fetching}
variant='link'
onClick={goToCartPage}
>
<span>{cart?.contents?.itemCount || 0}</span>
Cart
</Button>
</li>
<li className="group w-auto">
<Button
className={cn(
'p-0 hover:no-underline text-base font-normal',
linkClassName,
)}
disabled={fetching}
variant='link'
onClick={goToCheckoutPage}
>
Checkout
</Button>
</li>
{isAuthenticated ? (
<>
<li className="group">
<Button
className={cn(
'p-0 hover:no-underline text-base font-normal',
linkClassName,
)}
disabled={fetching}
variant='link'
onClick={goToAccountPage}
>
Account
</Button>
</li>
<li className="group">
<Button
className={cn(
'p-0 hover:no-underline text-base font-normal',
linkClassName,
)}
disabled={fetching}
variant='link'
onClick={logout}
>
Logout
</Button>
</li>
</>
) : (
<li className="group">
<NavLink href="/login">
Login
</NavLink>
</li>
)}
</>
)
}El UserNav componente es responsable de las acciones del usuario de visualización basadas en valores recibidos del SessionProvider.
const {
cart, // Cart data object.
customer, // Customer data object.
goToCartPage, // Callback for sending the user to the Cart Page.
goToAccountPage, // Callback for sending the user to the Account Page.
goToCheckoutPage, // Callback for sending the user to the Checkout Page.
logout, // Callback for deleting end-user's session.
isAuthenticated, // Flag determining end-user's login status
refetchUrls, // Callback to begin process of generating new session drop-off urls.
fetching, // Flag determining session handler fetcher status.
} = useSession();Vamos a añadir esto a la /server/TopNav componente antes de seguir adelante SessionProvider.
5. Actualización /server/TopNav/TopNav.tsx:
// servidor/TopNav/TopNav.tsx
...
import { UserNav } from '@/client/UserNav';
...
export function TopNav({ menu }: TopNavProps) {
return (
<nav className="w-full bg-white min-h-24 py-4 px-4">
<ul className="max-w-screen-lg m-auto w-full flex flex-row gap-x-4 justify-end items-center">
...
<UserNav/>
</ul>
</nav>
)
}Parte 2: Crear sesiónProvider
1. Crear /client/SessionProvider:
// cliente/SessionProvider/SessionProvider.tsx
import {
createContext,
PropsWithChildren,
useContext,
useReducer,
useEffect,
} from 'react';
import {
Customer,
Cart,
} from '@/graphql';
import { useToast } from '@/ui/use-toast';
export interface SessionContext {
isAuthenticated: boolean;
hasCredentials: boolean;
cart: Cart|null;
customer: Customer|null;
cartUrl: string;
checkoutUrl: string;
accountUrl: string;
fetching: boolean;
logout: (message?: string) => void;
login: (username: string, password: string, successMessage?: string) => Promise<boolean>;
refetch: () => Promise<boolean>;
}
const initialContext: SessionContext = {
isAuthenticated: false,
hasCredentials: false,
cart: null,
customer: null,
cartUrl: '',
checkoutUrl: '',
accountUrl: '',
fetching: false,
logout: (message?: string) => null,
login: (username: string, password: string) => new Promise((resolve) => { resolve(false); }),
refetch: () => new Promise((resolve) => { resolve(false); }),
};
export const sessionContext = createContext<SessionContext>(initialContext);
type SessionAction = {
type: 'UPDATE_STATE';
payload: SessionContext;
} | {
type: 'LOGOUT';
};
const reducer = (state: SessionContext, action: SessionAction): SessionContext => {
switch (action.type) {
case 'UPDATE_STATE':
return {
...state,
...action.payload,
};
case 'LOGOUT':
return {
...state,
customer: null,
cart: null,
};
default:
throw new Error('Invalid action dispatched to session data reducer');
}
};
const { Provider } = sessionContext;
export function SessionProvider({ children }: PropsWithChildren) {
const { toast } = useToast();
const [state, dispatch] = useReducer(reducer, initialContext);
// Eliminar el estado del logotipo y las credos almacenan localmente.
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
const store: SessionContext = {
...state,
isAuthenticated: !!state.customer?.id && 'guest' !== state.customer.id,
hasCredentials: false,
logout,
};
return (
<Provider value={store}>{children}</Provider>
);
}
export const useSession = () => useContext(sessionContext);Ahora agréguelo a la /app/layout.tsx
2. Actualización /app/layout.tsx:
// app/layout.tsx
...
import { SessionProvider } from '@/client/SessionProvider';
...
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
...
return (
<html lang="en">
<body>
<SessionProvider>
<TopNav menu={menu}/>
<main className="w-full">
{children}
</main>
</SessionProvider>
<Toaster/>
</body>
</html>
)
}Con esto la aplicación construirá y funcionará con éxito, pero ninguno de los componentes que hemos introducido realmente funcionan. Aquellos con un ojo agudo habrían notado que no hemos implementado ninguna lógica para comunicarse con nuestro punto final.

Esto se debe a que tratar con la seguridad del usuario final requiere una gestión sofisticada de las credenciales de sesión y una visibilidad restringida del punto final de GraphQL. Cabe señalar que a pesar de todo lo que se ha creado hasta ahora, así ocurren acciones en la aplicación en vivo que exponen el punto final de GraphQL. No creas. Abra las herramientas del desarrollador del navegador y compruebe Network tab. Refresque la página y haga clic en alrededor, haga lo que sea, pero no verá solicitudes visibles a su punto final de GraphQL están hechas.
Hasta ahora toda la solicitud de GraphQL se ha hecho en el nivel de servidor sólo cada ejecución durante las páginas tiempo de construcción, pero tendremos que hacer alguna solicitud de cliente-side para tratar la sesión de usuario final que expondría el punto final si realizamos las consultas de GraphQL directamente en el cliente.
Así que no vamos a ejecutarlos en el cliente pero en lugar de estar aprovechando las páginas Siguiente 13 Route. Haremos pocas páginas de ruta que ejecutarán nuestras solicitudes de GraphQL fuera de la vista del cliente. Antes de eso tendremos que crear algunos archivos de utilidad que definirán la lógica para comunicarse con estas páginas de ruta y gestionar las credenciales de sesión sensibles al tiempo. Empecemos.
Parte 3: Crear archivos de utilidad.
1. Instala crypto-js y jwt-decode paquetes:
npm install crypto-js jwt-decodeEstas bibliotecas con para recrear la funcionalidad de piratería de WordPress.
2. Crear /utils/nonce.ts:
// utils/nonce.ts
import HmacMD5 from 'crypto-js/hmac-md5';
import jwtDecode from 'jwt-decode';
export const MINUTE_IN_SECONDS = 60;
export const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS;
export const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS;
export function time() {
return Math.floor(new Date().getTime() / 1000);
}
export function nonceTick() {
const nonceLife = DAY_IN_SECONDS;
return Math.ceil(time() / (nonceLife / 2));
}
export function wpNonceHash(data: string) {
const nonceSalt = process.env.NONCE_KEY as string + process.env.NONCE_SALT as string;
const hash = HmacMD5(data, nonceSalt).toString();
return hash;
}
export function createNonce(action: string, uId:string|number, token:string) {
const i = nonceTick();
const nonce = wpNonceHash(`${i}|${action}|${uId}|${token}`).slice(-12, -2);
return nonce;
}
export enum ActionTypes {
Cart = 'cart',
Checkout = 'checkout',
Account = 'account',
}
function getAction(action: ActionTypes, uId: string|number) {
switch (action) {
case ActionTypes.Cart:
return `load-cart_${uId}`;
case ActionTypes.Checkout:
return `load-checkout_${uId}`;
case ActionTypes.Account:
return `load-account_${uId}`;
default:
throw new Error('Invalid nonce action provided.');
}
}
function getNonceParam(action: ActionTypes) {
switch (action) {
case ActionTypes.Cart:
return '_wc_cart';
case ActionTypes.Checkout:
return '_wc_checkout';
case ActionTypes.Account:
return '_wc_account';
default:
throw new Error('Invalid nonce action provided.');
}
}
type DecodedToken = {
data: { customer_id: string };
}
export function getUidFromToken(sessionToken: string) {
const decodedToken = jwtDecode<DecodedToken>(sessionToken);
if (!decodedToken?.data?.customer_id) {
throw new Error('Failed to decode session token');
}
return decodedToken.data.customer_id;
}
export function generateUrl(sessionToken:string, clientSessionId:string, actionType: ActionTypes) {
const uId = getUidFromToken(sessionToken);
const action = getAction(actionType, uId);
// Crear nonce
const nonce = createNonce(action, uId, clientSessionId);
// Crear URL.
const param = getNonceParam(actionType);
let url = `${process.env.BACKEND_URL}/wp/transfer-session?session_id=${uId}&${param}=${nonce}`;
return url;
}No voy a entrar en muchos detalles sobre este archivo aquí, pero en su mayoría es un clon JS de un par de funciones/constantes PHP/WP. En el caso de nuestro session la utilidad que nos importa time función y MINUTE_IN_SECONDS constante. Adelante.
3. Crear /utils/client.ts:
// utils/client.ts
import {
wpNonceHash,
time,
HOUR_IN_SECONDS,
MINUTE_IN_SECONDS,
DAY_IN_SECONDS,
} from '@/utils/nonce';
type Creds = {
userAgent: string;
ip: string;
issued: number;
}
async function createClientSessionId() {
const encodedCredentials = localStorage.getItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string);
let credentials: null|Creds = encodedCredentials ? JSON.parse(encodedCredentials) : null;
if (!credentials || time()> credentials.issued + (14 * DAY_IN_SECONDS)) {
// Crear un objeto de credenciales con UserAgent.
credentials = {
userAgent: window?.navigator?.userAgent || '',
ip: '',
issued: 0,
};
// Trae IP.
const response = await fetch('https://api.ipify.org/?format=json');
const { data } = await response.json();
credentials.ip = data?.ip || '';
}
// Tiempos de actualización para asegurar que nuevos noces se generan cada vez
// el usuario final comienza esa aplicación.
credentials.issued = time();
localStorage.setItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string, JSON.stringify(credentials));
// Generar ID de sesión de cliente.
const clientSessionId = wpNonceHash(JSON.stringify(credentials));
const timeout = `${credentials.issued + HOUR_IN_SECONDS}`;
// Guardar el ID de sesión del cliente.
sessionStorage.setItem(process.env.CLIENT_SESSION_SS_KEY as string, clientSessionId);
sessionStorage.setItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string, timeout);
// Devolver el ID de sesión del cliente.
return { clientSessionId, timeout };
}
function hasSessionToken() {
const sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);
return !!sessionToken;
}
let clientSetter: ReturnType<typeof setInterval>;
/**
* Creates timed fetcher for renewing client credentials and client session id.
*
* @returns {void}
*/
function setClientFetcher() {
if (clientSetter) {
clearInterval(clientSetter);
}
clientSetter = setInterval(
async () => {
if (!hasSessionToken()) {
clearInterval(clientSetter);
return;
}
createClientSessionId();
},
Number(45 * MINUTE_IN_SECONDS),
);
}
export async function getClientSessionId() {
let clientSessionId = sessionStorage.getItem(process.env.CLIENT_SESSION_SS_KEY as string);
let timeout = sessionStorage.getItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string);
if (!clientSessionId || !timeout || time()> Number(timeout)) {
({ clientSessionId, timeout } = await createClientSessionId());
setClientFetcher();
}
return { clientSessionId, timeout };
}
export function deleteClientSessionId() {
if (clientSetter) {
clearInterval(clientSetter);
}
sessionStorage.removeItem(process.env.CLIENT_SESSION_SS_KEY as string);
sessionStorage.removeItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string);
}
export function deleteClientCredentials() {
deleteClientSessionId();
localStorage.removeItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string);
}La lógica aquí es crear un clientSessionId. Esto es necesario para restringir el uso del enlace desplegable de sesión a la máquina del usuario final. Nos pondremos más en su uso que creamos las páginas de ruta. Una cosa a tener en cuenta por ahora es que es sensible al tiempo y la mayoría de la lógica aquí está dedicada a su evaluación y renovación.
4. Crear /utils/session.ts:
import { GraphQLError } from 'graphql/error';
import {
Customer,
Cart,
} from '@/graphql';
import { getClientSessionId } from '@/utils/client';
import { MINUTE_IN_SECONDS, time } from '@/utils/nonce';
type ResponseErrors = {
errors?: {
message: string;
data?: unknown;
}
}
async function apiCall<T>(url: string, input: globalThis.RequestInit) {
const response = await fetch(
url,
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
...input,
},
);
const json: T&ResponseErrors = await response.json();
// Errores de captura.
if (json?.errors || response.status !== 200) {
throw new Error(json.errors?.message || `Failed to fetch: ${url}`);
}
return json;
}
// Auth management.
function saveCredentials(authToken: string, sessionToken?: string, refreshToken?: string) {
sessionStorage.setItem(process.env.AUTH_TOKEN_SS_KEY as string, authToken);
if (!!sessionToken) {
localStorage.setItem(process.env.SESSION_TOKEN_LS_KEY as string, sessionToken);
}
if (refreshToken) {
localStorage.setItem(process.env.REFRESH_TOKEN_LS_KEY as string, refreshToken);
}
}
function saveSessionToken(sessionToken: string) {
localStorage.setItem(process.env.SESSION_TOKEN_LS_KEY as string, sessionToken);
}
export function hasCredentials() {
const sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);
const authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);
const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);
if (!!sessionToken && !!authToken && !!refreshToken) {
return true;
}
return false;
}
function setAuthTokenExpiry() {
const authTimeout = time() + (15 * MINUTE_IN_SECONDS);
sessionStorage.setItem(process.env.AUTH_TOKEN_EXPIRY_SS_KEY as string, `${authTimeout}`);
}
function authTokenIsExpired() {
const authTimeout = sessionStorage.getItem(process.env.AUTH_TOKEN_EXPIRY_SS_KEY as string);
if (!authTimeout || Number(authTimeout) < time()) {
return true;
}
}
type FetchAuthTokenResponse = {
authToken: string;
sessionToken: string;
}
async function fetchAuthToken() {
const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);
if (!refreshToken) {
// eslint-disable-next-line no-console
isDev() && console.error('Unauthorized');
return null;
}
const json = await apiCall<FetchAuthTokenResponse>(
'/api/auth',
{
method: 'POST',
body: JSON.stringify({ refreshToken }),
},
);
const { authToken, sessionToken } = json;
saveCredentials(authToken, sessionToken);
setAuthTokenExpiry();
return authToken;
}
let tokenSetter: ReturnType<typeof setInterval>;
function setAutoFetcher() {
if (tokenSetter) {
clearInterval(tokenSetter);
}
tokenSetter = setInterval(
async () => {
if (!hasCredentials()) {
clearInterval(tokenSetter);
return;
}
fetchAuthToken();
},
Number(process.env.AUTH_KEY_TIMEOUT || 30000),
);
}
type LoginResponse = {
authToken: string
refreshToken: string;
sessionToken: string;
}
export async function login(username: string, password: string): Promise<boolean|string> {
let json: LoginResponse;
try {
json = await apiCall<LoginResponse>(
'/api/login',
{
method: 'POST',
body: JSON.stringify({ username, password }),
},
);
} catch (error) {
return (error as GraphQLError)?.message || error as string;
}
const { authToken, refreshToken, sessionToken } = json;
saveCredentials(authToken, sessionToken, refreshToken);
setAutoFetcher();
return true;
}
export async function getAuthToken() {
let authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);
if (!authToken || authTokenIsExpired()) {
authToken = await fetchAuthToken();
}
if (authToken && !tokenSetter) {
setAutoFetcher();
}
return authToken;
}
type FetchSessionTokenResponse = {
sessionToken: string;
}
async function fetchSessionToken() {
const json = await apiCall<FetchSessionTokenResponse>(
'/api/auth',
{ method: 'GET' },
);
const { sessionToken } = json;
sessionToken && saveSessionToken(sessionToken);
return sessionToken;
}
async function getSessionToken() {
let sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);
if (!sessionToken) {
sessionToken = await fetchSessionToken();
}
return sessionToken;
}
export function hasRefreshToken() {
const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);
return !!refreshToken;
}
export function hasAuthToken() {
const authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);
return !!authToken;
}
export type FetchSessionResponse = {
customer: Customer;
cart: Cart;
}
export async function getSession(): Promise<FetchSessionResponse|string> {
const authToken = await getAuthToken();
const sessionToken = await getSessionToken();
let json: FetchSessionResponse;
try {
json = await apiCall<FetchSessionResponse>(
'/api/session',
{
method: 'POST',
body: JSON.stringify({
sessionToken,
authToken,
}),
},
);
} catch (error) {
return (error as GraphQLError)?.message || error as string;
}
const { customer } = json;
saveSessionToken(customer.sessionToken as string);
return json;
}
export type FetchAuthURLResponse = {
cartUrl: string;
checkoutUrl: string;
accountUrl: string
}
export async function fetchAuthURLs(): Promise<FetchAuthURLResponse|string> {
const authToken = await getAuthToken();
const sessionToken = await getSessionToken();
const { clientSessionId, timeout } = await getClientSessionId();
let json: FetchAuthURLResponse;
try {
json = await apiCall<FetchAuthURLResponse>(
'/api/nonce',
{
method: 'POST',
body: JSON.stringify({
sessionToken,
authToken,
clientSessionId,
timeout,
}),
},
);
} catch (error) {
return (error as GraphQLError)?.message || error as string;
}
return json;
}
export function deleteCredentials() {
if (tokenSetter) {
clearInterval(tokenSetter);
}
localStorage.removeItem(process.env.SESSION_TOKEN_LS_KEY as string);
sessionStorage.removeItem(process.env.AUTH_TOKEN_SS_KEY as string);
localStorage.removeItem(process.env.REFRESH_TOKEN_LS_KEY as string);
}Aunque es un poco tenebroso esto bastante directo. login, getSession, y fetchAuthURLs son callback para ser utilizado por nuestro SessionProvider.
login: Funciona enviando una solicitud a la/api/loginruta con el usuario finalusernameypasswordentrada, si con éxito yauthTokenyrefreshTokenes devuelto.getSession: Funciona por primera vez recuperar las credenciales de la sesión de usuario final delocalStorageentonces envía una solicitud POST/api/sessioncon el usuario finalauthTokenysessionTokenpara identificación.- Si el usuario final tiene un
refreshTokeny no válidoauthTokeny nuevosauthTokense recupera enviando una solicitud POST a/api/authdentrofetchAuthToken, - Si el usuario final no tiene credenciales de ninguna otra
sessionTokense recupera enviando una solicitud GET para/api/authdentrofetchSessionToken
- Si el usuario final tiene un
fetchAuthURLsFunciona exactamente igualgetSessionexcepto que envía elclientSessionIdyclientSessionIdTimeoutasí como para la generación de URL desplegable de sesión.- Si el usuario final no tiene validez
clientSessionId, se genera una nueva.
- Si el usuario final no tiene validez
Con todos los archivos de utilidad creados. Vayamos a las páginas de la ruta.
Parte 4: Crear páginas de ruta
1. Crear app/api/auth/route.ts:
// app/api/auth/route.ts
import { NextResponse } from 'next/server';
import { print } from 'graphql';
import {
GetSessionDocument,
GetSessionQuery,
RefreshAuthTokenDocument,
RefreshAuthTokenMutation,
getClient,
} from '@/graphql';
export async function GET(request: Request) {
try {
const client = getClient();
const { data, headers } = await client.rawRequest<GetSessionQuery>(
print(GetSessionDocument),
);
const cart = data?.cart;
const customer = data?.customer;
const sessionToken = headers.get('woocommerce-session');
if (!cart || !customer || !sessionToken) {
return NextResponse.json({ errors: { message: 'Failed to retrieve session credentials.' } }, { status: 500 });
}
return NextResponse.json({ sessionToken });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const client = getClient();
const body = await request.json();
let authToken = body.authToken;
const refreshToken = body.refreshToken;
if (!authToken && !refreshToken) {
return NextResponse.json({ errors: { message: 'No refresh token provided' } }, { status: 500 });
}
if (!authToken && refreshToken) {
client.setHeaders({});
const results = await client.request<RefreshAuthTokenMutation>(
RefreshAuthTokenDocument,
{ refreshToken },
);
authToken = results?.refreshJwtAuthToken?.authToken;
if (!authToken) {
return NextResponse.json({ errors: { message: 'Failed to retrieve auth token.' } }, { status: 500 });
}
}
client.setHeaders({ Authorization: `Bearer ${authToken}` });
const { data: cartData, headers } = await client.rawRequest<GetSessionQuery>(
print(GetSessionDocument),
);
const newSessionToken = cartData?.customer?.sessionToken;
if (!newSessionToken) {
return NextResponse.json({ errors: { message: 'Failed to validate auth token.' } }, { status: 500 });
}
const sessionToken = headers.get('woocommerce-session') || newSessionToken;
return NextResponse.json({ authToken, sessionToken });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
}
}Aunque esta ruta tiene ambos GET y POST Callback, ambos intentan el mismo objetivo bajo diferentes circunstancias.
POSTasume que el usuario es un usuario de retorno conrefreshTokenoauthTokeny si el éxito devuelve unauthTokenysessionToken.GETasume que el usuario es nuevo sin credenciales y devuelve un nuevosessionTokenpara un usuario invitado.
2. Crear /app/api/login/route.ts:
import { NextResponse } from 'next/server';
import { print } from 'graphql';
import {
GetSessionDocument,
GetSessionQuery,
LoginDocument,
LoginMutation,
getClient,
} from '@/graphql';
type RequestBody = ({
auth: string;
username: undefined;
password: undefined;
} | {
auth: undefined;
username: string;
password: string;
})
export async function POST(request: Request) {
try {
const { username, password } = await request.json() as RequestBody;
const graphQLClient = getClient();
if (!username || !password) {
return NextResponse.json({
errors: {
message: 'Proper credential must be provided for authentication',
},
}, { status: 500 });
}
const data = await graphQLClient.request<LoginMutation>(LoginDocument, { username, password });
if (!data?.login) {
return NextResponse.json({ errors: { message: 'Login failed.' } }, { status: 500 });
}
const { authToken, refreshToken } = data?.login;
if (!authToken || !refreshToken) {
return NextResponse.json({ errors: { message: 'Failed to retrieve credentials.' } }, { status: 500 });
}
graphQLClient.setHeader('Authorization', `Bearer ${data.login.authToken}`);
const { data:_, headers } = await graphQLClient.rawRequest<GetSessionQuery>(print(GetSessionDocument));
const sessionToken = headers.get('woocommerce-session');
if (!sessionToken) {
return NextResponse.json({ errors: { message: 'Failed to retrieve session token.' } }, { status: 500 });
}
return NextResponse.json({ authToken, refreshToken, sessionToken });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Login credentials invalid.' } }, { status: 500 });
}
}Este es similar al último pero más simple. Se necesita un username y password de la entrada y trata de devolver el usuario final authToken, refreshToken, y sessionToken.
Nota, que sessionToken se está recuperando de la respuesta HTTP GetSession consulta con la authToken establecido en el Authorization. El razonamiento para esto es conectar la última sesión al usuario en la base de datos. Si sessionToken desde customer la consulta se utiliza en lugar de esta una nueva sesión login cada vez y el viejo período de sesiones será borrado el segundo el nuevo sessionToken se utiliza. El patrón también se utilizó en el POST callback of the api/auth ruta también si te das cuenta.
3. Crear /app/api/session/route.ts:
import { NextResponse } from 'next/server';
import {
GetSessionDocument,
GetSessionQuery,
Cart,
Customer,
getClient,
} from '@/graphql';
type RequestBody = {
sessionToken: string;
authToken?: string;
}
type GraphQLRequestHeaders = {
'woocommerce-session': string;
Authorization?: string;
}
export async function POST(request: Request) {
try {
const { sessionToken, authToken } = await request.json() as RequestBody;
if (!sessionToken) {
return NextResponse.json({ errors: { message: 'Session not started' } }, { status: 500 });
}
const headers: GraphQLRequestHeaders = { 'woocommerce-session': `Session ${sessionToken}` };
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const graphQLClient = getClient();
graphQLClient.setHeaders(headers);
const results = await graphQLClient.request<GetSessionQuery>(
GetSessionDocument,
);
const customer = results?.customer as Customer;
const cart = results?.cart as Cart;
if (!customer) {
return NextResponse.json({ errors: { message: 'Failed to retrieve customer data.' } }, { status: 500 });
}
if (!cart) {
return NextResponse.json({ errors: { message: 'Failed to retrieve cart data.' } }, { status: 500 });
}
return NextResponse.json({ customer, cart });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
}
}Este espera un sessionToken y posiblemente authToken. Cuando se le dan las credenciales adecuadas devuelve el usuario final customer y cart datos del GetSession consulta.
4. Crear /api/nonce/route.ts:
import { NextResponse } from 'next/server';
import { UpdateSessionDocument, UpdateSessionMutation, getClient } from '@/graphql';
import { ActionTypes, generateUrl } from '@/utils/nonce';
type RequestBody = {
sessionToken: string;
authToken?: string;
clientSessionId: string;
timeout: number;
}
type GraphQLRequestHeaders = {
'woocommerce-session': string;
Authorization?: string;
}
export async function POST(request: Request) {
try {
const {
sessionToken,
authToken,
clientSessionId,
timeout,
} = await request.json() as RequestBody;
if (!sessionToken) {
return NextResponse.json({ errors: { message: 'No session started.' } }, { status: 500 });
}
if (!clientSessionId || !timeout) {
return NextResponse.json({ errors: { message: 'Client Session ID and expiration must be provided.' } }, { status: 500 });
}
const headers: GraphQLRequestHeaders = { 'woocommerce-session': `Session ${sessionToken}` };
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
const client = getClient();
client.setHeaders(headers);
const input = {
sessionData: [
{
key: 'client_session_id',
value: clientSessionId,
},
{
key: 'client_session_id_expiration',
value: timeout,
},
],
};
const results = await client.request<UpdateSessionMutation>(
UpdateSessionDocument,
{ input },
);
if (!results.updateSession) {
const message = 'Failed to update session';
return NextResponse.json({ errors: { message } }, { status: 500 });
}
const cartUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Cart);
const checkoutUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Checkout);
const accountUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Account);
return NextResponse.json({ cartUrl, checkoutUrl, accountUrl });
} catch (err) {
console.log(err);
return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
}
}
Esta ruta espera la misma entrada que el último más el usuario final clientSessionId y clientSessionIdTimeout. Cuando se le da la entrada adecuada establece el usuario final client_session_id y client_session_id_expiration en el objeto de la sesión WooCommerce del usuario final en el servidor. Estos valores se utilizan entonces para generar una serie de noces en el WP Backend para ser utilizado en las URL desplegable sesión.
Ahora podríamos preguntar por estos noces o incluso las URL desplegables de toda la sesión en el punto final de GraphQL, sin embargo, que corren los riesgos de que los noces/URLs que se filtran en tránsito. Así que en cambio utilizamos el resto de la funcionalidad definida en el utils/nonce.ts archivo y recrea los mismos noces exactos en nuestras páginas de Ruta. generateUrl genera una sesión desplegable URL hecha a medida para el usuario final y atada a la máquina allí. Con esto se han creado las últimas páginas de ruta y todo lo que queda es actualizar el SessionProvider para utilizar los archivos de utilidad y por extensión las páginas de ruta.
Parte 5: Final SessionProvider
1. Actualización /client/SessionProvider/SessionProvider.tsx:
// cliente/SessionProvider/SessionProvider.tsx
...
import {
hasCredentials,
deleteCredentials,
getSession as getSessionApiCall,
FetchSessionResponse as Session,
FetchAuthURLResponse as AuthUrls,
fetchAuthURLs as fetchAuthURLsApiCall,
login as loginApiCall,
} from '@/utils/session';
import {
deleteClientSessionId,
deleteClientCredentials,
} from '@/utils/client';
import { useToast } from '@/ui/use-toast';
...
export function SessionProvider({ children }: PropsWithChildren) {
...
// Eliminar el estado del logotipo y las credos almacenan localmente.
const logout = () => {
...
deleteCredentials();
deleteClientCredentials();
fetchSession();
};
// Sesión de proceso para buscar respuesta a la solicitud.
const setSession = (session: Session|string, authUrls: AuthUrls|string) => {
if (typeof session === 'string') {
toast({
title: 'Fetch Session Error',
description: 'Failed to fetch session.',
variant: 'destructive'
});
}
if (typeof authUrls === 'string') {
toast({
title: 'Session Error',
description: 'Failed to generate session URLs. Please refresh the page.',
variant: 'destructive'
});
}
if (typeof session === 'string' || typeof authUrls === 'string') {
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: false } as SessionContext,
});
return false;
}
dispatch({
type: 'UPDATE_STATE',
payload: {
...session,
...authUrls,
fetching: false
} as SessionContext,
});
return true;
};
const login = (username: string, password: string) => {
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: true } as SessionContext,
});
return loginApiCall(username, password)
.then((success) => {
if (typeof success === 'string') {
toast({
title: 'Login Error',
description: success,
variant: 'destructive'
});
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: false } as SessionContext,
});
return false;
}
return fetchSession();
});
};
// Trae datos de clientes.
const fetchSession = () => {
dispatch({
type: 'UPDATE_STATE',
payload: { fetching: true } as SessionContext,
});
return getSessionApiCall()
.then(async (sessionPayload) => {
const authUrlPayload = await fetchAuthURLsApiCall();
return setSession(sessionPayload, authUrlPayload);
});
};
useEffect(() => {
if (state.fetching) {
return;
}
if (!state.customer || !state.cart) {
fetchSession();
}
}, []);
const store: SessionContext = {
...
refetch: fetchSession,
login,
};
return (
<Provider value={store}>{children}</Provider>
);
}
export const useSession = () => useContext(sessionContext);Después de actualizar el SessionProvider, el Login y otras opciones de Usuario Nav deben funcionar como se esperaba, aunque es posible que necesites estiloar tu instalación WP para parecerte a ti Siguiente aplicación para una experiencia sin problemas.

Conclusión
En el siguiente tutorial completaremos nuestra aplicación con la página de producto individual y opciones de carrito. Con suerte, este tutorial te mantuvo entretenido y te veré en el siguiente.

Leave a Reply