fix: reuse cart token

This commit is contained in:
paolosantarsiero 2025-04-08 19:47:55 +02:00
parent 150d361791
commit b250d83534
16 changed files with 1660 additions and 647 deletions

View File

@ -1,21 +1,38 @@
import { authOptions } from 'lib/auth/config';
import { storeApi } from 'lib/woocomerce/storeApi';
import { getServerSession } from 'next-auth';
import { getStoreApiFromRequest } from 'lib/woocomerce/storeApi';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
storeApi._setAuthorizationToken(session?.user?.token ?? '');
const cart = await storeApi.getCart();
return NextResponse.json(cart, { status: 200 });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch cart', message: error }, { status: 500 });
const storeApi = await getStoreApiFromRequest(req);
const { cart, cartToken: updatedToken } = await storeApi.getCart();
const response = NextResponse.json(cart);
if (updatedToken) {
response.cookies.set('cart-token', updatedToken, {
httpOnly: true,
sameSite: 'lax',
path: '/', //
maxAge: 60 * 60 * 46 // 46 ore
});
}
return response;
} catch (error: any) {
if (error.message.includes('jwt_auth_invalid_token')) {
console.error('Token expired, please reauthenticate.');
return NextResponse.json({ error: 'Token expired, please reauthenticate.' }, { status: 401 });
}
return NextResponse.json(
{ error: 'Failed to fetch cart', message: error.message },
{ status: 500 }
);
}
}
export async function POST(req: NextRequest) {
try {
const storeApi = await getStoreApiFromRequest(req);
const { id, quantity, variation } = await req.json();
const cart = await storeApi.addToCart({ id, quantity, variation });
return NextResponse.json(cart, { status: 200 });
@ -29,6 +46,7 @@ export async function POST(req: NextRequest) {
export async function PUT(req: NextRequest) {
try {
const storeApi = await getStoreApiFromRequest(req);
const { key, quantity } = await req.json();
if (quantity > 0) {
const cart = await storeApi.updateItem({ key, quantity });
@ -47,6 +65,7 @@ export async function PUT(req: NextRequest) {
export async function DELETE(req: NextRequest) {
try {
const storeApi = await getStoreApiFromRequest(req);
const { key } = await req.json();
const cart = await storeApi.removeFromCart({ key });
return NextResponse.json(cart, { status: 200 });

View File

@ -0,0 +1,31 @@
import { authOptions } from 'lib/auth/config';
import { getStoreApiFromRequest, OrderPayload } from 'lib/woocomerce/storeApi';
import { getServerSession } from 'next-auth';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.customer_id) {
return NextResponse.json({ error: 'User not logged' }, { status: 401 });
}
const storeApi = await getStoreApiFromRequest(req);
const { billing_address, shipping_address, customer_note, payment_method, payment_data } =
await req.json();
const order: OrderPayload = {
shipping_address,
billing_address: billing_address || shipping_address,
customer_note,
payment_method: payment_method || 'bacs', // Ensure payment method is used
payment_data: payment_data || [] // Ensure payment data is used
};
console.log('Creating order', order);
const result = await storeApi.createOrder(order);
return NextResponse.json(result, { status: 200 });
} catch (error) {
console.error('Error creating order', error);
return NextResponse.json({ error: JSON.stringify(error) }, { status: 500 });
}
}

View File

@ -20,9 +20,8 @@ const shippingSchema = z.object({
address_1: z.string().min(3),
address_2: z.string().optional(),
city: z.string().min(3),
state: z.string().max(2).min(2),
postcode: z.string().min(3),
country: z.string().min(3),
country: z.string().min(2).max(2),
company: z.string().optional()
});

View File

@ -9,6 +9,18 @@ export default function CheckoutReview() {
const { cart } = useCart();
const { checkout } = useCheckout();
const handleCreateOrder = async () => {
const order = await fetch('/api/customer/order', {
method: 'POST',
body: JSON.stringify({
billing_address: checkout?.billing,
shipping_address: checkout?.shipping,
payment_method: checkout?.payment_method
})
}).catch((err) => {
console.error('Error creating order', err);
});
};
return (
<section className="mt-4 grid w-full gap-4 px-4 pb-4">
<h1 className="text-2xl font-bold">Riassunto</h1>
@ -41,8 +53,13 @@ export default function CheckoutReview() {
<span className="mt-4 text-lg font-bold">Metodo di pagamento</span>
<span>{checkout?.payment_method}</span>
<Button title="Vai al pagamento" color="primary" className="text-white">
Vai al pagamento
<Button
title="Vai al pagamento"
color="primary"
className="text-white"
onPress={handleCreateOrder}
>
Crea ordine
</Button>
</section>
);

View File

@ -6,7 +6,8 @@ export default async function ProductPage(props: { params: Promise<{ slug: strin
const slug = (await props.params).slug;
const category = (await woocommerce.get('products/categories', { slug }))?.[0];
const products: Product[] = await woocommerce.get('products', {
category: category.id.toString()
category: category.id.toString(),
author: 1
});
return (

View File

@ -103,7 +103,7 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
<div className="mx-auto max-w-screen-2xl px-4">
<div className="grid items-start gap-8 rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:flex-col-reverse lg:grid-cols-2 lg:flex-row lg:flex-col">
<h1 className="mb-2 text-5xl font-medium md:hidden lg:hidden">{product.name}</h1>
<div className="lg:sticky top-4 w-full self-start">
<div className="top-4 w-full self-start lg:sticky">
<Suspense
fallback={
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden" />

View File

@ -20,7 +20,8 @@ export default async function SearchPage(props: {
orderby: sortKey,
order,
min_price: minPrice ?? '0',
max_price: maxPrice ?? '1000'
max_price: maxPrice ?? '1000',
author: 1
});
const resultsText = products.length > 1 ? 'results' : 'result';

View File

@ -4,8 +4,7 @@ import Link from 'next/link';
import { GridTileImage } from './grid/tile';
export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page.
const products: Product[] = await woocommerce.get('products');
const products: Product[] = await woocommerce.get('products', { author: 1 });
if (!products?.length) return null;

View File

@ -5,24 +5,22 @@ import { Cart } from 'lib/woocomerce/models/cart';
import React, { createContext, useContext, useEffect, useState } from 'react';
type CartContextType = {
cart: Cart | undefined;
cart?: Cart;
setNewCart: (cart: Cart) => void;
};
const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [cart, setCart] = useState<Cart | undefined>(undefined);
const setNewCart = (cart: Cart) => {
setCart(cart);
};
const [cart, setCart] = useState<Cart>();
const fetchCart = async () => {
try {
const cart = await (await fetch('/api/cart')).json();
setNewCart(cart);
const res = await fetch('/api/cart');
const cart = await res.json();
setCart(cart);
} catch (err) {
console.error(err);
console.error('Error fetching cart', err);
}
};
@ -32,21 +30,14 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
return (
<NextUIProvider>
<CartContext.Provider
value={{
cart,
setNewCart
}}
>
{children}
</CartContext.Provider>
<CartContext.Provider value={{ cart, setNewCart: setCart }}>{children}</CartContext.Provider>
</NextUIProvider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (context === undefined) {
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;

View File

@ -32,7 +32,7 @@ export default function ShippingForm({
<div className={clsx('flex flex-col', className)}>
{title && <h2 className="mt-2 text-2xl font-bold">{title}</h2>}
{Object.entries(checkout?.shipping || {})
.filter(([key]) => key !== 'country')
.filter(([key]) => key !== 'country' && key !== 'state')
.map(([key, value], index) => (
<div className={index !== 0 ? 'mt-4' : ''} key={key}>
<Input
@ -74,7 +74,7 @@ export default function ShippingForm({
>
{countries.map((item) => (
<SelectItem
key={item.name}
key={item.code}
startContent={
<Avatar alt={item.name + '-img'} className="h-6 w-6" src={item.icon} />
}

View File

@ -1,4 +1,3 @@
import { storeApi } from 'lib/woocomerce/storeApi';
import { wordpress } from 'lib/wordpress/wordpress';
import { NextAuthOptions, Session, User } from 'next-auth';
import { JWT } from 'next-auth/jwt';
@ -25,8 +24,7 @@ export const authOptions = {
if (user) {
return user;
}
storeApi._seCartToken('');
storeApi._setAuthorizationToken('');
// Return null if user data could not be retrieved
return null;
}
@ -45,14 +43,5 @@ export const authOptions = {
session.user = token.user;
return session;
}
},
events: {
async signIn() {
storeApi._seCartToken('');
},
async signOut() {
storeApi._seCartToken('');
storeApi._setAuthorizationToken('');
}
}
} satisfies NextAuthOptions;

View File

@ -11,10 +11,11 @@ export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyUR
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
export const getCountries = (): { name: string; icon: string }[] =>
(countries as { country: string; flag_base64: string }[]).map(({ country, flag_base64 }) => ({
name: country,
icon: flag_base64
export const getCountries = (): { name: string; icon: string; code: string }[] =>
(countries as { name: string; emoji: string; code: string }[]).map(({ name, emoji, code }) => ({
name,
icon: emoji,
code
}));
export const isStrinInteger = (value: string) => {

View File

@ -72,7 +72,7 @@ export type OrderItem = {
export interface OrderNotes {
id: number;
author: string;
author: string | number;
date_created: Date;
date_created_gmt: Date;
note: string;

View File

@ -1,14 +1,12 @@
import axios, { AxiosInstance, RawAxiosRequestHeaders } from 'axios';
import axios, { RawAxiosRequestHeaders } from 'axios';
import { getToken } from 'next-auth/jwt';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import { Billing } from './models/billing';
import { Cart } from './models/cart';
import { Order } from './models/orders';
import { Shipping } from './models/shipping';
/**
* WooCommerce Store API Client for server-side requests.
* To use this in the client-side, you need to create a new route of api endpoint in your Next.js app.
*/
export type OrderPayload = {
billing_address: Billing;
shipping_address: Shipping;
@ -22,78 +20,98 @@ export type PaymentMethodData = {
value: string;
};
class WooCommerceStoreApiClient {
private client: AxiosInstance;
constructor(baseURL: string) {
function createStoreApiClient({
baseURL = process.env.WOOCOMMERCE_STORE_API_URL ?? 'http://localhost/wp-json/wc/store/v1',
authToken,
cartToken
}: {
baseURL?: string;
authToken?: string;
cartToken?: string;
}) {
const headers: RawAxiosRequestHeaders = {
'Content-Type': 'application/json',
Accept: '*/*'
Accept: '*/*',
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
...(cartToken ? { 'cart-token': cartToken } : {})
};
this.client = axios.create({
baseURL,
const client = axios.create({ baseURL, headers });
async function _request(method: 'get' | 'post' | 'put' | 'delete', url: string, data?: any) {
try {
console.debug('Request', method, url, data, headers);
const response = await axios({
method,
url: baseURL + url,
data,
headers
});
return response;
} catch (error: any) {
if (error.response) {
console.error('Error response:', error.response.data);
if (error.response.data.code === 'jwt_auth_invalid_token') {
console.debug('Token expired, regenerating...');
const newAuthToken = await regenerateAuthToken();
headers.Authorization = `Bearer ${newAuthToken}`;
return _request(method, url, data);
}
_setAuthorizationToken(token: string) {
if (token) {
this.client.defaults.headers['Authorization'] = `Bearer ${token}`;
} else {
this._deleteAuthorizationToken();
throw new Error(
`Request failed with status ${error.response.status}: ${error.response.data.message}`
);
}
throw error;
}
}
_deleteAuthorizationToken() {
this.client.defaults.headers['Authorization'] = '';
async function regenerateAuthToken(): Promise<string> {
console.debug('Regenerating auth token...');
const res = await axios.post(`${baseURL}/auth/refresh`, {}, { headers });
return res.data.token;
}
_seCartToken(cartToken: string) {
this.client.defaults.headers['cart-token'] = cartToken;
}
async getCart(params?: Record<string, string | number>): Promise<Cart> {
return this.client.get<Cart>('/cart', { params }).then(async (response) => {
this._seCartToken(response.headers['cart-token']);
return response.data;
});
}
return {
async getCart(
params?: Record<string, string | number>
): Promise<{ cart: Cart; cartToken?: string }> {
const res = await _request('get', '/cart', { params });
return { cart: res.data, cartToken: res.headers['cart-token'] };
},
async addToCart(payload: {
id: string | number;
quantity: number;
variation: { attribute: string; value: string }[];
}): Promise<Cart> {
return this.client.post<Cart>('/cart/add-item', payload).then((response) => response.data);
}
const res = await _request('post', '/cart/add-item', payload);
return res.data;
},
async updateItem(payload: { key: string | number; quantity: number }): Promise<Cart> {
return this.client.post<Cart>('/cart/update-item', payload).then((response) => response.data);
}
const res = await _request('post', '/cart/update-item', payload);
return res.data;
},
async removeFromCart(payload: { key: string | number }): Promise<Cart> {
return this.client
.post<Cart>(`/cart/remove-item?key=${payload.key}`)
.then((response) => response.data);
}
async createOrder(order: OrderPayload): Promise<Order> {
return this.client.post('/checkout', order).then((response) => response.data);
}
async getOrders(params?: Record<string, string | number>): Promise<Order[]> {
return this.client.get<Order[]>('/checkout', { params }).then((response) => response.data);
}
const res = await _request('post', `/cart/remove-item?key=${payload.key}`);
return res.data;
},
createOrder(order: OrderPayload): Promise<Order> {
return _request('post', '/checkout', order).then((res) => res.data);
},
async getOrders(): Promise<Order[]> {
const res = await _request('get', '/checkout');
return res.data;
},
async getOrder(id: string | number): Promise<Order> {
return this.client.get<Order>(`/checkout/${id}`).then((response) => response.data);
const res = await _request('get', `/checkout/${id}`);
return res.data;
}
};
}
// Example usage.
const baseURL =
process.env.WOOCOMMERCE_STORE_API_URL ?? 'http://wordpress.localhost/wp-json/wc/store/v1';
export async function getStoreApiFromRequest(req?: NextRequest) {
const cartToken = (await cookies()).get('cart-token')?.value;
const authToken = req ? (await getToken({ req }))?.user?.token : undefined;
export const storeApi = new WooCommerceStoreApiClient(baseURL);
return createStoreApiClient({ cartToken, authToken });
}

View File

@ -13,7 +13,6 @@ const nextConfig: NextConfig = {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: '**'
}
]

File diff suppressed because one or more lines are too long