En haut : ce tutoriel couvre une version WooGraphQL antérieure et n’est plus à jour. Une version réécrite est en cours — nous y lierons une fois qu’elle sera publiée.
Sommaire
- Configuration WordPress pour votre application eCommerce sans tête
- Création et mise en place de votre application eCommerce pour le développement
- Pages d’annonce de boutique de construction avec WooGraphQL
- Création de connexion utilisateur et navigation
- Création de la page Produit et des options de panier
Bienvenue au prochain épisode de notre série de tutoriels, où nous allons enrichir notre magasin eCommerce en intégrant la navigation utilisateur et une page de connexion. Ce chapitre vise à améliorer l’interface utilisateur et les capacités d’interaction, créant ainsi une expérience utilisateur plus engageante et transparente.
Création de connexion utilisateur et navigation
L’une des fonctionnalités essentielles de tout site eCommerce est la possibilité pour les utilisateurs de naviguer dans différentes sections du site et d’avoir la possibilité de se connecter et de gérer leur compte. La barre de navigation supérieure sert de centre de navigation, permettant aux utilisateurs d’accéder facilement et efficacement à diverses parties du site. L’ajout de la navigation utilisateur à la barre de navigation supérieure permettra non seulement d’améliorer la convivialité, mais aussi d’aider les utilisateurs à accéder sans effort à leur compte et aux détails du panier.
En outre, nous allons vous guider dans le processus d’ajout d’une page de connexion à votre magasin eCommerce. Une page de connexion bien conçue est essentielle à toute boutique en ligne car c’est la passerelle vers la personnalisation de l’expérience utilisateur, permettant aux utilisateurs de suivre leurs commandes, de sauvegarder leurs produits préférés et d’accélérer le processus de paiement.
Pour édulcorer davantage l’affaire, nous allons explorer un truc soigné qui pourrait vous faire gagner du temps et de l’effort. Nous vous montrerons comment utiliser les liens déroulants de session pour passer la session de l’utilisateur final de notre application Next.js à l’installation WordPress hébergeant notre magasin WooCommerce. L’avantage de cette méthode est qu’elle nous permet de contourner le besoin de créer certaines pages dans notre application, en particulier celles qui pourraient prendre du temps à construire ou qui dépendent de fonctionnalités qui ne sont pas facilement disponibles dans notre application front-end.
Préalables
Avant de commencer, nous devrions mettre à jour notre .env.local fichier avec certaines variables qui seront vitales pour ce chapitre.
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]Les valeurs peuvent être aléatoires sauf pour NONCE_KEY et NONCE_SALT le doit correspondre à leurs équivalents en WordPress. Vous pouvez généralement trouver ces valeurs en vous vérifiant l’installation WordPress wp-config.php fichier. N’oubliez pas de mettre à jour votre next.config.js ainsi que
...
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,
},
}
...
À la fin de ce tutoriel, vous serez équipé de la connaissance pour améliorer la navigation utilisateur, ajouter une page de connexion conviviale, et mettre en œuvre des liens de dépôt de session, emmenant votre boutique eCommerce au niveau suivant de l’interactivité utilisateur et de la fonctionnalité. Plongons et commençons !
Partie 1 Créer une page de connexion et UserNav
1. Installer zod, react-hook-formet @hookform/resolvers colis & #160;:
npm install zod react-hook-form @hookform/resolversNous utiliserons ces paquets pour gérer et valider le formulaire de connexion.
2. Créer /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>
);
}Assez simple. En avant.
3. Créer /client/Login:
// client/Connexion/Connexion.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 vous êtes familier avec react-hook-form ça ne devrait pas te paraître anormal. La partie que j’aimerais appeler est
const { login, isAuthenticated, fetching } = useSession();useSession est un crochet pour le SessionProvider que nous n’avons pas encore mis en œuvre, alors ne vous attendez pas à ce que cela fonctionne immédiatement. Nous y arriverons bientôt.
4. Créer /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>
)}
</>
)
}Les UserNav composante est responsable de l’affichage des actions de l’utilisateur basé sur les valeurs reçues de 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();Ajoutons ceci au /server/TopNav avant de finalement passer à la SessionProvider.
5. Mise à jour /server/TopNav/TopNav.tsx:
// serveur/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>
)
}Partie 2: Créer un fournisseur de session
1. Créer /client/SessionProvider:
// client/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);
// Supprimer l'état de déconnection et les creds stockent localement.
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);Maintenant, ajoutez-le au /app/layout.tsx
2. Mise à jour /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>
)
}Avec cela, l’application va construire et fonctionner avec succès, mais aucun des composants que nous avons introduits ne fonctionne réellement. Ceux qui ont un œil vif auraient remarqué que nous n’avons mis en place aucune logique pour communiquer avec notre point final.

Cela est dû au fait que la gestion sécuritaire des références de session et la visibilité restreinte du paramètre GraphQL exigent une gestion sophistiquée. Il convient de noter que malgré tout ce qui a été créé jusqu’à présent, des actions se produisent dans l’application en direct qui expose le paramètre GraphQL. Ne crois pas. Ouvrez les outils de développement de votre navigateur et vérifiez Network onglet. Rafraîchir la page et cliquer autour, faire n’importe quoi, mais vous ne verrez aucune requête visible à votre paramètre GraphQL sont faites.
Jusqu’à présent, toutes les requêtes GraphQL n’ont été faites au niveau du serveur que chaque fois que les pages ont été construites, mais nous devrons faire une requête côté client pour traiter la session de l’utilisateur final qui exposerait le paramètre si nous avons lancé les requêtes GraphQL directement sur le client.
Donc, nous ne les exécuterons pas sur le client, mais profiterons plutôt des 13 prochaines pages de Route. Nous ferons quelques pages Route qui exécuteront nos requêtes GraphQL hors de la vue du client. Avant cela, nous devrons créer des fichiers utilitaires qui définiront la logique pour communiquer avec ces pages de route et gérer les identifiants de session sensibles au temps. Commençons.
Partie 3: Créer des fichiers utilitaires.
1. Installer crypto-js et jwt-decode colis & #160;:
npm install crypto-js jwt-decodeCes bibliothèques avec pour recréer la fonctionnalité de hachage de WordPress.
2. Créer /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);
// Créer nonce
const nonce = createNonce(action, uId, clientSessionId);
// Créer une URL.
const param = getNonceParam(actionType);
let url = `${process.env.BACKEND_URL}/wp/transfer-session?session_id=${uId}&${param}=${nonce}`;
return url;
}Je ne vais pas entrer dans beaucoup de détails sur ce fichier ici, mais c’est principalement un clone JS d’un couple PHP/WP fonctions/constantes. Dans le cas de notre session utilitaire dont nous nous soucions time fonction et MINUTE_IN_SECONDS constante. En avant.
3. Créer /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)) {
// Créer un objet d'identification avec UserAgent.
credentials = {
userAgent: window?.navigator?.userAgent || '',
ip: '',
issued: 0,
};
// Renseignez-vous.
const response = await fetch('https://api.ipify.org/?format=json');
const { data } = await response.json();
credentials.ip = data?.ip || '';
}
// Mise à jour horodatage pour s'assurer que de nouveaux nonces sont générés à chaque fois
// l'utilisateur final commence cette application.
credentials.issued = time();
localStorage.setItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string, JSON.stringify(credentials));
// Générer l'identifiant de session du client.
const clientSessionId = wpNonceHash(JSON.stringify(credentials));
const timeout = `${credentials.issued + HOUR_IN_SECONDS}`;
// Enregistrer l'identifiant de session client.
sessionStorage.setItem(process.env.CLIENT_SESSION_SS_KEY as string, clientSessionId);
sessionStorage.setItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string, timeout);
// Retournez l'identifiant de session du client.
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 logique ici est de créer un clientSessionId. Ceci est nécessaire pour lier l’utilisation des liens de dépôt de session à la machine de l’utilisateur final. Nous allons en apprendre davantage sur l’utilisation que nous créons les pages de route. Une chose à noter pour l’instant est qu’il est sensible au temps et la plupart de la logique ici est consacrée à son évaluation et renouvellement.
4. Créer /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();
// Erreurs de capture.
if (json?.errors || response.status !== 200) {
throw new Error(json.errors?.message || `Failed to fetch: ${url}`);
}
return json;
}
// Gestion des ressources.
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 sans 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);
}Même si c’est un peu dur, tout droit. login, getSessionet fetchAuthURLs sont callback pour être utilisé par notre SessionProvider.
login: Fonctionne en envoyant une demande au/api/loginitinéraire avec l’utilisateur finalusernameetpasswordentrée, si avec succès etauthTokenetrefreshTokenest retourné.getSession: Fonctionne en récupérant d’abord les identifiants de session de l’utilisateur finallocalStoragepuis envoie une demande POST à/api/sessionavec les utilisateurs finauxauthTokenetsessionTokenpour identification.- Si l’utilisateur final a un
refreshTokenet non valableauthTokenet nouveauxauthTokenest récupéré en envoyant une requête POST à/api/authdansfetchAuthToken, - Si l’utilisateur final n’a pas d’identifications nouvelles
sessionTokenest récupéré en envoyant une requête GET à/api/authdansfetchSessionToken
- Si l’utilisateur final a un
fetchAuthURLs: Fonctionne exactement commegetSessionsauf qu’il envoie lesclientSessionIdetclientSessionIdTimeoutainsi que pour la génération d’URL déroulante de session.- Si l’utilisateur final n’a pas de validité
clientSessionId, un nouveau est généré.
- Si l’utilisateur final n’a pas de validité
Avec tous les fichiers utilitaires créés. Passons aux pages Route.
Partie 4: Créer des pages d’itinéraire
1. Créer 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 });
}
}Bien que cette route ait à la fois GET et POST callback, les deux tentent le même objectif juste dans des circonstances différentes.
POSTsuppose que l’utilisateur est un utilisateur de retour avecrefreshTokenouauthTokenet si le succèsauthTokenetsessionToken.GETsuppose que l’utilisateur est nouveau sans identifiant et renvoie un nouveausessionTokenpour un utilisateur invité.
2. Créer /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 });
}
}Celui-ci est similaire au dernier mais plus simple. Il faut username et password à partir de l’entrée et essaie de retourner l’utilisateur final authToken, refreshTokenet sessionToken.
Remarque, sessionToken est récupéré de la réponse HTTP de GetSession requête avec la authToken dans la Authorization. Le raisonnement pour cela est d’obtenir la dernière session connectée à l’utilisateur dans la base de données. Si la sessionToken de customer requête est utilisé au lieu de celui-ci une nouvelle session avec être démarrée sur login à chaque fois et l’ancienne session sera effacée la seconde la nouvelle sessionToken est utilisé. Le modèle a également été utilisé dans le POST rappel du api/auth aussi si vous avez remarqué.
3. Créer /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 });
}
}Celui-ci attend une sessionToken et éventuellement et authToken. Lorsqu’il reçoit les lettres d’identification appropriées, il retourne les customer et cart données de la GetSession requête.
4. Créer /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 });
}
}
Cette route est prévue pour la même entrée que la dernière plus l’utilisateur final clientSessionId et clientSessionIdTimeout. Lorsqu’il est donné la bonne entrée, il définit l’utilisateur final client_session_id et client_session_id_expiration dans l’objet de session WooCommerce de l’utilisateur final sur le serveur. Ces valeurs sont ensuite utilisées pour générer une série de nonces sur le moteur WP à utiliser dans les URL de dépôt de session.
Maintenant, nous pourrions demander pour ces nonces ou même toute la session URL déroulant sur le paramètre GraphQL, mais qui exécute les risques de fuite des nonces/URL dans le transit. Donc, nous utilisons le reste de la fonctionnalité définie dans le utils/nonce.ts fichier et recréer exactement les mêmes nonces dans nos pages Route. generateUrl génère une URL de dépôt de session sur mesure pour l’utilisateur final et liée à la machine. Avec cela, les dernières pages de route ont été créées et tout ce qui reste est de mettre à jour le SessionProvider pour utiliser les fichiers utilitaires et par extension les pages de route.
Partie 5: Finition SessionProvider
1. Mise à jour /client/SessionProvider/SessionProvider.tsx:
// client/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) {
...
// Supprimer l'état de déconnection et les creds stockent localement.
const logout = () => {
...
deleteCredentials();
deleteClientCredentials();
fetchSession();
};
// Session de processus chercher la réponse à la demande.
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();
});
};
// Récupérer les données du client.
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);Après la mise à jour de SessionProvider, les options de connexion et autres options de navigation utilisateur devraient fonctionner comme prévu, bien que vous puissiez avoir besoin de styler votre installation WP pour ressembler à vous.

Conclusion
Dans le prochain tutoriel, nous compléterons notre application avec la page de produit unique et les options de panier. J’espère que ce tutoriel vous a amusé et je vous verrai dans le prochain.

Leave a Reply