mirror of
https://github.com/vercel/commerce.git
synced 2025-06-27 17:01:21 +00:00
fix: reuse cart token
This commit is contained in:
parent
150d361791
commit
b250d83534
@ -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 });
|
||||
|
31
app/api/customer/order/route.ts
Normal file
31
app/api/customer/order/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
@ -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()
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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" />
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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} />
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ const nextConfig: NextConfig = {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**'
|
||||
}
|
||||
]
|
||||
|
1992
types/countries.json
1992
types/countries.json
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user